diff --git a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart index d60ed3faa..9715c77e1 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart @@ -273,8 +273,8 @@ class _InteractiveviewerGalleryState extends State onDoubleTap, ), onLongPress: !isFileImg ? () => onLongPress(item) : null, - onSecondaryTap: !isFileImg && !PlatformUtils.isMobile - ? () => onLongPress(item) + onSecondaryTapUp: PlatformUtils.isDesktop && !isFileImg + ? (e) => _showDesktopMenu(e.globalPosition, item) : null, child: widget.itemBuilder != null ? widget.itemBuilder!( @@ -479,9 +479,9 @@ class _InteractiveviewerGalleryState extends State ); }, dense: true, - title: const Text( - '保存 Live Photo', - style: TextStyle(fontSize: 14), + title: Text( + '保存${Platform.isIOS ? ' Live Photo' : '视频'}', + style: const TextStyle(fontSize: 14), ), ), ], @@ -490,4 +490,40 @@ class _InteractiveviewerGalleryState extends State }, ); } + + void _showDesktopMenu(Offset offset, SourceModel item) { + showMenu( + context: context, + position: RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, 0), + items: [ + PopupMenuItem( + height: 42, + onTap: () => Utils.copyText(item.url), + child: const Text('复制链接', style: TextStyle(fontSize: 14)), + ), + PopupMenuItem( + height: 42, + onTap: () => ImageUtils.downloadImg(context, [item.url]), + child: const Text('保存图片', style: TextStyle(fontSize: 14)), + ), + PopupMenuItem( + height: 42, + onTap: () => PageUtils.launchURL(item.url), + child: const Text('网页打开', style: TextStyle(fontSize: 14)), + ), + if (item.sourceType == SourceType.livePhoto) + PopupMenuItem( + height: 42, + onTap: () => ImageUtils.downloadLivePhoto( + context: context, + url: item.url, + liveUrl: item.liveUrl!, + width: item.width!, + height: item.height!, + ), + child: const Text('保存视频', style: TextStyle(fontSize: 14)), + ), + ], + ); + } } diff --git a/lib/models/dynamics/result.dart b/lib/models/dynamics/result.dart index 33bc2c2a2..c8fbc6bdf 100644 --- a/lib/models/dynamics/result.dart +++ b/lib/models/dynamics/result.dart @@ -159,6 +159,8 @@ class ItemModulesModel { // 动态 ModuleDynamicModel? moduleDynamic; // ModuleInterModel? moduleInter; + ModuleInteraction? moduleInteraction; + ModuleDispute? moduleDispute; // 专栏 ModuleTop? moduleTop; @@ -167,7 +169,6 @@ class ItemModulesModel { List? moduleContent; ModuleBlocked? moduleBlocked; ModuleFold? moduleFold; - ModuleInteraction? moduleInteraction; ItemModulesModel.fromJson(Map json) { moduleAuthor = json['module_author'] != null @@ -188,6 +189,9 @@ class ItemModulesModel { moduleInteraction = json['module_interaction'] != null ? ModuleInteraction.fromJson(json['module_interaction']) : null; + moduleDispute = json['module_dispute'] != null + ? ModuleDispute.fromJson(json['module_dispute']) + : null; } ItemModulesModel.fromOpusJson(List json) { @@ -238,6 +242,18 @@ class ItemModulesModel { } } +class ModuleDispute { + String? title; + String? desc; + String? jumpUrl; + + ModuleDispute.fromJson(Map json) { + title=json['title']; + desc=json['desc']; + jumpUrl=json['jump_url']; + } +} + class ModuleInteraction { List? items; diff --git a/lib/models_new/article/article_view/ops.dart b/lib/models_new/article/article_view/ops.dart index 08df3da58..ae2428236 100644 --- a/lib/models_new/article/article_view/ops.dart +++ b/lib/models_new/article/article_view/ops.dart @@ -71,8 +71,8 @@ class InsertCard { String? id; dynamic alt; String? url; - num? width; - num? height; + double? width; + double? height; num? size; String? status; @@ -92,8 +92,8 @@ class InsertCard { id = json['id'] == '' ? null : json['id']; alt = json['alt']; url = json['url']; - width = json['width']; - height = json['height']; + width = (json['width'] as num?)?.toDouble(); + height = (json['height'] as num?)?.toDouble(); size = json['size']; status = json['status']; } diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index 376fef444..571f292e2 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; @@ -16,6 +17,7 @@ import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart'; import 'package:PiliPlus/pages/dynamics_repost/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'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/num_utils.dart'; @@ -180,7 +182,10 @@ class _ArticlePageState extends CommonDynPageState { } else if (controller.articleData?.content != null) { if (controller.articleData?.type == 3) { // json - return ArticleOpus(ops: controller.articleData?.ops); + return ArticleOpus( + ops: controller.articleData?.ops, + maxWidth: maxWidth, + ); } // if (kDebugMode) debugPrint('html page'); final res = parser.parse(controller.articleData!.content!); @@ -238,14 +243,11 @@ class _ArticlePageState extends CommonDynPageState { final length = pics.length; final first = pics.first; double height; - double paddingRight; if (first.height != null && first.width != null) { final ratio = first.height! / first.width!; - height = min(maxWidth * ratio, Get.height * 0.55); - paddingRight = (maxWidth - height / ratio) / 2 + 12; + height = min(maxWidth * ratio, maxHeight * 0.55); } else { - height = Get.height * 0.55; - paddingRight = 12; + height = maxHeight * 0.55; } return Stack( clipBehavior: Clip.none, @@ -255,13 +257,25 @@ class _ArticlePageState extends CommonDynPageState { width: maxWidth, margin: const EdgeInsets.only(bottom: 10), child: PageView.builder( - physics: const ClampingScrollPhysics(), - onPageChanged: (value) { - controller.topIndex.value = value; - }, + physics: const CustomTabBarViewScrollPhysics( + parent: ClampingScrollPhysics(), + ), + onPageChanged: (value) => + controller.topIndex.value = value, itemCount: length, itemBuilder: (context, index) { final pic = pics[index]; + int? memCacheWidth, memCacheHeight; + if (pic.isLongPic ?? false) { + memCacheWidth = maxWidth.cacheSize(context); + } else if (pic.width != null && + pic.height != null) { + if (pic.width! > pic.height!) { + memCacheWidth = maxWidth.cacheSize(context); + } else { + memCacheHeight = height.cacheSize(context); + } + } return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => PageUtils.imageView( @@ -277,31 +291,33 @@ class _ArticlePageState extends CommonDynPageState { clipBehavior: Clip.none, alignment: Alignment.center, children: [ - Positioned.fill( - child: CachedNetworkImage( - fit: pic.isLongPic == true - ? BoxFit.cover - : null, - imageUrl: ImageUtils.thumbnailUrl( - pic.url, - 60, - ), - fadeInDuration: const Duration( - milliseconds: 120, - ), - fadeOutDuration: const Duration( - milliseconds: 120, - ), - placeholder: (_, _) => - const SizedBox.shrink(), + CachedNetworkImage( + height: height, + width: maxWidth, + memCacheWidth: memCacheWidth, + memCacheHeight: memCacheHeight, + fit: pic.isLongPic == true + ? BoxFit.cover + : null, + imageUrl: ImageUtils.thumbnailUrl( + pic.url, + 60, ), + fadeInDuration: const Duration( + milliseconds: 120, + ), + fadeOutDuration: const Duration( + milliseconds: 120, + ), + placeholder: (_, _) => + const SizedBox.shrink(), ), if (pic.isLongPic == true) - PBadge( - text: '长图', - type: PBadgeType.primary, - right: paddingRight, + const PBadge( + right: 12, bottom: 12, + text: '长图', + type: .primary, ), ], ), @@ -313,7 +329,7 @@ class _ArticlePageState extends CommonDynPageState { Obx( () => PBadge( top: 12, - right: paddingRight, + right: 12, type: PBadgeType.gray, text: '${controller.topIndex.value + 1}/$length', ), diff --git a/lib/pages/article/widgets/article_ops.dart b/lib/pages/article/widgets/article_ops.dart index 35a361a73..5237e318a 100644 --- a/lib/pages/article/widgets/article_ops.dart +++ b/lib/pages/article/widgets/article_ops.dart @@ -1,16 +1,24 @@ +import 'dart:math' as math; + import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/models_new/article/article_view/ops.dart'; import 'package:PiliPlus/pages/dynamics/widgets/vote.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; class ArticleOpus extends StatelessWidget { - const ArticleOpus({super.key, required List? ops}) : _ops = ops; + const ArticleOpus({ + super.key, + required List? ops, + required this.maxWidth, + }) : _ops = ops; final List? _ops; + final double maxWidth; @override Widget build(BuildContext context) { @@ -29,6 +37,13 @@ class ArticleOpus extends StatelessWidget { case Insert(:final card): if (card != null) { if (card.url?.isNotEmpty == true) { + double? width = card.width == null + ? null + : math.min(maxWidth, card.width!); + final height = width == null || card.height == null + ? null + : width * card.height! / card.width!; + width ??= maxWidth; return GestureDetector( onTap: () { switch (item.attributes?.clazz) { @@ -55,6 +70,9 @@ class ArticleOpus extends StatelessWidget { child: ClipRRect( borderRadius: StyleString.mdRadius, child: CachedNetworkImage( + width: width, + height: height, + memCacheWidth: width.cacheSize(context), imageUrl: ImageUtils.thumbnailUrl(card.url, 60), placeholder: (_, _) => const SizedBox.shrink(), ), diff --git a/lib/pages/article/widgets/html_render.dart b/lib/pages/article/widgets/html_render.dart index 6b31d102d..868b05254 100644 --- a/lib/pages/article/widgets/html_render.dart +++ b/lib/pages/article/widgets/html_render.dart @@ -41,13 +41,14 @@ Widget htmlRender({ if (clazz?.contains('cut-off') == true || height != null) { return CachedNetworkImage( width: maxWidth, + memCacheWidth: maxWidth.cacheSize(context), height: height != null ? double.parse(height) : null, imageUrl: ImageUtils.thumbnailUrl(imgUrl), fit: BoxFit.contain, placeholder: (_, _) => const SizedBox.shrink(), ); } - final size = isEmote ? 22.0 : null; + final width = isEmote ? 22.0 : maxWidth; return GestureDetector( onTap: () => PageUtils.imageView( imgList: [SourceModel(url: imgUrl)], @@ -56,9 +57,9 @@ Widget htmlRender({ child: Hero( tag: imgUrl, child: CachedNetworkImage( - width: size, - height: size, - memCacheWidth: size?.cacheSize(context), + width: width, + height: isEmote ? 22.0 : null, + memCacheWidth: width.cacheSize(context), imageUrl: ImageUtils.thumbnailUrl(imgUrl, 60), fadeInDuration: const Duration(milliseconds: 120), fadeOutDuration: const Duration(milliseconds: 120), diff --git a/lib/pages/article/widgets/opus_content.dart b/lib/pages/article/widgets/opus_content.dart index 87e8b5ab8..5975b5973 100644 --- a/lib/pages/article/widgets/opus_content.dart +++ b/lib/pages/article/widgets/opus_content.dart @@ -215,12 +215,13 @@ class OpusContent extends StatelessWidget { case 2 when (element.pic != null): if (element.pic!.pics!.length == 1) { final pic = element.pic!.pics!.first; - final width = pic.width == null + double? width = pic.width == null ? null : math.min(maxWidth, pic.width!); final height = width == null || pic.height == null ? null : width * pic.height! / pic.width!; + width ??= maxWidth; return GestureDetector( onTap: () => PageUtils.imageView( imgList: [SourceModel(url: pic.url!)], @@ -232,7 +233,7 @@ class OpusContent extends StatelessWidget { child: CachedNetworkImage( width: width, height: height, - memCacheWidth: width?.cacheSize(context), + memCacheWidth: width.cacheSize(context), imageUrl: ImageUtils.thumbnailUrl(pic.url!, 60), fadeInDuration: const Duration(milliseconds: 120), fadeOutDuration: const Duration(milliseconds: 120), @@ -259,10 +260,10 @@ class OpusContent extends StatelessWidget { case 3 when (element.line != null): final height = element.line!.pic!.height?.toDouble(); return CachedNetworkImage( - width: maxWidth, - fit: BoxFit.contain, + fit: .contain, height: height, - memCacheHeight: height?.cacheSize(context), + width: maxWidth, + memCacheWidth: maxWidth.cacheSize(context), imageUrl: ImageUtils.thumbnailUrl(element.line!.pic!.url!), placeholder: (_, _) => const SizedBox.shrink(), ); diff --git a/lib/pages/common/dyn/common_dyn_page.dart b/lib/pages/common/dyn/common_dyn_page.dart index 0c7d2d13f..f2234b89a 100644 --- a/lib/pages/common/dyn/common_dyn_page.dart +++ b/lib/pages/common/dyn/common_dyn_page.dart @@ -33,6 +33,7 @@ abstract class CommonDynPageState extends State late EdgeInsets padding; late bool isPortrait; late double maxWidth; + late double maxHeight; bool _showFab = true; @@ -89,6 +90,7 @@ abstract class CommonDynPageState extends State super.didChangeDependencies(); final size = MediaQuery.sizeOf(context); maxWidth = size.width; + maxHeight = size.height; isPortrait = size.isPortrait; padding = MediaQuery.viewPaddingOf(context); } diff --git a/lib/pages/dynamics/widgets/dynamic_panel.dart b/lib/pages/dynamics/widgets/dynamic_panel.dart index b02a886ff..eab217f96 100644 --- a/lib/pages/dynamics/widgets/dynamic_panel.dart +++ b/lib/pages/dynamics/widgets/dynamic_panel.dart @@ -6,6 +6,7 @@ import 'package:PiliPlus/pages/dynamics/widgets/action_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dyn_content.dart'; import 'package:PiliPlus/pages/dynamics/widgets/interaction.dart'; +import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/material.dart' hide InkWell; @@ -78,6 +79,8 @@ class DynamicPanel extends StatelessWidget { padding: const EdgeInsets.fromLTRB(12, 12, 12, 6), child: authorWidget, ), + if (item.modules.moduleDispute case final moduleDispute?) + _buildDispute(theme, moduleDispute), ...dynContent( context, theme: theme, @@ -242,4 +245,53 @@ class DynamicPanel extends StatelessWidget { ), ); } + + Widget _buildDispute(ThemeData theme, ModuleDispute moduleDispute) { + final child = Container( + width: .infinity, + margin: const .fromLTRB(12, 0, 12, 6), + padding: const .symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer.withValues( + alpha: theme.brightness.isLight ? 0.5 : 0.7, + ), + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + child: Text.rich( + style: TextStyle( + height: 1, + fontSize: 13, + color: theme.colorScheme.onSecondaryContainer, + ), + strutStyle: const StrutStyle( + leading: 0, + height: 1, + fontSize: 13, + ), + TextSpan( + children: [ + WidgetSpan( + alignment: .middle, + child: Padding( + padding: const .only(right: 4), + child: Icon( + size: 15, + Icons.warning_rounded, + color: theme.colorScheme.onSecondaryContainer, + ), + ), + ), + TextSpan(text: moduleDispute.title), + ], + ), + ), + ); + if (moduleDispute.jumpUrl?.isNotEmpty == true) { + return GestureDetector( + onTap: () => PageUtils.handleWebview(moduleDispute.jumpUrl!), + child: child, + ); + } + return child; + } } diff --git a/lib/pages/dynamics/widgets/video_panel.dart b/lib/pages/dynamics/widgets/video_panel.dart index 8cad25f82..883728b64 100644 --- a/lib/pages/dynamics/widgets/video_panel.dart +++ b/lib/pages/dynamics/widgets/video_panel.dart @@ -112,11 +112,11 @@ Widget videoSeasonWidget( ], if (video.stat case final stat?) ...[ Text( - '${NumUtils.numFormat(stat.play)}次围观', + '${NumUtils.numFormat(stat.play)}播放', ), const SizedBox(width: 6), Text( - '${NumUtils.numFormat(stat.danmu)}条弹幕', + '${NumUtils.numFormat(stat.danmu)}弹幕', ), ], const Spacer(), diff --git a/lib/pages/video/pay_coins/view.dart b/lib/pages/video/pay_coins/view.dart index 90308ad3b..1b5314ac1 100644 --- a/lib/pages/video/pay_coins/view.dart +++ b/lib/pages/video/pay_coins/view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:math' show max; +import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; @@ -343,7 +344,9 @@ class _PayCoinsPageState extends State height: 100, child: PageView( key: const PageStorageKey('PageView'), - physics: const ClampingScrollPhysics(), + physics: const CustomTabBarViewScrollPhysics( + parent: ClampingScrollPhysics(), + ), controller: _controller, onPageChanged: (index) { _scale(); diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index 3ec9497a4..d74c260dc 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -1591,6 +1591,7 @@ class _VideoDetailPageVState extends State return Positioned.fill( child: GestureDetector( onTap: handlePlay, + behavior: .opaque, child: Obx( () => NetworkImgLayer( type: .emote, diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index d1ff6b4c0..749281a7f 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; @@ -13,7 +14,7 @@ import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; -import 'package:PiliPlus/utils/extension/widget_ext.dart'; +import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -756,17 +757,34 @@ class ChatItem extends StatelessWidget { } Widget msgTypePictureCard_13(dynamic content) { - final url = content['jump_url']; - return GestureDetector( - onTap: url == null ? null : () => PiliScheme.routePushFromUrl(url), - child: ClipRRect( - borderRadius: StyleString.mdRadius, - child: CachedNetworkImage( - imageUrl: ImageUtils.thumbnailUrl(content['pic_url']), - placeholder: (_, _) => const SizedBox.shrink(), - ), - ), - ).constraintWidth(constraints: const BoxConstraints(maxWidth: 400.0)); + final String? url = content['jump_url']; + return LayoutBuilder( + builder: (context, constraints) { + final maxWidth = max(400.0, constraints.maxWidth); + Widget child = ClipRRect( + borderRadius: StyleString.mdRadius, + child: CachedNetworkImage( + width: maxWidth, + memCacheWidth: maxWidth.cacheSize(context), + imageUrl: ImageUtils.thumbnailUrl(content['pic_url']), + placeholder: (_, _) => const SizedBox.shrink(), + ), + ); + if (url != null && url.isNotEmpty) { + child = GestureDetector( + onTap: () => PiliScheme.routePushFromUrl(url), + child: child, + ); + } + return Align( + alignment: Alignment.topCenter, + child: SizedBox( + width: maxWidth, + child: child, + ), + ); + }, + ); } Widget def(Color textColor, {err}) { diff --git a/lib/utils/waterfall.dart b/lib/utils/waterfall.dart index b3fe0f3c2..c61bf2c6c 100644 --- a/lib/utils/waterfall.dart +++ b/lib/utils/waterfall.dart @@ -27,12 +27,9 @@ mixin DynMixin { final cardWidth = Grid.smallCardWidth * 2; final flag = cardWidth < maxWidth; this.maxWidth = flag ? cardWidth : maxWidth; - if (!flag) { - return child; - } return SliverPadding( padding: EdgeInsets.symmetric( - horizontal: (maxWidth - cardWidth) / 2, + horizontal: flag ? (maxWidth - cardWidth) / 2 : 0, ), sliver: child, );