From 9e0e35f245077a75078be2db077448227e9c59c3 Mon Sep 17 00:00:00 2001 From: lucinhu <1543498474@qq.com> Date: Sat, 1 Apr 2023 23:25:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=A8=E6=80=81=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E3=80=81=E5=8A=9F=E8=83=BD=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E3=80=81=E8=AF=84=E8=AE=BA=E5=8C=BA=E3=80=81=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E3=80=81=E5=9B=9E=E5=88=B0=E9=A1=B6=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/api/dynamic_api.dart | 55 +++- .../models/local/dynamic/dynamic_content.dart | 46 ++- .../models/local/dynamic/dynamic_item.dart | 14 +- lib/common/values/cache_keys.dart | 1 + lib/common/widget/foldable_text.dart | 5 +- lib/common/widget/icon_text_button.dart | 39 +++ lib/pages/bili_video/view.dart | 4 +- .../bili_video/widgets/introduction/view.dart | 39 +-- .../bili_video/widgets/reply/controller.dart | 10 +- lib/pages/bili_video/widgets/reply/view.dart | 17 +- lib/pages/dynamic/controller.dart | 6 + lib/pages/dynamic/view.dart | 2 + .../dynamic/widget/dynamic_item_card.dart | 263 +++++++++++++++++- lib/pages/main/view.dart | 6 + lib/pages/ui_test/controller.dart | 4 +- 15 files changed, 430 insertions(+), 81 deletions(-) create mode 100644 lib/common/widget/icon_text_button.dart diff --git a/lib/common/api/dynamic_api.dart b/lib/common/api/dynamic_api.dart index f79dd6f2..88af1904 100644 --- a/lib/common/api/dynamic_api.dart +++ b/lib/common/api/dynamic_api.dart @@ -4,6 +4,8 @@ import 'package:bili_you/common/models/local/dynamic/dynamic_content.dart'; import 'package:bili_you/common/models/local/dynamic/dynamic_item.dart'; import 'package:bili_you/common/models/local/dynamic/dynamic_stat.dart'; import 'package:bili_you/common/models/local/reply/official_verify.dart'; +import 'package:bili_you/common/models/local/reply/reply_content.dart'; +import 'package:bili_you/common/models/local/reply/reply_item.dart'; import 'package:bili_you/common/models/network/dynamic/dynamic.dart' as raw; import 'package:bili_you/common/utils/index.dart'; import 'package:dio/dio.dart'; @@ -79,41 +81,47 @@ class DynamicApi { switch (DynamicItemTypeCode.fromCode(i.type ?? "")) { case DynamicItemType.word: //消息 - dynamicContent = DynamicContent( - description: moduleDynamic?.desc?.text ?? "", imageUrls: []); + dynamicContent = _buildWordDynamicContent(moduleDynamic); break; case DynamicItemType.article: //文章 - dynamicContent = DynamicContent( - description: moduleDynamic?.desc?.text ?? "", imageUrls: []); + dynamicContent = _buildWordDynamicContent(moduleDynamic); break; case DynamicItemType.av: //视频投稿 - dynamicContent = DynamicContent( - description: moduleDynamic?.desc?.text ?? "", imageUrls: []); + dynamicContent = AVDynamicContent( + description: moduleDynamic?.desc?.text ?? '', + emotes: _buildEmoteList( + moduleDynamic?.desc?.richTextNodes ?? []), + picUrl: moduleDynamic?.major?.archive?.cover ?? '', + bvid: moduleDynamic?.major?.archive?.bvid ?? '', + title: moduleDynamic?.major?.archive?.title ?? '', + subTitle: moduleDynamic?.major?.archive?.desc ?? '', + duration: moduleDynamic?.major?.archive?.durationText ?? '', + damakuCount: moduleDynamic?.major?.archive?.stat?.danmaku ?? '', + playCount: moduleDynamic?.major?.archive?.stat?.play ?? ''); + break; case DynamicItemType.draw: //抽奖&互动 - dynamicContent = DynamicContent( - description: moduleDynamic?.desc?.text ?? "", imageUrls: []); + dynamicContent = _buildWordDynamicContent(moduleDynamic); break; case DynamicItemType.liveRecommend: //直播推荐 - dynamicContent = DynamicContent( - description: moduleDynamic?.desc?.text ?? "", imageUrls: []); + dynamicContent = _buildWordDynamicContent(moduleDynamic); break; case DynamicItemType.forward: //转发 - dynamicContent = DynamicContent( - description: moduleDynamic?.desc?.text ?? "", imageUrls: []); + dynamicContent = _buildWordDynamicContent(moduleDynamic); break; case DynamicItemType.unkown: //未知 - dynamicContent = DynamicContent( - description: moduleDynamic?.desc?.text ?? "", imageUrls: []); + dynamicContent = _buildWordDynamicContent(moduleDynamic); break; } list.add(DynamicItem( + replyId: i.basic?.commentIdStr ?? '', + replyType: ReplyTypeCode.fromCode(i.basic?.commentType ?? 0), author: dynamicAuthor, type: DynamicItemTypeCode.fromCode(i.type ?? ""), content: dynamicContent, @@ -121,4 +129,23 @@ class DynamicApi { } return list; } + + static List _buildEmoteList(List richTextNodes) { + return [ + for (var i in richTextNodes) + if (i.emoji != null) + Emote( + text: i.emoji?.text ?? '', + url: i.emoji?.iconUrl ?? '', + size: i.emoji?.size == 2 ? EmoteSize.big : EmoteSize.small) + ]; + } + + static DynamicContent _buildWordDynamicContent( + raw.ModuleDynamic? moduleDynamic) { + return WordDynamicContent( + description: moduleDynamic?.desc?.text ?? "", + emotes: _buildEmoteList( + moduleDynamic?.desc?.richTextNodes ?? [])); + } } diff --git a/lib/common/models/local/dynamic/dynamic_content.dart b/lib/common/models/local/dynamic/dynamic_content.dart index 9ccb1054..460d67fb 100644 --- a/lib/common/models/local/dynamic/dynamic_content.dart +++ b/lib/common/models/local/dynamic/dynamic_content.dart @@ -1,7 +1,43 @@ -class DynamicContent { - DynamicContent({required this.description, required this.imageUrls}); - static DynamicContent get zero => - DynamicContent(description: "", imageUrls: []); +import 'package:bili_you/common/models/local/reply/reply_content.dart'; + +abstract class DynamicContent { + DynamicContent({required this.description, required this.emotes}); String description; - List imageUrls = []; + List emotes; +} + +class WordDynamicContent extends DynamicContent { + WordDynamicContent({required super.description, required super.emotes}); + static WordDynamicContent get zero => + WordDynamicContent(description: "", emotes: []); +} + +class AVDynamicContent extends DynamicContent { + AVDynamicContent( + {required super.description, + required super.emotes, + required this.picUrl, + required this.bvid, + required this.title, + required this.subTitle, + required this.duration, + required this.damakuCount, + required this.playCount}); + static AVDynamicContent get zero => AVDynamicContent( + description: "", + emotes: [], + picUrl: '', + title: '', + subTitle: '', + duration: '', + damakuCount: '', + playCount: '', + bvid: ''); + final String picUrl; + final String bvid; + final String title; + final String subTitle; + final String duration; + final String playCount; + final String damakuCount; } diff --git a/lib/common/models/local/dynamic/dynamic_item.dart b/lib/common/models/local/dynamic/dynamic_item.dart index 85fddf99..030a2575 100644 --- a/lib/common/models/local/dynamic/dynamic_item.dart +++ b/lib/common/models/local/dynamic/dynamic_item.dart @@ -1,18 +1,28 @@ +import 'package:bili_you/common/models/local/reply/reply_item.dart'; + import 'dynamic_author.dart'; import 'dynamic_content.dart'; import 'dynamic_stat.dart'; class DynamicItem { DynamicItem( - {required this.author, + {required this.replyId, + required this.replyType, + required this.author, required this.type, required this.content, required this.stat}); static DynamicItem get zero => DynamicItem( + replyId: '', + replyType: ReplyType.unkown, author: DynamicAuthor.zero, type: DynamicItemType.unkown, - content: DynamicContent.zero, + content: WordDynamicContent.zero, stat: DynamicStat.zero); + + ///评论区id + String replyId; + ReplyType replyType; DynamicAuthor author; DynamicItemType type; DynamicContent content; diff --git a/lib/common/values/cache_keys.dart b/lib/common/values/cache_keys.dart index c621ef6d..be24d058 100644 --- a/lib/common/values/cache_keys.dart +++ b/lib/common/values/cache_keys.dart @@ -4,6 +4,7 @@ class CacheKeys { static const String recommendItemCoverKey = 'recommendItemCover'; static const String searchResultItemCoverKey = 'searchResultItemCover'; static const String relatedVideosItemCoverKey = 'relatedVideosItemCover'; + static const String dynamicVideoItemCoverKey = 'dynamicVideoItemCover'; static const String emoteKey = 'emote'; static const String replyImageKey = 'replyImage'; } diff --git a/lib/common/widget/foldable_text.dart b/lib/common/widget/foldable_text.dart index cf06c323..4e7a2bf5 100644 --- a/lib/common/widget/foldable_text.dart +++ b/lib/common/widget/foldable_text.dart @@ -126,8 +126,9 @@ class _FoldableTextState extends State { @override Widget build(BuildContext context) { return LayoutBuilder( - builder: (context, constraints) => - _richText(constraints.maxWidth, constraints.minWidth), + builder: (context, constraints) => (widget.text?.isNotEmpty ?? false) + ? _richText(constraints.maxWidth, constraints.minWidth) + : const SizedBox(), ); } } diff --git a/lib/common/widget/icon_text_button.dart b/lib/common/widget/icon_text_button.dart new file mode 100644 index 00000000..aabb5a84 --- /dev/null +++ b/lib/common/widget/icon_text_button.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class IconTextButton extends StatelessWidget { + const IconTextButton({ + super.key, + required this.onPressed, + required this.icon, + required this.text, + this.selected = false, + }); + final Function()? onPressed; + final Icon icon; + final Text text; + + ///是否被选上 + final bool selected; + @override + Widget build(BuildContext context) { + return ElevatedButton( + style: ButtonStyle( + foregroundColor: selected + ? MaterialStatePropertyAll(Theme.of(context).colorScheme.onPrimary) + : null, + backgroundColor: selected + ? MaterialStatePropertyAll(Theme.of(context).colorScheme.primary) + : null, + elevation: const MaterialStatePropertyAll(0), + padding: const MaterialStatePropertyAll(EdgeInsets.all(5)), + ), + onPressed: onPressed ?? () {}, + child: FittedBox( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [icon, text], + ), + ), + ); + } +} diff --git a/lib/pages/bili_video/view.dart b/lib/pages/bili_video/view.dart index cce385c3..208db7e2 100644 --- a/lib/pages/bili_video/view.dart +++ b/lib/pages/bili_video/view.dart @@ -1,3 +1,4 @@ +import 'package:bili_you/common/models/local/reply/reply_item.dart'; import 'package:bili_you/pages/bili_video/widgets/introduction/index.dart'; import 'package:bili_you/pages/bili_video/widgets/reply/view.dart'; import 'package:flutter/material.dart'; @@ -102,7 +103,8 @@ class _BiliVideoPageState extends State { Builder(builder: (context) { //Builder可以让ReplyPage在TabBarView显示到它的时候才取controller.bvid return ReplyPage( - bvid: controller.bvid, + replyId: controller.bvid, + replyType: ReplyType.video, pauseVideoCallback: controller.biliVideoPlayerController.pause, ); diff --git a/lib/pages/bili_video/widgets/introduction/view.dart b/lib/pages/bili_video/widgets/introduction/view.dart index 0081ddda..8f0a3501 100644 --- a/lib/pages/bili_video/widgets/introduction/view.dart +++ b/lib/pages/bili_video/widgets/introduction/view.dart @@ -1,6 +1,7 @@ import 'package:bili_you/common/utils/string_format_utils.dart'; import 'package:bili_you/common/widget/avatar.dart'; import 'package:bili_you/common/widget/foldable_text.dart'; +import 'package:bili_you/common/widget/icon_text_button.dart'; import 'package:bili_you/pages/user_space/view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -375,41 +376,3 @@ class _IntroductionPageState extends State ); } } - -class IconTextButton extends StatelessWidget { - const IconTextButton({ - super.key, - required this.onPressed, - required this.icon, - required this.text, - this.selected = false, - }); - final Function()? onPressed; - final Icon icon; - final Text text; - - ///是否被选上 - final bool selected; - @override - Widget build(BuildContext context) { - return ElevatedButton( - style: ButtonStyle( - foregroundColor: selected - ? MaterialStatePropertyAll(Theme.of(context).colorScheme.onPrimary) - : null, - backgroundColor: selected - ? MaterialStatePropertyAll(Theme.of(context).colorScheme.primary) - : null, - elevation: const MaterialStatePropertyAll(0), - padding: const MaterialStatePropertyAll(EdgeInsets.all(5)), - ), - onPressed: onPressed ?? () {}, - child: FittedBox( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [icon, text], - ), - ), - ); - } -} diff --git a/lib/pages/bili_video/widgets/reply/controller.dart b/lib/pages/bili_video/widgets/reply/controller.dart index f27e3f95..7e4c68cc 100644 --- a/lib/pages/bili_video/widgets/reply/controller.dart +++ b/lib/pages/bili_video/widgets/reply/controller.dart @@ -12,17 +12,21 @@ import 'package:get/get.dart'; import 'package:bili_you/pages/bili_video/widgets/reply/widgets/reply_item.dart'; class ReplyController extends GetxController { - ReplyController({required this.bvid, required this.pauseVideoCallback}); + ReplyController( + {required this.bvid, + required this.replyType, + required this.pauseVideoCallback}); String bvid; EasyRefreshController refreshController = EasyRefreshController( controlFinishLoad: true, controlFinishRefresh: true); ScrollController scrollController = ScrollController(); List replyList = []; int pageNum = 1; + final ReplyType replyType; RxString sortTypeText = "按热度".obs; RxString sortInfoText = "热门评论".obs; ReplySort _replySort = ReplySort.like; - final Function() pauseVideoCallback; + final Function()? pauseVideoCallback; //切换排列方式 void toggleSort() { @@ -65,7 +69,7 @@ class ReplyController extends GetxController { late ReplyInfo replyInfo; try { replyInfo = await ReplyApi.getReply( - oid: bvid, pageNum: pageNum, type: ReplyType.video, sort: _replySort); + oid: bvid, pageNum: pageNum, type: replyType, sort: _replySort); } catch (e) { log("评论区加载失败,_addReplyItems:$e"); return false; diff --git a/lib/pages/bili_video/widgets/reply/view.dart b/lib/pages/bili_video/widgets/reply/view.dart index ac100679..1b212516 100644 --- a/lib/pages/bili_video/widgets/reply/view.dart +++ b/lib/pages/bili_video/widgets/reply/view.dart @@ -1,3 +1,4 @@ +import 'package:bili_you/common/models/local/reply/reply_item.dart'; import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -7,13 +8,15 @@ import 'index.dart'; class ReplyPage extends StatefulWidget { const ReplyPage({ Key? key, - required this.bvid, - required this.pauseVideoCallback, - }) : tag = "ReplyPage:$bvid", + required this.replyId, + required this.replyType, + this.pauseVideoCallback, + }) : tag = "ReplyPage:$replyId", super(key: key); - final String bvid; + final String replyId; + final ReplyType replyType; final String tag; - final Function() pauseVideoCallback; + final Function()? pauseVideoCallback; @override State createState() => _ReplyPageState(); @@ -62,7 +65,9 @@ class _ReplyPageState extends State super.build(context); return GetBuilder( init: ReplyController( - bvid: widget.bvid, pauseVideoCallback: widget.pauseVideoCallback), + bvid: widget.replyId, + replyType: widget.replyType, + pauseVideoCallback: widget.pauseVideoCallback), tag: widget.tag, builder: (controller) => _buildView(controller), id: 'reply', diff --git a/lib/pages/dynamic/controller.dart b/lib/pages/dynamic/controller.dart index cb5e9edd..f18cb80f 100644 --- a/lib/pages/dynamic/controller.dart +++ b/lib/pages/dynamic/controller.dart @@ -4,6 +4,7 @@ import 'package:bili_you/common/api/dynamic_api.dart'; import 'package:bili_you/common/models/local/dynamic/dynamic_item.dart'; import 'package:bili_you/pages/dynamic/widget/dynamic_item_card.dart'; import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class DynamicController extends GetxController { @@ -15,6 +16,11 @@ class DynamicController extends GetxController { // } int currentPage = 1; List dynamicItemCards = []; + ScrollController scrollController = ScrollController(); + void animateToTop() { + scrollController.animateTo(0, + duration: const Duration(milliseconds: 200), curve: Curves.linear); + } Future _loadDynamicItemCards() async { late List items; diff --git a/lib/pages/dynamic/view.dart b/lib/pages/dynamic/view.dart index ceb3d761..29cd411c 100644 --- a/lib/pages/dynamic/view.dart +++ b/lib/pages/dynamic/view.dart @@ -42,6 +42,8 @@ class DynamicPage extends GetView { ), controller: controller.refreshController, childBuilder: (context, physics) => ListView.builder( + cacheExtent: MediaQuery.of(context).size.height, + controller: controller.scrollController, physics: physics, itemCount: controller.dynamicItemCards.length, itemBuilder: (context, index) => diff --git a/lib/pages/dynamic/widget/dynamic_item_card.dart b/lib/pages/dynamic/widget/dynamic_item_card.dart index f9fd7998..91b6a43a 100644 --- a/lib/pages/dynamic/widget/dynamic_item_card.dart +++ b/lib/pages/dynamic/widget/dynamic_item_card.dart @@ -1,13 +1,81 @@ +import 'package:bili_you/common/models/local/dynamic/dynamic_content.dart'; import 'package:bili_you/common/models/local/dynamic/dynamic_item.dart'; +import 'package:bili_you/common/models/local/reply/reply_content.dart'; import 'package:bili_you/common/widget/avatar.dart'; +import 'package:bili_you/common/widget/cached_network_image.dart'; +import 'package:bili_you/common/widget/foldable_text.dart'; +import 'package:bili_you/common/widget/icon_text_button.dart'; +import 'package:bili_you/index.dart'; +import 'package:bili_you/pages/bili_video/index.dart'; +import 'package:bili_you/pages/bili_video/widgets/reply/index.dart'; import 'package:bili_you/pages/user_space/view.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:get/get.dart'; -class DynamicItemCard extends StatelessWidget { +class DynamicItemCard extends StatefulWidget { const DynamicItemCard({super.key, required this.dynamicItem}); final DynamicItem dynamicItem; + @override + State createState() => _DynamicItemCardState(); +} + +class _DynamicItemCardState extends State { + final CacheManager videoCoverCacheManager = + CacheManager(Config(CacheKeys.dynamicVideoItemCoverKey)); + + @override + void dispose() { + videoCoverCacheManager.emptyCache(); + super.dispose(); + } + + TextSpan buildEmojiText(DynamicContent content, BuildContext context) { + if (content.emotes.isEmpty) { + return TextSpan(text: content.description); + } + List spans = []; + content.description.splitMapJoin(RegExp(r"\[.*?\]"), onMatch: (match) { + //匹配到是[]的位置时,有可能是表情 + String matched = match[0]!; + Emote? emote; + //判断是不是有这个表情 + for (var i in content.emotes) { + if (matched == i.text) { + emote = i; + break; + } + } + if (emote != null) { + //有的话就放表情进去 + spans.add(WidgetSpan( + child: SizedBox( + width: emote.size == EmoteSize.small ? 20 : 50, + height: emote.size == EmoteSize.small ? 20 : 50, + child: RepaintBoundary( + key: ValueKey('emote:${emote.url}'), + child: CachedNetworkImage( + cacheWidth: 200, + cacheHeight: 200, + cacheManager: CacheManager(Config(CacheKeys.emoteKey)), + imageUrl: emote.url, + ), + )), + )); + } else { + spans.add(TextSpan(text: matched)); + } + return matched; + }, onNonMatch: (noMatch) { + //匹配到不是[]的位置时,不是表情 + spans.add(TextSpan(text: noMatch)); + return noMatch; + }); + + return TextSpan(children: spans); + } + @override Widget build(BuildContext context) { return Card( @@ -25,7 +93,7 @@ class DynamicItemCard extends StatelessWidget { padding: const EdgeInsets.only(right: 8), child: AvatarWidget( radius: 45 / 2, - avatarUrl: dynamicItem.author.avatarUrl, + avatarUrl: widget.dynamicItem.author.avatarUrl, onPressed: () { // Get.to(() => UserSpacePage( // key: @@ -34,8 +102,8 @@ class DynamicItemCard extends StatelessWidget { Navigator.of(context).push(GetPageRoute( page: () => UserSpacePage( key: ValueKey( - 'UserSpacePage:${dynamicItem.author.mid}'), - mid: dynamicItem.author.mid))); + 'UserSpacePage:${widget.dynamicItem.author.mid}'), + mid: widget.dynamicItem.author.mid))); }, cacheWidthHeight: 100, ), @@ -45,7 +113,7 @@ class DynamicItemCard extends StatelessWidget { children: [ Text( //发动态的作者名字 - dynamicItem.author.name, + widget.dynamicItem.author.name, style: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 15, @@ -53,7 +121,7 @@ class DynamicItemCard extends StatelessWidget { ), //发布时间,动作 Text( - "${dynamicItem.author.pubTime} ${dynamicItem.author.pubAction}", + "${widget.dynamicItem.author.pubTime} ${widget.dynamicItem.author.pubAction}", style: TextStyle( color: Theme.of(context).hintColor, fontSize: 12), ) @@ -63,9 +131,68 @@ class DynamicItemCard extends StatelessWidget { Padding( padding: const EdgeInsets.only( top: 14.0, left: 8, right: 8, bottom: 8), //动态信息 - child: Text( - dynamicItem.content.description, - style: const TextStyle(fontSize: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ///动态文字消息 + widget.dynamicItem.content.emotes.isNotEmpty + ? SelectableText.rich( + buildEmojiText(widget.dynamicItem.content, context)) + : FoldableText.rich( + buildEmojiText(widget.dynamicItem.content, context), + maxLines: 6, + folderTextStyle: + TextStyle(color: Theme.of(context).primaryColor), + style: const TextStyle(fontSize: 15), + ), + + ///视频 + if (widget.dynamicItem.content is AVDynamicContent) + DynamicVideoCard( + content: widget.dynamicItem.content as AVDynamicContent, + cacheManager: videoCoverCacheManager, + ), + + ///分享,评论,点赞 + GridView.count( + padding: const EdgeInsets.only(top: 16), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + crossAxisCount: 3, + childAspectRatio: 16 / 9, + crossAxisSpacing: 10, + children: [ + IconTextButton( + onPressed: () { + Get.rawSnackbar(message: '功能暂未完成'); + }, + icon: const Icon(Icons.arrow_outward_outlined), + text: Text(StringFormatUtils.numFormat( + widget.dynamicItem.stat.shareCount))), + IconTextButton( + onPressed: () { + Navigator.of(context).push(GetPageRoute( + page: () => Scaffold( + appBar: AppBar(title: const Text("评论")), + body: ReplyPage( + replyId: widget.dynamicItem.replyId, + replyType: widget.dynamicItem.replyType, + ), + ))); + }, + icon: const Icon(Icons.message_outlined), + text: Text(StringFormatUtils.numFormat( + widget.dynamicItem.stat.replyCount))), + IconTextButton( + onPressed: () { + Get.rawSnackbar(message: '功能暂未完成'); + }, + icon: const Icon(Icons.thumb_up_outlined), + text: Text(StringFormatUtils.numFormat( + widget.dynamicItem.stat.likeCount))) + ], + ) + ], ), ) ], @@ -74,3 +201,121 @@ class DynamicItemCard extends StatelessWidget { ); } } + +class DynamicVideoCard extends StatelessWidget { + const DynamicVideoCard( + {super.key, required this.content, required this.cacheManager}); + final AVDynamicContent content; + final playInfoTextStyle = const TextStyle( + color: Colors.white, fontSize: 12, overflow: TextOverflow.ellipsis); + final CacheManager cacheManager; + + @override + Widget build(BuildContext context) { + int cid = 0; + return FutureBuilder(future: Future(() async { + cid = (await VideoInfoApi.getVideoParts(bvid: content.bvid)).first.cid; + }), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return GestureDetector( + onTap: () => Navigator.of(context).push(GetPageRoute( + page: () => BiliVideoPage(bvid: content.bvid, cid: cid), + )), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + alignment: Alignment.bottomLeft, + children: [ + AspectRatio( + aspectRatio: 16 / 9, + child: LayoutBuilder(builder: (context, boxConstraints) { + return Hero( + tag: "BiliVideoPlayer:${content.bvid}", + transitionOnUserGestures: true, + child: CachedNetworkImage( + cacheWidth: (boxConstraints.maxWidth * + MediaQuery.of(context).devicePixelRatio) + .toInt(), + cacheHeight: (boxConstraints.maxHeight * + MediaQuery.of(context).devicePixelRatio) + .toInt(), + cacheManager: cacheManager, + fit: BoxFit.cover, + imageUrl: content.picUrl, + placeholder: () => Container( + color: Theme.of(context) + .colorScheme + .surfaceVariant, + ), + errorWidget: () => const Center( + child: Icon(Icons.error), + ), + filterQuality: FilterQuality.none, + )); + }), + ), + Container( + decoration: const BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black38, + blurRadius: 12, + spreadRadius: 10, + offset: Offset(0, 12)), + ], + ), + padding: + const EdgeInsets.only(left: 6, right: 6, bottom: 3), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.slideshow_rounded, + color: Colors.white, + size: 14, + ), + Text( + " ${content.playCount} ", + maxLines: 1, + style: playInfoTextStyle, + ), + const Icon( + Icons.format_list_bulleted_rounded, + color: Colors.white, + size: 14, + ), + Text( + " ${content.damakuCount}", + maxLines: 1, + style: playInfoTextStyle, + ), + const Spacer(), + Text( + content.duration, + maxLines: 1, + style: playInfoTextStyle, + ), + ]), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + content.title, + maxLines: 2, + ), + ) + ], + ), + ); + } else { + return const SizedBox(); + } + }); + } +} diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 1c2af63a..6279e422 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -1,8 +1,10 @@ import 'package:bili_you/common/utils/bili_you_storage.dart'; import 'package:bili_you/common/utils/settings.dart'; +import 'package:bili_you/pages/dynamic/view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import '../dynamic/controller.dart'; import 'index.dart'; class MainPage extends StatefulWidget { @@ -49,6 +51,10 @@ class _MainPageState extends State { ], selectedIndex: controller.selectedIndex.value, onDestinationSelected: (value) { + if (value == controller.selectedIndex.value && + controller.pages[value] is DynamicPage) { + Get.find().animateToTop(); + } controller.selectedIndex.value = value; }, ), diff --git a/lib/pages/ui_test/controller.dart b/lib/pages/ui_test/controller.dart index fddefce0..f2ca3c86 100644 --- a/lib/pages/ui_test/controller.dart +++ b/lib/pages/ui_test/controller.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:bili_you/common/api/video_operation_api.dart'; +import 'package:bili_you/common/models/local/reply/reply_item.dart'; import 'package:bili_you/pages/about/about_page.dart'; import 'package:bili_you/pages/bili_video/widgets/bili_video_player/bili_danmaku.dart'; import 'package:bili_you/pages/bili_video/widgets/bili_video_player/bili_video_player_panel.dart'; @@ -53,7 +54,8 @@ class UiTestController extends GetxController { void onInit() { _testPages = { "评论测试": ReplyPage( - bvid: "170001", + replyId: "170001", + replyType: ReplyType.video, pauseVideoCallback: () {}, ), "许可": const LicensePage(