diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 226d460d..da971973 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -63,7 +63,6 @@ class BangumiIntroController extends CommonController { @override void onInit() { super.onInit(); - queryVideoTags(); if (Get.arguments.isNotEmpty as bool) { if (Get.arguments.containsKey('bangumiItem') as bool) { preRender = true; @@ -105,6 +104,12 @@ class BangumiIntroController extends CommonController { } } + @override + Future queryData([bool isRefresh = true]) async { + await queryVideoTags(); + return super.queryData(isRefresh); + } + Future queryVideoTags() async { var result = await UserHttp.videoTags(bvid: bvid); if (result['status']) { diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 7457858f..3b496f43 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -124,7 +124,7 @@ class VideoIntroController extends GetxController // 获取视频简介&分p void queryVideoIntro() async { - queryVideoTags(); + await queryVideoTags(); var result = await VideoHttp.videoIntro(bvid: bvid); if (result['status']) { videoDetail.value = result['data']!; diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index bb4d5d4d..5594704c 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,4 +1,8 @@ import 'package:PiliPalaX/common/widgets/self_sized_horizontal_list.dart'; +import 'package:PiliPalaX/pages/search/widgets/search_text.dart'; +import 'package:PiliPalaX/utils/extension.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -154,6 +158,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { late final _coinKey = GlobalKey(); late final _favKey = GlobalKey(); + late final _expandableCtr = ExpandableController(initialExpanded: false); @override void initState() { @@ -167,6 +172,12 @@ class _VideoInfoState extends State with TickerProviderStateMixin { enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); } + @override + void dispose() { + _expandableCtr.dispose(); + super.dispose(); + } + void _showFavBottomSheet() => showModalBottomSheet( context: context, useSafeArea: true, @@ -222,7 +233,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { return; } feedBack(); - widget.showIntroDetail(); + // widget.showIntroDetail(); + _expandableCtr.toggle(); } // 用户主页 @@ -382,86 +394,104 @@ class _VideoInfoState extends State with TickerProviderStateMixin { const SizedBox(height: 8), GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () => showIntroDetail(), - child: Row(children: [ - Expanded( - child: Text( - widget.videoDetail?.title ?? videoItem['title'] ?? "", - // !loadingStatus - // ? "${widget.videoDetail?.title}" - // : videoItem['title'] ?? "", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + onTap: showIntroDetail, + child: ExpandablePanel( + controller: _expandableCtr, + collapsed: GestureDetector( + onLongPress: () { + feedBack(); + Utils.copyText( + '${widget.videoDetail?.title ?? videoItem['title'] ?? ''}'); + }, + child: Text( + '${widget.videoDetail?.title ?? videoItem['title'] ?? ''}', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - )), - Icon( - Icons.arrow_forward_ios, - size: 16, - color: t.colorScheme.outline, ), - ]), + expanded: GestureDetector( + onLongPress: () { + feedBack(); + Utils.copyText( + '${widget.videoDetail?.title ?? videoItem['title'] ?? ''}'); + }, + child: Text( + widget.videoDetail?.title ?? videoItem['title'] ?? '', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + theme: const ExpandableThemeData( + animationDuration: Duration(milliseconds: 300), + scrollAnimationDuration: Duration(milliseconds: 300), + crossFadePoint: 0, + fadeCurve: Curves.ease, + sizeCurve: Curves.linear, + ), + ), ), + const SizedBox(height: 8), Stack( children: [ GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () => showIntroDetail(), - child: Padding( - padding: const EdgeInsets.only(top: 7, bottom: 6), - child: Row( - children: [ - StatView( - theme: 'gray', - view: !loadingStatus - ? widget.videoDetail?.stat?.view ?? '-' - : videoItem['stat']?.view ?? '-', - size: 'medium', + onTap: showIntroDetail, + child: Row( + children: [ + StatView( + theme: 'gray', + view: !loadingStatus + ? widget.videoDetail?.stat?.view ?? '-' + : videoItem['stat']?.view ?? '-', + size: 'medium', + ), + const SizedBox(width: 10), + StatDanMu( + theme: 'gray', + danmu: !loadingStatus + ? widget.videoDetail?.stat?.danmu ?? '-' + : videoItem['stat']?.danmu ?? '-', + size: 'medium', + ), + const SizedBox(width: 10), + Text( + Utils.dateFormat( + !loadingStatus + ? widget.videoDetail?.pubdate + : videoItem['pubdate'], + formatType: 'detail'), + style: TextStyle( + fontSize: 12, + color: t.colorScheme.outline, ), + ), + if (MineController.anonymity) ...[ const SizedBox(width: 10), - StatDanMu( - theme: 'gray', - danmu: !loadingStatus - ? widget.videoDetail?.stat?.danmu ?? '-' - : videoItem['stat']?.danmu ?? '-', - size: 'medium', + Icon( + MdiIcons.incognito, + size: 15, + color: t.colorScheme.outline, + semanticLabel: '无痕', ), - const SizedBox(width: 10), - Text( - Utils.dateFormat( - !loadingStatus - ? widget.videoDetail?.pubdate - : videoItem['pubdate'], - formatType: 'detail'), - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, - ), - ), - if (MineController.anonymity) ...[ - const SizedBox(width: 10), - Icon( - MdiIcons.incognito, - size: 15, - color: t.colorScheme.outline, - semanticLabel: '无痕', - ), - ], - const SizedBox(width: 10), - if (videoIntroController.isShowOnlineTotal) - Obx( - () => Text( - '${videoIntroController.total.value}人在看', - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, - ), + ], + const SizedBox(width: 10), + if (videoIntroController.isShowOnlineTotal) + Obx( + () => Text( + '${videoIntroController.total.value}人在看', + style: TextStyle( + fontSize: 12, + color: t.colorScheme.outline, ), ), - ], - ), + ), + ], ), ), if (enableAi) @@ -488,7 +518,80 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ) ], ), - + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: showIntroDetail, + child: ExpandablePanel( + controller: _expandableCtr, + collapsed: const SizedBox.shrink(), + expanded: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + GestureDetector( + onTap: () { + Utils.copyText( + '${videoIntroController.videoDetail.value.bvid}'); + }, + child: Text( + '${videoIntroController.videoDetail.value.bvid}', + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + if (videoIntroController + .videoDetail.value.descV2.isNullOrEmpty.not) ...[ + const SizedBox(height: 8), + SelectableText.rich( + style: const TextStyle( + height: 1.4, + // fontSize: 13, + ), + TextSpan( + children: [ + buildContent(context, + videoIntroController.videoDetail.value), + ], + ), + ), + ], + if (videoIntroController.videoTags is List && + videoIntroController.videoTags.isNotEmpty) ...[ + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: (videoIntroController.videoTags as List) + .map( + (item) => SearchText( + fontSize: 13, + searchText: item['tag_name'], + onSelect: (_) => Get.toNamed('/searchResult', + parameters: { + 'keyword': item['tag_name'] + }), + onLongSelect: (_) => + Utils.copyText(item['tag_name']), + ), + ) + .toList(), + ), + ], + ], + ), + theme: const ExpandableThemeData( + animationDuration: Duration(milliseconds: 300), + scrollAnimationDuration: Duration(milliseconds: 300), + crossFadePoint: 0, + fadeCurve: Curves.ease, + sizeCurve: Curves.linear, + ), + ), + ), + const SizedBox(height: 8), Obx( () => videoIntroController.queryVideoIntroData.value["status"] ? const SizedBox() @@ -748,4 +851,80 @@ class _VideoInfoState extends State with TickerProviderStateMixin { text: '转发'), ]); } + + InlineSpan buildContent(BuildContext context, VideoDetailData content) { + final List descV2 = content.descV2!; + // type + // 1 普通文本 + // 2 @用户 + final List spanChildren = List.generate(descV2.length, (index) { + final currentDesc = descV2[index]; + switch (currentDesc.type) { + case 1: + final List spanChildren = []; + final RegExp urlRegExp = RegExp(r'https?://\S+\b'); + final Iterable matches = + urlRegExp.allMatches(currentDesc.rawText); + + int previousEndIndex = 0; + for (final Match match in matches) { + if (match.start > previousEndIndex) { + spanChildren.add(TextSpan( + text: currentDesc.rawText + .substring(previousEndIndex, match.start))); + } + spanChildren.add( + TextSpan( + text: match.group(0), + style: TextStyle( + color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色 + recognizer: TapGestureRecognizer() + ..onTap = () { + // 处理点击事件 + try { + Get.toNamed( + '/webviewnew', + parameters: { + 'url': match.group(0)!, + 'type': 'url', + 'pageTitle': match.group(0)!, + }, + ); + } catch (err) { + SmartDialog.showToast(err.toString()); + } + }, + ), + ); + previousEndIndex = match.end; + } + + if (previousEndIndex < currentDesc.rawText.length) { + spanChildren.add(TextSpan( + text: currentDesc.rawText.substring(previousEndIndex))); + } + + final TextSpan result = TextSpan(children: spanChildren); + return result; + case 2: + final Color colorSchemePrimary = + Theme.of(context).colorScheme.primary; + final String heroTag = Utils.makeHeroTag(currentDesc.bizId); + return TextSpan( + text: '@${currentDesc.rawText}', + style: TextStyle(color: colorSchemePrimary), + recognizer: TapGestureRecognizer() + ..onTap = () { + Get.toNamed( + '/member?mid=${currentDesc.bizId}', + arguments: {'face': '', 'heroTag': heroTag}, + ); + }, + ); + default: + return const TextSpan(); + } + }); + return TextSpan(children: spanChildren); + } } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index c4026c75..e9ec1a36 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1273,6 +1273,9 @@ class _VideoDetailPageState extends State showIntroDetail: showIntroDetail, ), ), + SliverToBoxAdapter( + child: SizedBox(height: MediaQuery.paddingOf(context).bottom), + ) ], ); } diff --git a/pubspec.lock b/pubspec.lock index 52f1bfee..e477322f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -480,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + expandable: + dependency: "direct main" + description: + name: expandable + sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" + url: "https://pub.dev" + source: hosted + version: "5.0.1" extended_image: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b6e4b340..c845ba94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -168,6 +168,7 @@ dependencies: image_cropper: ^8.0.2 #解压直播消息 brotli: ^0.6.0 + expandable: ^5.0.1 dependency_overrides: screen_brightness: ^2.0.0+2