unify horizontal video models

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-04-28 10:32:15 +08:00
parent 8dd1028fdd
commit cca5149640
8 changed files with 128 additions and 129 deletions

View File

@@ -25,7 +25,7 @@ class VideoProgressIndicator extends LeafRenderObjectWidget {
this.radius = 10,
this.height = 4,
required this.progress,
}) : assert(progress >= 0 && progress <= 1);
});
final Color color;
final Color backgroundColor;
@@ -136,9 +136,9 @@ class RenderProgressBar extends RenderBox {
bottomRight: radius,
);
if (progress == 0) {
if (progress <= 0) {
canvas.drawRRect(rrect, paint..color = _backgroundColor);
} else if (progress == 1) {
} else if (progress >= 1) {
canvas.drawRRect(rrect, paint..color = _color);
} else {
final w = size.width * progress;

View File

@@ -7,18 +7,13 @@ import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.da
import 'package:PiliPlus/common/widgets/stat/stat.dart';
import 'package:PiliPlus/common/widgets/video_popup_menu.dart';
import 'package:PiliPlus/http/search.dart';
import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/models/common/stat_type.dart';
import 'package:PiliPlus/models/model_hot_video_item.dart';
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/models/search/result.dart';
import 'package:PiliPlus/models/horizontal_video_model.dart';
import 'package:PiliPlus/models_new/video/video_detail/dimension.dart';
import 'package:PiliPlus/utils/date_utils.dart';
import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:flutter/material.dart' hide LayoutBuilder;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
// 视频卡片 - 水平布局
class VideoCardH extends StatelessWidget {
@@ -29,37 +24,13 @@ class VideoCardH extends StatelessWidget {
this.onViewLater,
this.onRemove,
});
final BaseVideoItemModel videoItem;
final HorizontalVideoModel videoItem;
final VoidCallback? onTap;
final ValueChanged<int>? onViewLater;
final VoidCallback? onRemove;
@override
Widget build(BuildContext context) {
String type = 'video';
String? badge;
if (videoItem case final SearchVideoItemModel item) {
final typeOrNull = item.type;
if (typeOrNull != null && typeOrNull.isNotEmpty) {
type = typeOrNull;
if (type == 'ketang') {
badge = '课堂';
} else if (type == 'live_room') {
badge = '直播';
}
}
if (item.isUnionVideo == 1) {
badge = '合作';
}
} else if (videoItem case final HotVideoItemModel item) {
if (item.isCharging == true) {
badge = '充电专属';
} else if (item.isCooperation == 1) {
badge = '合作';
} else {
badge = item.pgcLabel;
}
}
void onLongPress() => imageSaveDialog(
bvid: videoItem.bvid,
title: videoItem.title,
@@ -67,9 +38,9 @@ class VideoCardH extends StatelessWidget {
);
final theme = Theme.of(context);
return Material(
type: MaterialType.transparency,
type: .transparency,
child: Stack(
clipBehavior: Clip.none,
clipBehavior: .none,
children: [
InkWell(
onLongPress: onLongPress,
@@ -77,33 +48,25 @@ class VideoCardH extends StatelessWidget {
onTap:
onTap ??
() async {
if (type == 'ketang') {
PageUtils.viewPugv(seasonId: videoItem.aid);
if (videoItem.isPugv ?? false) {
PageUtils.viewPugv(seasonId: videoItem.seasonId);
return;
} else if (type == 'live_room') {
if (videoItem case final SearchVideoItemModel item) {
int? roomId = item.id;
if (roomId != null) {
PageUtils.toLiveRoom(roomId);
}
} else {
SmartDialog.showToast(
'err: live_room : ${videoItem.runtimeType}',
);
}
if (videoItem.isLive ?? false) {
if (videoItem.roomId case final roomId?) {
PageUtils.toLiveRoom(roomId);
}
return;
}
Dimension? dimension;
if (videoItem case final HotVideoItemModel item) {
if (item.redirectUrl?.isNotEmpty == true &&
PageUtils.viewPgcFromUri(item.redirectUrl!)) {
return;
}
dimension = item.dimension;
if (videoItem.redirectUrl?.isNotEmpty == true &&
PageUtils.viewPgcFromUri(videoItem.redirectUrl!)) {
return;
}
int? cid = videoItem.cid;
Dimension? dimension = videoItem.dimension;
if (cid == null) {
if (await SearchHttp.ab2cWithDimension(
aid: videoItem.aid,
@@ -125,40 +88,38 @@ class VideoCardH extends StatelessWidget {
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
padding: const .symmetric(
horizontal: Style.safeSpace,
vertical: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
crossAxisAlignment: .start,
children: [
AspectRatio(
aspectRatio: Style.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
num? progress;
if (videoItem case final HotVideoItemModel item) {
progress = item.progress;
}
final progress = videoItem.progress;
return Stack(
clipBehavior: Clip.none,
clipBehavior: .none,
children: [
NetworkImgLayer(
src: videoItem.cover,
width: maxWidth,
height: maxHeight,
),
if (badge != null)
if (videoItem.badge case final badge?)
PBadge(
text: badge,
top: 6.0,
right: 6.0,
type: switch (badge) {
'充电专属' => PBadgeType.error,
_ => PBadgeType.primary,
'充电专属' => .error,
_ => .primary,
},
),
if (progress != null && progress != 0) ...[
@@ -168,7 +129,7 @@ class VideoCardH extends StatelessWidget {
: '${DurationUtils.formatDuration(progress)}/${DurationUtils.formatDuration(videoItem.duration)}',
right: 6,
bottom: 8,
type: PBadgeType.gray,
type: .gray,
),
Positioned(
left: 0,
@@ -190,7 +151,7 @@ class VideoCardH extends StatelessWidget {
),
right: 6.0,
bottom: 6.0,
type: PBadgeType.gray,
type: .gray,
),
],
);
@@ -224,45 +185,44 @@ class VideoCardH extends StatelessWidget {
if (pubdate != '') pubdate += ' ';
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: .start,
children: [
if (videoItem case final SearchVideoItemModel item) ...[
if (item.titleList?.isNotEmpty == true)
Expanded(
child: Text.rich(
overflow: TextOverflow.ellipsis,
maxLines: 2,
TextSpan(
children: item.titleList!
.map(
(e) => TextSpan(
text: e.text,
style: TextStyle(
fontSize: theme.textTheme.bodyMedium!.fontSize,
height: 1.42,
letterSpacing: 0.3,
color: e.isEm
? theme.colorScheme.primary
: theme.colorScheme.onSurface,
),
if (videoItem.titleList?.isNotEmpty == true)
Expanded(
child: Text.rich(
overflow: .ellipsis,
maxLines: 2,
TextSpan(
children: videoItem.titleList!
.map(
(e) => TextSpan(
text: e.text,
style: TextStyle(
fontSize: theme.textTheme.bodyMedium!.fontSize,
height: 1.42,
letterSpacing: 0.3,
color: e.isEm
? theme.colorScheme.primary
: theme.colorScheme.onSurface,
),
)
.toList(),
),
),
)
.toList(),
),
),
] else
)
else
Expanded(
child: Text(
videoItem.title,
textAlign: TextAlign.start,
textAlign: .start,
style: TextStyle(
fontSize: theme.textTheme.bodyMedium!.fontSize,
height: 1.42,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
overflow: .ellipsis,
),
),
Text(
@@ -272,7 +232,7 @@ class VideoCardH extends StatelessWidget {
fontSize: 12,
height: 1,
color: theme.colorScheme.outline,
overflow: TextOverflow.clip,
overflow: .clip,
),
),
const SizedBox(height: 3),
@@ -280,11 +240,11 @@ class VideoCardH extends StatelessWidget {
spacing: 8,
children: [
StatWidget(
type: StatType.play,
type: .play,
value: videoItem.stat.view,
),
StatWidget(
type: StatType.danmaku,
type: .danmaku,
value: videoItem.stat.danmu,
),
],

View File

@@ -0,0 +1,21 @@
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/models_new/video/video_detail/dimension.dart';
abstract class HorizontalVideoModel extends BaseVideoItemModel {
bool? isPugv;
int? seasonId;
int? roomId;
bool? isLive;
Dimension? dimension;
String? badge;
num? progress;
String? redirectUrl;
// search
List<({bool isEm, String text})>? titleList;
}

View File

@@ -1,25 +1,19 @@
import 'package:PiliPlus/models/horizontal_video_model.dart';
import 'package:PiliPlus/models/model_owner.dart';
import 'package:PiliPlus/models/model_rec_video_item.dart';
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/models_new/video/video_detail/dimension.dart';
import 'package:PiliPlus/pages/common/multi_select/base.dart';
// 稍后再看, 排行榜等网页返回也使用该类
class HotVideoItemModel extends BaseRcmdVideoItemModel with MultiSelectData {
class HotVideoItemModel extends HorizontalVideoModel with MultiSelectData {
int? videos;
int? tid;
String? tname;
int? copyright;
int? ctime;
int? state;
Dimension? dimension;
String? firstFrame;
String? pubLocation;
String? pgcLabel;
String? redirectUrl;
num? progress;
int? isCooperation;
bool? isCharging;
HotVideoItemModel.fromJson(Map<String, dynamic> json) {
aid = json["aid"];
@@ -43,23 +37,16 @@ class HotVideoItemModel extends BaseRcmdVideoItemModel with MultiSelectData {
: Dimension.fromJson(json['dimension']);
firstFrame = json["first_frame"];
pubLocation = json["pub_location"];
dynamic rcmd = json['rcmd_reason'];
rcmdReason = rcmd is Map ? rcmd['content'] : rcmd; // 相关视频里rcmd为String,
if (rcmdReason?.isEmpty == true) rcmdReason = null;
pgcLabel = json['pgc_label'];
redirectUrl = json['redirect_url'];
// uri = json['uri']; // 仅在稍后再看存在
progress = json['progress'];
isCooperation = json['rights']?['is_cooperation'];
isCharging = json['charging_pay']?['level'] != null;
if (json['charging_pay']?['level'] != null) {
badge = '充电专属';
} else if (json['rights']?['is_cooperation'] == 1) {
badge = '合作';
} else {
badge = json['pgc_label'];
}
}
// @override
// get isFollowed => false;
// @override
// get goto => 'av';
// @override
// get uri => 'bilibili://video/$aid';
}
class HotStat extends Stat {

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/models/horizontal_video_model.dart';
import 'package:PiliPlus/models/model_avatar.dart';
import 'package:PiliPlus/models/model_owner.dart';
import 'package:PiliPlus/models/model_video.dart';
@@ -64,18 +65,16 @@ class SearchVideoData extends SearchNumData<SearchVideoItemModel> {
}
}
class SearchVideoItemModel extends BaseVideoItemModel {
String? type;
class SearchVideoItemModel extends HorizontalVideoModel {
int? id;
String? arcurl;
String? tag;
int? ctime;
int? isUnionVideo;
List<({bool isEm, String text})>? titleList;
@override
int? get seasonId => aid;
SearchVideoItemModel.fromJson(Map<String, dynamic> json) {
type = json['type'];
id = json['id'];
arcurl = json['arcurl'];
aid = json['aid'];
@@ -89,7 +88,19 @@ class SearchVideoItemModel extends BaseVideoItemModel {
duration = DurationUtils.parseDuration(json['duration']);
owner = SearchOwner.fromJson(json);
stat = SearchStat.fromJson(json);
isUnionVideo = json['is_union_video'];
switch (json['type']) {
case 'ketang':
badge = '课堂';
isPugv = true;
case 'live_room':
badge = '直播';
isLive = true;
roomId = json['roomid'];
default:
if (json['is_union_video'] == 1) {
badge = '合作';
}
}
}
}

View File

@@ -1,7 +1,8 @@
import 'package:PiliPlus/models/horizontal_video_model.dart';
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/utils/duration_utils.dart';
class VListItemModel extends BaseVideoItemModel {
class VListItemModel extends HorizontalVideoModel {
VListItemModel.fromJson(Map<String, dynamic> json) {
cover = json['pic'];
desc = json['description'];
@@ -14,6 +15,24 @@ class VListItemModel extends BaseVideoItemModel {
bvid = json['bvid'];
stat = VListStat.fromJson(json);
owner = VListOwner.fromJson(json);
if (json['is_lesson_video'] == 1) {
isPugv = true;
badge = '课堂';
} else if (json['is_charging_arc'] == true) {
badge = '充电专属';
} else if (json['is_union_video'] == 1) {
badge = '合作';
}
seasonId = json['season_id'];
redirectUrl = json['jump_url'];
final position = json['playback_position'] as num?; // percent
if (position != null) {
if (position == 100) {
progress = -1;
} else {
progress = ((position / 100) * duration).round();
}
}
}
}

View File

@@ -1,6 +1,7 @@
import 'package:PiliPlus/models/horizontal_video_model.dart';
import 'package:PiliPlus/models/model_video.dart';
class SeasonArchive extends BaseVideoItemModel {
class SeasonArchive extends HorizontalVideoModel {
SeasonArchive.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];

View File

@@ -7,7 +7,7 @@ import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/enum_with_label.dart';
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/models/horizontal_video_model.dart';
import 'package:PiliPlus/pages/member_video_web/base/controller.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:PiliPlus/utils/grid.dart';
@@ -19,7 +19,7 @@ import 'package:get/get.dart';
abstract class BaseVideoWebState<
S extends StatefulWidget,
R,
T extends BaseVideoItemModel,
T extends HorizontalVideoModel,
V extends EnumWithLabel
>
extends State<S>