diff --git a/lib/common/widgets/image_grid/image_grid_view.dart b/lib/common/widgets/image_grid/image_grid_view.dart index a29fb6297..e7df76322 100644 --- a/lib/common/widgets/image_grid/image_grid_view.dart +++ b/lib/common/widgets/image_grid/image_grid_view.dart @@ -33,7 +33,7 @@ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show HapticFeedback; import 'package:get/get_core/src/get_main.dart'; -import 'package:get/get_navigation/get_navigation.dart'; +import 'package:get/get_navigation/src/extension_navigation.dart'; class ImageModel { ImageModel({ @@ -74,14 +74,14 @@ class ImageGridView extends StatelessWidget { final bool fullScreen; static bool horizontalPreview = Pref.horizontalPreview; - static const _routes = ['/videoV', '/dynamicDetail']; + static final _regex = RegExp(r'/videoV|/dynamicDetail$|/articlePage'); void _onTap(BuildContext context, int index) { final imgList = picArr.map( (item) { bool isLive = item.isLivePhoto; return SourceModel( - sourceType: isLive ? SourceType.livePhoto : SourceType.networkImage, + sourceType: isLive ? .livePhoto : .networkImage, url: item.url, liveUrl: isLive ? item.liveUrl : null, width: isLive ? item.width.toInt() : null, @@ -92,7 +92,7 @@ class ImageGridView extends StatelessWidget { ).toList(); if (horizontalPreview && !fullScreen && - _routes.contains(Get.currentRoute) && + Get.currentRoute.startsWith(_regex) && !context.mediaQuerySize.isPortrait) { final scaffoldState = Scaffold.maybeOf(context); if (scaffoldState != null) { diff --git a/lib/pages/article/controller.dart b/lib/pages/article/controller.dart index 716e94522..c2e1470d9 100644 --- a/lib/pages/article/controller.dart +++ b/lib/pages/article/controller.dart @@ -32,8 +32,6 @@ class ArticleController extends CommonDynController { late final RxInt topIndex = 0.obs; - late final showDynActionBar = Pref.showDynActionBar; - @override dynamic get sourceId => commentType == 12 ? 'cv$commentId' : id; diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index b0a2ff665..5d92d25f1 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -68,13 +68,12 @@ class _ArticlePageState extends CommonDynPageState { appBar: _buildAppBar(), body: Padding( padding: EdgeInsets.only(left: padding.left, right: padding.right), - child: Stack( - clipBehavior: Clip.none, - children: [ - _buildPage(theme), - _buildBottom(theme), - ], - ), + child: _buildPage(theme), + ), + floatingActionButtonLocation: floatingActionButtonLocation, + floatingActionButton: SlideTransition( + position: fabAnimation, + child: _buildBottom(theme), ), ); } @@ -494,177 +493,160 @@ class _ArticlePageState extends CommonDynPageState { ], ); - Widget _buildBottom(ThemeData theme) => Positioned( - left: 0, - bottom: 0, - right: 0, - child: SlideTransition( - position: fabAnim, - child: Builder( - builder: (context) { - if (!controller.showDynActionBar) { - return Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.only( - right: kFloatingActionButtonMargin, - bottom: padding.bottom + kFloatingActionButtonMargin, - ), - child: replyButton, - ), - ); - } + Widget _buildBottom(ThemeData theme) { + if (!controller.showDynActionBar) { + return fabButton; + } - late final primary = theme.colorScheme.primary; - late final outline = theme.colorScheme.outline; - late final btnStyle = TextButton.styleFrom( - tapTargetSize: .padded, - padding: const EdgeInsets.symmetric(horizontal: 15), - foregroundColor: outline, + late final primary = theme.colorScheme.primary; + late final outline = theme.colorScheme.outline; + late final btnStyle = TextButton.styleFrom( + tapTargetSize: .padded, + padding: const EdgeInsets.symmetric(horizontal: 15), + foregroundColor: outline, + ); + + Widget textIconButton({ + required IconData icon, + required String text, + required DynamicStat? stat, + required VoidCallback onPressed, + IconData? activatedIcon, + }) { + final status = stat?.status == true; + final color = status ? primary : outline; + return TextButton.icon( + onPressed: onPressed, + icon: Icon( + status ? activatedIcon : icon, + size: 16, + color: color, + ), + style: btnStyle, + label: Text( + stat?.count != null ? NumUtils.numFormat(stat!.count) : text, + style: TextStyle(color: color), + ), + ); + } + + return Padding( + padding: .only(left: padding.left, right: padding.right), + child: Obx(() { + final stats = controller.stats.value; + + Widget btn = Padding( + padding: EdgeInsets.only( + right: kFloatingActionButtonMargin, + bottom: + kFloatingActionButtonMargin + + (stats != null ? 0 : padding.bottom), + ), + child: replyButton, + ); + + if (stats == null) { + return Align( + alignment: Alignment.bottomRight, + child: btn, ); + } - Widget textIconButton({ - required IconData icon, - required String text, - required DynamicStat? stat, - required VoidCallback onPressed, - IconData? activatedIcon, - }) { - final status = stat?.status == true; - final color = status ? primary : outline; - return TextButton.icon( - onPressed: onPressed, - icon: Icon( - status ? activatedIcon : icon, - size: 16, - color: color, - ), - style: btnStyle, - label: Text( - stat?.count != null ? NumUtils.numFormat(stat!.count) : text, - style: TextStyle(color: color), - ), - ); - } - - return Obx(() { - final stats = controller.stats.value; - - Widget btn = Padding( - padding: EdgeInsets.only( - right: kFloatingActionButtonMargin, - bottom: - kFloatingActionButtonMargin + - (stats != null ? 0 : padding.bottom), - ), - child: replyButton, - ); - - if (stats == null) { - return Align( - alignment: Alignment.centerRight, - child: btn, - ); - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - btn, - Container( - decoration: BoxDecoration( - color: theme.colorScheme.surface, - border: Border( - top: BorderSide( - color: theme.colorScheme.outline.withValues( - alpha: 0.08, - ), - ), + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + btn, + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + top: BorderSide( + color: theme.colorScheme.outline.withValues( + alpha: 0.08, ), ), - padding: EdgeInsets.only(bottom: padding.bottom), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: Builder( - builder: (btnContext) { - final forward = stats.forward; - return textIconButton( - text: '转发', - icon: FontAwesomeIcons.shareFromSquare, - stat: forward, - onPressed: () { - if (controller.opusData == null && - controller.articleData?.dynIdStr == null) { - SmartDialog.showToast( - 'err: ${controller.id}', - ); - return; - } - final summary = controller.summary; - showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - builder: (context) => RepostPanel( - item: controller.opusData, - dynIdStr: controller.articleData?.dynIdStr, - pic: summary.cover, - title: summary.title, - uname: summary.author?.name, - onSuccess: () { - if (forward != null) { - int count = forward.count ?? 0; - forward.count = count + 1; - if (btnContext.mounted) { - (btnContext as Element?) - ?.markNeedsBuild(); - } - } - }, - ), - ); - }, + ), + ), + padding: EdgeInsets.only(bottom: padding.bottom), + child: Row( + children: [ + Expanded( + child: Builder( + builder: (btnContext) { + final forward = stats.forward; + return textIconButton( + text: '转发', + icon: FontAwesomeIcons.shareFromSquare, + stat: forward, + onPressed: () { + if (controller.opusData == null && + controller.articleData?.dynIdStr == null) { + SmartDialog.showToast( + 'err: ${controller.id}', + ); + return; + } + final summary = controller.summary; + showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => RepostPanel( + item: controller.opusData, + dynIdStr: controller.articleData?.dynIdStr, + pic: summary.cover, + title: summary.title, + uname: summary.author?.name, + onSuccess: () { + if (forward != null) { + int count = forward.count ?? 0; + forward.count = count + 1; + if (btnContext.mounted) { + (btnContext as Element?) + ?.markNeedsBuild(); + } + } + }, + ), ); }, - ), - ), - Expanded( - child: textIconButton( - text: '分享', - icon: CustomIcons.share_node, - stat: null, - onPressed: () => Utils.shareText(controller.url), - ), - ), - Expanded( - child: textIconButton( - icon: FontAwesomeIcons.star, - activatedIcon: FontAwesomeIcons.solidStar, - text: '收藏', - stat: stats.favorite, - onPressed: controller.onFav, - ), - ), - Expanded( - child: textIconButton( - icon: FontAwesomeIcons.thumbsUp, - activatedIcon: FontAwesomeIcons.solidThumbsUp, - text: '点赞', - stat: stats.like, - onPressed: controller.onLike, - ), - ), - ], + ); + }, + ), ), - ), - ], - ); - }); - }, - ), - ), - ); + Expanded( + child: textIconButton( + text: '分享', + icon: CustomIcons.share_node, + stat: null, + onPressed: () => Utils.shareText(controller.url), + ), + ), + Expanded( + child: textIconButton( + icon: FontAwesomeIcons.star, + activatedIcon: FontAwesomeIcons.solidStar, + text: '收藏', + stat: stats.favorite, + onPressed: controller.onFav, + ), + ), + Expanded( + child: textIconButton( + icon: FontAwesomeIcons.thumbsUp, + activatedIcon: FontAwesomeIcons.solidThumbsUp, + text: '点赞', + stat: stats.like, + onPressed: controller.onLike, + ), + ), + ], + ), + ), + ], + ); + }), + ); + } } diff --git a/lib/pages/common/dyn/common_dyn_controller.dart b/lib/pages/common/dyn/common_dyn_controller.dart index 99de93002..8aefd2fd2 100644 --- a/lib/pages/common/dyn/common_dyn_controller.dart +++ b/lib/pages/common/dyn/common_dyn_controller.dart @@ -14,6 +14,8 @@ abstract class CommonDynController extends ReplyController { late final horizontalPreview = Pref.horizontalPreview; late final List ratio = Pref.dynamicDetailRatio; + late final showDynActionBar = Pref.showDynActionBar; + @override Future> customGetData() => ReplyGrpc.mainList( type: replyType, diff --git a/lib/pages/common/dyn/common_dyn_page.dart b/lib/pages/common/dyn/common_dyn_page.dart index e8ad0b64b..cc0a32897 100644 --- a/lib/pages/common/dyn/common_dyn_page.dart +++ b/lib/pages/common/dyn/common_dyn_page.dart @@ -9,6 +9,7 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; @@ -22,7 +23,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; abstract class CommonDynPageState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, FabMixin { CommonDynController get controller; late final ScrollController scrollController; @@ -36,26 +37,9 @@ abstract class CommonDynPageState extends State late double maxWidth; late double maxHeight; - bool _showFab = true; - - final fabOffset = const Offset(0, 1); - - late final AnimationController _fabAnimationCtr; - late final Animation fabAnim; - @override void initState() { super.initState(); - _fabAnimationCtr = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - )..forward(); - fabAnim = _fabAnimationCtr.drive( - Tween( - begin: fabOffset, - end: Offset.zero, - ).chain(CurveTween(curve: Curves.easeInOut)), - ); scrollController = ScrollController()..addListener(listener); } @@ -69,20 +53,6 @@ abstract class CommonDynPageState extends State } } - void showFab() { - if (!_showFab) { - _showFab = true; - _fabAnimationCtr.forward(); - } - } - - void hideFab() { - if (_showFab) { - _showFab = false; - _fabAnimationCtr.reverse(); - } - } - @override void didChangeDependencies() { super.didChangeDependencies(); @@ -98,7 +68,6 @@ abstract class CommonDynPageState extends State scrollController ..removeListener(listener) ..dispose(); - _fabAnimationCtr.dispose(); super.dispose(); } @@ -304,6 +273,16 @@ abstract class CommonDynPageState extends State ), ); + FloatingActionButtonLocation get floatingActionButtonLocation => + controller.showDynActionBar + ? const ActionBarLocation() + : const NoBottomPaddingFabLocation(); + + Widget get fabButton => Padding( + padding: .only(bottom: padding.bottom + kFloatingActionButtonMargin), + child: replyButton, + ); + Widget get replyButton => FloatingActionButton( heroTag: null, onPressed: () { diff --git a/lib/pages/common/fab_mixin.dart b/lib/pages/common/fab_mixin.dart new file mode 100644 index 000000000..50255f8c7 --- /dev/null +++ b/lib/pages/common/fab_mixin.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +mixin FabMixin on State, TickerProvider { + bool _isFabVisible = true; + late final AnimationController _fabAnimationCtr; + late final Animation fabAnimation; + + @override + void initState() { + super.initState(); + _fabAnimationCtr = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + fabAnimation = _fabAnimationCtr.drive( + Tween( + begin: Offset.zero, + end: const Offset(0.0, 1.0), + ).chain(CurveTween(curve: Curves.easeInOut)), + ); + } + + void showFab() { + if (!_isFabVisible) { + _isFabVisible = true; + _fabAnimationCtr.reverse(); + } + } + + void hideFab() { + if (_isFabVisible) { + _isFabVisible = false; + _fabAnimationCtr.forward(); + } + } + + @override + void dispose() { + _fabAnimationCtr.dispose(); + super.dispose(); + } +} + +mixin _NoRightMarginMixin on StandardFabLocation { + @override + double getOffsetX(scaffoldGeometry, _) { + return scaffoldGeometry.scaffoldSize.width - + scaffoldGeometry.minInsets.right - + scaffoldGeometry.floatingActionButtonSize.width; + } +} + +mixin _NoBottomPaddingMixin on StandardFabLocation { + @override + double getOffsetY(scaffoldGeometry, _) { + return scaffoldGeometry.contentBottom - + scaffoldGeometry.floatingActionButtonSize.height; + } +} + +class NoRightMarginFabLocation extends StandardFabLocation + with FabFloatOffsetY, _NoRightMarginMixin { + const NoRightMarginFabLocation(); +} + +class NoBottomPaddingFabLocation extends StandardFabLocation + with FabEndOffsetX, _NoBottomPaddingMixin { + const NoBottomPaddingFabLocation(); +} + +class ActionBarLocation extends StandardFabLocation with _NoBottomPaddingMixin { + const ActionBarLocation(); + + @override + double getOffsetX(scaffoldGeometry, _) { + return 0.0; + } +} diff --git a/lib/pages/dynamics_detail/controller.dart b/lib/pages/dynamics_detail/controller.dart index 1d5a31f7c..261ea8155 100644 --- a/lib/pages/dynamics_detail/controller.dart +++ b/lib/pages/dynamics_detail/controller.dart @@ -4,7 +4,6 @@ import 'package:PiliPlus/http/reply.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; import 'package:PiliPlus/utils/id_utils.dart'; -import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -15,8 +14,6 @@ class DynamicDetailController extends CommonDynController { late int replyType; late DynamicItemModel dynItem; - late final showDynActionBar = Pref.showDynActionBar; - @override dynamic get sourceId => replyType == 1 ? IdUtils.av2bv(oid) : oid; diff --git a/lib/pages/dynamics_detail/view.dart b/lib/pages/dynamics_detail/view.dart index c1fdfc0fc..f6080dbea 100644 --- a/lib/pages/dynamics_detail/view.dart +++ b/lib/pages/dynamics_detail/view.dart @@ -70,6 +70,11 @@ class _DynamicDetailPageState extends CommonDynPageState { ) : _buildBody(theme), ), + floatingActionButtonLocation: floatingActionButtonLocation, + floatingActionButton: SlideTransition( + position: fabAnimation, + child: _buildBottom(theme), + ), ); } @@ -319,166 +324,139 @@ class _DynamicDetailPageState extends CommonDynPageState { ], ); } - return Stack( - clipBehavior: Clip.none, - children: [ - child, - _buildBottom(theme), - ], - ); + return child; } Widget _buildBottom(ThemeData theme) { - return Positioned( - left: 0, - right: 0, - bottom: 0, - child: SlideTransition( - position: fabAnim, - child: Builder( - builder: (context) { - if (!controller.showDynActionBar) { - return Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.only( - right: kFloatingActionButtonMargin, - bottom: padding.bottom + kFloatingActionButtonMargin, + if (!controller.showDynActionBar) { + return fabButton; + } + + final primary = theme.colorScheme.primary; + final outline = theme.colorScheme.outline; + final btnStyle = TextButton.styleFrom( + tapTargetSize: .padded, + padding: const EdgeInsets.symmetric(horizontal: 15), + foregroundColor: outline, + ); + + Widget textIconButton({ + required IconData icon, + required String text, + required DynamicStat? stat, + required ValueChanged onPressed, + IconData? activatedIcon, + }) { + final status = stat?.status == true; + final color = status ? primary : outline; + final iconWidget = Icon( + status ? activatedIcon : icon, + size: 16, + color: color, + ); + return TextButton.icon( + onPressed: () => onPressed(iconWidget.color!), + icon: iconWidget, + style: btnStyle, + label: Text( + stat?.count != null ? NumUtils.numFormat(stat!.count) : text, + style: TextStyle(color: color), + ), + ); + } + + final moduleStat = controller.dynItem.modules.moduleStat; + return Padding( + padding: .only(left: padding.left, right: padding.right), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only( + right: kFloatingActionButtonMargin, + bottom: kFloatingActionButtonMargin, + ), + child: replyButton, + ), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + top: BorderSide( + color: theme.colorScheme.outline.withValues( + alpha: 0.08, ), - child: replyButton, ), - ); - } - - final moduleStat = controller.dynItem.modules.moduleStat; - final primary = theme.colorScheme.primary; - final outline = theme.colorScheme.outline; - final btnStyle = TextButton.styleFrom( - tapTargetSize: .padded, - padding: const EdgeInsets.symmetric(horizontal: 15), - foregroundColor: outline, - ); - - Widget textIconButton({ - required IconData icon, - required String text, - required DynamicStat? stat, - required ValueChanged onPressed, - IconData? activatedIcon, - }) { - final status = stat?.status == true; - final color = status ? primary : outline; - final iconWidget = Icon( - status ? activatedIcon : icon, - size: 16, - color: color, - ); - return TextButton.icon( - onPressed: () => onPressed(iconWidget.color!), - icon: iconWidget, - style: btnStyle, - label: Text( - stat?.count != null ? NumUtils.numFormat(stat!.count) : text, - style: TextStyle(color: color), - ), - ); - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, + ), + ), + padding: EdgeInsets.only(bottom: padding.bottom), + child: Row( children: [ - Padding( - padding: const EdgeInsets.only( - right: kFloatingActionButtonMargin, - bottom: kFloatingActionButtonMargin, - ), - child: replyButton, - ), - Container( - decoration: BoxDecoration( - color: theme.colorScheme.surface, - border: Border( - top: BorderSide( - color: theme.colorScheme.outline.withValues( - alpha: 0.08, - ), - ), - ), - ), - padding: EdgeInsets.only(bottom: padding.bottom), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: Builder( - builder: (btnContext) { - final forward = moduleStat?.forward; - return textIconButton( - icon: FontAwesomeIcons.shareFromSquare, - text: '转发', - stat: forward, - onPressed: (_) => showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - builder: (context) => RepostPanel( - item: controller.dynItem, - onSuccess: () { - if (forward != null) { - int count = forward.count ?? 0; - forward.count = count + 1; - if (btnContext.mounted) { - (btnContext as Element) - .markNeedsBuild(); - } - } - }, - ), - ), - ); - }, - ), - ), - Expanded( - child: textIconButton( - icon: CustomIcons.share_node, - text: '分享', - stat: null, - onPressed: (_) => Utils.shareText( - '${HttpString.dynamicShareBaseUrl}/${controller.dynItem.idStr}', + Expanded( + child: Builder( + builder: (btnContext) { + final forward = moduleStat?.forward; + return textIconButton( + icon: FontAwesomeIcons.shareFromSquare, + text: '转发', + stat: forward, + onPressed: (_) => showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => RepostPanel( + item: controller.dynItem, + onSuccess: () { + if (forward != null) { + int count = forward.count ?? 0; + forward.count = count + 1; + if (btnContext.mounted) { + (btnContext as Element).markNeedsBuild(); + } + } + }, ), ), - ), - Expanded( - child: Builder( - builder: (context) { - return textIconButton( - icon: FontAwesomeIcons.thumbsUp, - activatedIcon: FontAwesomeIcons.solidThumbsUp, - text: '点赞', - stat: moduleStat?.like, - onPressed: (iconColor) => - RequestUtils.onLikeDynamic( - controller.dynItem, - iconColor == primary, - () { - if (context.mounted) { - (context as Element).markNeedsBuild(); - } - }, - ), - ); + ); + }, + ), + ), + Expanded( + child: textIconButton( + icon: CustomIcons.share_node, + text: '分享', + stat: null, + onPressed: (_) => Utils.shareText( + '${HttpString.dynamicShareBaseUrl}/${controller.dynItem.idStr}', + ), + ), + ), + Expanded( + child: Builder( + builder: (context) { + return textIconButton( + icon: FontAwesomeIcons.thumbsUp, + activatedIcon: FontAwesomeIcons.solidThumbsUp, + text: '点赞', + stat: moduleStat?.like, + onPressed: (iconColor) => RequestUtils.onLikeDynamic( + controller.dynItem, + iconColor == primary, + () { + if (context.mounted) { + (context as Element).markNeedsBuild(); + } }, ), - ), - ], + ); + }, ), ), ], - ); - }, - ), + ), + ), + ], ), ); } diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index bc60e9732..60a6db53b 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -8,6 +8,8 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/fav_order_type.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/media.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart' + show NoRightMarginFabLocation; import 'package:PiliPlus/pages/dynamics_repost/view.dart'; import 'package:PiliPlus/pages/fav_detail/controller.dart'; import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart'; @@ -58,7 +60,7 @@ class _FavDetailPageState extends State with GridMixin { }, child: Scaffold( resizeToAvoidBottomInset: false, - floatingActionButtonLocation: const CustomFabLocation(), + floatingActionButtonLocation: const NoRightMarginFabLocation(), floatingActionButton: Padding( padding: const EdgeInsets.only( right: kFloatingActionButtonMargin, @@ -507,18 +509,3 @@ class _FavDetailPageState extends State with GridMixin { }; } } - -class CustomFabLocation extends StandardFabLocation with FabFloatOffsetY { - const CustomFabLocation(); - - @override - double getOffsetX( - ScaffoldPrelayoutGeometry scaffoldGeometry, - double adjustment, - ) { - return scaffoldGeometry.scaffoldSize.width - - scaffoldGeometry.minInsets.right - - scaffoldGeometry.floatingActionButtonSize.width + - adjustment; - } -} diff --git a/lib/pages/later/view.dart b/lib/pages/later/view.dart index c38eeadd9..b33e30f32 100644 --- a/lib/pages/later/view.dart +++ b/lib/pages/later/view.dart @@ -5,7 +5,8 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/models/common/later_view_type.dart'; import 'package:PiliPlus/models_new/later/list.dart'; -import 'package:PiliPlus/pages/fav_detail/view.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart' + show NoRightMarginFabLocation; import 'package:PiliPlus/pages/later/base_controller.dart'; import 'package:PiliPlus/pages/later/controller.dart'; import 'package:PiliPlus/utils/accounts.dart'; @@ -74,7 +75,7 @@ class _LaterPageState extends State child: Scaffold( resizeToAvoidBottomInset: false, appBar: _buildAppbar(enableMultiSelect), - floatingActionButtonLocation: const CustomFabLocation(), + floatingActionButtonLocation: const NoRightMarginFabLocation(), floatingActionButton: Padding( padding: const .only(right: kFloatingActionButtonMargin), child: Obx( diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index bd938340c..880148314 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -38,7 +38,6 @@ import 'package:PiliPlus/utils/video_utils.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -373,9 +372,9 @@ class LiveRoomController extends GetxController { void listener() { final userScrollDirection = scrollController.position.userScrollDirection; - if (userScrollDirection == ScrollDirection.forward) { + if (userScrollDirection == .forward) { disableAutoScroll.value = true; - } else if (userScrollDirection == ScrollDirection.reverse) { + } else if (userScrollDirection == .reverse) { final pos = scrollController.position; if (pos.maxScrollExtent - pos.pixels <= 100) { disableAutoScroll.value = false; diff --git a/lib/pages/main_reply/controller.dart b/lib/pages/main_reply/controller.dart index b650f135f..b12ac75ce 100644 --- a/lib/pages/main_reply/controller.dart +++ b/lib/pages/main_reply/controller.dart @@ -3,35 +3,18 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' import 'package:PiliPlus/grpc/reply.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/common/reply_controller.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; -class MainReplyController extends ReplyController - with GetSingleTickerProviderStateMixin { +class MainReplyController extends ReplyController { late final int oid; late final int replyType; @override int get sourceId => oid; - bool _showFab = true; - - late final AnimationController _fabAnimationCtr; - late final Animation fabAnim; - @override void onInit() { super.onInit(); - _fabAnimationCtr = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - )..forward(); - fabAnim = _fabAnimationCtr.drive( - Tween( - begin: const Offset(0.0, 2.0), - end: Offset.zero, - ).chain(CurveTween(curve: Curves.easeInOut)), - ); final args = Get.arguments; oid = args['oid']; replyType = args['replyType']; @@ -39,20 +22,6 @@ class MainReplyController extends ReplyController queryData(); } - void showFab() { - if (!_showFab) { - _showFab = true; - _fabAnimationCtr.forward(); - } - } - - void hideFab() { - if (_showFab) { - _showFab = false; - _fabAnimationCtr.reverse(); - } - } - @override Future> customGetData() => ReplyGrpc.mainList( type: replyType, @@ -64,10 +33,4 @@ class MainReplyController extends ReplyController @override List? getDataList(MainListReply response) => response.replies; - - @override - void onClose() { - _fabAnimationCtr.dispose(); - super.onClose(); - } } diff --git a/lib/pages/main_reply/view.dart b/lib/pages/main_reply/view.dart index 097d91a88..a87fc0933 100644 --- a/lib/pages/main_reply/view.dart +++ b/lib/pages/main_reply/view.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/main_reply/controller.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart'; @@ -38,7 +39,8 @@ class MainReplyPage extends StatefulWidget { } } -class _MainReplyPageState extends State { +class _MainReplyPageState extends State + with SingleTickerProviderStateMixin, FabMixin { final _controller = Get.put( MainReplyController(), tag: Utils.generateRandomString(8), @@ -62,9 +64,9 @@ class _MainReplyPageState extends State { onNotification: (notification) { final direction = notification.direction; if (direction == .forward) { - _controller.showFab(); + showFab(); } else if (direction == .reverse) { - _controller.hideFab(); + hideFab(); } return false; }, @@ -87,22 +89,26 @@ class _MainReplyPageState extends State { ), ).constraintWidth(), ), + floatingActionButtonLocation: const NoBottomPaddingFabLocation(), floatingActionButton: SlideTransition( - position: _controller.fabAnim, - child: FloatingActionButton( - heroTag: null, - onPressed: () { - try { - feedBack(); - _controller.onReply( - null, - oid: _controller.oid, - replyType: _controller.replyType, - ); - } catch (_) {} - }, - tooltip: '评论', - child: const Icon(Icons.reply), + position: fabAnimation, + child: Padding( + padding: .only(bottom: padding.bottom + kFloatingActionButtonMargin), + child: FloatingActionButton( + heroTag: null, + onPressed: () { + try { + feedBack(); + _controller.onReply( + null, + oid: _controller.oid, + replyType: _controller.replyType, + ); + } catch (_) {} + }, + tooltip: '评论', + child: const Icon(Icons.reply), + ), ), ), ); diff --git a/lib/pages/match_info/view.dart b/lib/pages/match_info/view.dart index cea72e373..c903a4456 100644 --- a/lib/pages/match_info/view.dart +++ b/lib/pages/match_info/view.dart @@ -8,6 +8,8 @@ import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/match/match_info/contest.dart'; import 'package:PiliPlus/models_new/match/match_info/team.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart' + show NoBottomPaddingFabLocation; import 'package:PiliPlus/pages/match_info/controller.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/utils/date_utils.dart'; @@ -36,9 +38,6 @@ class _MatchInfoPageState extends CommonDynPageState { @override dynamic get arguments => null; - @override - Offset get fabOffset => const Offset(0, 2); - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -59,9 +58,10 @@ class _MatchInfoPageState extends CommonDynPageState { ), ), ).constraintWidth(), + floatingActionButtonLocation: const NoBottomPaddingFabLocation(), floatingActionButton: SlideTransition( - position: fabAnim, - child: replyButton, + position: fabAnimation, + child: fabButton, ), ); } diff --git a/lib/pages/music/controller.dart b/lib/pages/music/controller.dart index b7e6c62af..ac95011e9 100644 --- a/lib/pages/music/controller.dart +++ b/lib/pages/music/controller.dart @@ -2,7 +2,6 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/music.dart'; import 'package:PiliPlus/models_new/music/bgm_detail.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; -import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:get/get.dart'; class MusicDetailController extends CommonDynController { @@ -18,8 +17,6 @@ class MusicDetailController extends CommonDynController { late final String musicId; - bool get showDynActionBar => Pref.showDynActionBar; - String get shareUrl => 'https://music.bilibili.com/h5/music-detail?music_id=$musicId'; diff --git a/lib/pages/music/view.dart b/lib/pages/music/view.dart index 72a355e97..e637c6bfe 100644 --- a/lib/pages/music/view.dart +++ b/lib/pages/music/view.dart @@ -204,7 +204,24 @@ class _MusicDetailPageState extends CommonDynPageState { }); Widget _buildBottom(ThemeData theme, MusicDetail item) { + if (!controller.showDynActionBar) { + return Positioned( + right: kFloatingActionButtonMargin, + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: fabButton, + ), + ); + } + + final primary = theme.colorScheme.primary; final outline = theme.colorScheme.outline; + final style = TextButton.styleFrom( + tapTargetSize: .padded, + padding: const EdgeInsets.symmetric(horizontal: 15), + foregroundColor: outline, + ); Widget textIconButton({ required IconData icon, @@ -214,7 +231,7 @@ class _MusicDetailPageState extends CommonDynPageState { required VoidCallback onPressed, IconData? activatedIcon, }) { - final color = status ? theme.colorScheme.primary : outline; + final color = status ? primary : outline; return TextButton.icon( onPressed: onPressed, icon: Icon( @@ -222,11 +239,7 @@ class _MusicDetailPageState extends CommonDynPageState { size: 16, color: color, ), - style: TextButton.styleFrom( - tapTargetSize: .padded, - padding: const EdgeInsets.symmetric(horizontal: 15), - foregroundColor: outline, - ), + style: style, label: Text( count != null ? NumUtils.numFormat(count) : text, style: TextStyle(color: color), @@ -239,116 +252,104 @@ class _MusicDetailPageState extends CommonDynPageState { right: 0, bottom: 0, child: SlideTransition( - position: fabAnim, - child: controller.showDynActionBar - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only( - right: kFloatingActionButtonMargin, - bottom: kFloatingActionButtonMargin, + position: fabAnimation, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only( + right: kFloatingActionButtonMargin, + bottom: kFloatingActionButtonMargin, + ), + child: replyButton, + ), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + top: BorderSide( + color: theme.colorScheme.outline.withValues( + alpha: 0.08, ), - child: replyButton, ), - Container( - decoration: BoxDecoration( - color: theme.colorScheme.surface, - border: Border( - top: BorderSide( - color: theme.colorScheme.outline.withValues( - alpha: 0.08, - ), - ), - ), + ), + ), + padding: EdgeInsets.only(bottom: padding.bottom), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // TODO + // Expanded( + // child: textIconButton( + // icon: FontAwesomeIcons.shareFromSquare, + // text: '转发', + // count: item.musicShares, + // onPressed: () { + // final data = controller.infoState.value.dataOrNull; + // if (data != null) { + // showModalBottomSheet( + // context: context, + // isScrollControlled: true, + // useSafeArea: true, + // builder: (context) => RepostPanel( + // rid: controller.oid, + // dynType: null, + // pic: data.mvCover, + // title: data.musicTitle, + // ), + // ); + // } + // }, + // ), + // ), + Expanded( + child: textIconButton( + icon: CustomIcons.share_node, + text: '分享', + onPressed: () => Utils.shareText(controller.shareUrl), ), - padding: EdgeInsets.only(bottom: padding.bottom), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - // TODO - // Expanded( - // child: textIconButton( - // icon: FontAwesomeIcons.shareFromSquare, - // text: '转发', - // count: item.musicShares, - // onPressed: () { - // final data = controller.infoState.value.dataOrNull; - // if (data != null) { - // showModalBottomSheet( - // context: context, - // isScrollControlled: true, - // useSafeArea: true, - // builder: (context) => RepostPanel( - // rid: controller.oid, - // dynType: null, - // pic: data.mvCover, - // title: data.musicTitle, - // ), - // ); - // } - // }, - // ), - // ), - Expanded( - child: textIconButton( - icon: CustomIcons.share_node, - text: '分享', - onPressed: () => - Utils.shareText(controller.shareUrl), - ), - ), - Expanded( - child: Builder( - builder: (context) => textIconButton( - icon: FontAwesomeIcons.thumbsUp, - activatedIcon: FontAwesomeIcons.solidThumbsUp, - text: '点赞', - count: item.wishCount, - status: item.wishListen ?? false, - onPressed: () async { - if (!Accounts.main.isLogin) { - SmartDialog.showToast('请先登录'); - return; - } - final hasLike = item.wishListen ?? false; - final res = await MusicHttp.wishUpdate( - controller.musicId, - hasLike, - ); - if (res.isSuccess) { - if (hasLike) { - item.wishCount--; - } else { - item.wishCount++; - } - item.wishListen = !hasLike; - if (context.mounted) { - (context as Element).markNeedsBuild(); - } - } else { - res.toast(); - } - }, - ), - ), - ), - ], + ), + Expanded( + child: Builder( + builder: (context) => textIconButton( + icon: FontAwesomeIcons.thumbsUp, + activatedIcon: FontAwesomeIcons.solidThumbsUp, + text: '点赞', + count: item.wishCount, + status: item.wishListen ?? false, + onPressed: () async { + if (!Accounts.main.isLogin) { + SmartDialog.showToast('请先登录'); + return; + } + final hasLike = item.wishListen ?? false; + final res = await MusicHttp.wishUpdate( + controller.musicId, + hasLike, + ); + if (res.isSuccess) { + if (hasLike) { + item.wishCount--; + } else { + item.wishCount++; + } + item.wishListen = !hasLike; + if (context.mounted) { + (context as Element).markNeedsBuild(); + } + } else { + res.toast(); + } + }, + ), ), ), ], - ) - : Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.only( - right: kFloatingActionButtonMargin, - bottom: padding.bottom + kFloatingActionButtonMargin, - ), - child: replyButton, - ), ), + ), + ], + ), ), ); } diff --git a/lib/pages/video/reply/controller.dart b/lib/pages/video/reply/controller.dart index 61214c595..be234c4a4 100644 --- a/lib/pages/video/reply/controller.dart +++ b/lib/pages/video/reply/controller.dart @@ -6,11 +6,9 @@ import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/pages/common/reply_controller.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/utils/id_utils.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; -class VideoReplyController extends ReplyController - with GetSingleTickerProviderStateMixin { +class VideoReplyController extends ReplyController { VideoReplyController({ required this.aid, required this.videoType, @@ -26,39 +24,6 @@ class VideoReplyController extends ReplyController @override dynamic get sourceId => IdUtils.av2bv(aid); - bool _isFabVisible = true; - late final AnimationController _fabAnimationCtr; - late final Animation animation; - - @override - void onInit() { - super.onInit(); - _fabAnimationCtr = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 100), - )..forward(); - animation = _fabAnimationCtr.drive( - Tween( - begin: const Offset(0.0, 2.0), - end: Offset.zero, - ).chain(CurveTween(curve: Curves.easeInOut)), - ); - } - - void showFab() { - if (!_isFabVisible) { - _isFabVisible = true; - _fabAnimationCtr.forward(); - } - } - - void hideFab() { - if (_isFabVisible) { - _isFabVisible = false; - _fabAnimationCtr.reverse(); - } - } - @override List? getDataList(MainListReply response) { return response.replies; @@ -72,10 +37,4 @@ class VideoReplyController extends ReplyController cursorNext: cursorNext, offset: paginationReply?.nextOffset, ); - - @override - void onClose() { - _fabAnimationCtr.dispose(); - super.onClose(); - } } diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart index e3be23813..6f971b225 100644 --- a/lib/pages/video/reply/view.dart +++ b/lib/pages/video/reply/view.dart @@ -6,6 +6,7 @@ import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/video/reply/controller.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart'; @@ -13,7 +14,6 @@ import 'package:PiliPlus/utils/feed_back.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; class VideoReplyPanel extends StatefulWidget { @@ -33,7 +33,10 @@ class VideoReplyPanel extends StatefulWidget { } class _VideoReplyPanelState extends State - with AutomaticKeepAliveClientMixin { + with + AutomaticKeepAliveClientMixin, + SingleTickerProviderStateMixin, + FabMixin { late VideoReplyController _videoReplyController; String get heroTag => widget.heroTag; @@ -64,11 +67,12 @@ class _VideoReplyPanelState extends State final theme = Theme.of(context); final child = NotificationListener( onNotification: (notification) { - final direction = notification.direction; - if (direction == ScrollDirection.forward) { - _videoReplyController.showFab(); - } else if (direction == ScrollDirection.reverse) { - _videoReplyController.hideFab(); + switch (notification.direction) { + case .forward: + showFab(); + case .reverse: + hideFab(); + case _: } return false; }, @@ -129,22 +133,28 @@ class _VideoReplyPanelState extends State ], ), Positioned( - right: kFloatingActionButtonMargin, - bottom: kFloatingActionButtonMargin + bottom, + right: 0, + bottom: 0, child: SlideTransition( - position: _videoReplyController.animation, - child: FloatingActionButton( - heroTag: null, - onPressed: () { - feedBack(); - _videoReplyController.onReply( - null, - oid: _videoReplyController.aid, - replyType: _videoReplyController.videoType.replyType, - ); - }, - tooltip: '发表评论', - child: const Icon(Icons.reply), + position: fabAnimation, + child: Padding( + padding: .only( + right: kFloatingActionButtonMargin, + bottom: kFloatingActionButtonMargin + bottom, + ), + child: FloatingActionButton( + heroTag: null, + onPressed: () { + feedBack(); + _videoReplyController.onReply( + null, + oid: _videoReplyController.aid, + replyType: _videoReplyController.videoType.replyType, + ); + }, + tooltip: '发表评论', + child: const Icon(Icons.reply), + ), ), ), ),