opt fab location

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-03-16 10:12:36 +08:00
parent e04affd0fe
commit ed66a4655b
18 changed files with 556 additions and 619 deletions

View File

@@ -33,7 +33,7 @@ import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show HapticFeedback; import 'package:flutter/services.dart' show HapticFeedback;
import 'package:get/get_core/src/get_main.dart'; 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 { class ImageModel {
ImageModel({ ImageModel({
@@ -74,14 +74,14 @@ class ImageGridView extends StatelessWidget {
final bool fullScreen; final bool fullScreen;
static bool horizontalPreview = Pref.horizontalPreview; static bool horizontalPreview = Pref.horizontalPreview;
static const _routes = ['/videoV', '/dynamicDetail']; static final _regex = RegExp(r'/videoV|/dynamicDetail$|/articlePage');
void _onTap(BuildContext context, int index) { void _onTap(BuildContext context, int index) {
final imgList = picArr.map( final imgList = picArr.map(
(item) { (item) {
bool isLive = item.isLivePhoto; bool isLive = item.isLivePhoto;
return SourceModel( return SourceModel(
sourceType: isLive ? SourceType.livePhoto : SourceType.networkImage, sourceType: isLive ? .livePhoto : .networkImage,
url: item.url, url: item.url,
liveUrl: isLive ? item.liveUrl : null, liveUrl: isLive ? item.liveUrl : null,
width: isLive ? item.width.toInt() : null, width: isLive ? item.width.toInt() : null,
@@ -92,7 +92,7 @@ class ImageGridView extends StatelessWidget {
).toList(); ).toList();
if (horizontalPreview && if (horizontalPreview &&
!fullScreen && !fullScreen &&
_routes.contains(Get.currentRoute) && Get.currentRoute.startsWith(_regex) &&
!context.mediaQuerySize.isPortrait) { !context.mediaQuerySize.isPortrait) {
final scaffoldState = Scaffold.maybeOf(context); final scaffoldState = Scaffold.maybeOf(context);
if (scaffoldState != null) { if (scaffoldState != null) {

View File

@@ -32,8 +32,6 @@ class ArticleController extends CommonDynController {
late final RxInt topIndex = 0.obs; late final RxInt topIndex = 0.obs;
late final showDynActionBar = Pref.showDynActionBar;
@override @override
dynamic get sourceId => commentType == 12 ? 'cv$commentId' : id; dynamic get sourceId => commentType == 12 ? 'cv$commentId' : id;

View File

@@ -68,13 +68,12 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
appBar: _buildAppBar(), appBar: _buildAppBar(),
body: Padding( body: Padding(
padding: EdgeInsets.only(left: padding.left, right: padding.right), padding: EdgeInsets.only(left: padding.left, right: padding.right),
child: Stack( child: _buildPage(theme),
clipBehavior: Clip.none, ),
children: [ floatingActionButtonLocation: floatingActionButtonLocation,
_buildPage(theme), floatingActionButton: SlideTransition(
_buildBottom(theme), position: fabAnimation,
], child: _buildBottom(theme),
),
), ),
); );
} }
@@ -494,177 +493,160 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
], ],
); );
Widget _buildBottom(ThemeData theme) => Positioned( Widget _buildBottom(ThemeData theme) {
left: 0, if (!controller.showDynActionBar) {
bottom: 0, return fabButton;
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,
),
);
}
late final primary = theme.colorScheme.primary; late final primary = theme.colorScheme.primary;
late final outline = theme.colorScheme.outline; late final outline = theme.colorScheme.outline;
late final btnStyle = TextButton.styleFrom( late final btnStyle = TextButton.styleFrom(
tapTargetSize: .padded, tapTargetSize: .padded,
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
foregroundColor: outline, 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({ return Column(
required IconData icon, mainAxisSize: MainAxisSize.min,
required String text, crossAxisAlignment: CrossAxisAlignment.end,
required DynamicStat? stat, children: [
required VoidCallback onPressed, btn,
IconData? activatedIcon, Container(
}) { decoration: BoxDecoration(
final status = stat?.status == true; color: theme.colorScheme.surface,
final color = status ? primary : outline; border: Border(
return TextButton.icon( top: BorderSide(
onPressed: onPressed, color: theme.colorScheme.outline.withValues(
icon: Icon( alpha: 0.08,
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,
),
),
), ),
), ),
padding: EdgeInsets.only(bottom: padding.bottom), ),
child: Row( ),
mainAxisAlignment: MainAxisAlignment.spaceAround, padding: EdgeInsets.only(bottom: padding.bottom),
children: [ child: Row(
Expanded( children: [
child: Builder( Expanded(
builder: (btnContext) { child: Builder(
final forward = stats.forward; builder: (btnContext) {
return textIconButton( final forward = stats.forward;
text: '转发', return textIconButton(
icon: FontAwesomeIcons.shareFromSquare, text: '转发',
stat: forward, icon: FontAwesomeIcons.shareFromSquare,
onPressed: () { stat: forward,
if (controller.opusData == null && onPressed: () {
controller.articleData?.dynIdStr == null) { if (controller.opusData == null &&
SmartDialog.showToast( controller.articleData?.dynIdStr == null) {
'err: ${controller.id}', SmartDialog.showToast(
); 'err: ${controller.id}',
return; );
} return;
final summary = controller.summary; }
showModalBottomSheet( final summary = controller.summary;
context: context, showModalBottomSheet(
isScrollControlled: true, context: context,
useSafeArea: true, isScrollControlled: true,
builder: (context) => RepostPanel( useSafeArea: true,
item: controller.opusData, builder: (context) => RepostPanel(
dynIdStr: controller.articleData?.dynIdStr, item: controller.opusData,
pic: summary.cover, dynIdStr: controller.articleData?.dynIdStr,
title: summary.title, pic: summary.cover,
uname: summary.author?.name, title: summary.title,
onSuccess: () { uname: summary.author?.name,
if (forward != null) { onSuccess: () {
int count = forward.count ?? 0; if (forward != null) {
forward.count = count + 1; int count = forward.count ?? 0;
if (btnContext.mounted) { forward.count = count + 1;
(btnContext as Element?) if (btnContext.mounted) {
?.markNeedsBuild(); (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,
),
),
],
),
),
],
);
}),
);
}
} }

View File

@@ -14,6 +14,8 @@ abstract class CommonDynController extends ReplyController<MainListReply> {
late final horizontalPreview = Pref.horizontalPreview; late final horizontalPreview = Pref.horizontalPreview;
late final List<double> ratio = Pref.dynamicDetailRatio; late final List<double> ratio = Pref.dynamicDetailRatio;
late final showDynActionBar = Pref.showDynActionBar;
@override @override
Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList( Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList(
type: replyType, type: replyType,

View File

@@ -9,6 +9,7 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo; show ReplyInfo;
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.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/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart';
@@ -22,7 +23,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
abstract class CommonDynPageState<T extends StatefulWidget> extends State<T> abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, FabMixin {
CommonDynController get controller; CommonDynController get controller;
late final ScrollController scrollController; late final ScrollController scrollController;
@@ -36,26 +37,9 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
late double maxWidth; late double maxWidth;
late double maxHeight; late double maxHeight;
bool _showFab = true;
final fabOffset = const Offset(0, 1);
late final AnimationController _fabAnimationCtr;
late final Animation<Offset> fabAnim;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
)..forward();
fabAnim = _fabAnimationCtr.drive(
Tween<Offset>(
begin: fabOffset,
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeInOut)),
);
scrollController = ScrollController()..addListener(listener); scrollController = ScrollController()..addListener(listener);
} }
@@ -69,20 +53,6 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
} }
} }
void showFab() {
if (!_showFab) {
_showFab = true;
_fabAnimationCtr.forward();
}
}
void hideFab() {
if (_showFab) {
_showFab = false;
_fabAnimationCtr.reverse();
}
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -98,7 +68,6 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
scrollController scrollController
..removeListener(listener) ..removeListener(listener)
..dispose(); ..dispose();
_fabAnimationCtr.dispose();
super.dispose(); super.dispose();
} }
@@ -304,6 +273,16 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
), ),
); );
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( Widget get replyButton => FloatingActionButton(
heroTag: null, heroTag: null,
onPressed: () { onPressed: () {

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
mixin FabMixin<T extends StatefulWidget> on State<T>, TickerProvider {
bool _isFabVisible = true;
late final AnimationController _fabAnimationCtr;
late final Animation<Offset> fabAnimation;
@override
void initState() {
super.initState();
_fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
fabAnimation = _fabAnimationCtr.drive(
Tween<Offset>(
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;
}
}

View File

@@ -4,7 +4,6 @@ import 'package:PiliPlus/http/reply.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
import 'package:PiliPlus/utils/id_utils.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:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -15,8 +14,6 @@ class DynamicDetailController extends CommonDynController {
late int replyType; late int replyType;
late DynamicItemModel dynItem; late DynamicItemModel dynItem;
late final showDynActionBar = Pref.showDynActionBar;
@override @override
dynamic get sourceId => replyType == 1 ? IdUtils.av2bv(oid) : oid; dynamic get sourceId => replyType == 1 ? IdUtils.av2bv(oid) : oid;

View File

@@ -70,6 +70,11 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
) )
: _buildBody(theme), : _buildBody(theme),
), ),
floatingActionButtonLocation: floatingActionButtonLocation,
floatingActionButton: SlideTransition(
position: fabAnimation,
child: _buildBottom(theme),
),
); );
} }
@@ -319,166 +324,139 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
], ],
); );
} }
return Stack( return child;
clipBehavior: Clip.none,
children: [
child,
_buildBottom(theme),
],
);
} }
Widget _buildBottom(ThemeData theme) { Widget _buildBottom(ThemeData theme) {
return Positioned( if (!controller.showDynActionBar) {
left: 0, return fabButton;
right: 0, }
bottom: 0,
child: SlideTransition( final primary = theme.colorScheme.primary;
position: fabAnim, final outline = theme.colorScheme.outline;
child: Builder( final btnStyle = TextButton.styleFrom(
builder: (context) { tapTargetSize: .padded,
if (!controller.showDynActionBar) { padding: const EdgeInsets.symmetric(horizontal: 15),
return Align( foregroundColor: outline,
alignment: Alignment.bottomRight, );
child: Padding(
padding: EdgeInsets.only( Widget textIconButton({
right: kFloatingActionButtonMargin, required IconData icon,
bottom: padding.bottom + kFloatingActionButtonMargin, required String text,
required DynamicStat? stat,
required ValueChanged<Color> 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,
), ),
); ),
} ),
padding: EdgeInsets.only(bottom: padding.bottom),
final moduleStat = controller.dynItem.modules.moduleStat; child: Row(
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<Color> 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,
children: [ children: [
Padding( Expanded(
padding: const EdgeInsets.only( child: Builder(
right: kFloatingActionButtonMargin, builder: (btnContext) {
bottom: kFloatingActionButtonMargin, final forward = moduleStat?.forward;
), return textIconButton(
child: replyButton, icon: FontAwesomeIcons.shareFromSquare,
), text: '转发',
Container( stat: forward,
decoration: BoxDecoration( onPressed: (_) => showModalBottomSheet(
color: theme.colorScheme.surface, context: context,
border: Border( isScrollControlled: true,
top: BorderSide( useSafeArea: true,
color: theme.colorScheme.outline.withValues( builder: (context) => RepostPanel(
alpha: 0.08, item: controller.dynItem,
), onSuccess: () {
), if (forward != null) {
), int count = forward.count ?? 0;
), forward.count = count + 1;
padding: EdgeInsets.only(bottom: padding.bottom), if (btnContext.mounted) {
child: Row( (btnContext as Element).markNeedsBuild();
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: (context) { ),
return textIconButton( Expanded(
icon: FontAwesomeIcons.thumbsUp, child: textIconButton(
activatedIcon: FontAwesomeIcons.solidThumbsUp, icon: CustomIcons.share_node,
text: '点赞', text: '分享',
stat: moduleStat?.like, stat: null,
onPressed: (iconColor) => onPressed: (_) => Utils.shareText(
RequestUtils.onLikeDynamic( '${HttpString.dynamicShareBaseUrl}/${controller.dynItem.idStr}',
controller.dynItem, ),
iconColor == primary, ),
() { ),
if (context.mounted) { Expanded(
(context as Element).markNeedsBuild(); 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();
}
}, },
), ),
), );
], },
), ),
), ),
], ],
); ),
}, ),
), ],
), ),
); );
} }

View File

@@ -8,6 +8,8 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/fav_order_type.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_detail/media.dart';
import 'package:PiliPlus/models_new/fav/fav_folder/list.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/dynamics_repost/view.dart';
import 'package:PiliPlus/pages/fav_detail/controller.dart'; import 'package:PiliPlus/pages/fav_detail/controller.dart';
import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart'; import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart';
@@ -58,7 +60,7 @@ class _FavDetailPageState extends State<FavDetailPage> with GridMixin {
}, },
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
floatingActionButtonLocation: const CustomFabLocation(), floatingActionButtonLocation: const NoRightMarginFabLocation(),
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
right: kFloatingActionButtonMargin, right: kFloatingActionButtonMargin,
@@ -507,18 +509,3 @@ class _FavDetailPageState extends State<FavDetailPage> 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;
}
}

View File

@@ -5,7 +5,8 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/models/common/later_view_type.dart'; import 'package:PiliPlus/models/common/later_view_type.dart';
import 'package:PiliPlus/models_new/later/list.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/base_controller.dart';
import 'package:PiliPlus/pages/later/controller.dart'; import 'package:PiliPlus/pages/later/controller.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
@@ -74,7 +75,7 @@ class _LaterPageState extends State<LaterPage>
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: _buildAppbar(enableMultiSelect), appBar: _buildAppbar(enableMultiSelect),
floatingActionButtonLocation: const CustomFabLocation(), floatingActionButtonLocation: const NoRightMarginFabLocation(),
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const .only(right: kFloatingActionButtonMargin), padding: const .only(right: kFloatingActionButtonMargin),
child: Obx( child: Obx(

View File

@@ -38,7 +38,6 @@ import 'package:PiliPlus/utils/video_utils.dart';
import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -373,9 +372,9 @@ class LiveRoomController extends GetxController {
void listener() { void listener() {
final userScrollDirection = scrollController.position.userScrollDirection; final userScrollDirection = scrollController.position.userScrollDirection;
if (userScrollDirection == ScrollDirection.forward) { if (userScrollDirection == .forward) {
disableAutoScroll.value = true; disableAutoScroll.value = true;
} else if (userScrollDirection == ScrollDirection.reverse) { } else if (userScrollDirection == .reverse) {
final pos = scrollController.position; final pos = scrollController.position;
if (pos.maxScrollExtent - pos.pixels <= 100) { if (pos.maxScrollExtent - pos.pixels <= 100) {
disableAutoScroll.value = false; disableAutoScroll.value = false;

View File

@@ -3,35 +3,18 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
import 'package:PiliPlus/grpc/reply.dart'; import 'package:PiliPlus/grpc/reply.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/reply_controller.dart'; import 'package:PiliPlus/pages/common/reply_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class MainReplyController extends ReplyController<MainListReply> class MainReplyController extends ReplyController<MainListReply> {
with GetSingleTickerProviderStateMixin {
late final int oid; late final int oid;
late final int replyType; late final int replyType;
@override @override
int get sourceId => oid; int get sourceId => oid;
bool _showFab = true;
late final AnimationController _fabAnimationCtr;
late final Animation<Offset> fabAnim;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
)..forward();
fabAnim = _fabAnimationCtr.drive(
Tween<Offset>(
begin: const Offset(0.0, 2.0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeInOut)),
);
final args = Get.arguments; final args = Get.arguments;
oid = args['oid']; oid = args['oid'];
replyType = args['replyType']; replyType = args['replyType'];
@@ -39,20 +22,6 @@ class MainReplyController extends ReplyController<MainListReply>
queryData(); queryData();
} }
void showFab() {
if (!_showFab) {
_showFab = true;
_fabAnimationCtr.forward();
}
}
void hideFab() {
if (_showFab) {
_showFab = false;
_fabAnimationCtr.reverse();
}
}
@override @override
Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList( Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList(
type: replyType, type: replyType,
@@ -64,10 +33,4 @@ class MainReplyController extends ReplyController<MainListReply>
@override @override
List<ReplyInfo>? getDataList(MainListReply response) => response.replies; List<ReplyInfo>? getDataList(MainListReply response) => response.replies;
@override
void onClose() {
_fabAnimationCtr.dispose();
super.onClose();
}
} }

View File

@@ -7,6 +7,7 @@ import 'package:PiliPlus/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo; show ReplyInfo;
import 'package:PiliPlus/http/loading_state.dart'; 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/main_reply/controller.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart';
@@ -38,7 +39,8 @@ class MainReplyPage extends StatefulWidget {
} }
} }
class _MainReplyPageState extends State<MainReplyPage> { class _MainReplyPageState extends State<MainReplyPage>
with SingleTickerProviderStateMixin, FabMixin {
final _controller = Get.put( final _controller = Get.put(
MainReplyController(), MainReplyController(),
tag: Utils.generateRandomString(8), tag: Utils.generateRandomString(8),
@@ -62,9 +64,9 @@ class _MainReplyPageState extends State<MainReplyPage> {
onNotification: (notification) { onNotification: (notification) {
final direction = notification.direction; final direction = notification.direction;
if (direction == .forward) { if (direction == .forward) {
_controller.showFab(); showFab();
} else if (direction == .reverse) { } else if (direction == .reverse) {
_controller.hideFab(); hideFab();
} }
return false; return false;
}, },
@@ -87,22 +89,26 @@ class _MainReplyPageState extends State<MainReplyPage> {
), ),
).constraintWidth(), ).constraintWidth(),
), ),
floatingActionButtonLocation: const NoBottomPaddingFabLocation(),
floatingActionButton: SlideTransition( floatingActionButton: SlideTransition(
position: _controller.fabAnim, position: fabAnimation,
child: FloatingActionButton( child: Padding(
heroTag: null, padding: .only(bottom: padding.bottom + kFloatingActionButtonMargin),
onPressed: () { child: FloatingActionButton(
try { heroTag: null,
feedBack(); onPressed: () {
_controller.onReply( try {
null, feedBack();
oid: _controller.oid, _controller.onReply(
replyType: _controller.replyType, null,
); oid: _controller.oid,
} catch (_) {} replyType: _controller.replyType,
}, );
tooltip: '评论', } catch (_) {}
child: const Icon(Icons.reply), },
tooltip: '评论',
child: const Icon(Icons.reply),
),
), ),
), ),
); );

View File

@@ -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/contest.dart';
import 'package:PiliPlus/models_new/match/match_info/team.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/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/match_info/controller.dart';
import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart';
import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/date_utils.dart';
@@ -36,9 +38,6 @@ class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
@override @override
dynamic get arguments => null; dynamic get arguments => null;
@override
Offset get fabOffset => const Offset(0, 2);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -59,9 +58,10 @@ class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
), ),
), ),
).constraintWidth(), ).constraintWidth(),
floatingActionButtonLocation: const NoBottomPaddingFabLocation(),
floatingActionButton: SlideTransition( floatingActionButton: SlideTransition(
position: fabAnim, position: fabAnimation,
child: replyButton, child: fabButton,
), ),
); );
} }

View File

@@ -2,7 +2,6 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/music.dart'; import 'package:PiliPlus/http/music.dart';
import 'package:PiliPlus/models_new/music/bgm_detail.dart'; import 'package:PiliPlus/models_new/music/bgm_detail.dart';
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class MusicDetailController extends CommonDynController { class MusicDetailController extends CommonDynController {
@@ -18,8 +17,6 @@ class MusicDetailController extends CommonDynController {
late final String musicId; late final String musicId;
bool get showDynActionBar => Pref.showDynActionBar;
String get shareUrl => String get shareUrl =>
'https://music.bilibili.com/h5/music-detail?music_id=$musicId'; 'https://music.bilibili.com/h5/music-detail?music_id=$musicId';

View File

@@ -204,7 +204,24 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
}); });
Widget _buildBottom(ThemeData theme, MusicDetail item) { 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 outline = theme.colorScheme.outline;
final style = TextButton.styleFrom(
tapTargetSize: .padded,
padding: const EdgeInsets.symmetric(horizontal: 15),
foregroundColor: outline,
);
Widget textIconButton({ Widget textIconButton({
required IconData icon, required IconData icon,
@@ -214,7 +231,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
required VoidCallback onPressed, required VoidCallback onPressed,
IconData? activatedIcon, IconData? activatedIcon,
}) { }) {
final color = status ? theme.colorScheme.primary : outline; final color = status ? primary : outline;
return TextButton.icon( return TextButton.icon(
onPressed: onPressed, onPressed: onPressed,
icon: Icon( icon: Icon(
@@ -222,11 +239,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
size: 16, size: 16,
color: color, color: color,
), ),
style: TextButton.styleFrom( style: style,
tapTargetSize: .padded,
padding: const EdgeInsets.symmetric(horizontal: 15),
foregroundColor: outline,
),
label: Text( label: Text(
count != null ? NumUtils.numFormat(count) : text, count != null ? NumUtils.numFormat(count) : text,
style: TextStyle(color: color), style: TextStyle(color: color),
@@ -239,116 +252,104 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
right: 0, right: 0,
bottom: 0, bottom: 0,
child: SlideTransition( child: SlideTransition(
position: fabAnim, position: fabAnimation,
child: controller.showDynActionBar child: Column(
? Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end, children: [
children: [ Padding(
Padding( padding: const EdgeInsets.only(
padding: const EdgeInsets.only( right: kFloatingActionButtonMargin,
right: kFloatingActionButtonMargin, bottom: 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, padding: EdgeInsets.only(bottom: padding.bottom),
border: Border( child: Row(
top: BorderSide( mainAxisAlignment: MainAxisAlignment.spaceAround,
color: theme.colorScheme.outline.withValues( children: [
alpha: 0.08, // 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( Expanded(
mainAxisAlignment: MainAxisAlignment.spaceAround, child: Builder(
children: [ builder: (context) => textIconButton(
// TODO icon: FontAwesomeIcons.thumbsUp,
// Expanded( activatedIcon: FontAwesomeIcons.solidThumbsUp,
// child: textIconButton( text: '点赞',
// icon: FontAwesomeIcons.shareFromSquare, count: item.wishCount,
// text: '转发', status: item.wishListen ?? false,
// count: item.musicShares, onPressed: () async {
// onPressed: () { if (!Accounts.main.isLogin) {
// final data = controller.infoState.value.dataOrNull; SmartDialog.showToast('请先登录');
// if (data != null) { return;
// showModalBottomSheet( }
// context: context, final hasLike = item.wishListen ?? false;
// isScrollControlled: true, final res = await MusicHttp.wishUpdate(
// useSafeArea: true, controller.musicId,
// builder: (context) => RepostPanel( hasLike,
// rid: controller.oid, );
// dynType: null, if (res.isSuccess) {
// pic: data.mvCover, if (hasLike) {
// title: data.musicTitle, item.wishCount--;
// ), } else {
// ); item.wishCount++;
// } }
// }, item.wishListen = !hasLike;
// ), if (context.mounted) {
// ), (context as Element).markNeedsBuild();
Expanded( }
child: textIconButton( } else {
icon: CustomIcons.share_node, res.toast();
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();
}
},
),
),
),
],
), ),
), ),
], ],
)
: Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(
right: kFloatingActionButtonMargin,
bottom: padding.bottom + kFloatingActionButtonMargin,
),
child: replyButton,
),
), ),
),
],
),
), ),
); );
} }

View File

@@ -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/common/reply_controller.dart';
import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class VideoReplyController extends ReplyController<MainListReply> class VideoReplyController extends ReplyController<MainListReply> {
with GetSingleTickerProviderStateMixin {
VideoReplyController({ VideoReplyController({
required this.aid, required this.aid,
required this.videoType, required this.videoType,
@@ -26,39 +24,6 @@ class VideoReplyController extends ReplyController<MainListReply>
@override @override
dynamic get sourceId => IdUtils.av2bv(aid); dynamic get sourceId => IdUtils.av2bv(aid);
bool _isFabVisible = true;
late final AnimationController _fabAnimationCtr;
late final Animation<Offset> animation;
@override
void onInit() {
super.onInit();
_fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
)..forward();
animation = _fabAnimationCtr.drive(
Tween<Offset>(
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 @override
List<ReplyInfo>? getDataList(MainListReply response) { List<ReplyInfo>? getDataList(MainListReply response) {
return response.replies; return response.replies;
@@ -72,10 +37,4 @@ class VideoReplyController extends ReplyController<MainListReply>
cursorNext: cursorNext, cursorNext: cursorNext,
offset: paginationReply?.nextOffset, offset: paginationReply?.nextOffset,
); );
@override
void onClose() {
_fabAnimationCtr.dispose();
super.onClose();
}
} }

View File

@@ -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' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo; show ReplyInfo;
import 'package:PiliPlus/http/loading_state.dart'; 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/controller.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/pages/video/reply_reply/view.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:easy_debounce/easy_throttle.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class VideoReplyPanel extends StatefulWidget { class VideoReplyPanel extends StatefulWidget {
@@ -33,7 +33,10 @@ class VideoReplyPanel extends StatefulWidget {
} }
class _VideoReplyPanelState extends State<VideoReplyPanel> class _VideoReplyPanelState extends State<VideoReplyPanel>
with AutomaticKeepAliveClientMixin { with
AutomaticKeepAliveClientMixin,
SingleTickerProviderStateMixin,
FabMixin {
late VideoReplyController _videoReplyController; late VideoReplyController _videoReplyController;
String get heroTag => widget.heroTag; String get heroTag => widget.heroTag;
@@ -64,11 +67,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
final theme = Theme.of(context); final theme = Theme.of(context);
final child = NotificationListener<UserScrollNotification>( final child = NotificationListener<UserScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
final direction = notification.direction; switch (notification.direction) {
if (direction == ScrollDirection.forward) { case .forward:
_videoReplyController.showFab(); showFab();
} else if (direction == ScrollDirection.reverse) { case .reverse:
_videoReplyController.hideFab(); hideFab();
case _:
} }
return false; return false;
}, },
@@ -129,22 +133,28 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
], ],
), ),
Positioned( Positioned(
right: kFloatingActionButtonMargin, right: 0,
bottom: kFloatingActionButtonMargin + bottom, bottom: 0,
child: SlideTransition( child: SlideTransition(
position: _videoReplyController.animation, position: fabAnimation,
child: FloatingActionButton( child: Padding(
heroTag: null, padding: .only(
onPressed: () { right: kFloatingActionButtonMargin,
feedBack(); bottom: kFloatingActionButtonMargin + bottom,
_videoReplyController.onReply( ),
null, child: FloatingActionButton(
oid: _videoReplyController.aid, heroTag: null,
replyType: _videoReplyController.videoType.replyType, onPressed: () {
); feedBack();
}, _videoReplyController.onReply(
tooltip: '发表评论', null,
child: const Icon(Icons.reply), oid: _videoReplyController.aid,
replyType: _videoReplyController.videoType.replyType,
);
},
tooltip: '发表评论',
child: const Icon(Icons.reply),
),
), ),
), ),
), ),