diff --git a/lib/common/widgets/self_sized_horizontal_list.dart b/lib/common/widgets/self_sized_horizontal_list.dart new file mode 100644 index 00000000..650f8e84 --- /dev/null +++ b/lib/common/widgets/self_sized_horizontal_list.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +/// https://stackoverflow.com/a/76605401 + +class SelfSizedHorizontalList extends StatefulWidget { + final Widget Function(int) childBuilder; + final int itemCount; + final double gapSize; + final EdgeInsetsGeometry? padding; + const SelfSizedHorizontalList({ + super.key, + required this.childBuilder, + required this.itemCount, + this.gapSize = 5, + this.padding, + }); + + @override + State createState() => + _SelfSizedHorizontalListState(); +} + +class _SelfSizedHorizontalListState extends State { + final infoKey = GlobalKey(); + + double? prevHeight; + double? get height { + if (prevHeight != null) return prevHeight; + prevHeight = infoKey.globalPaintBounds?.height; + return prevHeight; + } + + bool get isInit => height == null; + + @override + Widget build(BuildContext context) { + if (height == null) { + WidgetsBinding.instance.addPostFrameCallback((v) => setState(() {})); + } + if (widget.itemCount == 0) return const SizedBox(); + if (isInit) return Container(key: infoKey, child: widget.childBuilder(0)); + + return SizedBox( + height: height, + child: ListView.separated( + padding: widget.padding, + scrollDirection: Axis.horizontal, + itemCount: widget.itemCount, + itemBuilder: (c, i) => widget.childBuilder.call(i), + separatorBuilder: (c, i) => SizedBox(width: widget.gapSize), + ), + ); + } +} + +extension GlobalKeyExtension on GlobalKey { + Rect? get globalPaintBounds { + final renderObject = currentContext?.findRenderObject(); + final translation = renderObject?.getTransformTo(null).getTranslation(); + if (translation != null && renderObject?.paintBounds != null) { + final offset = Offset(translation.x, translation.y); + return renderObject!.paintBounds.shift(offset); + } else { + return null; + } + } +} diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart index 4fd972cf..bdb7476c 100644 --- a/lib/models/video_detail_res.dart +++ b/lib/models/video_detail_res.dart @@ -67,6 +67,7 @@ class VideoDetailData { String? likeIcon; bool? needJumpBv; String? epId; + List? staff; VideoDetailData({ this.bvid, @@ -103,6 +104,7 @@ class VideoDetailData { this.likeIcon, this.needJumpBv, this.epId, + this.staff, }); VideoDetailData.fromJson(Map json) { @@ -152,6 +154,9 @@ class VideoDetailData { : HonorReply.fromJson(json["honor_reply"]); likeIcon = json["like_icon"]; needJumpBv = json["need_jump_bv"]; + staff = json["staff"] == null + ? null + : (json["staff"] as List).map((item) => Staff.fromJson(item)).toList(); if (json['redirect_url'] != null) { epId = resolveEpId(json['redirect_url']); } @@ -276,6 +281,42 @@ class Dimension { } } +class Staff { + dynamic mid; + String? title; + String? name; + String? face; + Vip? vip; + + Staff({ + this.mid, + this.title, + this.name, + this.face, + this.vip, + }); + + Staff.fromJson(Map json) { + mid = json["mid"]; + title = json["title"]; + name = json["name"]; + face = json["face"]; + vip = json["vip"] == null ? null : Vip.fromJson(json["vip"]); + } +} + +class Vip { + dynamic type; + + Vip({ + this.type, + }); + + Vip.fromJson(Map json) { + type = json["type"]; + } +} + class HonorReply { List? honor; diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 3e4dd05e..7194e4f9 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -126,6 +126,7 @@ class VideoIntroController extends GetxController { var result = await VideoHttp.videoIntro(bvid: bvid); if (result['status']) { videoDetail.value = result['data']!; + videoItem!['staff'] = result['data'].staff; if (videoDetailController.videoItem['pic'] == null || videoDetailController.videoItem['pic'] == '') { try { diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 1f5c5288..bdfa16bf 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPalaX/common/widgets/self_sized_horizontal_list.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -238,65 +239,120 @@ class _VideoInfoState extends State with TickerProviderStateMixin { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Expanded( - child: GestureDetector( - onTap: onPushMember, - child: Container( - padding: - const EdgeInsets.symmetric(vertical: 1, horizontal: 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - NetworkImgLayer( - type: 'avatar', - src: loadingStatus - ? videoItem['owner']?.face ?? "" - : widget.videoDetail!.owner!.face, - width: 30, - height: 30, - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - loadingStatus - ? videoItem['owner']?.name ?? "" - : widget.videoDetail!.owner!.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12, color: t.colorScheme.primary), - // semanticsLabel: "Up主:${owner.name}", + Row( + children: [ + Expanded( + child: videoItem['staff'] == null + ? GestureDetector( + onTap: onPushMember, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + NetworkImgLayer( + type: 'avatar', + src: loadingStatus + ? videoItem['owner']?.face ?? "" + : widget.videoDetail!.owner!.face, + width: 30, + height: 30, + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + loadingStatus + ? videoItem['owner']?.name ?? "" + : widget.videoDetail!.owner!.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: t.colorScheme.primary), + // semanticsLabel: "Up主:${owner.name}", + ), + const SizedBox(height: 0), + Obx( + () => Text( + Utils.numFormat(videoIntroController + .userStat.value['follower']), + semanticsLabel: + "${Utils.numFormat(videoIntroController.userStat.value['follower'])}粉丝", + style: TextStyle( + fontSize: 12, + color: t.colorScheme.outline, + ), + ), + ), + ], + ), + ), + followButton(context, t), + ], + ), ), - const SizedBox(height: 0), - Obx(() => Text( - Utils.numFormat(videoIntroController - .userStat.value['follower']), - semanticsLabel: - "${Utils.numFormat(videoIntroController.userStat.value['follower'])}粉丝", + ) + : SelfSizedHorizontalList( + gapSize: 10, + itemCount: videoItem['staff'].length, + childBuilder: (index) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () => Get.toNamed( + '/member?mid=${videoItem['staff'][index].mid}', + arguments: { + 'face': videoItem['staff'][index].face, + heroTag: Utils.makeHeroTag( + videoItem['staff'][index].mid), + }), + child: NetworkImgLayer( + type: 'avatar', + src: videoItem['staff'][index].face, + width: 40, + height: 40, + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + ), + ), + const SizedBox(height: 2), + Text( + videoItem['staff'][index].name.length > 5 + ? '${videoItem['staff'][index].name.toString().substring(0, 5)}...' + : videoItem['staff'][index].name, + style: TextStyle( + color: + videoItem['staff'][index].vip.type == 2 + ? Utils.vipColor + : null, + ), + ), + Text( + videoItem['staff'][index].title, style: TextStyle( fontSize: 12, - color: t.colorScheme.outline, + color: + Theme.of(context).colorScheme.outline, ), - )), - ], - )), - followButton(context, t), - ], - ), + ), + ], + ), + ), ), - )), - if (isHorizontal) ...[ - const SizedBox(width: 10), - Expanded(child: actionGrid(context, videoIntroController)), - ] - ]), + if (isHorizontal) ...[ + const SizedBox(width: 10), + Expanded(child: actionGrid(context, videoIntroController)), + ] + ], + ), const SizedBox(height: 8), GestureDetector( behavior: HitTestBehavior.translucent,