diff --git a/lib/pages/dynamics/widgets/blocked_item.dart b/lib/pages/dynamics/widgets/blocked_item.dart index 0cdc218b6..caa5c17c3 100644 --- a/lib/pages/dynamics/widgets/blocked_item.dart +++ b/lib/pages/dynamics/widgets/blocked_item.dart @@ -3,13 +3,13 @@ import 'package:PiliPlus/pages/article/widgets/opus_content.dart' show moduleBlockedItem; import 'package:flutter/material.dart'; -Widget blockedItem(ThemeData theme, ModuleBlocked moduleBlocked) { +Widget blockedItem( + ThemeData theme, + ModuleBlocked moduleBlocked, { + required double maxWidth, +}) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 1), - child: LayoutBuilder( - builder: (context, constraints) { - return moduleBlockedItem(theme, moduleBlocked, constraints.maxWidth); - }, - ), + child: moduleBlockedItem(theme, moduleBlocked, maxWidth - 26), ); } diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index 14e90dce3..fd30b0383 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -15,9 +15,12 @@ Widget content( bool isDetail, Function(List, int)? callback, { floor = 1, + required double maxWidth, }) { - TextSpan? richNodes = richNode(theme, item, context); - + if (floor == 1) { + maxWidth -= 24; + } + TextSpan? richNodes = richNode(theme, item, context, maxWidth: maxWidth); return Padding( padding: floor == 1 ? const EdgeInsets.fromLTRB(12, 0, 12, 6) @@ -77,23 +80,19 @@ Widget content( maxLines: isSave ? null : 6, ), if (item.modules.moduleDynamic?.major?.opus?.pics?.isNotEmpty == true) - LayoutBuilder( - builder: (context, constraints) { - return imageView( - constraints.maxWidth, - item.modules.moduleDynamic!.major!.opus!.pics! - .map( - (item) => ImageModel( - width: item.width, - height: item.height, - url: item.url ?? '', - liveUrl: item.liveUrl, - ), - ) - .toList(), - callback: callback, - ); - }, + imageView( + maxWidth, + item.modules.moduleDynamic!.major!.opus!.pics! + .map( + (item) => ImageModel( + width: item.width, + height: item.height, + url: item.url ?? '', + liveUrl: item.liveUrl, + ), + ) + .toList(), + callback: callback, ), ], ), diff --git a/lib/pages/dynamics/widgets/dynamic_panel.dart b/lib/pages/dynamics/widgets/dynamic_panel.dart index af67ccc65..470d736bb 100644 --- a/lib/pages/dynamics/widgets/dynamic_panel.dart +++ b/lib/pages/dynamics/widgets/dynamic_panel.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; class DynamicPanel extends StatelessWidget { final DynamicItemModel item; + final double maxWidth; final bool isDetail; final ValueChanged? onRemove; final Function(List, int)? callback; @@ -22,6 +23,7 @@ class DynamicPanel extends StatelessWidget { const DynamicPanel({ super.key, required this.item, + required this.maxWidth, this.isDetail = false, this.onRemove, this.callback, @@ -67,12 +69,32 @@ class DynamicPanel extends StatelessWidget { child: authorWidget, ), if (item.type != 'DYNAMIC_TYPE_NONE') - content(theme, isSave, context, item, isDetail, callback), - module(theme, isSave, item, context, isDetail, callback), + content( + theme, + isSave, + context, + item, + isDetail, + callback, + maxWidth: maxWidth, + ), + module( + theme, + isSave, + item, + context, + isDetail, + callback, + maxWidth: maxWidth, + ), if (item.modules.moduleDynamic?.additional != null) addWidget(theme, item, context), if (item.modules.moduleDynamic?.major?.blocked != null) - blockedItem(theme, item.modules.moduleDynamic!.major!.blocked!), + blockedItem( + theme, + item.modules.moduleDynamic!.major!.blocked!, + maxWidth: maxWidth, + ), const SizedBox(height: 2), if (!isDetail) ActionPanel(item: item), if (isDetail && !isSave) const SizedBox(height: 12), diff --git a/lib/pages/dynamics/widgets/live_panel_sub.dart b/lib/pages/dynamics/widgets/live_panel_sub.dart index 5d89926a9..acc373677 100644 --- a/lib/pages/dynamics/widgets/live_panel_sub.dart +++ b/lib/pages/dynamics/widgets/live_panel_sub.dart @@ -12,7 +12,9 @@ Widget livePanelSub( DynamicItemModel item, BuildContext context, { int floor = 1, + required double maxWidth, }) { + maxWidth -= StyleString.safeSpace * 2; SubscriptionNew? subItem = item.modules.moduleDynamic!.major?.subscriptionNew; LivePlayInfo? content = subItem?.liveRcmd?.content?.livePlayInfo; if (subItem == null || content == null) { @@ -25,84 +27,76 @@ Widget livePanelSub( padding: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace), child: GestureDetector( onTap: () => PageUtils.toLiveRoom(content.roomId), - child: LayoutBuilder( - builder: (context, box) { - double width = box.maxWidth; - return Stack( - clipBehavior: Clip.none, - children: [ - Hero( - tag: content.roomId.toString(), - child: NetworkImgLayer( - width: width, - height: width / StyleString.aspectRatio, - src: content.cover, - quality: 40, - ), + child: Stack( + clipBehavior: Clip.none, + children: [ + NetworkImgLayer( + width: maxWidth, + height: maxWidth / StyleString.aspectRatio, + src: content.cover, + quality: 40, + ), + PBadge( + text: content.watchedShow?.textLarge, + top: 6, + right: 65, + fontSize: 10.5, + type: PBadgeType.gray, + ), + if (content.liveStatus == 1) + Positioned( + right: 6, + top: 6, + child: Image.asset( + height: 16, + 'assets/images/live/live.gif', + filterQuality: FilterQuality.low, ), - PBadge( - text: content.watchedShow?.textLarge, - top: 6, - right: 65, - fontSize: 10.5, - type: PBadgeType.gray, - ), - if (content.liveStatus == 1) - Positioned( - right: 6, - top: 6, - child: Image.asset( - height: 16, - 'assets/images/live/live.gif', - filterQuality: FilterQuality.low, + ) + else + const PBadge( + text: '直播结束', + top: 6, + right: 6, + ), + if (content.areaName != null) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: 80, + alignment: Alignment.bottomLeft, + padding: const EdgeInsets.fromLTRB(12, 0, 10, 10), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black45, + ], ), - ) - else - const PBadge( - text: '直播结束', - top: 6, - right: 6, + borderRadius: floor == 1 + ? const BorderRadius.only( + bottomLeft: StyleString.imgRadius, + bottomRight: StyleString.imgRadius, + ) + : const BorderRadius.only( + bottomLeft: Radius.circular(6), + bottomRight: Radius.circular(6), + ), ), - if (content.areaName != null) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - height: 80, - alignment: Alignment.bottomLeft, - padding: const EdgeInsets.fromLTRB(12, 0, 10, 10), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black45, - ], - ), - borderRadius: floor == 1 - ? const BorderRadius.only( - bottomLeft: StyleString.imgRadius, - bottomRight: StyleString.imgRadius, - ) - : const BorderRadius.only( - bottomLeft: Radius.circular(6), - bottomRight: Radius.circular(6), - ), - ), - child: Text( - content.areaName!, - style: TextStyle( - fontSize: theme.textTheme.labelMedium!.fontSize, - color: Colors.white, - ), - ), + child: Text( + content.areaName!, + style: TextStyle( + fontSize: theme.textTheme.labelMedium!.fontSize, + color: Colors.white, ), ), - ], - ); - }, + ), + ), + ], ), ), ), diff --git a/lib/pages/dynamics/widgets/live_rcmd_panel.dart b/lib/pages/dynamics/widgets/live_rcmd_panel.dart index 12a5f2f20..8de0c3d3d 100644 --- a/lib/pages/dynamics/widgets/live_rcmd_panel.dart +++ b/lib/pages/dynamics/widgets/live_rcmd_panel.dart @@ -12,7 +12,9 @@ Widget liveRcmdPanel( DynamicItemModel item, BuildContext context, { int floor = 1, + required double maxWidth, }) { + maxWidth -= StyleString.safeSpace * 2; DynamicLiveModel? liveRcmd = item.modules.moduleDynamic?.major?.liveRcmd; if (liveRcmd == null) { return const SizedBox.shrink(); @@ -24,85 +26,77 @@ Widget liveRcmdPanel( padding: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace), child: GestureDetector( onTap: () => PageUtils.pushDynDetail(item, floor), - child: LayoutBuilder( - builder: (context, box) { - double width = box.maxWidth; - return Stack( - clipBehavior: Clip.none, - children: [ - Hero( - tag: liveRcmd.roomId.toString(), - child: NetworkImgLayer( - width: width, - height: width / StyleString.aspectRatio, - src: liveRcmd.cover, - quality: 40, - ), + child: Stack( + clipBehavior: Clip.none, + children: [ + NetworkImgLayer( + width: maxWidth, + height: maxWidth / StyleString.aspectRatio, + src: liveRcmd.cover, + quality: 40, + ), + PBadge( + text: liveRcmd.watchedShow?.textLarge, + top: 6, + right: 65, + fontSize: 10.5, + type: PBadgeType.gray, + ), + if (liveRcmd.liveStatus == 1) + Positioned( + right: 6, + top: 6, + child: Image.asset( + height: 16, + 'assets/images/live/live.gif', + filterQuality: FilterQuality.low, ), - PBadge( - text: liveRcmd.watchedShow?.textLarge, - top: 6, - right: 65, - fontSize: 10.5, - type: PBadgeType.gray, - ), - if (liveRcmd.liveStatus == 1) - Positioned( - right: 6, - top: 6, - child: Image.asset( - height: 16, - 'assets/images/live/live.gif', - filterQuality: FilterQuality.low, + ) + else + const PBadge( + text: '直播结束', + top: 6, + right: 6, + type: PBadgeType.gray, + ), + if (liveRcmd.areaName != null) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: 80, + alignment: Alignment.bottomLeft, + padding: const EdgeInsets.fromLTRB(12, 0, 10, 10), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black45, + ], ), - ) - else - const PBadge( - text: '直播结束', - top: 6, - right: 6, - type: PBadgeType.gray, + borderRadius: floor == 1 + ? const BorderRadius.only( + bottomLeft: StyleString.imgRadius, + bottomRight: StyleString.imgRadius, + ) + : const BorderRadius.only( + bottomLeft: Radius.circular(6), + bottomRight: Radius.circular(6), + ), ), - if (liveRcmd.areaName != null) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - height: 80, - alignment: Alignment.bottomLeft, - padding: const EdgeInsets.fromLTRB(12, 0, 10, 10), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black45, - ], - ), - borderRadius: floor == 1 - ? const BorderRadius.only( - bottomLeft: StyleString.imgRadius, - bottomRight: StyleString.imgRadius, - ) - : const BorderRadius.only( - bottomLeft: Radius.circular(6), - bottomRight: Radius.circular(6), - ), - ), - child: Text( - liveRcmd.areaName!, - style: TextStyle( - fontSize: theme.textTheme.labelMedium!.fontSize, - color: Colors.white, - ), - ), + child: Text( + liveRcmd.areaName!, + style: TextStyle( + fontSize: theme.textTheme.labelMedium!.fontSize, + color: Colors.white, ), ), - ], - ); - }, + ), + ), + ], ), ), ), diff --git a/lib/pages/dynamics/widgets/module_panel.dart b/lib/pages/dynamics/widgets/module_panel.dart index e8d548186..b9eebf920 100644 --- a/lib/pages/dynamics/widgets/module_panel.dart +++ b/lib/pages/dynamics/widgets/module_panel.dart @@ -27,6 +27,7 @@ Widget module( bool isDetail, Function(List, int)? callback, { floor = 1, + required double maxWidth, }) { switch (item.type) { // 图文 @@ -46,6 +47,7 @@ Widget module( 'archive', callback, floor: floor, + maxWidth: maxWidth, ); // 转发 case 'DYNAMIC_TYPE_FORWARD': @@ -78,6 +80,7 @@ Widget module( return const SizedBox.shrink(); } } + maxWidth -= 30; return InkWell( onTap: () => PageUtils.pushDynDetail(orig, floor + 1), onLongPress: () { @@ -160,6 +163,7 @@ Widget module( isDetail, callback, floor: floor + 1, + maxWidth: maxWidth, ), module( theme, @@ -169,18 +173,30 @@ Widget module( isDetail, callback, floor: floor + 1, + maxWidth: maxWidth, ), if (orig.modules.moduleDynamic?.additional != null) addWidget(theme, orig, context, floor: floor + 1), if (orig.modules.moduleDynamic?.major?.blocked != null) - blockedItem(theme, orig.modules.moduleDynamic!.major!.blocked!), + blockedItem( + theme, + orig.modules.moduleDynamic!.major!.blocked!, + maxWidth: maxWidth, + ), ], ), ), ); // 直播 case 'DYNAMIC_TYPE_LIVE_RCMD': - return liveRcmdPanel(theme, isDetail, item, context, floor: floor); + return liveRcmdPanel( + theme, + isDetail, + item, + context, + floor: floor, + maxWidth: maxWidth, + ); // 直播 case 'DYNAMIC_TYPE_LIVE': return livePanel(theme, isDetail, item, context, floor: floor); @@ -194,6 +210,7 @@ Widget module( context, 'ugcSeason', callback, + maxWidth: maxWidth, ); case 'DYNAMIC_TYPE_PGC': return videoSeasonWidget( @@ -205,6 +222,7 @@ Widget module( 'pgc', callback, floor: floor, + maxWidth: maxWidth, ); case 'DYNAMIC_TYPE_PGC_UNION': return videoSeasonWidget( @@ -216,6 +234,7 @@ Widget module( 'pgc', callback, floor: floor, + maxWidth: maxWidth, ); case 'DYNAMIC_TYPE_NONE': return Row( @@ -442,7 +461,14 @@ Widget module( case 'DYNAMIC_TYPE_SUBSCRIPTION_NEW' when item.modules.moduleDynamic?.major?.type == 'MAJOR_TYPE_SUBSCRIPTION_NEW': - return livePanelSub(theme, isDetail, item, context, floor: floor); + return livePanelSub( + theme, + isDetail, + item, + context, + floor: floor, + maxWidth: maxWidth, + ); default: return Padding( diff --git a/lib/pages/dynamics/widgets/rich_node_panel.dart b/lib/pages/dynamics/widgets/rich_node_panel.dart index 6b947703d..2b1a7f03c 100644 --- a/lib/pages/dynamics/widgets/rich_node_panel.dart +++ b/lib/pages/dynamics/widgets/rich_node_panel.dart @@ -21,8 +21,9 @@ import 'package:get/get.dart'; TextSpan? richNode( ThemeData theme, DynamicItemModel item, - BuildContext context, -) { + BuildContext context, { + required double maxWidth, +}) { try { late final style = TextStyle(color: theme.colorScheme.primary); List spanChildren = []; @@ -246,21 +247,17 @@ TextSpan? richNode( ..add(const TextSpan(text: '\n')) ..add( WidgetSpan( - child: LayoutBuilder( - builder: (context, constraints) { - return imageView( - constraints.maxWidth, - i.pics! - .map( - (item) => ImageModel( - url: item.src ?? '', - width: item.width, - height: item.height, - ), - ) - .toList(), - ); - }, + child: imageView( + maxWidth, + i.pics! + .map( + (item) => ImageModel( + url: item.src ?? '', + width: item.width, + height: item.height, + ), + ) + .toList(), ), ), ); diff --git a/lib/pages/dynamics/widgets/video_panel.dart b/lib/pages/dynamics/widgets/video_panel.dart index 0734f3623..4d7f47b1c 100644 --- a/lib/pages/dynamics/widgets/video_panel.dart +++ b/lib/pages/dynamics/widgets/video_panel.dart @@ -16,6 +16,7 @@ Widget videoSeasonWidget( String type, Function(List, int)? callback, { floor = 1, + required double maxWidth, }) { if (item.modules.moduleDynamic?.major?.type == 'MAJOR_TYPE_NONE') { return item.modules.moduleDynamic?.major?.none?.tips != null @@ -56,87 +57,82 @@ Widget videoSeasonWidget( } Widget buildCover() { - return LayoutBuilder( - builder: (context, box) { - double width = box.maxWidth; - return Stack( - clipBehavior: Clip.none, - children: [ - NetworkImgLayer( - width: width, - height: width / StyleString.aspectRatio, - src: itemContent.cover, - quality: 40, - ), - if (itemContent.badge?.text != null) - PBadge( - text: itemContent.badge!.text, - top: 8.0, - right: 10.0, - bottom: null, - left: null, - type: switch (itemContent.badge!.text) { - '充电专属' => PBadgeType.error, - _ => PBadgeType.primary, - }, + return Stack( + clipBehavior: Clip.none, + children: [ + NetworkImgLayer( + width: maxWidth, + height: maxWidth / StyleString.aspectRatio, + src: itemContent.cover, + quality: 40, + ), + if (itemContent.badge?.text != null) + PBadge( + text: itemContent.badge!.text, + top: 8.0, + right: 10.0, + bottom: null, + left: null, + type: switch (itemContent.badge!.text) { + '充电专属' => PBadgeType.error, + _ => PBadgeType.primary, + }, + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: 70, + alignment: Alignment.bottomLeft, + padding: const EdgeInsets.fromLTRB(10, 0, 8, 8), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black54, + ], ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - height: 70, - alignment: Alignment.bottomLeft, - padding: const EdgeInsets.fromLTRB(10, 0, 8, 8), - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black54, - ], - ), - borderRadius: BorderRadius.only( - bottomLeft: StyleString.imgRadius, - bottomRight: StyleString.imgRadius, - ), - ), - child: DefaultTextStyle.merge( - style: TextStyle( - fontSize: theme.textTheme.labelMedium!.fontSize, - color: Colors.white, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (itemContent.durationText != null) ...[ - DecoratedBox( - decoration: const BoxDecoration( - color: Colors.black45, - borderRadius: BorderRadius.all(Radius.circular(4)), - ), - child: Text(' ${itemContent.durationText} '), - ), - const SizedBox(width: 6), - ], - Text('${NumUtil.numFormat(itemContent.stat?.play)}次围观'), - const SizedBox(width: 6), - Text('${NumUtil.numFormat(itemContent.stat?.danmu)}条弹幕'), - const Spacer(), - Image.asset( - 'assets/images/play.png', - width: 50, - height: 50, + borderRadius: BorderRadius.only( + bottomLeft: StyleString.imgRadius, + bottomRight: StyleString.imgRadius, + ), + ), + child: DefaultTextStyle.merge( + style: TextStyle( + fontSize: theme.textTheme.labelMedium!.fontSize, + color: Colors.white, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (itemContent.durationText != null) ...[ + DecoratedBox( + decoration: const BoxDecoration( + color: Colors.black45, + borderRadius: BorderRadius.all(Radius.circular(4)), ), - ], + child: Text(' ${itemContent.durationText} '), + ), + const SizedBox(width: 6), + ], + Text('${NumUtil.numFormat(itemContent.stat?.play)}次围观'), + const SizedBox(width: 6), + Text('${NumUtil.numFormat(itemContent.stat?.danmu)}条弹幕'), + const Spacer(), + Image.asset( + 'assets/images/play.png', + width: 50, + height: 50, ), - ), + ], ), ), - ], - ); - }, + ), + ), + ], ); } diff --git a/lib/pages/dynamics_detail/view.dart b/lib/pages/dynamics_detail/view.dart index ac1224e79..aea0f0988 100644 --- a/lib/pages/dynamics_detail/view.dart +++ b/lib/pages/dynamics_detail/view.dart @@ -154,10 +154,13 @@ class _DynamicDetailPageState extends CommonDynPageState { sliver: SliverMainAxisGroup( slivers: [ SliverToBoxAdapter( - child: DynamicPanel( - item: controller.dynItem, - isDetail: true, - callback: imageCallback, + child: LayoutBuilder( + builder: (_, constrains) => DynamicPanel( + item: controller.dynItem, + isDetail: true, + callback: imageCallback, + maxWidth: constrains.maxWidth, + ), ), ), buildReplyHeader(theme), @@ -184,10 +187,13 @@ class _DynamicDetailPageState extends CommonDynPageState { bottom: MediaQuery.paddingOf(context).bottom + 80, ), sliver: SliverToBoxAdapter( - child: DynamicPanel( - item: controller.dynItem, - isDetail: true, - callback: imageCallback, + child: LayoutBuilder( + builder: (_, constraints) => DynamicPanel( + item: controller.dynItem, + isDetail: true, + callback: imageCallback, + maxWidth: constraints.maxWidth, + ), ), ), ), diff --git a/lib/pages/dynamics_tab/view.dart b/lib/pages/dynamics_tab/view.dart index 1a8fb4fee..2915b01a6 100644 --- a/lib/pages/dynamics_tab/view.dart +++ b/lib/pages/dynamics_tab/view.dart @@ -15,9 +15,11 @@ import 'package:PiliPlus/pages/dynamics_tab/controller.dart'; import 'package:PiliPlus/pages/main/controller.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:waterfall_flow/waterfall_flow.dart'; +import 'package:waterfall_flow/waterfall_flow.dart' + hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; class DynamicsTabPage extends CommonPage { const DynamicsTabPage({super.key, required this.dynamicsType}); @@ -133,6 +135,8 @@ class _DynamicsTabPageState ); } + late double _maxWidth; + Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { Loading() => DynamicsTabPage.dynSkeleton( @@ -141,26 +145,28 @@ class _DynamicsTabPageState Success(:var response) => response?.isNotEmpty == true ? GlobalData().dynamicsWaterfallFlow - ? SliverWaterfallFlow.extent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - crossAxisSpacing: StyleString.cardSpace / 2, - lastChildLayoutTypeBuilder: (index) { - if (index == response.length - 1) { - controller.onLoadMore(); - } - return index == response.length - ? LastChildLayoutType.foot - : LastChildLayoutType.none; - }, - children: [ - for (int index = 0; index < response!.length; index++) - DynamicPanel( + ? SliverWaterfallFlow( + gridDelegate: + SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + crossAxisSpacing: StyleString.cardSpace / 2, + callback: (value) => _maxWidth = value, + ), + delegate: SliverChildBuilderDelegate( + (_, index) { + if (index == response.length - 1) { + controller.onLoadMore(); + } + return DynamicPanel( item: response[index], onRemove: (idStr) => controller.onRemove(index, idStr), onBlock: () => controller.onBlock(index), - ), - ], + maxWidth: _maxWidth, + ); + }, + childCount: response!.length, + ), ) : SliverCrossAxisGroup( slivers: [ @@ -178,6 +184,7 @@ class _DynamicsTabPageState onRemove: (idStr) => controller.onRemove(index, idStr), onBlock: () => controller.onBlock(index), + maxWidth: _maxWidth, ); }, itemCount: response!.length, diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart index 86ce4566f..829e6561e 100644 --- a/lib/pages/dynamics_topic/view.dart +++ b/lib/pages/dynamics_topic/view.dart @@ -19,12 +19,14 @@ import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/num_util.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/material.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:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:waterfall_flow/waterfall_flow.dart'; +import 'package:waterfall_flow/waterfall_flow.dart' + hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; class DynTopicPage extends StatefulWidget { const DynTopicPage({super.key}); @@ -344,6 +346,8 @@ class _DynTopicPageState extends State { }; } + late double _maxWidth; + Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { Loading() => DynamicsTabPage.dynSkeleton( @@ -352,24 +356,31 @@ class _DynTopicPageState extends State { Success(:var response) => response?.isNotEmpty == true ? GlobalData().dynamicsWaterfallFlow - ? SliverWaterfallFlow.extent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - crossAxisSpacing: StyleString.cardSpace / 2, - lastChildLayoutTypeBuilder: (index) { - if (index == response.length - 1) { - _controller.onLoadMore(); - } - return index == response.length - ? LastChildLayoutType.foot - : LastChildLayoutType.none; - }, - children: [ - for (var item in response!) - if (item.dynamicCardItem != null) - DynamicPanel(item: item.dynamicCardItem!) - else - Text(item.topicType ?? 'err'), - ], + ? SliverWaterfallFlow( + gridDelegate: + SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + crossAxisSpacing: StyleString.cardSpace / 2, + callback: (value) => _maxWidth = value, + ), + delegate: SliverChildBuilderDelegate( + (_, index) { + if (index == response.length - 1) { + _controller.onLoadMore(); + } + + final item = response[index]; + if (item.dynamicCardItem != null) { + return DynamicPanel( + item: item.dynamicCardItem!, + maxWidth: _maxWidth, + ); + } + + return Text(item.topicType ?? 'err'); + }, + childCount: response!.length, + ), ) : SliverCrossAxisGroup( slivers: [ @@ -385,6 +396,7 @@ class _DynTopicPageState extends State { if (item.dynamicCardItem != null) { return DynamicPanel( item: item.dynamicCardItem!, + maxWidth: _maxWidth, ); } else { return Text(item.topicType ?? 'err'); @@ -396,9 +408,7 @@ class _DynTopicPageState extends State { const SliverFillRemaining(), ], ) - : HttpError( - onReload: _controller.onReload, - ), + : HttpError(onReload: _controller.onReload), Error(:var errMsg) => HttpError( errMsg: errMsg, onReload: _controller.onReload, diff --git a/lib/pages/member_dynamics/view.dart b/lib/pages/member_dynamics/view.dart index 9cbecf733..2f3dd50cd 100644 --- a/lib/pages/member_dynamics/view.dart +++ b/lib/pages/member_dynamics/view.dart @@ -9,9 +9,11 @@ import 'package:PiliPlus/pages/member_dynamics/controller.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:waterfall_flow/waterfall_flow.dart'; +import 'package:waterfall_flow/waterfall_flow.dart' + hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; class MemberDynamicsPage extends StatefulWidget { const MemberDynamicsPage({super.key, this.mid}); @@ -72,6 +74,8 @@ class _MemberDynamicsPageState extends State ), ); + late double _maxWidth; + Widget _buildContent(LoadingState?> loadingState) { return switch (loadingState) { Loading() => DynamicsTabPage.dynSkeleton( @@ -80,26 +84,27 @@ class _MemberDynamicsPageState extends State Success(:var response) => response?.isNotEmpty == true ? GlobalData().dynamicsWaterfallFlow - ? SliverWaterfallFlow.extent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - crossAxisSpacing: StyleString.safeSpace, - lastChildLayoutTypeBuilder: (index) { - if (index == response.length - 1) { - _memberDynamicController.onLoadMore(); - } - return index == response.length - ? LastChildLayoutType.foot - : LastChildLayoutType.none; - }, - children: response! - .map( - (item) => DynamicPanel( - item: item, - onRemove: _memberDynamicController.onRemove, - onSetTop: _memberDynamicController.onSetTop, - ), - ) - .toList(), + ? SliverWaterfallFlow( + gridDelegate: + SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + crossAxisSpacing: StyleString.cardSpace / 2, + callback: (value) => _maxWidth = value, + ), + delegate: SliverChildBuilderDelegate( + (_, index) { + if (index == response.length - 1) { + _memberDynamicController.onLoadMore(); + } + return DynamicPanel( + item: response[index], + onRemove: _memberDynamicController.onRemove, + onSetTop: _memberDynamicController.onSetTop, + maxWidth: _maxWidth, + ); + }, + childCount: response!.length, + ), ) : SliverCrossAxisGroup( slivers: [ @@ -115,6 +120,7 @@ class _MemberDynamicsPageState extends State item: response[index], onRemove: _memberDynamicController.onRemove, onSetTop: _memberDynamicController.onSetTop, + maxWidth: _maxWidth, ); }, itemCount: response!.length, diff --git a/lib/pages/member_opus/view.dart b/lib/pages/member_opus/view.dart index eb93426a6..4c7584818 100644 --- a/lib/pages/member_opus/view.dart +++ b/lib/pages/member_opus/view.dart @@ -7,9 +7,11 @@ import 'package:PiliPlus/models_new/space/space_opus/item.dart'; import 'package:PiliPlus/pages/member_opus/controller.dart'; import 'package:PiliPlus/pages/member_opus/widgets/space_opus_item.dart'; import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:waterfall_flow/waterfall_flow.dart'; +import 'package:waterfall_flow/waterfall_flow.dart' + hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; class MemberOpus extends StatefulWidget { const MemberOpus({ @@ -125,21 +127,23 @@ class _MemberOpusState extends State ), Success(:var response) => response?.isNotEmpty == true - ? SliverWaterfallFlow.extent( - maxCrossAxisExtent: Grid.smallCardWidth, - mainAxisSpacing: StyleString.safeSpace, - crossAxisSpacing: StyleString.safeSpace, - lastChildLayoutTypeBuilder: (index) { - if (index == response.length - 1) { - _controller.onLoadMore(); - } - return index == response.length - ? LastChildLayoutType.foot - : LastChildLayoutType.none; - }, - children: response! - .map((item) => SpaceOpusItem(item: item)) - .toList(), + ? SliverWaterfallFlow( + gridDelegate: SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth, + mainAxisSpacing: StyleString.safeSpace, + crossAxisSpacing: StyleString.safeSpace, + ), + delegate: SliverChildBuilderDelegate( + (_, index) { + if (index == response.length - 1) { + _controller.onLoadMore(); + } + return SpaceOpusItem( + item: response[index], + ); + }, + childCount: response!.length, + ), ) : HttpError( onReload: _controller.onReload, diff --git a/lib/pages/member_search/child/view.dart b/lib/pages/member_search/child/view.dart index 79f431ecf..f2685dc0a 100644 --- a/lib/pages/member_search/child/view.dart +++ b/lib/pages/member_search/child/view.dart @@ -10,9 +10,11 @@ import 'package:PiliPlus/pages/dynamics_tab/view.dart'; import 'package:PiliPlus/pages/member_search/child/controller.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:waterfall_flow/waterfall_flow.dart'; +import 'package:waterfall_flow/waterfall_flow.dart' + hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; class MemberSearchChildPage extends StatefulWidget { const MemberSearchChildPage({ @@ -70,6 +72,8 @@ class _MemberSearchChildPageState extends State }; } + late double _maxWidth; + Widget _buildBody(LoadingState loadingState) { return switch (loadingState) { Loading() => _buildLoading, @@ -94,21 +98,25 @@ class _MemberSearchChildPageState extends State ), MemberSearchType.dynamic => GlobalData().dynamicsWaterfallFlow - ? SliverWaterfallFlow.extent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - crossAxisSpacing: StyleString.safeSpace, - mainAxisSpacing: StyleString.safeSpace, - lastChildLayoutTypeBuilder: (index) { - if (index == response.length - 1) { - _controller.onLoadMore(); - } - return index == response.length - ? LastChildLayoutType.foot - : LastChildLayoutType.none; - }, - children: response! - .map((item) => DynamicPanel(item: item)) - .toList(), + ? SliverWaterfallFlow( + gridDelegate: + SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + crossAxisSpacing: StyleString.safeSpace, + callback: (value) => _maxWidth = value, + ), + delegate: SliverChildBuilderDelegate( + (_, index) { + if (index == response.length - 1) { + _controller.onLoadMore(); + } + return DynamicPanel( + item: response[index], + maxWidth: _maxWidth, + ); + }, + childCount: response!.length, + ), ) : SliverCrossAxisGroup( slivers: [ @@ -122,6 +130,7 @@ class _MemberSearchChildPageState extends State } return DynamicPanel( item: response[index], + maxWidth: _maxWidth, ); }, itemCount: response!.length, diff --git a/lib/pages/save_panel/view.dart b/lib/pages/save_panel/view.dart index 7a4563909..24193ee85 100644 --- a/lib/pages/save_panel/view.dart +++ b/lib/pages/save_panel/view.dart @@ -340,10 +340,13 @@ class _SavePanelState extends State { ) else if (_item is DynamicItemModel) IgnorePointer( - child: DynamicPanel( - item: _item, - isDetail: true, - isSave: true, + child: LayoutBuilder( + builder: (_, constrains) => DynamicPanel( + item: _item, + isDetail: true, + isSave: true, + maxWidth: constrains.maxWidth, + ), ), ), if (cover?.isNotEmpty == true && diff --git a/lib/pages/search_panel/all/view.dart b/lib/pages/search_panel/all/view.dart index 04623b6ee..5be5f84e6 100644 --- a/lib/pages/search_panel/all/view.dart +++ b/lib/pages/search_panel/all/view.dart @@ -7,9 +7,11 @@ import 'package:PiliPlus/pages/search_panel/pgc/widgets/item.dart'; import 'package:PiliPlus/pages/search_panel/user/widgets/item.dart'; import 'package:PiliPlus/pages/search_panel/view.dart'; import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:waterfall_flow/waterfall_flow.dart'; +import 'package:waterfall_flow/waterfall_flow.dart' + hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; class SearchAllPanel extends CommonSearchPanel { const SearchAllPanel({ @@ -37,63 +39,59 @@ class _SearchAllPanelState @override Widget buildList(ThemeData theme, List list) { - return SliverWaterfallFlow.extent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - crossAxisSpacing: StyleString.safeSpace, - lastChildLayoutTypeBuilder: (index) { - if (index == list.length - 1) { - controller.onLoadMore(); - } - return index == list.length - ? LastChildLayoutType.foot - : LastChildLayoutType.none; - }, - children: list - .map( - (item) => switch (item) { - SearchVideoItemModel() => SizedBox( - height: 120, - child: VideoCardH(videoItem: item), - ), - List() => - item.length == 1 - ? SizedBox( - height: 160, - child: SearchPgcItem(item: item.first), - ) - : SizedBox( - height: - Grid.smallCardWidth / 2 / 0.75 + - MediaQuery.textScalerOf(context).scale(60), - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 7), - physics: const AlwaysScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - itemCount: item.length, - itemBuilder: (context, index) { - return Container( - width: Grid.smallCardWidth / 2, - margin: EdgeInsets.only( - left: StyleString.safeSpace, - right: index == item.length - 1 - ? StyleString.safeSpace - : 0, - ), - child: PgcCardVSearch(item: item[index]), - ); - }, - ), + return SliverWaterfallFlow( + gridDelegate: SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + crossAxisSpacing: StyleString.safeSpace, + ), + delegate: SliverChildBuilderDelegate( + (_, index) { + if (index == list.length - 1) { + controller.onLoadMore(); + } + return switch (list[index]) { + SearchVideoItemModel e => SizedBox( + height: 120, + child: VideoCardH(videoItem: e), + ), + List e => + e.length == 1 + ? SizedBox( + height: 160, + child: SearchPgcItem(item: e.first), + ) + : SizedBox( + height: + Grid.smallCardWidth / 2 / 0.75 + + MediaQuery.textScalerOf(context).scale(60), + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 7), + physics: const AlwaysScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: e.length, + itemBuilder: (context, index) { + return Container( + width: Grid.smallCardWidth / 2, + margin: EdgeInsets.only( + left: StyleString.safeSpace, + right: index == e.length - 1 + ? StyleString.safeSpace + : 0, + ), + child: PgcCardVSearch(item: e[index]), + ); + }, ), - SearchUserItemModel() => Padding( - padding: const EdgeInsets.only(bottom: 5), - child: SearchUserItem( - item: item, - ), - ), - _ => const SizedBox.shrink(), - }, - ) - .toList(), + ), + SearchUserItemModel e => Padding( + padding: const EdgeInsets.only(bottom: 5), + child: SearchUserItem(item: e), + ), + _ => const SizedBox.shrink(), + }; + }, + childCount: list.length, + ), ); } } diff --git a/lib/pages/settings_search/view.dart b/lib/pages/settings_search/view.dart index f8dcdc7af..04601b2a7 100644 --- a/lib/pages/settings_search/view.dart +++ b/lib/pages/settings_search/view.dart @@ -8,9 +8,11 @@ import 'package:PiliPlus/pages/setting/models/recommend_settings.dart'; import 'package:PiliPlus/pages/setting/models/style_settings.dart'; import 'package:PiliPlus/pages/setting/models/video_settings.dart'; import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:waterfall_flow/waterfall_flow.dart'; +import 'package:waterfall_flow/waterfall_flow.dart' + hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; class SettingsSearchPage extends StatefulWidget { const SettingsSearchPage({super.key}); @@ -96,9 +98,15 @@ class _SettingsSearchPageState extends SearchState { sliver: Obx( () => _list.isEmpty ? const HttpError() - : SliverWaterfallFlow.extent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - children: _list.map((item) => item.widget).toList(), + : SliverWaterfallFlow( + gridDelegate: + SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + ), + delegate: SliverChildBuilderDelegate( + (_, index) => _list[index].widget, + childCount: _list.length, + ), ), ), ), diff --git a/lib/utils/waterfall.dart b/lib/utils/waterfall.dart new file mode 100644 index 000000000..857c6ae8c --- /dev/null +++ b/lib/utils/waterfall.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart' show ValueChanged; +import 'package:flutter/rendering.dart' show SliverConstraints; +import 'package:waterfall_flow/waterfall_flow.dart' + show SliverWaterfallFlowDelegate; + +class SliverWaterfallFlowDelegateWithMaxCrossAxisExtent + extends SliverWaterfallFlowDelegate { + /// Creates a delegate that makes masonry layouts with tiles that have a maximum + /// cross-axis extent. + /// + /// All of the arguments must not be null. The [maxCrossAxisExtent], + /// [mainAxisSpacing], and [crossAxisSpacing] arguments must not be negative. + SliverWaterfallFlowDelegateWithMaxCrossAxisExtent({ + required this.maxCrossAxisExtent, + super.mainAxisSpacing, + super.crossAxisSpacing, + super.lastChildLayoutTypeBuilder, + super.collectGarbage, + super.viewportBuilder, + super.closeToTrailing, + this.callback, + }) : assert(maxCrossAxisExtent >= 0); + + /// The maximum extent of tiles in the cross axis. + /// + /// This delegate will select a cross-axis extent for the tiles that is as + /// large as possible subject to the following conditions: + /// + /// - The extent evenly divides the cross-axis extent of the grid. + /// - The extent is at most [maxCrossAxisExtent]. + /// + /// For example, if the grid is vertical, the grid is 500.0 pixels wide, and + /// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4 + /// columns that are 125.0 pixels wide. + final double maxCrossAxisExtent; + + int? crossAxisCount; + double? crossAxisExtent; + + final ValueChanged? callback; + + @override + int getCrossAxisCount(SliverConstraints constraints) { + if (crossAxisCount != null && + constraints.crossAxisExtent == crossAxisExtent) { + return crossAxisCount!; + } + crossAxisExtent = constraints.crossAxisExtent; + crossAxisCount = + (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)) + .ceil(); + callback?.call(constraints.crossAxisExtent / crossAxisCount!); + return crossAxisCount!; + } + + @override + bool shouldRelayout(SliverWaterfallFlowDelegate oldDelegate) { + final flag = + (oldDelegate.runtimeType != runtimeType) || + (oldDelegate is SliverWaterfallFlowDelegateWithMaxCrossAxisExtent && + (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || + super.shouldRelayout(oldDelegate))); + if (flag) { + crossAxisCount = null; + } + return flag; + } +}