diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index ab90770634..a82ddc9252 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; @@ -11,7 +12,8 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; -import 'package:PiliPlus/models/dynamics/article_content_model.dart' show Pic; +import 'package:PiliPlus/models/dynamics/article_content_model.dart' + show ArticleContentModel, Pic; import 'package:PiliPlus/models/dynamics/result.dart' show DynamicStat; import 'package:PiliPlus/pages/article/controller.dart'; import 'package:PiliPlus/pages/article/widgets/article_ops.dart'; @@ -19,6 +21,7 @@ import 'package:PiliPlus/pages/article/widgets/html_render.dart'; import 'package:PiliPlus/pages/article/widgets/opus_content.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart'; import 'package:PiliPlus/pages/dynamics_repost/view.dart'; +import 'package:PiliPlus/pages/save_panel/view.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; @@ -28,6 +31,7 @@ import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:characters/characters.dart'; import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -286,36 +290,22 @@ class _ArticlePageState extends CommonDynPageState { ], ), ), + PopupMenuItem( + onTap: _saveArticleAsImage, + child: const Row( + spacing: 10, + mainAxisSize: .min, + children: [ + Icon(Icons.save_alt, size: 19), + Text('保存为图片'), + ], + ), + ), if (controller.commentType == 12 && controller.stats.value != null && controller.opusData?.modules.moduleBlocked == null) PopupMenuItem( - onTap: () async { - final summary = controller.summary; - try { - if (summary.cover == null) { - if (!await controller.getArticleInfo(true)) { - return; - } - } - if (mounted) { - PageUtils.pmShare( - this.context, - content: { - "id": controller.commentId, - "title": "- 哔哩哔哩专栏", - "headline": summary.title!, // throw - "source": 6, - "thumb": summary.cover!, - "author": summary.author!.name, - "author_id": summary.author!.mid.toString(), - }, - ); - } - } catch (e) { - SmartDialog.showToast(e.toString()); - } - }, + onTap: _pmShareArticle, child: const Row( spacing: 10, mainAxisSize: .min, @@ -331,6 +321,159 @@ class _ArticlePageState extends CommonDynPageState { ], ); + String _extractArticleContent(List? opus) { + if (opus == null) return ''; + final buffer = StringBuffer(); + for (final para in opus) { + if (para.paraType != 1) continue; + final nodes = para.text?.nodes; + if (nodes == null) continue; + for (final node in nodes) { + final text = node.word?.words ?? node.rich?.text ?? ''; + if (text.isNotEmpty) { + buffer.write(text); + if (buffer.length >= 100) { + return '${buffer.toString().characters.take(100).toString()}…'; + } + } + } + } + return buffer.toString(); + } + + void _saveArticleAsImage() { + final content = _extractArticleContent(controller.opus); + String? cover = controller.summary.cover; + if (cover == null && controller.opus != null) { + for (final para in controller.opus!) { + if (para.paraType == 2 && para.pic != null) { + cover = para.pic!.pics?.firstOrNull?.url; + break; + } + } + } + SavePanel.toSavePanel( + item: ArticleShareData( + cover: cover, + title: controller.summary.title ?? '', + content: content, + uname: controller.summary.author?.name, + uri: controller.url, + ), + ); + } + + Future _pmShareArticle() async { + final summary = controller.summary; + try { + if (summary.cover == null) { + if (!await controller.getArticleInfo(true)) { + return; + } + } + if (mounted) { + PageUtils.pmShare( + this.context, + content: { + "id": controller.commentId, + "title": "- 哔哩哔哩专栏", + "headline": summary.title!, + "source": 6, + "thumb": summary.cover!, + "author": summary.author!.name, + "author_id": summary.author!.mid.toString(), + }, + ); + } + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } + + void _showArticleShareSheet() { + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: min(640, maxWidth), + ), + builder: (context1) { + final theme = Theme.of(context); + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewPaddingOf(context1).bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: Get.back, + borderRadius: Style.bottomSheetRadius, + child: SizedBox( + height: 35, + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: theme.colorScheme.outline, + borderRadius: const BorderRadius.all( + Radius.circular(1.5), + ), + ), + ), + ), + ), + ), + ListTile( + onTap: () { + Get.back(); + _saveArticleAsImage(); + }, + minLeadingWidth: 0, + leading: const Icon(Icons.save_alt, size: 19), + title: Text('保存为图片', style: theme.textTheme.titleSmall), + ), + ListTile( + onTap: () { + Get.back(); + ShareUtils.shareText(controller.url); + }, + minLeadingWidth: 0, + leading: const Icon(Icons.share_outlined, size: 19), + title: Text('分享链接', style: theme.textTheme.titleSmall), + ), + if (controller.commentType == 12 && + controller.stats.value != null && + controller.opusData?.modules.moduleBlocked == null) + ListTile( + onTap: () { + Get.back(); + _pmShareArticle(); + }, + minLeadingWidth: 0, + leading: const Icon(Icons.forward_to_inbox, size: 19), + title: Text('分享至消息', style: theme.textTheme.titleSmall), + ), + const Divider(thickness: 0.1, height: 1), + ListTile( + onTap: Get.back, + minLeadingWidth: 0, + dense: true, + title: Text( + '取消', + style: TextStyle(color: theme.colorScheme.outline), + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + }, + ); + } + Widget _buildBottom() { if (!controller.showDynActionBar) { return fabButton; @@ -453,7 +596,7 @@ class _ArticlePageState extends CommonDynPageState { text: '分享', icon: CustomIcons.share_node, stat: null, - onPressed: () => ShareUtils.shareText(controller.url), + onPressed: _showArticleShareSheet, ), ), Expanded( diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart index 2ace4f5433..2f73b15e25 100644 --- a/lib/pages/dynamics/widgets/author_panel.dart +++ b/lib/pages/dynamics/widgets/author_panel.dart @@ -241,6 +241,140 @@ class AuthorPanel extends StatelessWidget { return header; } + // 抽取分享相关三项 ListTile,供 morePanel 与动态详情页复用 + static List shareSheetItems( + BuildContext context, + DynamicItemModel item, { + ModuleAuthorModel? moduleAuthor, + }) { + final theme = Theme.of(context); + final ma = moduleAuthor ?? item.modules.moduleAuthor; + final showPmShare = + (item.basic?.commentType == 17 || item.basic?.commentType == 11) && + item.modules.moduleDynamic?.major?.blocked == null; + return [ + ListTile( + onTap: () { + Get.back(); + SavePanel.toSavePanel(item: item); + }, + minLeadingWidth: 0, + leading: const Icon(Icons.save_alt, size: 19), + title: Text('保存为图片', style: theme.textTheme.titleSmall!), + ), + ListTile( + title: Text('分享链接', style: theme.textTheme.titleSmall), + leading: const Icon(Icons.share_outlined, size: 19), + onTap: () { + Get.back(); + ShareUtils.shareText( + '${HttpString.dynamicShareBaseUrl}/${item.idStr}', + ); + }, + minLeadingWidth: 0, + ), + if (showPmShare && ma != null) + ListTile( + title: Text('分享至消息', style: theme.textTheme.titleSmall), + leading: const Icon(Icons.forward_to_inbox, size: 19), + onTap: () { + Get.back(); + try { + bool isDyn = item.basic!.commentType == 17; + String id = isDyn ? item.idStr : item.basic!.ridStr!; + int source = isDyn ? 11 : 2; + final moduleDynamic = item.modules.moduleDynamic!; + final title = + moduleDynamic.desc?.text ?? + moduleDynamic.major!.opus!.summary!.text!; + String? thumb = isDyn + ? ma.face + : moduleDynamic.major?.opus?.pics?.firstOrNull?.url; + PageUtils.pmShare( + context, + content: { + "id": id, + "title": title, + "headline": "", + "source": source, + if (thumb?.isNotEmpty == true) "thumb": thumb, + "author": ma.name, + "author_id": ma.mid.toString(), + }, + ); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + }, + minLeadingWidth: 0, + ), + ]; + } + + // 弹出分享列表 BottomSheet(供动态详情页底部"分享"按钮调用) + static void showShareSheet( + BuildContext context, + DynamicItemModel item, { + ModuleAuthorModel? moduleAuthor, + }) { + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: min(640, context.mediaQueryShortestSide), + ), + builder: (context1) { + final theme = Theme.of(context); + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewPaddingOf(context1).bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: Get.back, + borderRadius: Style.bottomSheetRadius, + child: SizedBox( + height: 35, + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: theme.colorScheme.outline, + borderRadius: const BorderRadius.all( + Radius.circular(1.5), + ), + ), + ), + ), + ), + ), + ...shareSheetItems( + context, + item, + moduleAuthor: moduleAuthor, + ), + const Divider(thickness: 0.1, height: 1), + ListTile( + onTap: Get.back, + minLeadingWidth: 0, + dense: true, + title: Text( + '取消', + style: TextStyle(color: theme.colorScheme.outline), + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + }, + ); + } + void morePanel(BuildContext context) { String? bvid; try { @@ -305,69 +439,11 @@ class AuthorPanel extends StatelessWidget { style: theme.textTheme.titleSmall, ), ), - ListTile( - onTap: () { - Get.back(); - SavePanel.toSavePanel(item: item); - }, - minLeadingWidth: 0, - leading: const Icon(Icons.save_alt, size: 19), - title: Text('保存动态', style: theme.textTheme.titleSmall!), - ), - ListTile( - title: Text( - '分享动态', - style: theme.textTheme.titleSmall, - ), - leading: const Icon(Icons.share_outlined, size: 19), - onTap: () { - Get.back(); - ShareUtils.shareText( - '${HttpString.dynamicShareBaseUrl}/${item.idStr}', - ); - }, - minLeadingWidth: 0, + ...shareSheetItems( + context, + item, + moduleAuthor: moduleAuthor, ), - if ((item.basic!.commentType == 17 || - item.basic!.commentType == 11) && - item.modules.moduleDynamic?.major?.blocked == null) - ListTile( - title: Text( - '分享至消息', - style: theme.textTheme.titleSmall, - ), - leading: const Icon(Icons.forward_to_inbox, size: 19), - onTap: () { - Get.back(); - try { - bool isDyn = item.basic!.commentType == 17; - String id = isDyn ? item.idStr : item.basic!.ridStr!; - int source = isDyn ? 11 : 2; - final moduleDynamic = item.modules.moduleDynamic!; - final title = - moduleDynamic.desc?.text ?? - moduleDynamic.major!.opus!.summary!.text!; - String? thumb = isDyn - ? moduleAuthor.face - : moduleDynamic.major?.opus?.pics?.firstOrNull?.url; - PageUtils.pmShare( - context, - content: { - "id": id, - "title": title, - "headline": "", - "source": source, - if (thumb?.isNotEmpty == true) "thumb": thumb, - "author": moduleAuthor.name, - "author_id": moduleAuthor.mid.toString(), - }, - ); - } catch (e) { - SmartDialog.showToast(e.toString()); - } - }, - minLeadingWidth: 0, - ), ListTile( title: Text( '临时屏蔽:${moduleAuthor.name}', diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index b99454c8e8..b530de21e3 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -1,4 +1,6 @@ // 内容 +import 'package:PiliPlus/common/style.dart'; +import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart'; @@ -22,6 +24,7 @@ Widget content( required DynamicItemModel item, required bool isSave, required bool isDetail, + bool isLongImageMode = false, }) { TextSpan? richNodes = richNode( context, @@ -100,19 +103,84 @@ Widget content( primary: theme.colorScheme.primary, ), if (pics != null && pics.isNotEmpty) - ImageGridView( - fullScreen: true, - picArr: pics - .map( - (item) => ImageModel( - width: item.width, - height: item.height, - url: item.url ?? '', - liveUrl: item.liveUrl, + isLongImageMode + ? Padding( + padding: const EdgeInsets.only(top: 6), + child: LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + return Column( + spacing: 5, + children: pics.map((pic) { + final w = (pic.width ?? 1).toDouble(); + final h = (pic.height ?? 1).toDouble(); + final ratio = h / w; + final height = (maxWidth * ratio).clamp( + 0.0, + maxWidth * 4, + ); + final isTruncated = ratio > 4; + return ClipRRect( + borderRadius: BorderRadius.all(Style.imgRadius), + child: SizedBox( + width: maxWidth, + height: height, + child: Stack( + children: [ + NetworkImgLayer( + src: pic.url ?? '', + width: maxWidth, + height: height, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + if (isTruncated) ...[ + Positioned( + left: 0, + right: 0, + bottom: 0, + height: 60, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + theme.colorScheme.surface, + ], + ), + ), + ), + ), + const PBadge( + text: '长图', + right: 8, + bottom: 8, + ), + ], + ], + ), + ), + ); + }).toList(), + ); + }, ), ) - .toList(), - ), + : ImageGridView( + fullScreen: true, + picArr: pics + .map( + (item) => ImageModel( + width: item.width, + height: item.height, + url: item.url ?? '', + liveUrl: item.liveUrl, + ), + ) + .toList(), + ), ], ), ); diff --git a/lib/pages/dynamics/widgets/dyn_content.dart b/lib/pages/dynamics/widgets/dyn_content.dart index 6c3410393b..9ac92d0b47 100644 --- a/lib/pages/dynamics/widgets/dyn_content.dart +++ b/lib/pages/dynamics/widgets/dyn_content.dart @@ -12,6 +12,7 @@ List dynContent( required DynamicItemModel item, required bool isSave, required bool isDetail, + bool isLongImageMode = false, }) { final moduleDynamic = item.modules.moduleDynamic; return [ @@ -23,6 +24,7 @@ List dynContent( isDetail: isDetail, item: item, floor: floor, + isLongImageMode: isLongImageMode, ), module( context, diff --git a/lib/pages/dynamics/widgets/dynamic_panel.dart b/lib/pages/dynamics/widgets/dynamic_panel.dart index 74ffe9bc68..e94d757937 100644 --- a/lib/pages/dynamics/widgets/dynamic_panel.dart +++ b/lib/pages/dynamics/widgets/dynamic_panel.dart @@ -24,6 +24,7 @@ class DynamicPanel extends StatelessWidget { onSetPubSetting; final VoidCallback? onEdit; final ValueChanged? onSetReplySubject; + final bool isLongImageMode; const DynamicPanel({ super.key, @@ -38,6 +39,7 @@ class DynamicPanel extends StatelessWidget { this.onSetPubSetting, this.onEdit, this.onSetReplySubject, + this.isLongImageMode = false, }); @override @@ -96,6 +98,7 @@ class DynamicPanel extends StatelessWidget { isDetail: isDetail, item: item, floor: 1, + isLongImageMode: isLongImageMode, ), const SizedBox(height: 2), if (!isDetail) ...[ diff --git a/lib/pages/dynamics_detail/view.dart b/lib/pages/dynamics_detail/view.dart index 635ceb4275..1fa6e0ae5e 100644 --- a/lib/pages/dynamics_detail/view.dart +++ b/lib/pages/dynamics_detail/view.dart @@ -15,9 +15,8 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart' import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; import 'package:PiliPlus/common/widgets/tap_region_surface.dart'; -import 'package:PiliPlus/http/constants.dart'; -import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/models/common/reply/reply_option_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart'; @@ -33,7 +32,6 @@ import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart'; -import 'package:PiliPlus/utils/share_utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -614,8 +612,10 @@ class _DynamicDetailPageState icon: CustomIcons.share_node, text: '分享', stat: null, - onPressed: (_) => ShareUtils.shareText( - '${HttpString.dynamicShareBaseUrl}/${controller.dynItem.idStr}', + onPressed: (_) => AuthorPanel.showShareSheet( + context, + controller.dynItem, + moduleAuthor: controller.dynItem.modules.moduleAuthor, ), ), ), diff --git a/lib/pages/save_panel/view.dart b/lib/pages/save_panel/view.dart index edead0bd10..ff8509deb3 100644 --- a/lib/pages/save_panel/view.dart +++ b/lib/pages/save_panel/view.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; +import 'package:PiliPlus/models_new/video/video_detail/data.dart'; import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; import 'package:PiliPlus/pages/music/controller.dart'; @@ -18,6 +19,7 @@ import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; +import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -67,6 +69,7 @@ class _SavePanelState extends State { final boundaryKey = GlobalKey(); bool showBottom = true; + bool isLongImageMode = false; // item Object get _item => widget.item; @@ -113,11 +116,7 @@ class _SavePanelState extends State { pubdate = episode.pubTime; uname = pgcItem.upInfo?.uname; - final oid = reply.oid; - final type = reply.type.toInt(); - final anchor = hasRoot ? 'anchor=${reply.id}&' : ''; - uri = - 'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=bilibili://pgc/season/ep/${ctr.epId}'; + uri = 'https://www.bilibili.com/bangumi/play/ep${ctr.epId}'; } else { final ctr = Get.find(tag: heroTag); final videoDetail = ctr.videoDetail.value; @@ -147,11 +146,7 @@ class _SavePanelState extends State { uri = 'https://www.bilibili.com/video/av$oid?comment_on=1&comment_root_id=$rootId${hasRoot ? '&comment_secondary_id=${reply.id}' : ''}'; } else { - final enterUri = dynItem == null - ? '' - : 'enterUri=${parseDyn(dynItem)}'; - uri = - 'bilibili://comment/detail/$type/$oid/$rootId/?${hasRoot ? 'anchor=${reply.id}&' : ''}$enterUri'; + uri = dynItem == null ? '' : parseDyn(dynItem); } } else if (currentRoute.startsWith('/Scaffold')) { try { @@ -162,26 +157,17 @@ class _SavePanelState extends State { uri = 'https://www.bilibili.com/video/av$oid?comment_on=1&comment_root_id=$rootId${hasRoot ? '&comment_secondary_id=${reply.id}' : ''}'; } else { - String enterUri = Get.arguments['enterUri'] ?? ''; - if (enterUri.isNotEmpty) { - enterUri = 'enterUri=${Uri.encodeComponent(enterUri)}'; - } else if (const [11, 12, 17].contains(type)) { - enterUri = 'enterUri=bilibili://following/detail/$oid'; + uri = Get.arguments['enterUri'] ?? ''; + if (uri.isEmpty && const [11, 12, 17].contains(type)) { + uri = 'https://t.bilibili.com/$oid'; } - uri = - 'bilibili://comment/detail/$type/$oid/$rootId/?${hasRoot ? 'anchor=${reply.id}&' : ''}$enterUri'; } } catch (_) {} } else if (currentRoute.startsWith('/articlePage')) { try { - final type = reply.type.toInt(); - final oid = reply.oid; - final rootId = hasRoot ? reply.root : reply.id; - final anchor = hasRoot ? 'anchor=${reply.id}&' : ''; - final enterUri = - 'bilibili://following/detail/${Get.parameters['id'] ?? Get.arguments?['id']}'; - uri = - 'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri'; + final articleId = + Get.parameters['id'] ?? Get.arguments?['id']; + uri = 'https://www.bilibili.com/opus/$articleId'; } catch (_) {} } else if (currentRoute.startsWith('/musicDetail')) { final type = reply.type.toInt(); @@ -219,6 +205,16 @@ class _SavePanelState extends State { uri = parseDyn(i); if (kDebugMode) debugPrint(uri); + } else if (_item case final VideoDetailData video) { + itemType = '视频'; + viewType = '观看'; + uname = video.owner?.name; + uri = 'https://www.bilibili.com/video/${video.bvid}'; + } else if (_item case final ArticleShareData article) { + itemType = '专栏'; + viewType = '查看'; + uname = article.uname; + uri = article.uri; } } @@ -229,26 +225,27 @@ class _SavePanelState extends State { case 'DYNAMIC_TYPE_AV': viewType = '观看'; itemType = '视频'; - uri = 'bilibili://video/${item.basic!.commentIdStr}'; + final bvid = item.modules.moduleDynamic!.major!.archive!.bvid; + uri = 'https://www.bilibili.com/video/$bvid'; break; case 'DYNAMIC_TYPE_ARTICLE': itemType = '专栏'; - uri = 'bilibili://following/detail/${item.idStr}'; + uri = 'https://www.bilibili.com/opus/${item.idStr}'; break; case 'DYNAMIC_TYPE_LIVE_RCMD': viewType = '观看'; itemType = '直播'; final roomId = item.modules.moduleDynamic!.major!.liveRcmd!.roomId; - uri = 'bilibili://live/$roomId'; + uri = 'https://live.bilibili.com/$roomId'; break; case 'DYNAMIC_TYPE_UGC_SEASON': viewType = '观看'; itemType = '合集'; - final aid = item.modules.moduleDynamic!.major!.ugcSeason!.aid; - uri = 'bilibili://video/$aid'; + final bvid = item.modules.moduleDynamic!.major!.ugcSeason!.bvid; + uri = 'https://www.bilibili.com/video/$bvid'; break; case 'DYNAMIC_TYPE_PGC': @@ -257,14 +254,14 @@ class _SavePanelState extends State { itemType = item.modules.moduleDynamic?.major?.pgc?.badge?.text ?? '番剧'; final epid = item.modules.moduleDynamic!.major!.pgc!.epid; - uri = 'bilibili://pgc/season/ep/$epid'; + uri = 'https://www.bilibili.com/bangumi/play/ep$epid'; break; // https://www.bilibili.com/medialist/detail/ml12345678 case 'DYNAMIC_TYPE_MEDIALIST': itemType = '收藏夹'; final mediaId = item.modules.moduleDynamic!.major!.medialist!.id; - uri = 'bilibili://medialist/detail/$mediaId'; + uri = 'https://www.bilibili.com/medialist/detail/ml$mediaId'; break; // 纯文字动态查看 @@ -277,7 +274,7 @@ class _SavePanelState extends State { // case 'DYNAMIC_TYPE_DRAW': default: itemType = '动态'; - uri = 'bilibili://following/detail/${item.idStr}'; + uri = 'https://t.bilibili.com/${item.idStr}'; break; } } catch (_) {} @@ -381,6 +378,134 @@ class _SavePanelState extends State { item: dyn, isDetail: true, isSave: true, + isLongImageMode: isLongImageMode, + ), + ), + VideoDetailData video => IgnorePointer( + child: LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final coverHeight = width * 9 / 16; + final duration = video.duration ?? 0; + String durationStr = ''; + if (duration > 0) { + final h = duration ~/ 3600; + final m = (duration % 3600) ~/ 60; + final s = duration % 60; + durationStr = h > 0 + ? '$h:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}' + : '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; + } + final stat = video.stat; + final overlayText = [ + if (durationStr.isNotEmpty) durationStr, + if (stat?.view != null) + '${NumUtils.numFormat(stat!.view)}播放', + if (stat?.danmaku != null) + '${NumUtils.numFormat(stat!.danmaku)}弹幕', + ].join(' | '); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (video.pic?.isNotEmpty == true) + Stack( + children: [ + NetworkImgLayer( + src: video.pic!, + width: width, + height: coverHeight, + fit: BoxFit.cover, + ), + if (overlayText.isNotEmpty) + Positioned( + left: 8, + bottom: 8, + child: Container( + padding: + const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + overlayText, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ), + ], + ), + if (video.title?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.all(12), + child: Text( + video.title!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + if (video.desc?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.fromLTRB( + 12, + 0, + 12, + 12, + ), + child: Text(video.desc!), + ), + ], + ); + }, + ), + ), + ArticleShareData article => IgnorePointer( + child: LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (article.cover?.isNotEmpty == true) + NetworkImgLayer( + src: article.cover!, + width: width, + height: width * 9 / 16, + fit: BoxFit.cover, + ), + if (article.title.isNotEmpty) + Padding( + padding: const EdgeInsets.all(12), + child: Text( + article.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + if (article.content.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB( + 12, + 0, + 12, + 12, + ), + child: Text(article.content), + ), + ], + ); + }, ), ), _ => throw UnsupportedError(_item.toString()), @@ -546,7 +671,7 @@ class _SavePanelState extends State { bottom: 25 + padding.bottom, ), child: Row( - spacing: 40, + spacing: 30, mainAxisAlignment: .center, children: [ iconButton( @@ -568,6 +693,15 @@ class _SavePanelState extends State { showBottom = !showBottom; }), ), + iconButton( + size: 42, + tooltip: '长图切换', + context: context, + icon: const Icon(Icons.view_day), + onPressed: () => setState(() { + isLongImageMode = !isLongImageMode; + }), + ), if (PlatformUtils.isMobile) iconButton( size: 42, @@ -594,3 +728,19 @@ class _SavePanelState extends State { } enum _CoverType { def16_9, square } + +class ArticleShareData { + final String? cover; + final String title; + final String content; + final String? uname; + final String uri; + + const ArticleShareData({ + this.cover, + required this.title, + required this.content, + this.uname, + required this.uri, + }); +} diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index d30a2c1c33..45697f4a79 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -24,6 +24,7 @@ import 'package:PiliPlus/models_new/video/video_detail/stat_detail.dart'; import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/dynamics_repost/view.dart'; +import 'package:PiliPlus/pages/save_panel/view.dart'; import 'package:PiliPlus/pages/video/related/controller.dart'; import 'package:PiliPlus/pages/video/reply/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; @@ -339,6 +340,17 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { ); }, ), + ListTile( + dense: true, + title: const Text( + '保存为图片', + style: TextStyle(fontSize: 14), + ), + onTap: () { + Get.back(); + SavePanel.toSavePanel(item: videoDetail); + }, + ), if (isLogin) ListTile( dense: true,