import 'dart:async'; import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/id_utils.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'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/video/detail/index.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/danmu.dart'; import 'package:PiliPlus/common/widgets/stat/view.dart'; import 'package:PiliPlus/models/video_detail_res.dart'; import 'package:PiliPlus/pages/video/detail/introduction/controller.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'widgets/action_item.dart'; import 'widgets/action_row_item.dart'; import 'widgets/fav_panel.dart'; import 'widgets/page.dart'; import 'widgets/season.dart'; class VideoIntroPanel extends StatefulWidget { const VideoIntroPanel({ super.key, required this.heroTag, required this.showAiBottomSheet, required this.showIntroDetail, required this.showEpisodes, required this.onShowMemberPage, }); final String heroTag; final Function showAiBottomSheet; final Function showIntroDetail; final Function showEpisodes; final ValueChanged onShowMemberPage; @override State createState() => _VideoIntroPanelState(); } class _VideoIntroPanelState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { late VideoIntroController videoIntroController; // 添加页面缓存 @override bool get wantKeepAlive => true; @override void initState() { super.initState(); videoIntroController = Get.put(VideoIntroController(), tag: widget.heroTag) ..heroTag = widget.heroTag; } @override Widget build(BuildContext context) { super.build(context); return Obx( () => videoIntroController.videoDetail.value.title == null ? VideoInfo( loadingStatus: true, videoIntroController: videoIntroController, heroTag: widget.heroTag, showAiBottomSheet: widget.showAiBottomSheet, showIntroDetail: () => widget.showIntroDetail( videoIntroController.videoDetail.value, videoIntroController.videoTags, ), showEpisodes: widget.showEpisodes, onShowMemberPage: widget.onShowMemberPage, ) : VideoInfo( key: ValueKey(widget.heroTag), loadingStatus: false, videoIntroController: videoIntroController, heroTag: widget.heroTag, showAiBottomSheet: widget.showAiBottomSheet, showIntroDetail: () => widget.showIntroDetail( videoIntroController.videoDetail.value, videoIntroController.videoTags, ), showEpisodes: widget.showEpisodes, onShowMemberPage: widget.onShowMemberPage, ), ); } } class VideoInfo extends StatefulWidget { final bool loadingStatus; final String heroTag; final Function showAiBottomSheet; final Function showIntroDetail; final Function showEpisodes; final ValueChanged onShowMemberPage; final VideoIntroController videoIntroController; const VideoInfo({ super.key, this.loadingStatus = false, required this.heroTag, required this.showAiBottomSheet, required this.showIntroDetail, required this.showEpisodes, required this.onShowMemberPage, required this.videoIntroController, }); @override State createState() => _VideoInfoState(); } class _VideoInfoState extends State with TickerProviderStateMixin { late final VideoDetailController videoDetailCtr; Map get videoItem => videoIntroController.videoItem; VideoIntroController get videoIntroController => widget.videoIntroController; VideoDetailData get videoDetail => widget.videoIntroController.videoDetail.value; late final _coinKey = GlobalKey(); late final _favKey = GlobalKey(); bool isProcessing = false; late final _horizontalMemberPage = GStorage.horizontalMemberPage; Widget _buildVideoTitle([bool isExpand = false]) => videoDetailCtr .enableSponsorBlock ? Obx( () => Text.rich( TextSpan( children: [ if (videoDetailCtr.videoLabel.value.isNotEmpty) ...[ WidgetSpan( alignment: PlaceholderAlignment.middle, child: Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Stack( alignment: Alignment.center, children: [ Icon( Icons.shield_outlined, size: MediaQuery.textScalerOf(context).scale(16), color: Theme.of(context) .colorScheme .onSecondaryContainer, ), Icon( Icons.play_arrow_rounded, size: MediaQuery.textScalerOf(context).scale(12), color: Theme.of(context) .colorScheme .onSecondaryContainer, ), ], ), Text( videoDetailCtr.videoLabel.value, strutStyle: StrutStyle(leading: 0, height: 1), style: TextStyle( height: 1, fontSize: 13, color: Theme.of(context) .colorScheme .onSecondaryContainer, ), ), ], ), ), ), TextSpan(text: ' '), ], TextSpan( text: '${videoDetail.title ?? videoItem['title'] ?? ''}'), ], ), maxLines: isExpand ? null : 2, overflow: isExpand ? null : TextOverflow.ellipsis, style: const TextStyle(fontSize: 16), ), ) : Text( '${videoDetail.title ?? videoItem['title'] ?? ''}', maxLines: isExpand ? null : 2, overflow: isExpand ? null : TextOverflow.ellipsis, style: const TextStyle(fontSize: 16), ); void handleState(Future Function() action) async { if (isProcessing.not) { isProcessing = true; await action(); isProcessing = false; } } @override void initState() { super.initState(); videoDetailCtr = Get.find(tag: widget.heroTag); if (videoIntroController.expandableCtr == null) { bool alwaysExapndIntroPanel = GStorage.alwaysExapndIntroPanel; videoIntroController.expandableCtr = ExpandableController( initialExpanded: alwaysExapndIntroPanel, ); if (alwaysExapndIntroPanel.not && GStorage.exapndIntroPanelH) { WidgetsBinding.instance.addPostFrameCallback((_) { if (context.orientation == Orientation.landscape && videoIntroController.expandableCtr?.expanded == false) { videoIntroController.expandableCtr?.toggle(); } }); } } } void _showFavBottomSheet() => showModalBottomSheet( context: context, useSafeArea: true, isScrollControlled: true, transitionAnimationController: AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ), sheetAnimationStyle: AnimationStyle(curve: Curves.ease), builder: (BuildContext context) { return DraggableScrollableSheet( minChildSize: 0, maxChildSize: 1, initialChildSize: 0.7, snap: true, expand: false, snapSizes: const [0.7], builder: (BuildContext context, ScrollController scrollController) { return FavPanel( ctr: videoIntroController, scrollController: scrollController, ); }, ); }, ); // 收藏 showFavBottomSheet({type = 'tap'}) { if (videoIntroController.userInfo == null) { SmartDialog.showToast('账号未登录'); return; } final bool enableDragQuickFav = GStorage.setting.get(SettingBoxKey.enableQuickFav, defaultValue: false); // 快速收藏 & // 点按 收藏至默认文件夹 // 长按选择文件夹 if (enableDragQuickFav) { if (type == 'tap') { videoIntroController.actionFavVideo(type: 'default'); } else { _showFavBottomSheet(); } } else if (type != 'longPress') { _showFavBottomSheet(); } } // 视频介绍 showIntroDetail() { if (widget.loadingStatus) { return; } feedBack(); // widget.showIntroDetail(); videoIntroController.expandableCtr?.toggle(); } // 用户主页 onPushMember() { feedBack(); int? mid = !widget.loadingStatus ? videoDetail.owner?.mid : videoItem['owner']?.mid; if (mid != null) { if (context.orientation == Orientation.landscape && _horizontalMemberPage) { widget.onShowMemberPage(mid); } else { // memberHeroTag = Utils.makeHeroTag(mid); // String face = !loadingStatus // ? videoDetail.owner!.face // : videoItem['owner'].face; Get.toNamed( '/member?mid=$mid', // arguments: { // 'face': face, // 'heroTag': memberHeroTag, // }, ); } } } @override Widget build(BuildContext context) { final ThemeData t = Theme.of(context); return SliverLayoutBuilder( builder: (BuildContext context, SliverConstraints constraints) { bool isHorizontal = constraints.crossAxisExtent > constraints.viewportMainAxisExtent * 1.25; return SliverPadding( padding: const EdgeInsets.only( left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10), sliver: SliverToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: videoItem['staff'] == null ? GestureDetector( onTap: onPushMember, child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ NetworkImgLayer( type: 'avatar', src: widget.loadingStatus ? videoItem['owner']?.face ?? "" : videoDetail.owner!.face, width: 35, height: 35, fadeInDuration: Duration.zero, fadeOutDuration: Duration.zero, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.loadingStatus ? videoItem['owner']?.name ?? "" : videoDetail.owner!.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, // color: t.colorScheme.primary, ), // semanticsLabel: "UP主:${owner.name}", ), const SizedBox(height: 0), Obx( () => Text( '${Utils.numFormat(videoIntroController.userStat.value['follower'])}粉丝', style: TextStyle( fontSize: 12, color: t.colorScheme.outline, ), ), ), ], ), ), followButton(context, t), ], ), ) : SelfSizedHorizontalList( gapSize: 25, itemCount: videoItem['staff'].length, childBuilder: (index) => GestureDetector( onTap: () { int? ownerMid = !widget.loadingStatus ? videoDetail.owner?.mid : videoItem['owner']?.mid; if (videoItem['staff'][index].mid == ownerMid && context.orientation == Orientation.landscape && _horizontalMemberPage) { widget.onShowMemberPage(ownerMid); } else { Get.toNamed( '/member?mid=${videoItem['staff'][index].mid}', // arguments: { // 'face': // videoItem['staff'][index].face, // 'heroTag': Utils.makeHeroTag( // videoItem['staff'][index].mid), // }, ); } }, child: Row( children: [ NetworkImgLayer( type: 'avatar', src: videoItem['staff'][index].face, width: 35, height: 35, fadeInDuration: Duration.zero, fadeOutDuration: Duration.zero, ), const SizedBox(width: 5), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( videoItem['staff'][index].name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, color: videoItem['staff'][index] .vip .status > 0 && videoItem['staff'][index] .vip .type == 2 ? context.vipColor : null, ), ), Text( videoItem['staff'][index].title, style: TextStyle( fontSize: 12, color: Theme.of(context) .colorScheme .outline, ), ), ], ), ], ), ), ), ), if (isHorizontal) ...[ const SizedBox(width: 10), Expanded(child: actionGrid(context, videoIntroController)), ] ], ), if (videoIntroController.videoDetail.value.argueMsg?.isNotEmpty == true && videoIntroController.showArgueMsg) ...[ const SizedBox(height: 8), Container( width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: Theme.of(context).colorScheme.secondaryContainer, ), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), child: Text.rich( TextSpan(children: [ WidgetSpan( alignment: PlaceholderAlignment.middle, child: Icon( size: 17, Icons.warning_rounded, color: Theme.of(context) .colorScheme .onSecondaryContainer, ), ), TextSpan( text: ' ${videoIntroController.videoDetail.value.argueMsg}') ]), style: TextStyle( fontSize: 13, color: Theme.of(context).colorScheme.onSecondaryContainer, ), ), ) ], const SizedBox(height: 8), GestureDetector( behavior: HitTestBehavior.translucent, onTap: showIntroDetail, child: ExpandablePanel( controller: videoIntroController.expandableCtr, collapsed: GestureDetector( onLongPress: () { feedBack(); Utils.copyText( '${videoDetail.title ?? videoItem['title'] ?? ''}'); }, child: _buildVideoTitle(), ), expanded: GestureDetector( onLongPress: () { feedBack(); Utils.copyText( '${videoDetail.title ?? videoItem['title'] ?? ''}'); }, child: _buildVideoTitle(true), ), 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: Row( children: [ statView( context: context, theme: 'gray', view: !widget.loadingStatus ? videoDetail.stat?.view ?? '-' : videoItem['stat']?.view ?? '-', size: 'medium', ), const SizedBox(width: 10), statDanMu( context: context, theme: 'gray', danmu: !widget.loadingStatus ? videoDetail.stat?.danmu ?? '-' : videoItem['stat']?.danmu ?? '-', size: 'medium', ), const SizedBox(width: 10), Text( Utils.dateFormat( !widget.loadingStatus ? videoDetail.pubdate : videoItem['pubdate'], formatType: 'detail'), style: TextStyle( fontSize: 12, color: t.colorScheme.outline, ), ), if (MineController.anonymity.value) ...[ 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, ), ), ), ], ), ), if (videoIntroController.enableAi) Positioned( right: 10, top: 0, bottom: 0, child: Center( child: Semantics( label: 'AI总结', child: GestureDetector( onTap: () async { final res = await videoIntroController.aiConclusion(); if (res['status']) { widget.showAiBottomSheet(); } }, child: Image.asset('assets/images/ai.png', height: 22), ), ), ), ) ], ), GestureDetector( behavior: HitTestBehavior.translucent, onTap: showIntroDetail, child: ExpandablePanel( controller: videoIntroController.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, text: item['tag_name'], onTap: (_) => Get.toNamed('/searchResult', parameters: { 'keyword': item['tag_name'] }), onLongPress: (_) => 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, ), ), ), Obx( () => videoIntroController.queryVideoIntroData.value["status"] ? const SizedBox.shrink() : Center( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: () { videoIntroController .queryVideoIntroData.value["status"] = true; videoIntroController.queryVideoIntro(); if (videoDetailCtr.videoUrl.isNullOrEmpty && videoDetailCtr.isQuerying.not) { videoDetailCtr.queryVideoUrl(); } }, label: const Text("点此重新加载"), ), ), ), // 点赞收藏转发 布局样式1 // SingleChildScrollView( // padding: const EdgeInsets.only(top: 7, bottom: 7), // scrollDirection: Axis.horizontal, // child: actionRow( // context, // videoIntroController, // videoDetailCtr, // ), // ), // 点赞收藏转发 布局样式2 if (!isHorizontal) ...[ const SizedBox(height: 8), actionGrid(context, videoIntroController), ], // 合集 if (!widget.loadingStatus && videoDetail.ugcSeason != null && (context.orientation != Orientation.landscape || (context.orientation == Orientation.landscape && videoDetailCtr.horizontalSeasonPanel.not))) SeasonPanel( heroTag: widget.heroTag, ugcSeason: videoDetail.ugcSeason!, changeFuc: videoIntroController.changeSeasonOrbangu, showEpisodes: widget.showEpisodes, pages: videoDetail.pages, videoIntroController: videoIntroController, ), if (!widget.loadingStatus && videoDetail.pages != null && videoDetail.pages!.length > 1 && (context.orientation != Orientation.landscape || (context.orientation == Orientation.landscape && videoDetailCtr.horizontalSeasonPanel.not))) ...[ PagesPanel( heroTag: widget.heroTag, videoIntroController: videoIntroController, bvid: videoIntroController.bvid, changeFuc: videoIntroController.changeSeasonOrbangu, showEpisodes: widget.showEpisodes, ), ], ], )), ); }, ); } Obx followButton(BuildContext context, ThemeData t) { return Obx( () { int attr = videoIntroController.followStatus['attribute'] ?? 0; return TextButton( onPressed: () => videoIntroController.actionRelationMod(context), style: TextButton.styleFrom( visualDensity: const VisualDensity(vertical: -3), foregroundColor: attr != 0 ? t.colorScheme.outline : t.colorScheme.onSecondaryContainer, backgroundColor: attr != 0 ? t.colorScheme.onInverseSurface : t.colorScheme.secondaryContainer, ), child: Text( attr == 128 ? '已拉黑' : attr != 0 ? '已关注' : '关注', style: TextStyle(fontSize: t.textTheme.labelMedium!.fontSize), ), ); }, ); } Widget actionGrid(BuildContext context, videoIntroController) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( margin: const EdgeInsets.only(top: 1), height: 48, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Obx( () => ActionItem( icon: const Icon(FontAwesomeIcons.thumbsUp), selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), onTap: () => handleState(videoIntroController.actionLikeVideo), onLongPress: () => handleState(videoIntroController.actionOneThree), selectStatus: videoIntroController.hasLike.value, loadingStatus: widget.loadingStatus, semanticsLabel: '点赞', text: !widget.loadingStatus ? Utils.numFormat(videoDetail.stat!.like!) : '-', needAnim: true, hasOneThree: videoIntroController.hasLike.value && videoIntroController.hasCoin.value && videoIntroController.hasFav.value, callBack: (start) { if (start) { _coinKey.currentState?.controller?.forward(); _favKey.currentState?.controller?.forward(); } else { _coinKey.currentState?.controller?.reverse(); _favKey.currentState?.controller?.reverse(); } }, ), ), Obx( () => ActionItem( icon: const Icon(FontAwesomeIcons.thumbsDown), selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown), onTap: () => handleState(videoIntroController.actionDislikeVideo), selectStatus: videoIntroController.hasDislike.value, loadingStatus: widget.loadingStatus, semanticsLabel: '点踩', text: "点踩"), ), // ActionItem( // icon: const Icon(FontAwesomeIcons.clock), // onTap: () => videoIntroController.actionShareVideo(), // selectStatus: false, // loadingStatus: loadingStatus, // text: '稍后再看'), Obx( () => ActionItem( key: _coinKey, icon: const Icon(FontAwesomeIcons.b), selectIcon: const Icon(FontAwesomeIcons.b), onTap: () => handleState(videoIntroController.actionCoinVideo), selectStatus: videoIntroController.hasCoin.value, loadingStatus: widget.loadingStatus, semanticsLabel: '投币', text: !widget.loadingStatus ? Utils.numFormat(videoDetail.stat!.coin!) : '-', needAnim: true, ), ), Obx( () => ActionItem( key: _favKey, icon: const Icon(FontAwesomeIcons.star), selectIcon: const Icon(FontAwesomeIcons.solidStar), onTap: () => showFavBottomSheet(), onLongPress: () => showFavBottomSheet(type: 'longPress'), selectStatus: videoIntroController.hasFav.value, loadingStatus: widget.loadingStatus, semanticsLabel: '收藏', text: !widget.loadingStatus ? Utils.numFormat(videoDetail.stat!.favorite!) : '-', needAnim: true, ), ), ActionItem( icon: const Icon(FontAwesomeIcons.comment), onTap: () => videoDetailCtr.tabCtr .animateTo(videoDetailCtr.tabCtr.index == 1 ? 0 : 1), selectStatus: false, loadingStatus: widget.loadingStatus, semanticsLabel: '评论', text: !widget.loadingStatus ? Utils.numFormat(videoDetail.stat!.reply!) : '评论'), ActionItem( icon: const Icon(FontAwesomeIcons.shareFromSquare), onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, loadingStatus: widget.loadingStatus, semanticsLabel: '分享', text: !widget.loadingStatus ? Utils.numFormat(videoDetail.stat!.share!) : '分享'), ], ), ); }); } Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) { return Row(children: [ Obx( () => ActionRowItem( icon: const Icon(FontAwesomeIcons.thumbsUp), onTap: () => handleState(videoIntroController.actionLikeVideo), selectStatus: videoIntroController.hasLike.value, loadingStatus: widget.loadingStatus, text: !widget.loadingStatus ? videoDetail.stat!.like!.toString() : '-', ), ), const SizedBox(width: 8), Obx( () => ActionRowItem( icon: const Icon(FontAwesomeIcons.b), onTap: () => handleState(videoIntroController.actionCoinVideo), selectStatus: videoIntroController.hasCoin.value, loadingStatus: widget.loadingStatus, text: !widget.loadingStatus ? videoDetail.stat!.coin!.toString() : '-', ), ), const SizedBox(width: 8), Obx( () => ActionRowItem( icon: const Icon(FontAwesomeIcons.heart), onTap: () => showFavBottomSheet(), onLongPress: () => showFavBottomSheet(type: 'longPress'), selectStatus: videoIntroController.hasFav.value, loadingStatus: widget.loadingStatus, text: !widget.loadingStatus ? videoDetail.stat!.favorite!.toString() : '-', ), ), const SizedBox(width: 8), ActionRowItem( icon: const Icon(FontAwesomeIcons.comment), onTap: () { videoDetailCtr.tabCtr.animateTo(1); }, selectStatus: false, loadingStatus: widget.loadingStatus, text: !widget.loadingStatus ? videoDetail.stat!.reply!.toString() : '-', ), const SizedBox(width: 8), ActionRowItem( icon: const Icon(FontAwesomeIcons.share), onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, loadingStatus: widget.loadingStatus, // text: !loadingStatus // ? videoDetail.stat!.share!.toString() // : '-', 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|av\d+|bv\S+\b', caseSensitive: false); (currentDesc.rawText as String).splitMapJoin( urlRegExp, onMatch: (Match match) { String matchStr = match[0]!; if (RegExp(r'^av\d+$', caseSensitive: false).hasMatch(matchStr)) { try { // validate int aid = int.parse(matchStr.substring(2)); IdUtils.av2bv(aid); spanChildren.add( TextSpan( text: matchStr, style: TextStyle( color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer() ..onTap = () { PiliScheme.videoPush(aid, null); }, ), ); } catch (e) { spanChildren.add(TextSpan(text: matchStr)); } } else if (RegExp(r'^bv\S+\b$', caseSensitive: false) .hasMatch(matchStr)) { try { // validate IdUtils.bv2av(matchStr); spanChildren.add( TextSpan( text: matchStr, style: TextStyle( color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer() ..onTap = () { PiliScheme.videoPush(null, matchStr); }, ), ); } catch (e) { spanChildren.add(TextSpan(text: matchStr)); } } else { spanChildren.add( TextSpan( text: matchStr, style: TextStyle(color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer() ..onTap = () { try { Get.toNamed( '/webview', parameters: {'url': matchStr}, ); } catch (err) { SmartDialog.showToast(err.toString()); } }, ), ); } return ''; }, onNonMatch: (String nonMatchStr) { spanChildren.add(TextSpan(text: nonMatchStr)); return ''; }, ); return TextSpan(children: spanChildren); 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); } }