From 1a9d8e35bae5658799abb0105eba67961b560ae6 Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Sat, 25 Oct 2025 14:54:21 +0800 Subject: [PATCH] feat: show aiConclusion (#1698) --- lib/common/widgets/video_popup_menu.dart | 71 ++++-- lib/pages/video/ai_conclusion/view.dart | 211 ++++++++++-------- .../video/introduction/ugc/controller.dart | 23 +- 3 files changed, 191 insertions(+), 114 deletions(-) diff --git a/lib/common/widgets/video_popup_menu.dart b/lib/common/widgets/video_popup_menu.dart index 7413e751..0a71e3bc 100644 --- a/lib/common/widgets/video_popup_menu.dart +++ b/lib/common/widgets/video_popup_menu.dart @@ -6,6 +6,8 @@ import 'package:PiliPlus/models/model_video.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/pages/video/ai_conclusion/view.dart'; +import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; @@ -14,11 +16,10 @@ import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class VideoCustomAction { - String title; - String value; - Widget icon; - VoidCallback? onTap; - VideoCustomAction(this.title, this.value, this.icon, this.onTap); + final String title; + final Widget icon; + final VoidCallback? onTap; + const VideoCustomAction(this.title, this.icon, this.onTap); } class VideoCustomActions { @@ -32,7 +33,6 @@ class VideoCustomActions { if (videoItem.bvid?.isNotEmpty == true) ...[ VideoCustomAction( videoItem.bvid!, - 'copy', const Stack( clipBehavior: Clip.none, children: [ @@ -44,25 +44,72 @@ class VideoCustomActions { ), VideoCustomAction( '稍后再看', - 'pause', const Icon(MdiIcons.clockTimeEightOutline, size: 16), () async { var res = await UserHttp.toViewLater(bvid: videoItem.bvid); SmartDialog.showToast(res['msg']); }, ), + VideoCustomAction( + 'AI总结', + const Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Icon(Icons.circle_outlined, size: 16), + ExcludeSemantics( + child: Text( + 'AI', + style: TextStyle( + fontSize: 8, + height: 1, + fontWeight: FontWeight.w900, + ), + textScaler: TextScaler.noScaling, + ), + ), + ], + ), + () async { + final res = await UgcIntroController.getAiConclusion( + videoItem.bvid!, + videoItem.cid!, + videoItem.owner.mid, + ); + if (res != null && context.mounted) { + showDialog( + context: context, + builder: (context) { + final theme = Theme.of(context); + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Padding( + padding: const EdgeInsets.only(top: 14), + child: AiConclusionPanel.buildContent( + context, + theme, + res, + tap: false, + ), + ), + ), + ); + }, + ); + } + }, + ), ], if (videoItem is! SpaceArchiveItem) VideoCustomAction( '访问:${videoItem.owner.name}', - 'visit', const Icon(MdiIcons.accountCircleOutline, size: 16), () => Get.toNamed('/member?mid=${videoItem.owner.mid}'), ), if (videoItem is! SpaceArchiveItem) VideoCustomAction( '不感兴趣', - 'dislike', const Icon(MdiIcons.thumbDownOutline, size: 16), () { String? accessKey = Accounts.get(AccountType.recommend).accessKey; @@ -229,7 +276,6 @@ class VideoCustomActions { if (videoItem is! SpaceArchiveItem) VideoCustomAction( '拉黑:${videoItem.owner.name}', - 'block', const Icon(MdiIcons.cancel, size: 16), () => showDialog( context: context, @@ -272,7 +318,6 @@ class VideoCustomActions { ), VideoCustomAction( "${MineController.anonymity.value ? '退出' : '进入'}无痕模式", - 'anonymity', MineController.anonymity.value ? const Icon(MdiIcons.incognitoOff, size: 16) : const Icon(MdiIcons.incognito, size: 16), @@ -312,11 +357,9 @@ class VideoPopupMenu extends StatelessWidget { size: iconSize, ), position: PopupMenuPosition.under, - onSelected: (String type) {}, itemBuilder: (BuildContext context) => VideoCustomActions(videoItem, context, onRemove).actions.map((e) { - return PopupMenuItem( - value: e.value, + return PopupMenuItem( height: menuItemHeight, onTap: e.onTap, child: Row( diff --git a/lib/pages/video/ai_conclusion/view.dart b/lib/pages/video/ai_conclusion/view.dart index ce263a08..b005c951 100644 --- a/lib/pages/video/ai_conclusion/view.dart +++ b/lib/pages/video/ai_conclusion/view.dart @@ -18,6 +18,118 @@ class AiConclusionPanel extends CommonSlidePage { @override State createState() => _AiDetailState(); + + static Widget buildContent( + BuildContext context, + ThemeData theme, + AiConclusionResult res, { + Key? key, + bool tap = true, + }) { + return CustomScrollView( + key: key, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + if (res.summary?.isNotEmpty == true) ...[ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: selectableText( + res.summary!, + style: const TextStyle( + fontSize: 15, + height: 1.5, + ), + ), + ), + ), + if (res.outline?.isNotEmpty == true) + SliverToBoxAdapter( + child: Divider( + height: 20, + color: theme.dividerColor.withValues(alpha: 0.1), + thickness: 6, + ), + ), + ], + if (res.outline?.isNotEmpty == true) + SliverPadding( + padding: EdgeInsets.only( + left: 14, + right: 14, + bottom: MediaQuery.viewPaddingOf(context).bottom + 100, + ), + sliver: SliverList.builder( + itemCount: res.outline!.length, + itemBuilder: (context, index) { + final item = res.outline![index]; + return SelectionArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (index != 0) const SizedBox(height: 10), + Text( + item.title!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + const SizedBox(height: 6), + ...?item.partOutline?.map( + (item) => Wrap( + children: [ + Text.rich( + TextSpan( + style: TextStyle( + fontSize: 14, + color: theme.colorScheme.onSurface, + height: 1.5, + ), + children: [ + TextSpan( + text: DurationUtils.formatDuration( + item.timestamp, + ), + style: tap + ? TextStyle( + color: theme.colorScheme.primary, + ) + : null, + recognizer: tap + ? (TapGestureRecognizer() + ..onTap = () { + try { + Get.find( + tag: Get.arguments['heroTag'], + ).plPlayerController.seekTo( + Duration( + seconds: item.timestamp!, + ), + isSeek: false, + ); + } catch (_) {} + }) + : null, + ), + const TextSpan(text: ' '), + TextSpan(text: item.content!), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } } class _AiDetailState extends State @@ -65,102 +177,11 @@ class _AiDetailState extends State @override Widget buildList(ThemeData theme) { - final child = CustomScrollView( + final child = AiConclusionPanel.buildContent( + context, + theme, + widget.item, key: _key, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - if (widget.item.summary?.isNotEmpty == true) ...[ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: selectableText( - widget.item.summary!, - style: const TextStyle( - fontSize: 15, - height: 1.5, - ), - ), - ), - ), - if (widget.item.outline?.isNotEmpty == true) - SliverToBoxAdapter( - child: Divider( - height: 20, - color: theme.dividerColor.withValues(alpha: 0.1), - thickness: 6, - ), - ), - ], - if (widget.item.outline?.isNotEmpty == true) - SliverPadding( - padding: EdgeInsets.only( - left: 14, - right: 14, - bottom: MediaQuery.viewPaddingOf(context).bottom + 100, - ), - sliver: SliverList.builder( - itemCount: widget.item.outline!.length, - itemBuilder: (context, index) { - final item = widget.item.outline![index]; - return SelectionArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (index != 0) const SizedBox(height: 10), - Text( - item.title!, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - height: 1.5, - ), - ), - const SizedBox(height: 6), - ...?item.partOutline?.map( - (item) => Wrap( - children: [ - Text.rich( - TextSpan( - style: TextStyle( - fontSize: 14, - color: theme.colorScheme.onSurface, - height: 1.5, - ), - children: [ - TextSpan( - text: DurationUtils.formatDuration( - item.timestamp, - ), - style: TextStyle( - color: theme.colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - try { - Get.find( - tag: Get.arguments['heroTag'], - ).plPlayerController.seekTo( - Duration(seconds: item.timestamp!), - isSeek: false, - ); - } catch (_) {} - }, - ), - const TextSpan(text: ' '), - TextSpan(text: item.content!), - ], - ), - ), - ], - ), - ), - ], - ), - ); - }, - ), - ), - ], ); if (_isNested) { return ExtendedVisibilityDetector( diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index e6b966ea..5f869124 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -753,25 +753,38 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { } // ai总结 - Future aiConclusion() async { + static Future getAiConclusion( + String bvid, + int cid, + int? mid, + ) async { if (!Accounts.heartbeat.isLogin) { SmartDialog.showToast("账号未登录"); - return; + return null; } SmartDialog.showLoading(msg: '正在获取AI总结'); final res = await VideoHttp.aiConclusion( bvid: bvid, - cid: cid.value, - upMid: videoDetail.value.owner?.mid, + cid: cid, + upMid: mid, ); SmartDialog.dismiss(); if (res['status']) { AiConclusionData data = res['data']; - aiConclusionResult = data.modelResult; + return data.modelResult; } else if (res['handling']) { SmartDialog.showToast("AI处理中,请稍后再试"); } else { SmartDialog.showToast("当前视频暂不支持AI视频总结"); } + return null; + } + + Future aiConclusion() async { + aiConclusionResult = await getAiConclusion( + bvid, + cid.value, + videoDetail.value.owner?.mid, + ); } }