diff --git a/CHANGELOG.md b/CHANGELOG.md index e481418c6..40ec61f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added access to saved comments from account page - contribution from @CTalvio - Added Polish translation - contribution from @pazdikan - Show default avatar for users without an avatar - contribution from @coslu +- Added the ability to combine the post FAB with the comment navigation buttons - contribution from @micahmo ### Changed - Prioritize and label the default accent color - contribution from @micahmo diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index 309b3b864..ce2ce1efa 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -92,6 +92,7 @@ enum LocalSettings { postFabSinglePressAction(name: 'settings_post_fab_single_press_action', label: ''), postFabLongPressAction(name: 'settings_post_fab_long_press_action', label: ''), enableCommentNavigation(name: 'setting_enable_comment_navigation', label: 'Enable Comment Navigation Buttons'), + combineNavAndFab(name: 'setting_combine_nav_and_fab', label: 'Combine FAB and Navigation Buttons'), ; const LocalSettings({ diff --git a/lib/post/pages/post_page.dart b/lib/post/pages/post_page.dart index e94f984b2..5dec637a0 100644 --- a/lib/post/pages/post_page.dart +++ b/lib/post/pages/post_page.dart @@ -57,6 +57,7 @@ class _PostPageState extends State { bool isFabSummoned = true; bool enableFab = false; bool enableCommentNavigation = true; + bool combineNavAndFab = true; CommentSortType? sortType; IconData? sortTypeIcon; @@ -95,6 +96,7 @@ class _PostPageState extends State { PostFabAction longPressAction = thunderState.postFabLongPressAction; enableCommentNavigation = thunderState.enableCommentNavigation; + combineNavAndFab = enableCommentNavigation && thunderState.combineNavAndFab; if (thunderState.isFabOpen != _previousIsFabOpen) { isFabOpen = thunderState.isFabOpen; @@ -165,13 +167,29 @@ class _PostPageState extends State { floatingActionButton: Stack( alignment: Alignment.center, children: [ + if (enableCommentNavigation) + Positioned.fill( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Align( + alignment: Alignment.bottomCenter, + child: CommentNavigatorFab( + itemPositionsListener: _itemPositionsListener, + ), + ), + ), + ), if (enableFab) Padding( - padding: const EdgeInsets.only(right: 16), + padding: EdgeInsets.only( + right: combineNavAndFab ? 0 : 16, + bottom: combineNavAndFab ? 5 : 0, + ), child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), child: isFabSummoned ? GestureFab( + centered: combineNavAndFab, distance: 60, icon: Icon( singlePressAction.getIcon(override: singlePressAction == PostFabAction.changeSort ? sortTypeIcon : null), @@ -211,6 +229,7 @@ class _PostPageState extends State { children: [ if (enableReplyToPost) ActionButton( + centered: combineNavAndFab, onPressed: () { HapticFeedback.mediumImpact(); PostFabAction.replyToPost.execute( @@ -224,6 +243,7 @@ class _PostPageState extends State { ), if (enableChangeSort) ActionButton( + centered: combineNavAndFab, onPressed: () { HapticFeedback.mediumImpact(); PostFabAction.changeSort.execute( @@ -237,6 +257,7 @@ class _PostPageState extends State { ), if (enableBackToTop) ActionButton( + centered: combineNavAndFab, onPressed: () { PostFabAction.backToTop.execute( override: () => { @@ -257,18 +278,6 @@ class _PostPageState extends State { : null, ), ), - if (enableCommentNavigation) - Positioned.fill( - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.bottomCenter, - child: CommentNavigatorFab( - itemPositionsListener: _itemPositionsListener, - ), - ), - ), - ), ], ), body: Stack( diff --git a/lib/settings/pages/general_settings_page.dart b/lib/settings/pages/general_settings_page.dart index 94734047d..670311254 100644 --- a/lib/settings/pages/general_settings_page.dart +++ b/lib/settings/pages/general_settings_page.dart @@ -67,6 +67,7 @@ class _GeneralSettingsPageState extends State with SingleTi NestedCommentIndicatorStyle nestedIndicatorStyle = DEFAULT_NESTED_COMMENT_INDICATOR_STYLE; NestedCommentIndicatorColor nestedIndicatorColor = DEFAULT_NESTED_COMMENT_INDICATOR_COLOR; bool enableCommentNavigation = true; + bool combineNavAndFab = true; // Page State bool isLoading = true; @@ -206,6 +207,10 @@ class _GeneralSettingsPageState extends State with SingleTi await prefs.setBool(LocalSettings.enableCommentNavigation.name, value); setState(() => enableCommentNavigation = value); break; + case LocalSettings.combineNavAndFab: + await prefs.setBool(LocalSettings.combineNavAndFab.name, value); + setState(() => combineNavAndFab = value); + break; } if (context.mounted) { @@ -258,6 +263,7 @@ class _GeneralSettingsPageState extends State with SingleTi nestedIndicatorColor = NestedCommentIndicatorColor.values.byName(prefs.getString(LocalSettings.nestedCommentIndicatorColor.name) ?? DEFAULT_NESTED_COMMENT_INDICATOR_COLOR.name); enableCommentNavigation = prefs.getBool(LocalSettings.enableCommentNavigation.name) ?? true; + combineNavAndFab = prefs.getBool(LocalSettings.combineNavAndFab.name) ?? true; // Links openInExternalBrowser = prefs.getBool(LocalSettings.openLinksInExternalBrowser.name) ?? false; @@ -608,6 +614,31 @@ class _GeneralSettingsPageState extends State with SingleTi iconDisabled: Icons.unfold_less_rounded, onToggle: (bool value) => setPreferences(LocalSettings.enableCommentNavigation, value), ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition( + sizeFactor: animation, + child: SlideTransition(position: _offsetAnimation, child: child), + ); + }, + child: enableCommentNavigation + ? Padding( + padding: const EdgeInsets.only(left: 16.0), + key: ValueKey(enableCommentNavigation), + child: ToggleOption( + description: LocalSettings.combineNavAndFab.label, + subtitle: 'Floating Action Button will be shown between navigation buttons', + value: combineNavAndFab, + iconEnabled: Icons.join_full_rounded, + iconDisabled: Icons.join_inner_rounded, + onToggle: (bool value) => setPreferences(LocalSettings.combineNavAndFab, value), + ), + ) + : Container(), + ), ], ), ), diff --git a/lib/shared/gesture_fab.dart b/lib/shared/gesture_fab.dart index 85de5d081..6cf5a16a9 100644 --- a/lib/shared/gesture_fab.dart +++ b/lib/shared/gesture_fab.dart @@ -19,6 +19,7 @@ class GestureFab extends StatefulWidget { this.onSlideDown, this.onPressed, this.onLongPress, + this.centered = false, }); final bool? initialOpen; @@ -30,6 +31,7 @@ class GestureFab extends StatefulWidget { final Function? onSlideDown; final Function? onPressed; final Function? onLongPress; + final bool centered; @override State createState() => _GestureFabState(); @@ -79,7 +81,7 @@ class _GestureFabState extends State with SingleTickerProviderStateM return SizedBox.expand( child: Stack( - alignment: Alignment.bottomRight, + alignment: widget.centered ? Alignment.bottomCenter : Alignment.bottomRight, clipBehavior: Clip.none, children: [ _buildTapToCloseFab(), @@ -92,8 +94,8 @@ class _GestureFabState extends State with SingleTickerProviderStateM Widget _buildTapToCloseFab() { return SizedBox( - width: 56, - height: 56, + width: widget.centered ? 45 : 56, + height: widget.centered ? 45 : 56, child: AnimatedBuilder( animation: _expandAnimation, builder: (context, child) => child!, @@ -101,17 +103,19 @@ class _GestureFabState extends State with SingleTickerProviderStateM opacity: _expandAnimation, child: Center( child: Material( - shape: const CircleBorder(), - clipBehavior: Clip.antiAlias, - elevation: 4, + shape: widget.centered ? null : const CircleBorder(), + clipBehavior: widget.centered ? Clip.none : Clip.antiAlias, + elevation: widget.centered ? 0 : 4, child: InkWell( + borderRadius: BorderRadius.circular(50), onTap: () { context.read().add(const OnFabToggle(false)); }, child: Padding( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(widget.centered ? 12 : 8), child: Icon( Icons.close, + size: widget.centered ? 20 : 25, color: Theme.of(context).primaryColor, semanticLabel: AppLocalizations.of(context)!.close, ), @@ -133,6 +137,7 @@ class _GestureFabState extends State with SingleTickerProviderStateM maxDistance: distance, progress: _expandAnimation, focus: isFabOpen && i == count - 1, + centered: widget.centered, child: widget.children[i], ), ); @@ -169,12 +174,30 @@ class _GestureFabState extends State with SingleTickerProviderStateM onLongPress: () { widget.onLongPress?.call(); }, - child: FloatingActionButton( - onPressed: () { - widget.onPressed?.call(); - }, - child: widget.icon, - ), + child: widget.centered + ? SizedBox( + width: 45, + height: 45, + child: Material( + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () => widget.onPressed?.call(), + child: Icon( + widget.icon.icon, + size: 20, + ), + ), + ), + ) + : FloatingActionButton( + onPressed: () { + widget.onPressed?.call(); + }, + child: widget.icon, + ), ), ), ), @@ -189,76 +212,143 @@ class ActionButton extends StatelessWidget { this.onPressed, this.title, required this.icon, + this.centered = false, }); final VoidCallback? onPressed; - final Widget icon; + final Icon icon; final String? title; + final bool centered; @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Row( - children: [ - title != null ? Text(title!) : Container(), - const SizedBox(width: 16), - SizedBox( - height: 40, - width: 40, - child: Material( - borderRadius: const BorderRadius.all(Radius.circular(8)), - clipBehavior: Clip.antiAlias, - color: theme.colorScheme.primaryContainer, - elevation: 4, - child: InkWell( - onTap: () { - context.read().add(const OnFabToggle(true)); - onPressed?.call(); - }, - child: icon, + return centered + ? Material( + color: Colors.transparent, + elevation: 3, + borderRadius: BorderRadius.circular(50), + child: Stack( + children: [ + Positioned.fill( + child: Align( + child: SizedBox( + height: 35, + child: Material( + borderRadius: BorderRadius.circular(50), + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () { + context.read().add(const OnFabToggle(true)); + onPressed?.call(); + }, + ), + ), + ), + ), + ), + IgnorePointer( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10, right: 5), + child: title != null ? Text(title!) : Container(), + ), + const SizedBox( + height: 35, + ), + Padding( + padding: const EdgeInsets.only(left: 5, right: 10), + child: Icon( + icon.icon, + size: 20, + ), + ), + ], + ), + ), + ], ), - ), - ), - ], - ); + ) + : Row( + children: [ + title != null ? Text(title!) : Container(), + const SizedBox(width: 16), + SizedBox( + height: 40, + width: 40, + child: Material( + borderRadius: const BorderRadius.all(Radius.circular(8)), + clipBehavior: Clip.antiAlias, + color: theme.colorScheme.primaryContainer, + elevation: 4, + child: InkWell( + onTap: () { + context.read().add(const OnFabToggle(true)); + onPressed?.call(); + }, + child: icon, + ), + ), + ), + ], + ); } } @immutable -class _ExpandingActionButton extends StatelessWidget { +class _ExpandingActionButton extends StatefulWidget { const _ExpandingActionButton({ required this.maxDistance, required this.progress, required this.child, required this.focus, + this.centered = false, }); final double maxDistance; final Animation progress; final Widget child; final bool focus; + final bool centered; + + @override + State<_ExpandingActionButton> createState() => _ExpandingActionButtonState(); +} + +class _ExpandingActionButtonState extends State<_ExpandingActionButton> { + bool _visible = false; @override Widget build(BuildContext context) { return AnimatedBuilder( - animation: progress, + animation: widget.progress, builder: (context, child) { final offset = Offset.fromDirection( 90 * (math.pi / 180.0), - progress.value * maxDistance, + widget.progress.value * widget.maxDistance, ); - return Positioned( - right: 8.0 + offset.dx, - bottom: 10.0 + offset.dy, - child: Semantics( - focused: focus, - child: child!, + if (widget.progress.value == 1) { + _visible = true; + } else if (widget.progress.value == 0) { + _visible = false; + } + return Visibility( + visible: _visible, + child: Positioned( + right: widget.centered ? null : 8.0 + offset.dx, + bottom: 10.0 + offset.dy, + child: Semantics( + focused: widget.focus, + child: child!, + ), ), ); }, child: FadeTransition( - opacity: progress, - child: child, + opacity: widget.progress, + child: widget.child, ), ); } diff --git a/lib/thunder/bloc/thunder_bloc.dart b/lib/thunder/bloc/thunder_bloc.dart index 1f8a46d0c..f5f38398f 100644 --- a/lib/thunder/bloc/thunder_bloc.dart +++ b/lib/thunder/bloc/thunder_bloc.dart @@ -183,6 +183,7 @@ class ThunderBloc extends Bloc { PostFabAction postFabLongPressAction = PostFabAction.values.byName(prefs.getString(LocalSettings.postFabLongPressAction.name) ?? PostFabAction.openFab.name); bool enableCommentNavigation = prefs.getBool(LocalSettings.enableCommentNavigation.name) ?? true; + bool combineNavAndFab = prefs.getBool(LocalSettings.combineNavAndFab.name) ?? true; return emit(state.copyWith( status: ThunderStatus.success, @@ -284,6 +285,7 @@ class ThunderBloc extends Bloc { postFabLongPressAction: postFabLongPressAction, enableCommentNavigation: enableCommentNavigation, + combineNavAndFab: combineNavAndFab, )); } catch (e) { return emit(state.copyWith(status: ThunderStatus.failure, errorMessage: e.toString())); diff --git a/lib/thunder/bloc/thunder_state.dart b/lib/thunder/bloc/thunder_state.dart index dfdb9fc7a..25fb0f26e 100644 --- a/lib/thunder/bloc/thunder_state.dart +++ b/lib/thunder/bloc/thunder_state.dart @@ -104,6 +104,7 @@ class ThunderState extends Equatable { this.postFabSinglePressAction = PostFabAction.replyToPost, this.postFabLongPressAction = PostFabAction.openFab, this.enableCommentNavigation = true, + this.combineNavAndFab = true, /// --------------------------------- UI Events --------------------------------- // Scroll to top event @@ -220,6 +221,7 @@ class ThunderState extends Equatable { final PostFabAction postFabLongPressAction; final bool enableCommentNavigation; + final bool combineNavAndFab; /// --------------------------------- UI Events --------------------------------- // Scroll to top event @@ -332,6 +334,7 @@ class ThunderState extends Equatable { PostFabAction? postFabSinglePressAction, PostFabAction? postFabLongPressAction, bool? enableCommentNavigation, + bool? combineNavAndFab, /// --------------------------------- UI Events --------------------------------- // Scroll to top event @@ -446,6 +449,7 @@ class ThunderState extends Equatable { postFabLongPressAction: postFabLongPressAction ?? this.postFabLongPressAction, enableCommentNavigation: enableCommentNavigation ?? this.enableCommentNavigation, + combineNavAndFab: combineNavAndFab ?? this.combineNavAndFab, /// --------------------------------- UI Events --------------------------------- // Scroll to top event @@ -560,6 +564,7 @@ class ThunderState extends Equatable { postFabLongPressAction, enableCommentNavigation, + combineNavAndFab, /// --------------------------------- UI Events --------------------------------- // Scroll to top event