From 71bf420ae085782e7f0547312621fc8f18ceab9a Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Thu, 1 Feb 2024 17:16:02 -0800 Subject: [PATCH 01/11] experiment to replace the snackbar with an overlay --- lib/post/cubit/create_post_cubit.dart | 24 ++-- lib/shared/snackbar.dart | 164 +++++++++++++++++++------- 2 files changed, 136 insertions(+), 52 deletions(-) diff --git a/lib/post/cubit/create_post_cubit.dart b/lib/post/cubit/create_post_cubit.dart index 3f407f267..f86b1de4e 100644 --- a/lib/post/cubit/create_post_cubit.dart +++ b/lib/post/cubit/create_post_cubit.dart @@ -6,6 +6,7 @@ import 'package:lemmy_api_client/v3.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/utils/post.dart'; import 'package:thunder/post/utils/post.dart'; import 'package:thunder/utils/error_messages.dart'; @@ -45,18 +46,21 @@ class CreatePostCubit extends Cubit { emit(state.copyWith(status: CreatePostStatus.submitting)); try { - PostView postView = await createPost( - communityId: communityId, - name: name, - body: body, - url: url, - nsfw: nsfw, - postIdBeingEdited: postIdBeingEdited, - languageId: languageId, - ); + final lemmy = LemmyClient.instance.lemmyApiV3; + // PostView postView = await createPost( + // communityId: communityId, + // name: name, + // body: body, + // url: url, + // nsfw: nsfw, + // postIdBeingEdited: postIdBeingEdited, + // languageId: languageId, + // ); + + GetPostResponse getPostResponse = await lemmy.run(GetPost(id: 14462486)); // Parse the newly created post - List postViewMedias = await parsePostViews([postView]); + List postViewMedias = await parsePostViews([getPostResponse.postView]); emit(state.copyWith(status: CreatePostStatus.success, postViewMedia: postViewMedias.firstOrNull)); return postViewMedias.firstOrNull?.postView.post.id; diff --git a/lib/shared/snackbar.dart b/lib/shared/snackbar.dart index 344436ed9..4092f592a 100644 --- a/lib/shared/snackbar.dart +++ b/lib/shared/snackbar.dart @@ -2,6 +2,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:overlay_support/overlay_support.dart'; + void showSnackbar( BuildContext context, String text, { @@ -16,54 +18,132 @@ void showSnackbar( void Function()? trailingAction, }) { int wordCount = RegExp(r'[\w-]+').allMatches(text).length; - SnackBar snackBar = SnackBar( - duration: duration ?? Duration(milliseconds: max(4000, 1000 * wordCount)), // Assuming 60 WPM or 1 WPS - backgroundColor: backgroundColor, - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (leadingIcon != null) - Icon( - leadingIcon, - color: leadingIconColor, - ), - if (leadingIcon != null) const SizedBox(width: 8.0), - Expanded( - child: Text( - text, - ), + + if (clearSnackBars) { + // OverlaySupportEntry.of(context)?.dismiss(); + } + + showOverlayNotification( + (context) { + return ThunderSnackbar( + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leadingIcon != null) + Icon( + leadingIcon, + color: leadingIconColor, + ), + if (leadingIcon != null) const SizedBox(width: 8.0), + Expanded( + child: Text( + text, + ), + ), + if (trailingIcon != null) + SizedBox( + height: 20, + child: IconButton( + visualDensity: VisualDensity.compact, + onPressed: trailingAction != null + ? () { + OverlaySupportEntry.of(context)?.dismiss(); + trailingAction(); + } + : null, + icon: Icon( + trailingIcon, + color: trailingIconColor ?? Theme.of(context).colorScheme.inversePrimary, + ), + ), + ), + ], ), - if (trailingIcon != null) - SizedBox( - height: 20, - child: IconButton( - visualDensity: VisualDensity.compact, - onPressed: trailingAction != null - ? () { - (customState ?? ScaffoldMessenger.of(context)).clearSnackBars(); - trailingAction(); - } - : null, - icon: Icon( - trailingIcon, - color: trailingIconColor ?? Theme.of(context).colorScheme.inversePrimary, + ); + }, + position: NotificationPosition.bottom, + duration: duration ?? Duration(milliseconds: max(4000, 1000 * wordCount)), // Assuming 60 WPM or 1 WPS + ); +} + +class ThunderSnackbar extends StatelessWidget { + final Widget content; + + const ThunderSnackbar({super.key, required this.content}); + + @override + Widget build(BuildContext context) { + const double horizontalPadding = 16.0; + const double singleLineVerticalPadding = 14.0; + + final ThemeData theme = Theme.of(context); + final SnackBarThemeData snackBarTheme = theme.snackBarTheme; + + final double elevation = snackBarTheme.elevation ?? 6.0; + final Color backgroundColor = theme.colorScheme.inverseSurface; + final ShapeBorder shape = snackBarTheme.shape ?? RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)); + + Widget snackBar = Container( + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom + kBottomNavigationBarHeight + singleLineVerticalPadding), + child: ClipRect( + child: Align( + alignment: AlignmentDirectional.bottomStart, + child: Semantics( + container: true, + liveRegion: true, + onDismiss: () { + ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss); + }, + child: Dismissible( + key: const Key('dismissible'), + direction: DismissDirection.down, + resizeDuration: null, + behavior: HitTestBehavior.deferToChild, + onDismissed: (DismissDirection direction) { + // ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0), + child: Material( + shape: shape, + elevation: elevation, + color: backgroundColor, + clipBehavior: Clip.none, + child: Theme( + data: theme, + child: Padding( + padding: const EdgeInsetsDirectional.only(start: horizontalPadding, end: horizontalPadding), + child: Wrap( + children: [ + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: singleLineVerticalPadding), + child: DefaultTextStyle( + style: theme.textTheme.bodyMedium!.copyWith(color: theme.colorScheme.onInverseSurface), + child: content, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), ), ), ), - ], - ), - ); + ), + ), + ); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (clearSnackBars) { - (customState ?? ScaffoldMessenger.of(context)).clearSnackBars(); - } - (customState ?? ScaffoldMessenger.of(context)).showSnackBar(snackBar); - }); + return snackBar; + } } void hideSnackbar(BuildContext context, {ScaffoldMessengerState? customState}) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - (customState ?? ScaffoldMessenger.of(context)).clearSnackBars(); - }); + OverlaySupportEntry.of(context)?.dismiss(); } From b424ce8b130df6bff3232e927c42292ddaee7c7f Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Thu, 1 Feb 2024 17:25:09 -0800 Subject: [PATCH 02/11] removed checks from create post page --- lib/community/pages/create_post_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/community/pages/create_post_page.dart b/lib/community/pages/create_post_page.dart index d9a7b6d8e..d2137fd99 100644 --- a/lib/community/pages/create_post_page.dart +++ b/lib/community/pages/create_post_page.dart @@ -355,7 +355,7 @@ class _CreatePostPageState extends State { languageId: languageId, ); - if (context.mounted && widget.scaffoldMessengerKey?.currentContext != null && widget.postView?.post.id == null && postId != null) { + if (postId != null) { showSnackbar( context, l10n.postCreatedSuccessfully, From e12d39ae251441e435009b9023c7fb23363b88a4 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Fri, 2 Feb 2024 14:36:02 -0800 Subject: [PATCH 03/11] removed fields that are no longer required for snackbar --- lib/account/pages/login_page.dart | 4 +- lib/account/widgets/profile_modal_body.dart | 672 ++++++++------- lib/community/pages/create_post_page.dart | 22 +- lib/community/utils/post_actions.dart | 4 +- .../utils/post_card_action_helpers.dart | 4 +- lib/feed/view/feed_page.dart | 343 ++++---- lib/feed/widgets/feed_fab.dart | 10 +- lib/feed/widgets/feed_page_app_bar.dart | 4 +- lib/inbox/widgets/inbox_mentions_view.dart | 2 +- lib/inbox/widgets/inbox_replies_view.dart | 2 +- lib/instance/instance_page.dart | 98 ++- lib/instance/utils/navigate_instance.dart | 2 +- lib/post/pages/create_comment_page.dart | 8 +- lib/post/pages/post_page.dart | 769 +++++++++--------- lib/post/pages/post_page_success.dart | 7 +- lib/post/utils/comment_action_helpers.dart | 2 +- lib/post/utils/comment_actions.dart | 4 +- lib/post/utils/navigate_create_post.dart | 4 +- lib/post/widgets/comment_view.dart | 5 - lib/post/widgets/post_view.dart | 9 +- lib/post/widgets/report_comment_dialog.dart | 5 +- lib/search/pages/search_page.dart | 4 +- lib/settings/pages/debug_settings_page.dart | 6 +- .../widgets/accessibility_profile.dart | 4 +- lib/shared/cross_posts.dart | 5 - lib/shared/image_viewer.dart | 696 ++++++++-------- lib/shared/snackbar.dart | 201 +++-- lib/thunder/pages/thunder_page.dart | 288 +++---- lib/user/pages/user_page.dart | 2 +- lib/user/pages/user_page_success.dart | 8 +- lib/user/pages/user_settings_page.dart | 13 +- lib/utils/global_context.dart | 1 + lib/utils/image.dart | 2 +- 33 files changed, 1599 insertions(+), 1611 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index cc4391937..d5eac2b81 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -145,11 +145,11 @@ class _LoginPageState extends State with SingleTickerProviderStateMix isLoading = false; }); - showSnackbar(context, AppLocalizations.of(context)!.loginFailed(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage)); + showSnackbar(AppLocalizations.of(context)!.loginFailed(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage)); } else if (state.status == AuthStatus.success && context.read().state.isLoggedIn) { context.pop(); - showSnackbar(context, AppLocalizations.of(context)!.loginSucceeded); + showSnackbar(AppLocalizations.of(context)!.loginSucceeded); } }, ), diff --git a/lib/account/widgets/profile_modal_body.dart b/lib/account/widgets/profile_modal_body.dart index ff5d79e20..54122979d 100644 --- a/lib/account/widgets/profile_modal_body.dart +++ b/lib/account/widgets/profile_modal_body.dart @@ -92,7 +92,6 @@ class ProfileSelect extends StatefulWidget { } class _ProfileSelectState extends State { - final GlobalKey _scaffoldMessengerKey = GlobalKey(); List? accounts; List? anonymousInstances; @@ -139,375 +138,372 @@ class _ProfileSelectState extends State { } return true; }, - child: ScaffoldMessenger( - key: _scaffoldMessengerKey, - child: Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: Text(l10n.account(2)), - centerTitle: false, - scrolledUnderElevation: 0, - pinned: true, - actions: [ - IconButton( - icon: const Icon(Icons.person_add), - tooltip: l10n.addAccount, - onPressed: () => widget.pushRegister(), - ), - IconButton( - icon: const Icon(Icons.add), - tooltip: l10n.addAnonymousInstance, - onPressed: () => widget.pushRegister(anonymous: true), - ), - const SizedBox(width: 12.0), - ], - ), - SliverList.builder( - itemBuilder: (context, index) { - if (index < (accounts?.length ?? 0)) { - return Padding( - padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), - child: Material( - color: currentAccountId == accounts![index].account.id ? selectedColor : null, + child: Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + title: Text(l10n.account(2)), + centerTitle: false, + scrolledUnderElevation: 0, + pinned: true, + actions: [ + IconButton( + icon: const Icon(Icons.person_add), + tooltip: l10n.addAccount, + onPressed: () => widget.pushRegister(), + ), + IconButton( + icon: const Icon(Icons.add), + tooltip: l10n.addAnonymousInstance, + onPressed: () => widget.pushRegister(anonymous: true), + ), + const SizedBox(width: 12.0), + ], + ), + SliverList.builder( + itemBuilder: (context, index) { + if (index < (accounts?.length ?? 0)) { + return Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: Material( + color: currentAccountId == accounts![index].account.id ? selectedColor : null, + borderRadius: BorderRadius.circular(50), + child: InkWell( + onTap: (currentAccountId == accounts![index].account.id) + ? null + : () { + context.read().add(SwitchAccount(accountId: accounts![index].account.id)); + context.pop(); + }, borderRadius: BorderRadius.circular(50), - child: InkWell( - onTap: (currentAccountId == accounts![index].account.id) - ? null - : () { - context.read().add(SwitchAccount(accountId: accounts![index].account.id)); - context.pop(); - }, - borderRadius: BorderRadius.circular(50), - child: AnimatedSize( - duration: const Duration(milliseconds: 250), - child: ListTile( - leading: Stack( - children: [ - AnimatedCrossFade( - crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 500), - firstChild: const SizedBox( - child: Padding( - padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), - child: Icon( - Icons.person, - ), + child: AnimatedSize( + duration: const Duration(milliseconds: 250), + child: ListTile( + leading: Stack( + children: [ + AnimatedCrossFade( + crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 500), + firstChild: const SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), + child: Icon( + Icons.person, ), ), - secondChild: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundImage: accounts![index].instanceIcon == null ? null : CachedNetworkImageProvider(accounts![index].instanceIcon!), - maxRadius: 20, - ), ), - // This widget creates a slight border around the status indicator - Positioned( - right: 0, - bottom: 0, - child: SizedBox( - width: 12, - height: 12, - child: Material( - borderRadius: BorderRadius.circular(10), - color: currentAccountId == accounts![index].account.id ? selectedColor : null, - ), + secondChild: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: accounts![index].instanceIcon == null ? null : CachedNetworkImageProvider(accounts![index].instanceIcon!), + maxRadius: 20, + ), + ), + // This widget creates a slight border around the status indicator + Positioned( + right: 0, + bottom: 0, + child: SizedBox( + width: 12, + height: 12, + child: Material( + borderRadius: BorderRadius.circular(10), + color: currentAccountId == accounts![index].account.id ? selectedColor : null, ), ), - // This is the status indicator - Positioned( - right: 1, - bottom: 1, - child: AnimatedOpacity( - opacity: accounts![index].alive == null ? 0 : 1, - duration: const Duration(milliseconds: 500), - child: Icon( - accounts![index].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, - size: 10, - color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), accounts![index].alive == true ? Colors.green : Colors.red), - ), + ), + // This is the status indicator + Positioned( + right: 1, + bottom: 1, + child: AnimatedOpacity( + opacity: accounts![index].alive == null ? 0 : 1, + duration: const Duration(milliseconds: 500), + child: Icon( + accounts![index].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, + size: 10, + color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), accounts![index].alive == true ? Colors.green : Colors.red), ), ), - ], - ), - title: Text( - accounts![index].account.username ?? 'N/A', - style: theme.textTheme.titleMedium?.copyWith(), - ), - subtitle: Wrap( - children: [ - Text(accounts![index].account.instance?.replaceAll('https://', '') ?? 'N/A'), - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: accounts![index].version == null - ? const SizedBox(height: 20, width: 0) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 5), - Text( - '•', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + ], + ), + title: Text( + accounts![index].account.username ?? 'N/A', + style: theme.textTheme.titleMedium?.copyWith(), + ), + subtitle: Wrap( + children: [ + Text(accounts![index].account.instance?.replaceAll('https://', '') ?? 'N/A'), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: accounts![index].version == null + ? const SizedBox(height: 20, width: 0) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), ), - const SizedBox(width: 5), - Text( - 'v${accounts![index].version}', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + const SizedBox(width: 5), + Text( + 'v${accounts![index].version}', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), ), - ], - ), - ), - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: accounts![index].latency == null - ? const SizedBox(height: 20, width: 0) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 5), - Text( - '•', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + ], + ), + ), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: accounts![index].latency == null + ? const SizedBox(height: 20, width: 0) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), ), - const SizedBox(width: 5), - Text( - '${accounts![index].latency?.inMilliseconds}ms', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + const SizedBox(width: 5), + Text( + '${accounts![index].latency?.inMilliseconds}ms', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), ), - ], - ), - ), - ], - ), - trailing: (accounts!.length > 1 || anonymousInstances?.isNotEmpty == true) - ? (currentAccountId == accounts![index].account.id) - ? IconButton( - icon: loggingOutId == accounts![index].account.id - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ) - : Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.logOut), - onPressed: () => _logOutOfActiveAccount(activeAccountId: accounts![index].account.id), - ) - : IconButton( - icon: loggingOutId == accounts![index].account.id - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ) - : Icon( - Icons.delete, - semanticLabel: AppLocalizations.of(context)!.removeAccount, - ), - onPressed: () async { - context.read().add(RemoveAccount(accountId: accounts![index].account.id)); - - setState(() => loggingOutId = accounts![index].account.id); - - if (currentAccountId != null) { - await Future.delayed(const Duration(milliseconds: 1000), () { - context.read().add(SwitchAccount(accountId: currentAccountId)); - }); - } - - setState(() { - accounts = null; - loggingOutId = null; - }); - }) - : null, + ), + ], + ), + ), + ], ), + trailing: (accounts!.length > 1 || anonymousInstances?.isNotEmpty == true) + ? (currentAccountId == accounts![index].account.id) + ? IconButton( + icon: loggingOutId == accounts![index].account.id + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.logOut), + onPressed: () => _logOutOfActiveAccount(activeAccountId: accounts![index].account.id), + ) + : IconButton( + icon: loggingOutId == accounts![index].account.id + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon( + Icons.delete, + semanticLabel: AppLocalizations.of(context)!.removeAccount, + ), + onPressed: () async { + context.read().add(RemoveAccount(accountId: accounts![index].account.id)); + + setState(() => loggingOutId = accounts![index].account.id); + + if (currentAccountId != null) { + await Future.delayed(const Duration(milliseconds: 1000), () { + context.read().add(SwitchAccount(accountId: currentAccountId)); + }); + } + + setState(() { + accounts = null; + loggingOutId = null; + }); + }) + : null, ), ), ), - ); - } else { - int realIndex = index - (accounts?.length ?? 0); - return Padding( - padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), - child: Material( - color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance ? selectedColor : null, + ), + ); + } else { + int realIndex = index - (accounts?.length ?? 0); + return Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: Material( + color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance ? selectedColor : null, + borderRadius: BorderRadius.circular(50), + child: InkWell( + onTap: (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance) + ? null + : () async { + context.read().add(const LogOutOfAllAccounts()); + context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances![realIndex].instance)); + context.read().add(InstanceChanged(instance: anonymousInstances![realIndex].instance)); + context.pop(); + }, borderRadius: BorderRadius.circular(50), - child: InkWell( - onTap: (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance) - ? null - : () async { - context.read().add(const LogOutOfAllAccounts()); - context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances![realIndex].instance)); - context.read().add(InstanceChanged(instance: anonymousInstances![realIndex].instance)); - context.pop(); - }, - borderRadius: BorderRadius.circular(50), - child: AnimatedSize( - duration: const Duration(milliseconds: 250), - child: ListTile( - leading: Stack( - children: [ - AnimatedCrossFade( - crossFadeState: anonymousInstances![realIndex].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 500), - firstChild: const SizedBox( - child: Padding( - padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), - child: Icon( - Icons.language, - ), + child: AnimatedSize( + duration: const Duration(milliseconds: 250), + child: ListTile( + leading: Stack( + children: [ + AnimatedCrossFade( + crossFadeState: anonymousInstances![realIndex].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 500), + firstChild: const SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), + child: Icon( + Icons.language, ), ), - secondChild: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundImage: anonymousInstances![realIndex].instanceIcon == null ? null : CachedNetworkImageProvider(anonymousInstances![realIndex].instanceIcon!), - maxRadius: 20, - ), ), - Positioned( - right: 0, - bottom: 0, - child: SizedBox( - width: 12, - height: 12, - child: Material( - borderRadius: BorderRadius.circular(10), - color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance ? selectedColor : null, - ), - ), + secondChild: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: anonymousInstances![realIndex].instanceIcon == null ? null : CachedNetworkImageProvider(anonymousInstances![realIndex].instanceIcon!), + maxRadius: 20, ), - // This is the status indicator - Positioned( - right: 1, - bottom: 1, - child: AnimatedOpacity( - opacity: anonymousInstances![realIndex].alive == null ? 0 : 1, - duration: const Duration(milliseconds: 500), - child: Icon( - anonymousInstances![realIndex].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, - size: 10, - color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), anonymousInstances![realIndex].alive == true ? Colors.green : Colors.red), - ), + ), + Positioned( + right: 0, + bottom: 0, + child: SizedBox( + width: 12, + height: 12, + child: Material( + borderRadius: BorderRadius.circular(10), + color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance ? selectedColor : null, ), ), - ], - ), - title: Row( - children: [ - const Icon( - Icons.person_off_rounded, - size: 15, - ), - const SizedBox(width: 5), - Text( - AppLocalizations.of(context)!.anonymous, - style: theme.textTheme.titleMedium?.copyWith(), + ), + // This is the status indicator + Positioned( + right: 1, + bottom: 1, + child: AnimatedOpacity( + opacity: anonymousInstances![realIndex].alive == null ? 0 : 1, + duration: const Duration(milliseconds: 500), + child: Icon( + anonymousInstances![realIndex].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, + size: 10, + color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), anonymousInstances![realIndex].alive == true ? Colors.green : Colors.red), + ), ), - ], - ), - subtitle: Wrap( - children: [ - Text(anonymousInstances![realIndex].instance), - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: anonymousInstances![realIndex].version == null - ? const SizedBox(height: 20, width: 0) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 5), - Text( - '•', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + ], + ), + title: Row( + children: [ + const Icon( + Icons.person_off_rounded, + size: 15, + ), + const SizedBox(width: 5), + Text( + AppLocalizations.of(context)!.anonymous, + style: theme.textTheme.titleMedium?.copyWith(), + ), + ], + ), + subtitle: Wrap( + children: [ + Text(anonymousInstances![realIndex].instance), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: anonymousInstances![realIndex].version == null + ? const SizedBox(height: 20, width: 0) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), ), - const SizedBox(width: 5), - Text( - 'v${anonymousInstances![realIndex].version}', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + const SizedBox(width: 5), + Text( + 'v${anonymousInstances![realIndex].version}', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), ), - ], - ), - ), - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: anonymousInstances![realIndex].latency == null - ? const SizedBox(height: 20, width: 0) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 5), - Text( - '•', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + ], + ), + ), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: anonymousInstances![realIndex].latency == null + ? const SizedBox(height: 20, width: 0) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), ), - const SizedBox(width: 5), - Text( - '${anonymousInstances![realIndex].latency?.inMilliseconds}ms', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + const SizedBox(width: 5), + Text( + '${anonymousInstances![realIndex].latency?.inMilliseconds}ms', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), ), - ], - ), - ), - ], - ), - trailing: ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1) - ? (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance) - ? IconButton( - icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.removeInstance), - onPressed: () async { - context.read().add(OnRemoveAnonymousInstance(anonymousInstances![realIndex].instance)); - - if (anonymousInstances!.length > 1) { - context - .read() - .add(OnSetCurrentAnonymousInstance(anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![realIndex]).instance)); - context.read().add(InstanceChanged(instance: anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![realIndex]).instance)); - } else { - context.read().add(SwitchAccount(accountId: accounts!.last.account.id)); - } - - setState(() => anonymousInstances = null); - }, - ) - : IconButton( - icon: Icon( - Icons.delete, - semanticLabel: AppLocalizations.of(context)!.removeInstance, - ), - onPressed: () async { - context.read().add(OnRemoveAnonymousInstance(anonymousInstances![realIndex].instance)); - setState(() { - anonymousInstances = null; - }); - }) - : null, + ), + ], + ), + ), + ], ), + trailing: ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1) + ? (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance) + ? IconButton( + icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.removeInstance), + onPressed: () async { + context.read().add(OnRemoveAnonymousInstance(anonymousInstances![realIndex].instance)); + + if (anonymousInstances!.length > 1) { + context + .read() + .add(OnSetCurrentAnonymousInstance(anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![realIndex]).instance)); + context.read().add(InstanceChanged(instance: anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![realIndex]).instance)); + } else { + context.read().add(SwitchAccount(accountId: accounts!.last.account.id)); + } + + setState(() => anonymousInstances = null); + }, + ) + : IconButton( + icon: Icon( + Icons.delete, + semanticLabel: AppLocalizations.of(context)!.removeInstance, + ), + onPressed: () async { + context.read().add(OnRemoveAnonymousInstance(anonymousInstances![realIndex].instance)); + setState(() { + anonymousInstances = null; + }); + }) + : null, ), ), ), - ); - } - }, - itemCount: (accounts?.length ?? 0) + (anonymousInstances?.length ?? 0), - ), - const SliverToBoxAdapter(child: SizedBox(height: 100)) - ], - ), + ), + ); + } + }, + itemCount: (accounts?.length ?? 0) + (anonymousInstances?.length ?? 0), + ), + const SliverToBoxAdapter(child: SizedBox(height: 100)) + ], ), ), ); diff --git a/lib/community/pages/create_post_page.dart b/lib/community/pages/create_post_page.dart index d2137fd99..805e831ed 100644 --- a/lib/community/pages/create_post_page.dart +++ b/lib/community/pages/create_post_page.dart @@ -13,6 +13,7 @@ import 'package:markdown_editable_textinput/format_markdown.dart'; import 'package:markdown_editable_textinput/markdown_buttons.dart'; import 'package:markdown_editable_textinput/markdown_text_input_field.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:overlay_support/overlay_support.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:thunder/account/models/account.dart'; @@ -33,6 +34,7 @@ import 'package:thunder/shared/link_preview_card.dart'; import 'package:thunder/user/widgets/user_indicator.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/utils/debounce.dart'; +import 'package:thunder/utils/global_context.dart'; import 'package:thunder/utils/image.dart'; import 'package:thunder/utils/instance.dart'; import 'package:thunder/post/utils/navigate_post.dart'; @@ -62,9 +64,6 @@ class CreatePostPage extends StatefulWidget { /// Callback function that is triggered whenever the post is successfully created or updated final Function(PostViewMedia postViewMedia)? onPostSuccess; - // The scaffold key for the main Thunder page - final GlobalKey? scaffoldMessengerKey; - const CreatePostPage({ super.key, required this.communityId, @@ -76,7 +75,6 @@ class CreatePostPage extends StatefulWidget { this.prePopulated = false, this.postView, this.onPostSuccess, - this.scaffoldMessengerKey, }); @override @@ -218,7 +216,10 @@ class _CreatePostPageState extends State { if (draftPost.isNotEmpty && draftPost.saveAsDraft) { sharedPreferences?.setString(draftId, jsonEncode(draftPost.toJson())); - if (context.mounted) showSnackbar(context, AppLocalizations.of(context)!.postSavedAsDraft); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + showSnackbar(AppLocalizations.of(GlobalContext.context)!.postSavedAsDraft); + }); } else { sharedPreferences?.remove(draftId); } @@ -260,7 +261,6 @@ class _CreatePostPageState extends State { if (context.mounted && draftPost.isNotEmpty) { showSnackbar( - context, AppLocalizations.of(context)!.restoredPostFromDraft, trailingIcon: Icons.delete_forever_rounded, trailingIconColor: Theme.of(context).colorScheme.errorContainer, @@ -306,7 +306,7 @@ class _CreatePostPageState extends State { } if (state.status == CreatePostStatus.error && state.message != null) { - showSnackbar(context, state.message!); + showSnackbar(state.message!); context.read().clearMessage(); } @@ -319,7 +319,7 @@ class _CreatePostPageState extends State { break; case CreatePostStatus.imageUploadFailure: case CreatePostStatus.postImageUploadFailure: - showSnackbar(context, l10n.postUploadImageError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); + showSnackbar(l10n.postUploadImageError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); default: break; } @@ -355,15 +355,13 @@ class _CreatePostPageState extends State { languageId: languageId, ); - if (postId != null) { + if (widget.postView?.post.id == null && postId != null) { showSnackbar( - context, l10n.postCreatedSuccessfully, trailingIcon: Icons.remove_red_eye_rounded, trailingAction: () { - navigateToPost(widget.scaffoldMessengerKey!.currentContext!, postId: postId); + navigateToPost(context, postId: postId); }, - customState: widget.scaffoldMessengerKey?.currentState, ); } }, diff --git a/lib/community/utils/post_actions.dart b/lib/community/utils/post_actions.dart index 7a326e8b0..9d35309eb 100644 --- a/lib/community/utils/post_actions.dart +++ b/lib/community/utils/post_actions.dart @@ -28,7 +28,7 @@ void triggerPostAction({ bool downvotesEnabled = context.read().state.downvotesEnabled; if (downvotesEnabled == false) { - showSnackbar(context, AppLocalizations.of(context)!.downvotesDisabled); + showSnackbar(AppLocalizations.of(context)!.downvotesDisabled); return; } @@ -36,7 +36,7 @@ void triggerPostAction({ return; case SwipeAction.reply: case SwipeAction.edit: - showSnackbar(context, AppLocalizations.of(context)!.replyNotSupported); + showSnackbar(AppLocalizations.of(context)!.replyNotSupported); break; case SwipeAction.save: onSaveAction(postViewMedia.postView.post.id, !(saved ?? false)); diff --git a/lib/community/utils/post_card_action_helpers.dart b/lib/community/utils/post_card_action_helpers.dart index a9257bd12..38abfd5d4 100644 --- a/lib/community/utils/post_card_action_helpers.dart +++ b/lib/community/utils/post_card_action_helpers.dart @@ -308,7 +308,7 @@ void onSelected(BuildContext context, PostCardAction postCardAction, PostViewMed if (media == null) { // Tell user we're downloading the image - showSnackbar(context, AppLocalizations.of(context)!.downloadingMedia); + showSnackbar(AppLocalizations.of(context)!.downloadingMedia); // Download mediaFile = await DefaultCacheManager().getSingleFile(postViewMedia.media.first.mediaUrl!); @@ -321,7 +321,7 @@ void onSelected(BuildContext context, PostCardAction postCardAction, PostViewMed await Share.shareXFiles([XFile(mediaFile!.path)]); } catch (e) { // Tell the user that the download failed - showSnackbar(context, AppLocalizations.of(context)!.errorDownloadingMedia(e)); + showSnackbar(AppLocalizations.of(context)!.errorDownloadingMedia(e)); } } break; diff --git a/lib/feed/view/feed_page.dart b/lib/feed/view/feed_page.dart index a06964223..7de4dd00f 100644 --- a/lib/feed/view/feed_page.dart +++ b/lib/feed/view/feed_page.dart @@ -52,7 +52,6 @@ class FeedPage extends StatefulWidget { this.userId, this.username, this.scaffoldStateKey, - this.scaffoldMessengerKey, }); /// The type of feed to display. @@ -85,9 +84,6 @@ class FeedPage extends StatefulWidget { /// The scaffold key which holds the drawer final GlobalKey? scaffoldStateKey; - /// The messenger key back to the main Thunder page - final GlobalKey? scaffoldMessengerKey; - @override State createState() => _FeedPageState(); } @@ -131,7 +127,7 @@ class _FeedPageState extends State with AutomaticKeepAliveClientMixin< return BlocProvider.value( value: bloc, - child: FeedView(scaffoldStateKey: widget.scaffoldStateKey, scaffoldMessengerKey: widget.scaffoldMessengerKey), + child: FeedView(scaffoldStateKey: widget.scaffoldStateKey), ); } @@ -147,20 +143,17 @@ class _FeedPageState extends State with AutomaticKeepAliveClientMixin< username: widget.username, reset: true, )), - child: FeedView(scaffoldStateKey: widget.scaffoldStateKey, scaffoldMessengerKey: widget.scaffoldMessengerKey), + child: FeedView(scaffoldStateKey: widget.scaffoldStateKey), ); } } class FeedView extends StatefulWidget { - const FeedView({super.key, this.scaffoldStateKey, this.scaffoldMessengerKey}); + const FeedView({super.key, this.scaffoldStateKey}); /// The scaffold key which holds the drawer final GlobalKey? scaffoldStateKey; - /// The messenger key back to the main Thunder page - final GlobalKey? scaffoldMessengerKey; - @override State createState() => _FeedViewState(); } @@ -177,8 +170,6 @@ class _FeedViewState extends State { /// List of post ids to queue for removal. The ids in this list allow us to remove posts in a staggered method List queuedForRemoval = []; - final GlobalKey _key = GlobalKey(); - @override void initState() { super.initState(); @@ -245,205 +236,199 @@ class _FeedViewState extends State { BlocListener( listener: (context, state) { if (state.message != null) { - showSnackbar(context, state.message!); + showSnackbar(state.message!); } }, ), BlocListener( listener: (context, state) { if ((state.status == UserStatus.failure || state.status == UserStatus.failedToBlock) && state.errorMessage != null) { - showSnackbar(context, state.errorMessage!); + showSnackbar(state.errorMessage!); } else if (state.status == UserStatus.success && state.blockedPerson != null) { - showSnackbar(context, l10n.successfullyBlocked); + showSnackbar(l10n.successfullyBlocked); } }, ), BlocListener( listener: (context, state) { if (state.message != null) { - showSnackbar(context, state.message!); + showSnackbar(state.message!); } }, ), ], - child: ScaffoldMessenger( - key: _key, - child: Scaffold( - body: SafeArea( - top: hideTopBarOnScroll, // Don't apply to top of screen to allow for the status bar colour to extend - child: BlocConsumer( - listenWhen: (previous, current) { - if (current.status == FeedStatus.initial) setState(() => showAppBarTitle = false); - if (previous.scrollId != current.scrollId) _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); - if (previous.dismissReadId != current.dismissReadId) dismissRead(); - return true; - }, - listener: (context, state) { - // Continue to fetch more posts as long as the device view is not scrollable. - // This is to avoid cases where more posts cannot be fetched because the conditions are not met - if (state.status == FeedStatus.success && state.hasReachedEnd == false) { - bool isScrollable = _scrollController.position.maxScrollExtent > _scrollController.position.viewportDimension; - if (!isScrollable) context.read().add(const FeedFetchedEvent()); - } - - if ((state.status == FeedStatus.failure || state.status == FeedStatus.failureLoadingCommunity) && state.message != null) { - showSnackbar(context, state.message!, customState: _key.currentState); - context.read().add(FeedClearMessageEvent()); // Clear the message so that it does not spam - } - }, - builder: (context, state) { - final theme = Theme.of(context); - List postViewMedias = state.postViewMedias; - - return RefreshIndicator( - onRefresh: () async { - HapticFeedback.mediumImpact(); - triggerRefresh(context); - }, - edgeOffset: 95.0, // This offset is placed to allow the correct positioning of the refresh indicator - child: Stack( - children: [ - CustomScrollView( - physics: showCommunitySidebar ? const NeverScrollableScrollPhysics() : null, // Disable scrolling on the feed page when the community sidebar is open - controller: _scrollController, - slivers: [ - FeedPageAppBar( - showAppBarTitle: (state.feedType == FeedType.general && state.status != FeedStatus.initial) ? true : showAppBarTitle, - scaffoldStateKey: widget.scaffoldStateKey, + child: Scaffold( + body: SafeArea( + top: hideTopBarOnScroll, // Don't apply to top of screen to allow for the status bar colour to extend + child: BlocConsumer( + listenWhen: (previous, current) { + if (current.status == FeedStatus.initial) setState(() => showAppBarTitle = false); + if (previous.scrollId != current.scrollId) _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + if (previous.dismissReadId != current.dismissReadId) dismissRead(); + return true; + }, + listener: (context, state) { + // Continue to fetch more posts as long as the device view is not scrollable. + // This is to avoid cases where more posts cannot be fetched because the conditions are not met + if (state.status == FeedStatus.success && state.hasReachedEnd == false) { + bool isScrollable = _scrollController.position.maxScrollExtent > _scrollController.position.viewportDimension; + if (!isScrollable) context.read().add(const FeedFetchedEvent()); + } + + if ((state.status == FeedStatus.failure || state.status == FeedStatus.failureLoadingCommunity) && state.message != null) { + showSnackbar(state.message!); + context.read().add(FeedClearMessageEvent()); // Clear the message so that it does not spam + } + }, + builder: (context, state) { + final theme = Theme.of(context); + List postViewMedias = state.postViewMedias; + + return RefreshIndicator( + onRefresh: () async { + HapticFeedback.mediumImpact(); + triggerRefresh(context); + }, + edgeOffset: 95.0, // This offset is placed to allow the correct positioning of the refresh indicator + child: Stack( + children: [ + CustomScrollView( + physics: showCommunitySidebar ? const NeverScrollableScrollPhysics() : null, // Disable scrolling on the feed page when the community sidebar is open + controller: _scrollController, + slivers: [ + FeedPageAppBar( + showAppBarTitle: (state.feedType == FeedType.general && state.status != FeedStatus.initial) ? true : showAppBarTitle, + scaffoldStateKey: widget.scaffoldStateKey, + ), + // Display loading indicator until the feed is fetched + if (state.status == FeedStatus.initial) + const SliverFillRemaining( + hasScrollBody: false, + child: Center(child: CircularProgressIndicator()), ), - // Display loading indicator until the feed is fetched - if (state.status == FeedStatus.initial) - const SliverFillRemaining( - hasScrollBody: false, - child: Center(child: CircularProgressIndicator()), - ), - if (state.status == FeedStatus.failureLoadingCommunity) - SliverToBoxAdapter( - child: Container(), + if (state.status == FeedStatus.failureLoadingCommunity) + SliverToBoxAdapter( + child: Container(), + ), + // Display tagline and list of posts once they are fetched + if (state.status != FeedStatus.initial && state.status != FeedStatus.failureLoadingCommunity) ...[ + SliverToBoxAdapter( + child: Visibility( + visible: state.feedType == FeedType.general && state.status != FeedStatus.initial, + child: const TagLine(), ), - // Display tagline and list of posts once they are fetched - if (state.status != FeedStatus.initial && state.status != FeedStatus.failureLoadingCommunity) ...[ + ), + if (state.fullCommunityView != null) SliverToBoxAdapter( child: Visibility( - visible: state.feedType == FeedType.general && state.status != FeedStatus.initial, - child: const TagLine(), + visible: state.feedType == FeedType.community, + child: CommunityHeader( + getCommunityResponse: state.fullCommunityView!, + showCommunitySidebar: showCommunitySidebar, + onToggle: (bool toggled) { + // Scroll to top first before showing the sidebar + _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + setState(() => showCommunitySidebar = toggled); + }, + ), ), ), - if (state.fullCommunityView != null) + SliverStack( + children: [ + // Widget representing the list of posts on the feed + FeedPostList( + postViewMedias: postViewMedias, + tabletMode: tabletMode, + queuedForRemoval: queuedForRemoval, + ), + // Widgets to display on the feed when feedType == FeedType.community SliverToBoxAdapter( - child: Visibility( - visible: state.feedType == FeedType.community, - child: CommunityHeader( - getCommunityResponse: state.fullCommunityView!, - showCommunitySidebar: showCommunitySidebar, - onToggle: (bool toggled) { - // Scroll to top first before showing the sidebar - _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); - setState(() => showCommunitySidebar = toggled); - }, - ), + child: AnimatedSwitcher( + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeOut, + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: animation, curve: const Interval(0, 1.0)), + ), + child: child, + ); + }, + duration: const Duration(milliseconds: 300), + child: showCommunitySidebar + ? GestureDetector( + onTap: () => setState(() => showCommunitySidebar = !showCommunitySidebar), + child: Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + color: Colors.black.withOpacity(0.5), + ), + ) + : null, ), ), - SliverStack( - children: [ - // Widget representing the list of posts on the feed - FeedPostList( - postViewMedias: postViewMedias, - tabletMode: tabletMode, - queuedForRemoval: queuedForRemoval, - ), - // Widgets to display on the feed when feedType == FeedType.community - SliverToBoxAdapter( - child: AnimatedSwitcher( - switchInCurve: Curves.easeOut, - switchOutCurve: Curves.easeOut, - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: animation, curve: const Interval(0, 1.0)), - ), - child: child, - ); - }, - duration: const Duration(milliseconds: 300), - child: showCommunitySidebar - ? GestureDetector( - onTap: () => setState(() => showCommunitySidebar = !showCommunitySidebar), - child: Container( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - color: Colors.black.withOpacity(0.5), - ), - ) - : null, - ), + // Contains the widget for the community sidebar + SliverToBoxAdapter( + child: AnimatedSwitcher( + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeOut, + transitionBuilder: (child, animation) { + return SlideTransition( + position: Tween(begin: const Offset(1.2, 0), end: const Offset(0, 0)).animate(animation), + child: child, + ); + }, + duration: const Duration(milliseconds: 300), + child: showCommunitySidebar + ? CommunitySidebar( + getCommunityResponse: state.fullCommunityView, + onDismiss: () => setState(() => showCommunitySidebar = false), + ) + : Container(), ), - // Contains the widget for the community sidebar - SliverToBoxAdapter( - child: AnimatedSwitcher( - switchInCurve: Curves.easeOut, - switchOutCurve: Curves.easeOut, - transitionBuilder: (child, animation) { - return SlideTransition( - position: Tween(begin: const Offset(1.2, 0), end: const Offset(0, 0)).animate(animation), - child: child, - ); - }, - duration: const Duration(milliseconds: 300), - child: showCommunitySidebar - ? CommunitySidebar( - getCommunityResponse: state.fullCommunityView, - onDismiss: () => setState(() => showCommunitySidebar = false), - ) - : Container(), + ), + ], + ), + // Widget representing the bottom of the feed (reached end or loading more posts indicators) + SliverToBoxAdapter( + child: state.hasReachedEnd + ? const FeedReachedEnd() + : Container( + height: state.status == FeedStatus.initial ? MediaQuery.of(context).size.height * 0.5 : null, // Might have to adjust this to be more robust + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: const CircularProgressIndicator(), ), - ), - ], - ), - // Widget representing the bottom of the feed (reached end or loading more posts indicators) - SliverToBoxAdapter( - child: state.hasReachedEnd - ? const FeedReachedEnd() - : Container( - height: state.status == FeedStatus.initial ? MediaQuery.of(context).size.height * 0.5 : null, // Might have to adjust this to be more robust - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: const CircularProgressIndicator(), - ), - ), - ], + ), ], - ), - // Widget to host the feed FAB when navigating to new page + ], + ), + // Widget to host the feed FAB when navigating to new page + AnimatedOpacity( + opacity: thunderBloc.state.isFabOpen ? 1.0 : 0.0, + duration: const Duration(milliseconds: 150), + child: thunderBloc.state.isFabOpen + ? ModalBarrier( + color: theme.colorScheme.background.withOpacity(0.95), + dismissible: true, + onDismiss: () => context.read().add(const OnFabToggle(false)), + ) + : null, + ), + if (Navigator.of(context).canPop() && (state.communityId != null || state.communityName != null) && thunderBloc.state.enableFeedsFab) AnimatedOpacity( - opacity: thunderBloc.state.isFabOpen ? 1.0 : 0.0, + opacity: (thunderBloc.state.enableFeedsFab) ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), - child: thunderBloc.state.isFabOpen - ? ModalBarrier( - color: theme.colorScheme.background.withOpacity(0.95), - dismissible: true, - onDismiss: () => context.read().add(const OnFabToggle(false)), - ) - : null, - ), - if (Navigator.of(context).canPop() && (state.communityId != null || state.communityName != null) && thunderBloc.state.enableFeedsFab) - AnimatedOpacity( - opacity: (thunderBloc.state.enableFeedsFab) ? 1.0 : 0.0, - duration: const Duration(milliseconds: 150), - curve: Curves.easeIn, - child: Container( - margin: const EdgeInsets.all(16), - child: FeedFAB( - heroTag: state.communityName, - scaffoldMessengerKey: widget.scaffoldMessengerKey, - ), - ), + curve: Curves.easeIn, + child: Container( + margin: const EdgeInsets.all(16), + child: FeedFAB(heroTag: state.communityName), ), - ], - ), - ); - }, - ), + ), + ], + ), + ); + }, ), ), ), diff --git a/lib/feed/widgets/feed_fab.dart b/lib/feed/widgets/feed_fab.dart index d1801d593..1b1ad0f48 100644 --- a/lib/feed/widgets/feed_fab.dart +++ b/lib/feed/widgets/feed_fab.dart @@ -21,13 +21,10 @@ import 'package:thunder/shared/sort_picker.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; class FeedFAB extends StatelessWidget { - const FeedFAB({super.key, this.heroTag, this.scaffoldMessengerKey}); + const FeedFAB({super.key, this.heroTag}); final String? heroTag; - /// The messenger key back to the main Thunder page - final GlobalKey? scaffoldMessengerKey; - @override build(BuildContext context) { final theme = Theme.of(context); @@ -283,11 +280,11 @@ class FeedFAB extends StatelessWidget { final l10n = AppLocalizations.of(context)!; if (!context.read().state.isLoggedIn) { - return showSnackbar(context, l10n.mustBeLoggedInPost); + return showSnackbar(l10n.mustBeLoggedInPost); } if (isPostingLocked) { - return showSnackbar(context, l10n.onlyModsCanPostInCommunity); + return showSnackbar(l10n.onlyModsCanPostInCommunity); } FeedBloc feedBloc = context.read(); @@ -312,7 +309,6 @@ class FeedFAB extends StatelessWidget { child: CreatePostPage( communityId: feedBloc.state.communityId, communityView: feedBloc.state.fullCommunityView?.communityView, - scaffoldMessengerKey: scaffoldMessengerKey, ), ); }, diff --git a/lib/feed/widgets/feed_page_app_bar.dart b/lib/feed/widgets/feed_page_app_bar.dart index 92eb95e69..36aafe4e7 100644 --- a/lib/feed/widgets/feed_page_app_bar.dart +++ b/lib/feed/widgets/feed_page_app_bar.dart @@ -236,9 +236,9 @@ void _onSubscribeIconPressed(BuildContext context) { if (currentSubscriptions.contains(community.id)) { context.read().add(DeleteSubscriptionsEvent(ids: {community.id})); - showSnackbar(context, AppLocalizations.of(context)!.unsubscribed); + showSnackbar(AppLocalizations.of(context)!.unsubscribed); } else { context.read().add(AddSubscriptionsEvent(communities: {community})); - showSnackbar(context, AppLocalizations.of(context)!.subscribed); + showSnackbar(AppLocalizations.of(context)!.subscribed); } } diff --git a/lib/inbox/widgets/inbox_mentions_view.dart b/lib/inbox/widgets/inbox_mentions_view.dart index e2bd9d778..573428ab5 100644 --- a/lib/inbox/widgets/inbox_mentions_view.dart +++ b/lib/inbox/widgets/inbox_mentions_view.dart @@ -173,7 +173,7 @@ class InboxMentionsView extends StatelessWidget { if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true) { await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, AppLocalizations.of(context)!.commentSavedAsDraft); + showSnackbar(AppLocalizations.of(context)!.commentSavedAsDraft); prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { prefs.remove(draftId); diff --git a/lib/inbox/widgets/inbox_replies_view.dart b/lib/inbox/widgets/inbox_replies_view.dart index 1e90f5cf9..11934b229 100644 --- a/lib/inbox/widgets/inbox_replies_view.dart +++ b/lib/inbox/widgets/inbox_replies_view.dart @@ -160,7 +160,7 @@ class _InboxRepliesViewState extends State { if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true && (!isEdit || commentView.comment.content != newDraftComment?.text)) { await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, AppLocalizations.of(context)!.commentSavedAsDraft); + showSnackbar(AppLocalizations.of(context)!.commentSavedAsDraft); prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { prefs.remove(draftId); diff --git a/lib/instance/instance_page.dart b/lib/instance/instance_page.dart index eae296bb4..b959a21e1 100644 --- a/lib/instance/instance_page.dart +++ b/lib/instance/instance_page.dart @@ -32,7 +32,6 @@ class InstancePage extends StatefulWidget { } class _InstancePageState extends State { - final GlobalKey _key = GlobalKey(); bool? isBlocked; bool currentlyTogglingBlock = false; @@ -46,7 +45,7 @@ class _InstancePageState extends State { return BlocListener( listener: (context, state) { if (state.message != null) { - showSnackbar(context, state.message!, customState: _key.currentState); + showSnackbar(state.message!); } if (state.status == InstanceStatus.success && currentlyTogglingBlock) { @@ -56,63 +55,60 @@ class _InstancePageState extends State { }); } }, - child: ScaffoldMessenger( - key: _key, - child: Scaffold( - body: Container( - color: theme.colorScheme.background, - child: SafeArea( - top: false, - child: CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - title: ListTile( - title: Text( - fetchInstanceNameFromUrl(widget.getSiteResponse.siteView.site.actorId) ?? '', - style: theme.textTheme.titleLarge, - ), - subtitle: Text("v${widget.getSiteResponse.version} · ${l10n.countUsers(formatLongNumber(widget.getSiteResponse.siteView.counts.users))}"), - contentPadding: const EdgeInsets.symmetric(horizontal: 0), + child: Scaffold( + body: Container( + color: theme.colorScheme.background, + child: SafeArea( + top: false, + child: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: ListTile( + title: Text( + fetchInstanceNameFromUrl(widget.getSiteResponse.siteView.site.actorId) ?? '', + style: theme.textTheme.titleLarge, ), - actions: [ - if (LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance) && widget.instanceId != null) - IconButton( - tooltip: isBlocked! ? l10n.unblockInstance : l10n.blockInstance, - onPressed: () { - currentlyTogglingBlock = true; - context.read().add(InstanceActionEvent( - instanceAction: InstanceAction.block, - instanceId: widget.instanceId!, - domain: fetchInstanceNameFromUrl(widget.getSiteResponse.siteView.site.actorId), - value: !isBlocked!, - )); - }, - icon: Icon( - isBlocked! ? Icons.undo_rounded : Icons.block, - semanticLabel: isBlocked! ? l10n.unblockInstance : l10n.blockInstance, - ), - ), + subtitle: Text("v${widget.getSiteResponse.version} · ${l10n.countUsers(formatLongNumber(widget.getSiteResponse.siteView.counts.users))}"), + contentPadding: const EdgeInsets.symmetric(horizontal: 0), + ), + actions: [ + if (LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance) && widget.instanceId != null) IconButton( - tooltip: l10n.openInBrowser, - onPressed: () => handleLink(context, url: widget.getSiteResponse.siteView.site.actorId), + tooltip: isBlocked! ? l10n.unblockInstance : l10n.blockInstance, + onPressed: () { + currentlyTogglingBlock = true; + context.read().add(InstanceActionEvent( + instanceAction: InstanceAction.block, + instanceId: widget.instanceId!, + domain: fetchInstanceNameFromUrl(widget.getSiteResponse.siteView.site.actorId), + value: !isBlocked!, + )); + }, icon: Icon( - Icons.open_in_browser_rounded, - semanticLabel: l10n.openInBrowser, + isBlocked! ? Icons.undo_rounded : Icons.block, + semanticLabel: isBlocked! ? l10n.unblockInstance : l10n.blockInstance, ), ), - ], - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(20), - child: Material( - child: InstanceView(site: widget.getSiteResponse.siteView.site), + IconButton( + tooltip: l10n.openInBrowser, + onPressed: () => handleLink(context, url: widget.getSiteResponse.siteView.site.actorId), + icon: Icon( + Icons.open_in_browser_rounded, + semanticLabel: l10n.openInBrowser, ), ), + ], + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(20), + child: Material( + child: InstanceView(site: widget.getSiteResponse.siteView.site), + ), ), - ], - ), + ), + ], ), ), ), diff --git a/lib/instance/utils/navigate_instance.dart b/lib/instance/utils/navigate_instance.dart index 9697dc81c..8418d4a53 100644 --- a/lib/instance/utils/navigate_instance.dart +++ b/lib/instance/utils/navigate_instance.dart @@ -51,7 +51,7 @@ Future navigateToInstancePage(BuildContext context, {required String insta ), ); } else { - showSnackbar(context, l10n.unableToNavigateToInstance(instanceHost)); + showSnackbar(l10n.unableToNavigateToInstance(instanceHost)); } } } diff --git a/lib/post/pages/create_comment_page.dart b/lib/post/pages/create_comment_page.dart index 32e4c52cb..0faa1f374 100644 --- a/lib/post/pages/create_comment_page.dart +++ b/lib/post/pages/create_comment_page.dart @@ -123,7 +123,7 @@ class _CreateCommentPageState extends State { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, AppLocalizations.of(context)!.restoredCommentFromDraft); + showSnackbar(AppLocalizations.of(context)!.restoredCommentFromDraft); }); } } @@ -163,7 +163,7 @@ class _CreateCommentPageState extends State { setState(() { isLoading = false; }); - showSnackbar(context, state.errorMessage ?? AppLocalizations.of(context)!.unexpectedError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); + showSnackbar(state.errorMessage ?? AppLocalizations.of(context)!.unexpectedError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); } }, ), @@ -185,7 +185,7 @@ class _CreateCommentPageState extends State { if (state.status == InboxStatus.failure) { setState(() { isLoading = false; - showSnackbar(context, AppLocalizations.of(context)!.unexpectedError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); + showSnackbar(AppLocalizations.of(context)!.unexpectedError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); }); } }, @@ -201,7 +201,7 @@ class _CreateCommentPageState extends State { setState(() => imageUploading = true); } if (state.status == ImageStatus.failure) { - showSnackbar(context, AppLocalizations.of(context)!.postUploadImageError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); + showSnackbar(AppLocalizations.of(context)!.postUploadImageError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); setState(() => imageUploading = false); } }, diff --git a/lib/post/pages/post_page.dart b/lib/post/pages/post_page.dart index a685a324e..3a579c2a1 100644 --- a/lib/post/pages/post_page.dart +++ b/lib/post/pages/post_page.dart @@ -70,9 +70,6 @@ class _PostPageState extends State { IconData? sortTypeIcon; String? sortTypeLabel; - /// The messenger key for this post page - final GlobalKey _scaffoldMessengerKey = GlobalKey(); - @override void initState() { super.initState(); @@ -123,425 +120,419 @@ class _PostPageState extends State { _previousIsFabSummoned = isFabSummoned; } - return ScaffoldMessenger( - key: _scaffoldMessengerKey, - child: BlocProvider( - create: (context) => PostBloc(), - child: BlocConsumer( - listenWhen: (previousState, currentState) { - if (previousState.sortType != currentState.sortType) { - setState(() { - sortType = currentState.sortType; - final sortTypeItem = CommentSortPicker.getCommentSortTypeItems(includeVersionSpecificFeature: IncludeVersionSpecificFeature.always) - .firstWhere((sortTypeItem) => sortTypeItem.payload == currentState.sortType); - sortTypeIcon = sortTypeItem.icon; - sortTypeLabel = sortTypeItem.label; - }); - } - return true; - }, - listener: (context, state) {}, - builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: ListTile( - title: Text( - sortTypeLabel?.isNotEmpty == true ? l10n.comments : '', - style: theme.textTheme.titleLarge, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Row( - children: [ - Icon(sortTypeIcon, size: 13), - const SizedBox(width: 4), - Text(sortTypeLabel ?? ''), - ], - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 0), + return BlocProvider( + create: (context) => PostBloc(), + child: BlocConsumer( + listenWhen: (previousState, currentState) { + if (previousState.sortType != currentState.sortType) { + setState(() { + sortType = currentState.sortType; + final sortTypeItem = CommentSortPicker.getCommentSortTypeItems(includeVersionSpecificFeature: IncludeVersionSpecificFeature.always) + .firstWhere((sortTypeItem) => sortTypeItem.payload == currentState.sortType); + sortTypeIcon = sortTypeItem.icon; + sortTypeLabel = sortTypeItem.label; + }); + } + return true; + }, + listener: (context, state) {}, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: ListTile( + title: Text( + sortTypeLabel?.isNotEmpty == true ? l10n.comments : '', + style: theme.textTheme.titleLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Row( + children: [ + Icon(sortTypeIcon, size: 13), + const SizedBox(width: 4), + Text(sortTypeLabel ?? ''), + ], + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 0), + ), + flexibleSpace: Semantics( + excludeSemantics: true, + child: GestureDetector( + onTap: () { + if (context.read().state.isFabOpen) { + context.read().add(const OnFabToggle(false)); + } + }, + ), + ), + leading: IconButton( + icon: Icon( + Icons.arrow_back_rounded, + semanticLabel: l10n.back, ), - flexibleSpace: Semantics( - excludeSemantics: true, - child: GestureDetector( - onTap: () { + onPressed: () { + if (context.read().state.isFabOpen) { + context.read().add(const OnFabToggle(false)); + } + Navigator.pop(context); + }, + ), + actions: [ + IconButton( + icon: Icon(Icons.refresh_rounded, semanticLabel: l10n.refresh), + onPressed: () { if (context.read().state.isFabOpen) { context.read().add(const OnFabToggle(false)); } - }, - ), - ), - leading: IconButton( + HapticFeedback.mediumImpact(); + return context + .read() + .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath)); + }), + IconButton( icon: Icon( - Icons.arrow_back_rounded, - semanticLabel: l10n.back, + Icons.sort, + semanticLabel: l10n.sortBy, ), + tooltip: l10n.sortBy, onPressed: () { if (context.read().state.isFabOpen) { context.read().add(const OnFabToggle(false)); } - Navigator.pop(context); + showSortBottomSheet(context, state); }, ), - actions: [ - IconButton( - icon: Icon(Icons.refresh_rounded, semanticLabel: l10n.refresh), - onPressed: () { - if (context.read().state.isFabOpen) { - context.read().add(const OnFabToggle(false)); - } - HapticFeedback.mediumImpact(); - return context - .read() - .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath)); - }), - IconButton( - icon: Icon( - Icons.sort, - semanticLabel: l10n.sortBy, + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () => createCrossPost( + context, + title: widget.postView?.postView.post.name ?? state.postView?.postView.post.name ?? '', + url: widget.postView?.postView.post.url ?? state.postView?.postView.post.url, + text: widget.postView?.postView.post.body ?? state.postView?.postView.post.body, + postUrl: widget.postView?.postView.post.apId ?? state.postView?.postView.post.apId, + ), + child: ListTile( + dense: true, + horizontalTitleGap: 5, + leading: const Icon(Icons.repeat_rounded, size: 20), + title: Text(l10n.createNewCrossPost), + ), ), - tooltip: l10n.sortBy, - onPressed: () { - if (context.read().state.isFabOpen) { - context.read().add(const OnFabToggle(false)); - } - showSortBottomSheet(context, state); - }, - ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - onTap: () => createCrossPost( - context, - title: widget.postView?.postView.post.name ?? state.postView?.postView.post.name ?? '', - url: widget.postView?.postView.post.url ?? state.postView?.postView.post.url, - text: widget.postView?.postView.post.body ?? state.postView?.postView.post.body, - postUrl: widget.postView?.postView.post.apId ?? state.postView?.postView.post.apId, - scaffoldMessengerKey: _scaffoldMessengerKey, - ), - child: ListTile( - dense: true, - horizontalTitleGap: 5, - leading: const Icon(Icons.repeat_rounded, size: 20), - title: Text(l10n.createNewCrossPost), + ], + ), + ], + centerTitle: false, + toolbarHeight: 70.0, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + 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, ), ), - ], + ), ), - ], - centerTitle: false, - toolbarHeight: 70.0, - ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - 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: EdgeInsets.only( + right: combineNavAndFab ? 0 : 16, + bottom: combineNavAndFab ? 5 : 0, ), - if (enableFab) - Padding( - padding: EdgeInsets.only( - right: combineNavAndFab ? 0 : 16, - bottom: combineNavAndFab ? 5 : 0, - ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: isFabSummoned - ? GestureFab( - centered: combineNavAndFab, - distance: combineNavAndFab ? 45 : 60, - icon: Icon( - state.status == PostStatus.searchInProgress ? Icons.youtube_searched_for_rounded : singlePressAction.getIcon(postLocked: postLocked), - semanticLabel: state.status == PostStatus.searchInProgress ? l10n.search : singlePressAction.getTitle(context, postLocked: postLocked), - size: 35, - ), - onPressed: state.status == PostStatus.searchInProgress - ? () { - context.read().add(const ContinueCommentSearchEvent()); - } - : () => singlePressAction.execute( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: isFabSummoned + ? GestureFab( + centered: combineNavAndFab, + distance: combineNavAndFab ? 45 : 60, + icon: Icon( + state.status == PostStatus.searchInProgress ? Icons.youtube_searched_for_rounded : singlePressAction.getIcon(postLocked: postLocked), + semanticLabel: state.status == PostStatus.searchInProgress ? l10n.search : singlePressAction.getTitle(context, postLocked: postLocked), + size: 35, + ), + onPressed: state.status == PostStatus.searchInProgress + ? () { + context.read().add(const ContinueCommentSearchEvent()); + } + : () => singlePressAction.execute( + context: context, + postView: state.postView, + postId: state.postId, + selectedCommentId: state.selectedCommentId, + selectedCommentPath: state.selectedCommentPath, + override: singlePressAction == PostFabAction.backToTop + ? () => { + _itemScrollController.scrollTo( + index: 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + } + : singlePressAction == PostFabAction.changeSort + ? () => showSortBottomSheet(context, state) + : singlePressAction == PostFabAction.replyToPost + ? () => replyToPost(context, postLocked: postLocked) + : null), + onLongPress: () => longPressAction.execute( + context: context, + postView: state.postView, + postId: state.postId, + selectedCommentId: state.selectedCommentId, + selectedCommentPath: state.selectedCommentPath, + override: longPressAction == PostFabAction.backToTop + ? () => { + _itemScrollController.scrollTo( + index: 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + } + : longPressAction == PostFabAction.changeSort + ? () => showSortBottomSheet(context, state) + : longPressAction == PostFabAction.replyToPost + ? () => replyToPost(context, postLocked: postLocked) + : null), + children: [ + if (enableRefresh) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + HapticFeedback.mediumImpact(); + PostFabAction.refresh.execute( context: context, postView: state.postView, postId: state.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath, - override: singlePressAction == PostFabAction.backToTop - ? () => { - _itemScrollController.scrollTo( - index: 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ) - } - : singlePressAction == PostFabAction.changeSort - ? () => showSortBottomSheet(context, state) - : singlePressAction == PostFabAction.replyToPost - ? () => replyToPost(context, postLocked: postLocked) - : null), - onLongPress: () => longPressAction.execute( - context: context, - postView: state.postView, - postId: state.postId, - selectedCommentId: state.selectedCommentId, - selectedCommentPath: state.selectedCommentPath, - override: longPressAction == PostFabAction.backToTop - ? () => { - _itemScrollController.scrollTo( - index: 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ) - } - : longPressAction == PostFabAction.changeSort - ? () => showSortBottomSheet(context, state) - : longPressAction == PostFabAction.replyToPost - ? () => replyToPost(context, postLocked: postLocked) - : null), - children: [ - if (enableRefresh) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - HapticFeedback.mediumImpact(); - PostFabAction.refresh.execute( - context: context, - postView: state.postView, - postId: state.postId, - selectedCommentId: state.selectedCommentId, - selectedCommentPath: state.selectedCommentPath, - ); - }, - title: PostFabAction.refresh.getTitle(context), - icon: Icon( - PostFabAction.refresh.getIcon(), - ), + ); + }, + title: PostFabAction.refresh.getTitle(context), + icon: Icon( + PostFabAction.refresh.getIcon(), ), - if (enableReplyToPost) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - HapticFeedback.mediumImpact(); - PostFabAction.replyToPost.execute( - override: () => replyToPost(context, postLocked: postLocked), - ); - }, - title: PostFabAction.replyToPost.getTitle(context), - icon: Icon( - postLocked ? Icons.lock : PostFabAction.replyToPost.getIcon(), - ), + ), + if (enableReplyToPost) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + HapticFeedback.mediumImpact(); + PostFabAction.replyToPost.execute( + override: () => replyToPost(context, postLocked: postLocked), + ); + }, + title: PostFabAction.replyToPost.getTitle(context), + icon: Icon( + postLocked ? Icons.lock : PostFabAction.replyToPost.getIcon(), ), - if (enableChangeSort) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - HapticFeedback.mediumImpact(); - PostFabAction.changeSort.execute( - override: () => showSortBottomSheet(context, state), - ); - }, - title: PostFabAction.changeSort.getTitle(context), - icon: Icon( - PostFabAction.changeSort.getIcon(), - ), + ), + if (enableChangeSort) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + HapticFeedback.mediumImpact(); + PostFabAction.changeSort.execute( + override: () => showSortBottomSheet(context, state), + ); + }, + title: PostFabAction.changeSort.getTitle(context), + icon: Icon( + PostFabAction.changeSort.getIcon(), ), - if (enableBackToTop) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - PostFabAction.backToTop.execute( - override: () => { - _itemScrollController.scrollTo( - index: 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ) - }); - }, - title: PostFabAction.backToTop.getTitle(context), - icon: Icon( - PostFabAction.backToTop.getIcon(), - ), + ), + if (enableBackToTop) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + PostFabAction.backToTop.execute( + override: () => { + _itemScrollController.scrollTo( + index: 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + }); + }, + title: PostFabAction.backToTop.getTitle(context), + icon: Icon( + PostFabAction.backToTop.getIcon(), ), - if (enableSearch) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - PostFabAction.search.execute(override: () { - if (state.status == PostStatus.searchInProgress) { - context.read().add(const EndCommentSearchEvent()); - } else { - showInputDialog( - context: context, - title: l10n.searchComments, - inputLabel: l10n.searchTerm, - onSubmitted: ({payload, value}) { - Navigator.of(context).pop(); - - List commentMatches = []; - - /// Recursive function which checks if any child of the given [commentViewTrees] contains the query - void findMatches(List commentViewTrees) { - for (CommentViewTree commentViewTree in commentViewTrees) { - if (commentViewTree.commentView?.comment.content.contains(RegExp(value!, caseSensitive: false)) == true) { - commentMatches.add(commentViewTree.commentView!.comment); - } - findMatches(commentViewTree.replies); + ), + if (enableSearch) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + PostFabAction.search.execute(override: () { + if (state.status == PostStatus.searchInProgress) { + context.read().add(const EndCommentSearchEvent()); + } else { + showInputDialog( + context: context, + title: l10n.searchComments, + inputLabel: l10n.searchTerm, + onSubmitted: ({payload, value}) { + Navigator.of(context).pop(); + + List commentMatches = []; + + /// Recursive function which checks if any child of the given [commentViewTrees] contains the query + void findMatches(List commentViewTrees) { + for (CommentViewTree commentViewTree in commentViewTrees) { + if (commentViewTree.commentView?.comment.content.contains(RegExp(value!, caseSensitive: false)) == true) { + commentMatches.add(commentViewTree.commentView!.comment); } + findMatches(commentViewTree.replies); } - - // Find all comments which contain the query - findMatches(state.comments); - - if (commentMatches.isEmpty) { - showSnackbar(context, l10n.noResultsFound); - } else { - context.read().add(StartCommentSearchEvent(commentMatches: commentMatches)); - } - - return Future.value(null); - }, - getSuggestions: (_) => Future.value(const Iterable.empty()), - suggestionBuilder: (payload) => Container(), - ); - } - }); - }, - title: state.status == PostStatus.searchInProgress ? l10n.endSearch : PostFabAction.search.getTitle(context), - icon: Icon( - state.status == PostStatus.searchInProgress ? Icons.search_off_rounded : PostFabAction.search.getIcon(), - ), + } + + // Find all comments which contain the query + findMatches(state.comments); + + if (commentMatches.isEmpty) { + showSnackbar(l10n.noResultsFound); + } else { + context.read().add(StartCommentSearchEvent(commentMatches: commentMatches)); + } + + return Future.value(null); + }, + getSuggestions: (_) => Future.value(const Iterable.empty()), + suggestionBuilder: (payload) => Container(), + ); + } + }); + }, + title: state.status == PostStatus.searchInProgress ? l10n.endSearch : PostFabAction.search.getTitle(context), + icon: Icon( + state.status == PostStatus.searchInProgress ? Icons.search_off_rounded : PostFabAction.search.getIcon(), ), - ], - ) - : null, - ), + ), + ], + ) + : null, ), - ], - ), - body: Stack( - alignment: Alignment.bottomRight, - children: [ - SafeArea( - child: BlocConsumer( - listenWhen: (previous, current) { - if ((previous.status != PostStatus.failure && current.status == PostStatus.failure) || (previous.errorMessage != current.errorMessage)) { - setState(() => resetFailureMessage = true); - } - return true; - }, - listener: (context, state) { - if (state.status == PostStatus.success && widget.postView != null && state.postView != null) { - widget.onPostUpdated(state.postView!); - } - }, - builder: (context, state) { - if (state.status == PostStatus.failure && resetFailureMessage == true) { - showSnackbar( - context, - state.errorMessage ?? l10n.missingErrorMessage, - backgroundColor: Theme.of(context).colorScheme.onErrorContainer, - leadingIcon: Icons.warning_rounded, - leadingIconColor: Theme.of(context).colorScheme.errorContainer, - ); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - setState(() => resetFailureMessage = false); - }); - } - switch (state.status) { - case PostStatus.initial: - context - .read() - .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentPath: widget.selectedCommentPath, selectedCommentId: widget.selectedCommentId)); - return const Center(child: CircularProgressIndicator()); - case PostStatus.loading: - return const Center(child: CircularProgressIndicator()); - case PostStatus.refreshing: - case PostStatus.success: - case PostStatus.failure: - case PostStatus.searchInProgress: - if (state.postView != null) { - return RefreshIndicator( - onRefresh: () async { - HapticFeedback.mediumImpact(); - return context - .read() - .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath)); - }, - child: PostPageSuccess( - postView: state.postView!, - comments: state.comments, - selectedCommentId: state.selectedCommentId, - selectedCommentPath: state.selectedCommentPath, - newlyCreatedCommentId: state.newlyCreatedCommentId, - moddingCommentId: state.moddingCommentId, - viewFullCommentsRefreshing: state.viewAllCommentsRefresh, - itemScrollController: _itemScrollController, - itemPositionsListener: _itemPositionsListener, - hasReachedCommentEnd: state.hasReachedCommentEnd, - moderators: state.moderators, - crossPosts: state.crossPosts, - scaffoldMessengerKey: _scaffoldMessengerKey, - ), - ); - } - return ErrorMessage( - message: state.errorMessage, - action: () { - context.read().add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: null)); - }, - actionText: l10n.refreshContent, - ); - case PostStatus.empty: - return ErrorMessage( - message: state.errorMessage, - action: () { - context.read().add(GetPostEvent(postView: widget.postView, postId: widget.postId)); + ), + ], + ), + body: Stack( + alignment: Alignment.bottomRight, + children: [ + SafeArea( + child: BlocConsumer( + listenWhen: (previous, current) { + if ((previous.status != PostStatus.failure && current.status == PostStatus.failure) || (previous.errorMessage != current.errorMessage)) { + setState(() => resetFailureMessage = true); + } + return true; + }, + listener: (context, state) { + if (state.status == PostStatus.success && widget.postView != null && state.postView != null) { + widget.onPostUpdated(state.postView!); + } + }, + builder: (context, state) { + if (state.status == PostStatus.failure && resetFailureMessage == true) { + showSnackbar( + state.errorMessage ?? l10n.missingErrorMessage, + backgroundColor: Theme.of(context).colorScheme.onErrorContainer, + leadingIcon: Icons.warning_rounded, + leadingIconColor: Theme.of(context).colorScheme.errorContainer, + ); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + setState(() => resetFailureMessage = false); + }); + } + switch (state.status) { + case PostStatus.initial: + context + .read() + .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentPath: widget.selectedCommentPath, selectedCommentId: widget.selectedCommentId)); + return const Center(child: CircularProgressIndicator()); + case PostStatus.loading: + return const Center(child: CircularProgressIndicator()); + case PostStatus.refreshing: + case PostStatus.success: + case PostStatus.failure: + case PostStatus.searchInProgress: + if (state.postView != null) { + return RefreshIndicator( + onRefresh: () async { + HapticFeedback.mediumImpact(); + return context + .read() + .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath)); }, - actionText: l10n.refreshContent, + child: PostPageSuccess( + postView: state.postView!, + comments: state.comments, + selectedCommentId: state.selectedCommentId, + selectedCommentPath: state.selectedCommentPath, + newlyCreatedCommentId: state.newlyCreatedCommentId, + moddingCommentId: state.moddingCommentId, + viewFullCommentsRefreshing: state.viewAllCommentsRefresh, + itemScrollController: _itemScrollController, + itemPositionsListener: _itemPositionsListener, + hasReachedCommentEnd: state.hasReachedCommentEnd, + moderators: state.moderators, + crossPosts: state.crossPosts, + ), ); + } + return ErrorMessage( + message: state.errorMessage, + action: () { + context.read().add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: null)); + }, + actionText: l10n.refreshContent, + ); + case PostStatus.empty: + return ErrorMessage( + message: state.errorMessage, + action: () { + context.read().add(GetPostEvent(postView: widget.postView, postId: widget.postId)); + }, + actionText: l10n.refreshContent, + ); + } + }, + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: isFabOpen + ? Listener( + onPointerUp: (details) { + context.read().add(const OnFabToggle(false)); + }, + child: Container( + color: theme.colorScheme.background.withOpacity(0.95), + ), + ) + : null, + ), + if (enableFab) + SizedBox( + height: 70, + width: 70, + child: GestureDetector( + onVerticalDragUpdate: (details) { + if (details.delta.dy < -5) { + context.read().add(const OnFabSummonToggle(true)); } }, ), ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: isFabOpen - ? Listener( - onPointerUp: (details) { - context.read().add(const OnFabToggle(false)); - }, - child: Container( - color: theme.colorScheme.background.withOpacity(0.95), - ), - ) - : null, - ), - if (enableFab) - SizedBox( - height: 70, - width: 70, - child: GestureDetector( - onVerticalDragUpdate: (details) { - if (details.delta.dy < -5) { - context.read().add(const OnFabSummonToggle(true)); - } - }, - ), - ), - ], - ), - ); - }, - ), + ], + ), + ); + }, ), ); } @@ -579,7 +570,7 @@ class _PostPageState extends State { final AppLocalizations l10n = AppLocalizations.of(context)!; if (postLocked) { - showSnackbar(context, l10n.postLocked); + showSnackbar(l10n.postLocked); return; } PostBloc postBloc = context.read(); @@ -591,7 +582,7 @@ class _PostPageState extends State { final bool reduceAnimations = state.reduceAnimations; if (!authBloc.state.isLoggedIn) { - showSnackbar(context, l10n.mustBeLoggedInComment); + showSnackbar(l10n.mustBeLoggedInComment); } else { SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; DraftComment? newDraftComment; @@ -633,7 +624,7 @@ class _PostPageState extends State { if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true) { await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, l10n.commentSavedAsDraft); + showSnackbar(l10n.commentSavedAsDraft); prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { prefs.remove(draftId); diff --git a/lib/post/pages/post_page_success.dart b/lib/post/pages/post_page_success.dart index cc1de0fd0..baad711ed 100644 --- a/lib/post/pages/post_page_success.dart +++ b/lib/post/pages/post_page_success.dart @@ -40,9 +40,6 @@ class PostPageSuccess extends StatefulWidget { final List? moderators; final List? crossPosts; - /// The messenger key back to the post page - final GlobalKey? scaffoldMessengerKey; - const PostPageSuccess({ super.key, required this.postView, @@ -57,7 +54,6 @@ class PostPageSuccess extends StatefulWidget { this.viewFullCommentsRefreshing = false, required this.moderators, required this.crossPosts, - this.scaffoldMessengerKey, }); @override @@ -158,7 +154,7 @@ class _PostPageSuccessState extends State { if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true && (!isEdit || commentView.comment.content != newDraftComment?.text)) { await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, AppLocalizations.of(context)!.commentSavedAsDraft); + showSnackbar(AppLocalizations.of(context)!.commentSavedAsDraft); prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { prefs.remove(draftId); @@ -167,7 +163,6 @@ class _PostPageSuccessState extends State { }, moderators: widget.moderators, crossPosts: widget.crossPosts, - scaffoldMessengerKey: widget.scaffoldMessengerKey, ), ), ], diff --git a/lib/post/utils/comment_action_helpers.dart b/lib/post/utils/comment_action_helpers.dart index cd858185c..be0923e86 100644 --- a/lib/post/utils/comment_action_helpers.dart +++ b/lib/post/utils/comment_action_helpers.dart @@ -246,7 +246,7 @@ void onSelected( break; case CommentCardAction.copyText: Clipboard.setData(ClipboardData(text: commentView.comment.content)).then((_) { - showSnackbar(context, AppLocalizations.of(context)!.copiedToClipboard); + showSnackbar(AppLocalizations.of(context)!.copiedToClipboard); }); break; case CommentCardAction.shareLink: diff --git a/lib/post/utils/comment_actions.dart b/lib/post/utils/comment_actions.dart index 2dc63ddc5..0f05ec2eb 100644 --- a/lib/post/utils/comment_actions.dart +++ b/lib/post/utils/comment_actions.dart @@ -39,7 +39,7 @@ void triggerCommentAction({ bool downvotesEnabled = context.read().state.downvotesEnabled; if (downvotesEnabled == false) { - showSnackbar(context, AppLocalizations.of(context)!.downvotesDisabled); + showSnackbar(AppLocalizations.of(context)!.downvotesDisabled); return; } onVoteAction(commentView.comment.id, voteType == -1 ? 0 : -1); @@ -105,7 +105,7 @@ void triggerCommentAction({ if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true && (swipeAction != SwipeAction.edit || commentView.comment.content != newDraftComment?.text)) { await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, AppLocalizations.of(context)!.commentSavedAsDraft); + showSnackbar(AppLocalizations.of(context)!.commentSavedAsDraft); prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { prefs.remove(draftId); diff --git a/lib/post/utils/navigate_create_post.dart b/lib/post/utils/navigate_create_post.dart index 6c8dedeaa..2be3be52f 100644 --- a/lib/post/utils/navigate_create_post.dart +++ b/lib/post/utils/navigate_create_post.dart @@ -19,7 +19,6 @@ Future navigateToCreatePostPage( File? image, String? url, bool? prePopulated, - GlobalKey? scaffoldMessengerKey, }) async { try { ThunderBloc thunderBloc = context.read(); @@ -48,12 +47,11 @@ Future navigateToCreatePostPage( context.read().add(FeedItemUpdatedEvent(postViewMedia: postViewMedia)); } catch (e) {} }, - scaffoldMessengerKey: scaffoldMessengerKey, ), ); }, )); } catch (e) { - if (context.mounted) showSnackbar(context, AppLocalizations.of(context)!.unexpectedError); + if (context.mounted) showSnackbar(AppLocalizations.of(context)!.unexpectedError); } } diff --git a/lib/post/widgets/comment_view.dart b/lib/post/widgets/comment_view.dart index 31c5b199e..9c0206882 100644 --- a/lib/post/widgets/comment_view.dart +++ b/lib/post/widgets/comment_view.dart @@ -38,9 +38,6 @@ class CommentSubview extends StatefulWidget { final List? moderators; final List? crossPosts; - /// The messenger key back to the post page - final GlobalKey? scaffoldMessengerKey; - const CommentSubview({ super.key, required this.comments, @@ -62,7 +59,6 @@ class CommentSubview extends StatefulWidget { required this.now, required this.moderators, required this.crossPosts, - this.scaffoldMessengerKey, }); @override @@ -139,7 +135,6 @@ class _CommentSubviewState extends State with SingleTickerProvid postViewMedia: widget.postViewMedia!, moderators: widget.moderators, crossPosts: widget.crossPosts, - scaffoldMessengerKey: widget.scaffoldMessengerKey, ); } if (widget.hasReachedCommentEnd == false && widget.comments.isEmpty) { diff --git a/lib/post/widgets/post_view.dart b/lib/post/widgets/post_view.dart index f73d1c9fe..366d840ec 100644 --- a/lib/post/widgets/post_view.dart +++ b/lib/post/widgets/post_view.dart @@ -55,9 +55,6 @@ class PostSubview extends StatefulWidget { final List? moderators; final List? crossPosts; - /// The messenger key back to the post page - final GlobalKey? scaffoldMessengerKey; - const PostSubview({ super.key, this.selectedCommentId, @@ -65,7 +62,6 @@ class PostSubview extends StatefulWidget { required this.postViewMedia, required this.moderators, required this.crossPosts, - this.scaffoldMessengerKey, }); @override @@ -179,7 +175,6 @@ class _PostSubviewState extends State with SingleTickerProviderStat CrossPosts( crossPosts: sortedCrossPosts, originalPost: widget.postViewMedia, - scaffoldMessengerKey: widget.scaffoldMessengerKey, ), const SizedBox(height: 16.0), SizedBox( @@ -402,7 +397,7 @@ class _PostSubviewState extends State with SingleTickerProviderStat onPressed: isUserLoggedIn ? () async { if (postView.post.locked) { - showSnackbar(context, l10n.postLocked); + showSnackbar(l10n.postLocked); return; } @@ -497,7 +492,7 @@ class _PostSubviewState extends State with SingleTickerProviderStat if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true) { await Future.delayed(const Duration(milliseconds: 300)); if (context.mounted) { - showSnackbar(context, l10n.commentSavedAsDraft); + showSnackbar(l10n.commentSavedAsDraft); } prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { diff --git a/lib/post/widgets/report_comment_dialog.dart b/lib/post/widgets/report_comment_dialog.dart index ec9b83bc5..f5ee6bd6c 100644 --- a/lib/post/widgets/report_comment_dialog.dart +++ b/lib/post/widgets/report_comment_dialog.dart @@ -108,10 +108,7 @@ class _ReportCommentDialogState extends State { hasError = false; }); case PostStatus.success: - showSnackbar( - context, - AppLocalizations.of(context)!.commentReported, - ); + showSnackbar(AppLocalizations.of(context)!.commentReported); Navigator.of(context).pop(); break; case PostStatus.failure: diff --git a/lib/search/pages/search_page.dart b/lib/search/pages/search_page.dart index 620d25a16..b5e59162a 100644 --- a/lib/search/pages/search_page.dart +++ b/lib/search/pages/search_page.dart @@ -742,7 +742,7 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi onPressed: () { SubscribedType subscriptionStatus = _getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions); _onSubscribeIconPressed(isUserLoggedIn, context, communityView); - showSnackbar(context, subscriptionStatus == SubscribedType.notSubscribed ? l10n.addedCommunityToSubscriptions : l10n.removedCommunityFromSubscriptions); + showSnackbar(subscriptionStatus == SubscribedType.notSubscribed ? l10n.addedCommunityToSubscriptions : l10n.removedCommunityFromSubscriptions); context.read().add(GetAccountSubscriptions()); }, icon: Icon( @@ -862,7 +862,7 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true && (!isEdit || commentView.comment.content != newDraftComment?.text)) { await Future.delayed(const Duration(milliseconds: 300)); - if (context.mounted) showSnackbar(context, l10n.commentSavedAsDraft); + if (context.mounted) showSnackbar(l10n.commentSavedAsDraft); prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { prefs.remove(draftId); diff --git a/lib/settings/pages/debug_settings_page.dart b/lib/settings/pages/debug_settings_page.dart index 1e7054391..8f49388ca 100644 --- a/lib/settings/pages/debug_settings_page.dart +++ b/lib/settings/pages/debug_settings_page.dart @@ -76,7 +76,7 @@ class _DebugSettingsPageState extends State { if (context.mounted) { context.read().add(UserPreferencesChangeEvent()); - showSnackbar(context, AppLocalizations.of(context)!.clearedUserPreferences); + showSnackbar(AppLocalizations.of(context)!.clearedUserPreferences); } }); @@ -111,7 +111,7 @@ class _DebugSettingsPageState extends State { await databaseFactory.deleteDatabase(path); if (context.mounted) { - showSnackbar(context, AppLocalizations.of(context)!.clearedDatabase); + showSnackbar(AppLocalizations.of(context)!.clearedDatabase); Navigator.of(context).pop(); } }, @@ -146,7 +146,7 @@ class _DebugSettingsPageState extends State { ), onTap: () async { await clearDiskCachedImages(); - if (context.mounted) showSnackbar(context, l10n.clearedCache); + if (context.mounted) showSnackbar(l10n.clearedCache); setState(() {}); // Trigger a rebuild to refresh the cache size }, ); diff --git a/lib/settings/widgets/accessibility_profile.dart b/lib/settings/widgets/accessibility_profile.dart index 31f9d83da..32700b117 100644 --- a/lib/settings/widgets/accessibility_profile.dart +++ b/lib/settings/widgets/accessibility_profile.dart @@ -68,12 +68,12 @@ class SettingProfile extends StatelessWidget { // before adding a profile containing those types. success = false; if (context.mounted) { - showSnackbar(context, AppLocalizations.of(context)!.settingTypeNotSupported(entry.value.runtimeType)); + showSnackbar(AppLocalizations.of(context)!.settingTypeNotSupported(entry.value.runtimeType)); } } } if (context.mounted && success) { - showSnackbar(context, AppLocalizations.of(context)!.profileAppliedSuccessfully(name)); + showSnackbar(AppLocalizations.of(context)!.profileAppliedSuccessfully(name)); setState(() => recentSuccess = true); Future.delayed(const Duration(seconds: 5), () async { setState(() => recentSuccess = false); diff --git a/lib/shared/cross_posts.dart b/lib/shared/cross_posts.dart index 8ef98fc8c..b7f3dd22c 100644 --- a/lib/shared/cross_posts.dart +++ b/lib/shared/cross_posts.dart @@ -14,14 +14,12 @@ class CrossPosts extends StatefulWidget { final List crossPosts; final PostViewMedia? originalPost; final bool? isNewPost; - final GlobalKey? scaffoldMessengerKey; const CrossPosts({ super.key, required this.crossPosts, this.originalPost, this.isNewPost, - this.scaffoldMessengerKey, }) : assert(originalPost != null || isNewPost == true); @override @@ -176,7 +174,6 @@ class _CrossPostsState extends State with SingleTickerProviderStateM context, title: widget.originalPost!.postView.post.name, url: widget.originalPost!.postView.post.url, - scaffoldMessengerKey: widget.scaffoldMessengerKey, ), borderRadius: BorderRadius.circular(10), child: Padding( @@ -210,7 +207,6 @@ void createCrossPost( String? url, String? text, String? postUrl, - GlobalKey? scaffoldMessengerKey, }) async { final AppLocalizations l10n = AppLocalizations.of(context)!; @@ -227,6 +223,5 @@ void createCrossPost( url: url, text: text, prePopulated: true, - scaffoldMessengerKey: scaffoldMessengerKey, ); } diff --git a/lib/shared/image_viewer.dart b/lib/shared/image_viewer.dart index 3d5f89b2b..152b129d6 100644 --- a/lib/shared/image_viewer.dart +++ b/lib/shared/image_viewer.dart @@ -42,7 +42,6 @@ class ImageViewer extends StatefulWidget { class _ImageViewerState extends State with TickerProviderStateMixin { GlobalKey slidePagekey = GlobalKey(); - final GlobalKey _imageViewer = GlobalKey(); final GlobalKey gestureKey = GlobalKey(); bool downloaded = false; double slideTransparency = 0.92; @@ -137,383 +136,380 @@ class _ImageViewerState extends State with TickerProviderStateMixin AnimationController animationController = AnimationController(duration: const Duration(milliseconds: 140), vsync: this); Function() animationListener = () {}; Animation? animation; - return ScaffoldMessenger( - key: _imageViewer, - child: Stack( - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 400), - color: fullscreen ? Colors.black : Colors.transparent, - ), - Scaffold( - appBar: AppBar( - iconTheme: IconThemeData( - color: fullscreen ? Colors.transparent : Colors.white, - shadows: fullscreen ? null : [const Shadow(color: Colors.black, blurRadius: 50.0)], - ), - backgroundColor: Colors.transparent, - toolbarHeight: 70.0, + return Stack( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 400), + color: fullscreen ? Colors.black : Colors.transparent, + ), + Scaffold( + appBar: AppBar( + iconTheme: IconThemeData( + color: fullscreen ? Colors.transparent : Colors.white, + shadows: fullscreen ? null : [const Shadow(color: Colors.black, blurRadius: 50.0)], ), - backgroundColor: Colors.black.withOpacity(slideTransparency), - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: GestureDetector( - onLongPress: () { - HapticFeedback.lightImpact(); + backgroundColor: Colors.transparent, + toolbarHeight: 70.0, + ), + backgroundColor: Colors.black.withOpacity(slideTransparency), + body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: GestureDetector( + onLongPress: () { + HapticFeedback.lightImpact(); + setState(() { + fullscreen = !fullscreen; + }); + }, + onTap: () { + if (!fullscreen) { + slidePagekey.currentState!.popPage(); + Navigator.pop(context); + } else { setState(() { - fullscreen = !fullscreen; + fullscreen = false; }); - }, - onTap: () { - if (!fullscreen) { - slidePagekey.currentState!.popPage(); - Navigator.pop(context); - } else { - setState(() { - fullscreen = false; - }); - } - }, - // Start doubletap zoom if conditions are met - onVerticalDragStart: maybeSlideZooming - ? (details) { - setState(() { - slideZooming = true; - }); + } + }, + // Start doubletap zoom if conditions are met + onVerticalDragStart: maybeSlideZooming + ? (details) { + setState(() { + slideZooming = true; + }); + } + : null, + // Zoom image in an out based on movement in vertical axis if conditions are met + onVerticalDragUpdate: maybeSlideZooming || slideZooming + ? (details) { + // Need to catch the drag during "maybe" phase or it wont activate fast enough + if (slideZooming) { + double newScale = max(gestureKey.currentState!.gestureDetails!.totalScale! * (1 + (details.delta.dy / 150)), 1); + gestureKey.currentState?.handleDoubleTap(scale: newScale, doubleTapPosition: gestureKey.currentState!.pointerDownPosition); } - : null, - // Zoom image in an out based on movement in vertical axis if conditions are met - onVerticalDragUpdate: maybeSlideZooming || slideZooming - ? (details) { - // Need to catch the drag during "maybe" phase or it wont activate fast enough - if (slideZooming) { - double newScale = max(gestureKey.currentState!.gestureDetails!.totalScale! * (1 + (details.delta.dy / 150)), 1); - gestureKey.currentState?.handleDoubleTap(scale: newScale, doubleTapPosition: gestureKey.currentState!.pointerDownPosition); + } + : null, + // End doubltap zoom + onVerticalDragEnd: slideZooming + ? (details) { + setState(() { + slideZooming = false; + }); + } + : null, + child: areImageDimensionsLoaded // Only display the image if dimensions are loaded + ? Listener( + // Start watching for double tap zoom + onPointerUp: (details) { + downCoord = details.position; + if (!slideZooming) { + _maybeSlide(context); } - } - : null, - // End doubltap zoom - onVerticalDragEnd: slideZooming - ? (details) { - setState(() { - slideZooming = false; - }); - } - : null, - child: areImageDimensionsLoaded // Only display the image if dimensions are loaded - ? Listener( - // Start watching for double tap zoom - onPointerUp: (details) { - downCoord = details.position; - if (!slideZooming) { - _maybeSlide(context); + }, + child: ExtendedImageSlidePage( + key: slidePagekey, + slideAxis: SlideAxis.both, + slideType: SlideType.onlyImage, + slidePageBackgroundHandler: (offset, pageSize) { + return Colors.transparent; + }, + onSlidingPage: (state) { + // Fade out image and background when sliding to dismiss + var offset = state.offset; + var pageSize = state.pageSize; + + var scale = offset.distance / Offset(pageSize.width, pageSize.height).distance; + + if (state.isSliding) { + setState(() { + slideTransparency = 0.9 - min(0.9, scale * 0.5); + imageTransparency = 1.0 - min(1.0, scale * 10); + }); } }, - child: ExtendedImageSlidePage( - key: slidePagekey, - slideAxis: SlideAxis.both, - slideType: SlideType.onlyImage, - slidePageBackgroundHandler: (offset, pageSize) { - return Colors.transparent; - }, - onSlidingPage: (state) { - // Fade out image and background when sliding to dismiss + slideEndHandler: ( + // Decrease slide to dismiss threshold so it can be done easier + Offset offset, { + ExtendedImageSlidePageState? state, + ScaleEndDetails? details, + }) { + if (state != null) { var offset = state.offset; var pageSize = state.pageSize; - - var scale = offset.distance / Offset(pageSize.width, pageSize.height).distance; - - if (state.isSliding) { - setState(() { - slideTransparency = 0.9 - min(0.9, scale * 0.5); - imageTransparency = 1.0 - min(1.0, scale * 10); - }); - } - }, - slideEndHandler: ( - // Decrease slide to dismiss threshold so it can be done easier - Offset offset, { - ExtendedImageSlidePageState? state, - ScaleEndDetails? details, - }) { - if (state != null) { - var offset = state.offset; - var pageSize = state.pageSize; - return offset.distance.greaterThan(Offset(pageSize.width, pageSize.height).distance / 10); - } - return true; - }, - child: widget.url != null - ? ExtendedImage.network( - widget.url!, - color: Colors.white.withOpacity(imageTransparency), - colorBlendMode: BlendMode.dstIn, - enableSlideOutPage: true, - mode: ExtendedImageMode.gesture, - extendedImageGestureKey: gestureKey, - cache: true, - clearMemoryCacheWhenDispose: false, - cacheMaxAge: const Duration(minutes: 1), - initGestureConfigHandler: (ExtendedImageState state) { - return GestureConfig( - minScale: 0.8, - animationMinScale: 0.8, - maxScale: maxZoomLevel.toDouble(), - animationMaxScale: maxZoomLevel.toDouble(), - speed: 1.0, - inertialSpeed: 250.0, - initialScale: 1.0, - inPageView: false, - initialAlignment: InitialAlignment.center, - reverseMousePointerScrollDirection: true, - gestureDetailsIsChanged: (GestureDetails? details) {}, + return offset.distance.greaterThan(Offset(pageSize.width, pageSize.height).distance / 10); + } + return true; + }, + child: widget.url != null + ? ExtendedImage.network( + widget.url!, + color: Colors.white.withOpacity(imageTransparency), + colorBlendMode: BlendMode.dstIn, + enableSlideOutPage: true, + mode: ExtendedImageMode.gesture, + extendedImageGestureKey: gestureKey, + cache: true, + clearMemoryCacheWhenDispose: false, + cacheMaxAge: const Duration(minutes: 1), + initGestureConfigHandler: (ExtendedImageState state) { + return GestureConfig( + minScale: 0.8, + animationMinScale: 0.8, + maxScale: maxZoomLevel.toDouble(), + animationMaxScale: maxZoomLevel.toDouble(), + speed: 1.0, + inertialSpeed: 250.0, + initialScale: 1.0, + inPageView: false, + initialAlignment: InitialAlignment.center, + reverseMousePointerScrollDirection: true, + gestureDetailsIsChanged: (GestureDetails? details) {}, + ); + }, + onDoubleTap: (ExtendedImageGestureState state) { + var pointerDownPosition = state.pointerDownPosition; + double begin = state.gestureDetails!.totalScale!; + double end; + + animation?.removeListener(animationListener); + animationController.stop(); + animationController.reset(); + + if (begin == 1) { + end = 2; + } else if (begin > 1.99 && begin < 2.01) { + end = 4; + } else { + end = 1; + } + animationListener = () { + state.handleDoubleTap(scale: animation!.value, doubleTapPosition: pointerDownPosition); + }; + animation = animationController.drive(Tween(begin: begin, end: end)); + + animation!.addListener(animationListener); + + animationController.forward(); + }, + loadStateChanged: (state) { + if (state.extendedImageLoadState == LoadState.loading) { + return Center( + child: CircularProgressIndicator( + color: Colors.white.withOpacity(0.90), + ), ); - }, - onDoubleTap: (ExtendedImageGestureState state) { - var pointerDownPosition = state.pointerDownPosition; - double begin = state.gestureDetails!.totalScale!; - double end; - - animation?.removeListener(animationListener); - animationController.stop(); - animationController.reset(); - - if (begin == 1) { - end = 2; - } else if (begin > 1.99 && begin < 2.01) { - end = 4; - } else { - end = 1; - } - animationListener = () { - state.handleDoubleTap(scale: animation!.value, doubleTapPosition: pointerDownPosition); - }; - animation = animationController.drive(Tween(begin: begin, end: end)); - - animation!.addListener(animationListener); - - animationController.forward(); - }, - loadStateChanged: (state) { - if (state.extendedImageLoadState == LoadState.loading) { - return Center( - child: CircularProgressIndicator( - color: Colors.white.withOpacity(0.90), - ), - ); - } - }, - ) - : ExtendedImage.memory( - widget.bytes!, - color: Colors.white.withOpacity(imageTransparency), - colorBlendMode: BlendMode.dstIn, - enableSlideOutPage: true, - mode: ExtendedImageMode.gesture, - extendedImageGestureKey: gestureKey, - clearMemoryCacheWhenDispose: true, - initGestureConfigHandler: (ExtendedImageState state) { - return GestureConfig( - minScale: 0.8, - animationMinScale: 0.8, - maxScale: 4.0, - animationMaxScale: 4.0, - speed: 1.0, - inertialSpeed: 250.0, - initialScale: 1.0, - inPageView: false, - initialAlignment: InitialAlignment.center, - reverseMousePointerScrollDirection: true, - gestureDetailsIsChanged: (GestureDetails? details) {}, + } + }, + ) + : ExtendedImage.memory( + widget.bytes!, + color: Colors.white.withOpacity(imageTransparency), + colorBlendMode: BlendMode.dstIn, + enableSlideOutPage: true, + mode: ExtendedImageMode.gesture, + extendedImageGestureKey: gestureKey, + clearMemoryCacheWhenDispose: true, + initGestureConfigHandler: (ExtendedImageState state) { + return GestureConfig( + minScale: 0.8, + animationMinScale: 0.8, + maxScale: 4.0, + animationMaxScale: 4.0, + speed: 1.0, + inertialSpeed: 250.0, + initialScale: 1.0, + inPageView: false, + initialAlignment: InitialAlignment.center, + reverseMousePointerScrollDirection: true, + gestureDetailsIsChanged: (GestureDetails? details) {}, + ); + }, + onDoubleTap: (ExtendedImageGestureState state) { + var pointerDownPosition = state.pointerDownPosition; + double begin = state.gestureDetails!.totalScale!; + double end; + + animation?.removeListener(animationListener); + animationController.stop(); + animationController.reset(); + + if (begin == 1) { + end = 2; + } else if (begin > 1.99 && begin < 2.01) { + end = 4; + } else { + end = 1; + } + animationListener = () { + state.handleDoubleTap(scale: animation!.value, doubleTapPosition: pointerDownPosition); + }; + animation = animationController.drive(Tween(begin: begin, end: end)); + + animation!.addListener(animationListener); + + animationController.forward(); + }, + loadStateChanged: (state) { + if (state.extendedImageLoadState == LoadState.loading) { + return Center( + child: CircularProgressIndicator( + color: Colors.white.withOpacity(0.90), + ), ); - }, - onDoubleTap: (ExtendedImageGestureState state) { - var pointerDownPosition = state.pointerDownPosition; - double begin = state.gestureDetails!.totalScale!; - double end; - - animation?.removeListener(animationListener); - animationController.stop(); - animationController.reset(); - - if (begin == 1) { - end = 2; - } else if (begin > 1.99 && begin < 2.01) { - end = 4; - } else { - end = 1; - } - animationListener = () { - state.handleDoubleTap(scale: animation!.value, doubleTapPosition: pointerDownPosition); - }; - animation = animationController.drive(Tween(begin: begin, end: end)); - - animation!.addListener(animationListener); - - animationController.forward(); - }, - loadStateChanged: (state) { - if (state.extendedImageLoadState == LoadState.loading) { - return Center( - child: CircularProgressIndicator( - color: Colors.white.withOpacity(0.90), - ), - ); - } - }, - ), - ), - ) - : Center( - child: CircularProgressIndicator( - color: Colors.white.withOpacity(0.90), - ), - )), - ), - AnimatedOpacity( - opacity: fullscreen ? 0.0 : 1.0, - duration: const Duration(milliseconds: 200), - child: Container( - decoration: const BoxDecoration(color: Colors.transparent), - padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (widget.url != null) - Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - onPressed: fullscreen - ? null - : () async { - try { - // Try to get the cached image first - var media = await DefaultCacheManager().getFileFromCache(widget.url!); - File? mediaFile = media?.file; - - if (media == null) { - setState(() => isDownloadingMedia = true); - - // Download - mediaFile = await DefaultCacheManager().getSingleFile(widget.url!); } - - // Share - await Share.shareXFiles([XFile(mediaFile!.path)]); - } catch (e) { - // Tell the user that the download failed - showSnackbar(context, AppLocalizations.of(context)!.errorDownloadingMedia(e), customState: _imageViewer.currentState); - } finally { - setState(() => isDownloadingMedia = false); + }, + ), + ), + ) + : Center( + child: CircularProgressIndicator( + color: Colors.white.withOpacity(0.90), + ), + )), + ), + AnimatedOpacity( + opacity: fullscreen ? 0.0 : 1.0, + duration: const Duration(milliseconds: 200), + child: Container( + decoration: const BoxDecoration(color: Colors.transparent), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (widget.url != null) + Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + onPressed: fullscreen + ? null + : () async { + try { + // Try to get the cached image first + var media = await DefaultCacheManager().getFileFromCache(widget.url!); + File? mediaFile = media?.file; + + if (media == null) { + setState(() => isDownloadingMedia = true); + + // Download + mediaFile = await DefaultCacheManager().getSingleFile(widget.url!); } - }, - icon: isDownloadingMedia - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: Colors.white.withOpacity(0.90), - ), - ) - : Icon( - Icons.share_rounded, - semanticLabel: "Share", + + // Share + await Share.shareXFiles([XFile(mediaFile!.path)]); + } catch (e) { + // Tell the user that the download failed + showSnackbar(AppLocalizations.of(context)!.errorDownloadingMedia(e)); + } finally { + setState(() => isDownloadingMedia = false); + } + }, + icon: isDownloadingMedia + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( color: Colors.white.withOpacity(0.90), - shadows: const [Shadow(color: Colors.black, blurRadius: 50.0)], ), - ), + ) + : Icon( + Icons.share_rounded, + semanticLabel: "Share", + color: Colors.white.withOpacity(0.90), + shadows: const [Shadow(color: Colors.black, blurRadius: 50.0)], + ), ), - if (widget.url != null) - Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - onPressed: (fullscreen || widget.url == null || kIsWeb) - ? null - : () async { - File file = await DefaultCacheManager().getSingleFile(widget.url!); - bool hasPermission = await _requestPermission(); - - if (!hasPermission) { - if (context.mounted) showPermissionDeniedDialog(context); - return; + ), + if (widget.url != null) + Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + onPressed: (fullscreen || widget.url == null || kIsWeb) + ? null + : () async { + File file = await DefaultCacheManager().getSingleFile(widget.url!); + bool hasPermission = await _requestPermission(); + + if (!hasPermission) { + if (context.mounted) showPermissionDeniedDialog(context); + return; + } + + setState(() => isSavingMedia = true); + + try { + // Save image on Linux platform + if (Platform.isLinux) { + final filePath = '${(await getApplicationDocumentsDirectory()).path}/Thunder/${basename(file.path)}'; + + File(filePath) + ..createSync(recursive: true) + ..writeAsBytesSync(file.readAsBytesSync()); + + return setState(() => downloaded = true); } - setState(() => isSavingMedia = true); - + // Save image on all other supported platforms (Android, iOS, macOS, Windows) try { - // Save image on Linux platform - if (Platform.isLinux) { - final filePath = '${(await getApplicationDocumentsDirectory()).path}/Thunder/${basename(file.path)}'; - - File(filePath) - ..createSync(recursive: true) - ..writeAsBytesSync(file.readAsBytesSync()); - - return setState(() => downloaded = true); - } - - // Save image on all other supported platforms (Android, iOS, macOS, Windows) - try { - await Gal.putImage(file.path, album: "Thunder"); - setState(() => downloaded = true); - } on GalException catch (e) { - if (context.mounted) showSnackbar(context, e.type.message, customState: _imageViewer.currentState); - setState(() => downloaded = false); - } - } finally { - setState(() => isSavingMedia = false); + await Gal.putImage(file.path, album: "Thunder"); + setState(() => downloaded = true); + } on GalException catch (e) { + if (context.mounted) showSnackbar(e.type.message); + setState(() => downloaded = false); } - }, - icon: isSavingMedia - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( + } finally { + setState(() => isSavingMedia = false); + } + }, + icon: isSavingMedia + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white.withOpacity(0.90), + ), + ) + : downloaded + ? const Icon( + Icons.check_circle, + semanticLabel: 'Downloaded', + color: Colors.white, + shadows: [Shadow(color: Colors.black45, blurRadius: 50.0)], + ) + : Icon( + Icons.download, + semanticLabel: "Download", color: Colors.white.withOpacity(0.90), + shadows: const [Shadow(color: Colors.black, blurRadius: 50.0)], ), - ) - : downloaded - ? const Icon( - Icons.check_circle, - semanticLabel: 'Downloaded', - color: Colors.white, - shadows: [Shadow(color: Colors.black45, blurRadius: 50.0)], - ) - : Icon( - Icons.download, - semanticLabel: "Download", - color: Colors.white.withOpacity(0.90), - shadows: const [Shadow(color: Colors.black, blurRadius: 50.0)], - ), - ), ), - if (widget.navigateToPost != null) - Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - onPressed: () { - Navigator.pop(context); - widget.navigateToPost!(); - }, - icon: Icon( - Icons.chat_rounded, - semanticLabel: "Comments", - color: Colors.white.withOpacity(0.90), - shadows: const [Shadow(color: Colors.black, blurRadius: 50.0)], - ), + ), + if (widget.navigateToPost != null) + Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + onPressed: () { + Navigator.pop(context); + widget.navigateToPost!(); + }, + icon: Icon( + Icons.chat_rounded, + semanticLabel: "Comments", + color: Colors.white.withOpacity(0.90), + shadows: const [Shadow(color: Colors.black, blurRadius: 50.0)], ), ), - ], - ), + ), + ], ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ); } } diff --git a/lib/shared/snackbar.dart b/lib/shared/snackbar.dart index 4092f592a..1e8e60b09 100644 --- a/lib/shared/snackbar.dart +++ b/lib/shared/snackbar.dart @@ -3,12 +3,12 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:overlay_support/overlay_support.dart'; +import 'package:thunder/utils/global_context.dart'; + +const Duration _snackBarTransitionDuration = Duration(milliseconds: 500); void showSnackbar( - BuildContext context, String text, { - ScaffoldMessengerState? customState, - bool clearSnackBars = true, Duration? duration, Color? backgroundColor, Color? leadingIconColor, @@ -19,58 +19,141 @@ void showSnackbar( }) { int wordCount = RegExp(r'[\w-]+').allMatches(text).length; - if (clearSnackBars) { - // OverlaySupportEntry.of(context)?.dismiss(); - } + // Allows us to clear the previous overlay before showing the next one + const key = TransientKey('transient'); - showOverlayNotification( - (context) { - return ThunderSnackbar( - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (leadingIcon != null) - Icon( - leadingIcon, - color: leadingIconColor, - ), - if (leadingIcon != null) const SizedBox(width: 8.0), - Expanded( - child: Text( - text, + showOverlay( + (context, progress) { + return SnackbarNotification( + builder: (context) => ThunderSnackbar( + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leadingIcon != null) + Icon( + leadingIcon, + color: leadingIconColor, + ), + if (leadingIcon != null) const SizedBox(width: 8.0), + Expanded( + child: Text( + text, + ), ), - ), - if (trailingIcon != null) - SizedBox( - height: 20, - child: IconButton( - visualDensity: VisualDensity.compact, - onPressed: trailingAction != null - ? () { - OverlaySupportEntry.of(context)?.dismiss(); - trailingAction(); - } - : null, - icon: Icon( - trailingIcon, - color: trailingIconColor ?? Theme.of(context).colorScheme.inversePrimary, + if (trailingIcon != null) + SizedBox( + height: 20, + child: IconButton( + visualDensity: VisualDensity.compact, + onPressed: trailingAction != null + ? () { + OverlaySupportEntry.of(context)?.dismiss(); + trailingAction(); + } + : null, + icon: Icon( + trailingIcon, + color: trailingIconColor ?? Theme.of(context).colorScheme.inversePrimary, + ), ), ), - ), - ], + ], + ), ), + progress: progress, ); }, - position: NotificationPosition.bottom, - duration: duration ?? Duration(milliseconds: max(4000, 1000 * wordCount)), // Assuming 60 WPM or 1 WPS + animationDuration: _snackBarTransitionDuration, + duration: duration ?? Duration(milliseconds: max(kNotificationDuration.inMilliseconds, max(4000, 1000 * wordCount))), // Assuming 60 WPM or 1 WPS + context: GlobalContext.context, + key: key, ); } -class ThunderSnackbar extends StatelessWidget { +/// Build the slide translation for the snack bar +class SnackbarNotification extends StatefulWidget { + final WidgetBuilder builder; + + final double progress; + + const SnackbarNotification({super.key, required this.builder, required this.progress}); + + @override + State createState() => _SnackbarNotificationState(); +} + +class _SnackbarNotificationState extends State with TickerProviderStateMixin { + late AnimationController _controller; + + static const Curve _snackBarM3HeightCurve = Curves.easeInOutQuart; + static const Curve _snackBarM3FadeInCurve = Interval(0.4, 0.6, curve: Curves.easeInCirc); + static const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlowIn); + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: _snackBarTransitionDuration, // Set the duration of the animation. + ); + } + + @override + void didUpdateWidget(SnackbarNotification oldWidget) { + super.didUpdateWidget(oldWidget); + + if ((widget.progress - oldWidget.progress) > 0) { + if (!_controller.isAnimating) _controller.forward(); + } else if ((widget.progress - oldWidget.progress) < 0) { + if (!_controller.isAnimating) _controller.reverse(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final CurvedAnimation fadeInM3Animation = CurvedAnimation(parent: _controller, curve: _snackBarM3FadeInCurve, reverseCurve: _snackBarFadeOutCurve); + + final CurvedAnimation heightM3Animation = CurvedAnimation( + parent: _controller, + curve: _snackBarM3HeightCurve, + reverseCurve: const Threshold(0.0), + ); + + return FadeTransition( + opacity: fadeInM3Animation, + child: AnimatedBuilder( + animation: heightM3Animation, + builder: (BuildContext context, Widget? child) { + return Align( + alignment: AlignmentDirectional.bottomStart, + heightFactor: heightM3Animation.value, + child: child, + ); + }, + child: widget.builder(context), + ), + ); + } +} + +class ThunderSnackbar extends StatefulWidget { + /// The content of the snackbar. final Widget content; const ThunderSnackbar({super.key, required this.content}); + @override + State createState() => _ThunderSnackbarState(); +} + +class _ThunderSnackbarState extends State { @override Widget build(BuildContext context) { const double horizontalPadding = 16.0; @@ -83,25 +166,19 @@ class ThunderSnackbar extends StatelessWidget { final Color backgroundColor = theme.colorScheme.inverseSurface; final ShapeBorder shape = snackBarTheme.shape ?? RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)); - Widget snackBar = Container( - margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom + kBottomNavigationBarHeight + singleLineVerticalPadding), - child: ClipRect( - child: Align( - alignment: AlignmentDirectional.bottomStart, - child: Semantics( - container: true, - liveRegion: true, - onDismiss: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss); - }, - child: Dismissible( - key: const Key('dismissible'), - direction: DismissDirection.down, - resizeDuration: null, - behavior: HitTestBehavior.deferToChild, - onDismissed: (DismissDirection direction) { - // ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe); - }, + Widget snackBar = Dismissible( + key: UniqueKey(), + direction: DismissDirection.down, + behavior: HitTestBehavior.deferToChild, + onDismissed: (direction) {}, + child: Container( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom + kBottomNavigationBarHeight + singleLineVerticalPadding), + child: ClipRect( + child: Align( + alignment: AlignmentDirectional.bottomStart, + child: Semantics( + container: true, + liveRegion: true, child: Padding( padding: const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0), child: Material( @@ -122,7 +199,7 @@ class ThunderSnackbar extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: singleLineVerticalPadding), child: DefaultTextStyle( style: theme.textTheme.bodyMedium!.copyWith(color: theme.colorScheme.onInverseSurface), - child: content, + child: widget.content, ), ), ), @@ -144,6 +221,6 @@ class ThunderSnackbar extends StatelessWidget { } } -void hideSnackbar(BuildContext context, {ScaffoldMessengerState? customState}) { +void hideSnackbar(BuildContext context) { OverlaySupportEntry.of(context)?.dismiss(); } diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index ee1199c13..7d9ce0cf7 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -79,8 +79,6 @@ class _ThunderState extends State { final GlobalKey scaffoldStateKey = GlobalKey(); - final GlobalKey scaffoldMessengerKey = GlobalKey(); - late final StreamSubscription mediaIntentDataStreamSubscription; late final StreamSubscription textIntentDataStreamSubscription; @@ -123,7 +121,7 @@ class _ThunderState extends State { handleSharedImages(); handleSharedText(); } catch (e) { - if (context.mounted) showSnackbar(context, AppLocalizations.of(context)!.unexpectedError); + if (context.mounted) showSnackbar(AppLocalizations.of(context)!.unexpectedError); } } @@ -135,7 +133,6 @@ class _ThunderState extends State { context, image: File(initialMedia.first.path), prePopulated: true, - scaffoldMessengerKey: scaffoldMessengerKey, ); } // For sharing images while the app is in the memory @@ -147,7 +144,6 @@ class _ThunderState extends State { context, image: File(value.first.path), prePopulated: true, - scaffoldMessengerKey: scaffoldMessengerKey, ); } }); @@ -163,14 +159,12 @@ class _ThunderState extends State { context, url: uri.toString(), prePopulated: true, - scaffoldMessengerKey: scaffoldMessengerKey, ); } else { navigateToCreatePostPage( context, text: initialText, prePopulated: true, - scaffoldMessengerKey: scaffoldMessengerKey, ); } } @@ -186,30 +180,26 @@ class _ThunderState extends State { context, url: uri.toString(), prePopulated: true, - scaffoldMessengerKey: scaffoldMessengerKey, ); } else { navigateToCreatePostPage( context, text: value, prePopulated: true, - scaffoldMessengerKey: scaffoldMessengerKey, ); } } }); } - void _showExitWarning(ScaffoldMessengerState? currentState) { + void _showExitWarning() { showSnackbar( - context, AppLocalizations.of(context)!.tapToExit, duration: const Duration(milliseconds: 3500), - customState: currentState, ); } - Future _handleBackButtonPress(ScaffoldMessengerState? currentState) async { + Future _handleBackButtonPress() async { if (selectedPageIndex != 0) { setState(() { selectedPageIndex = 0; @@ -229,7 +219,7 @@ class _ThunderState extends State { if (appExitCounter == 0) { appExitCounter++; - _showExitWarning(currentState); + _showExitWarning(); Timer(const Duration(milliseconds: 3500), () { appExitCounter = 0; }); @@ -382,11 +372,9 @@ class _ThunderState extends State { void _showLinkProcessingError(BuildContext context, String error, String link) { showSnackbar( - context, error, trailingIcon: Icons.open_in_browser_rounded, duration: const Duration(seconds: 10), - clearSnackBars: false, trailingAction: () => handleLink(context, url: link), ); } @@ -402,7 +390,7 @@ class _ThunderState extends State { BlocProvider(create: (context) => FeedBloc(lemmyClient: LemmyClient.instance)), ], child: WillPopScope( - onWillPop: () async => _handleBackButtonPress(scaffoldMessengerKey.currentState), + onWillPop: () async => _handleBackButtonPress(), child: MultiBlocListener( listeners: [ BlocListener( @@ -416,16 +404,11 @@ class _ThunderState extends State { listener: (context, state) { switch (state.deepLinkStatus) { case DeepLinkStatus.loading: - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - scaffoldMessengerKey.currentState?.showSnackBar( - const SnackBar(content: CircularProgressIndicator.adaptive()), - ); - }); - + return; case DeepLinkStatus.empty: - showSnackbar(context, state.error ?? l10n.emptyUri); + showSnackbar(state.error ?? l10n.emptyUri); case DeepLinkStatus.error: - showSnackbar(context, state.error ?? l10n.exceptionProcessingUri); + showSnackbar(state.error ?? l10n.exceptionProcessingUri); case DeepLinkStatus.success: try { @@ -435,7 +418,7 @@ class _ThunderState extends State { } case DeepLinkStatus.unknown: - showSnackbar(context, state.error ?? l10n.uriNotSupported); + showSnackbar(state.error ?? l10n.uriNotSupported); } }, ), @@ -457,138 +440,135 @@ class _ThunderState extends State { // Update the variable so that it can be used in _handleBackButtonPress _isFabOpen = thunderBlocState.isFabOpen; - return ScaffoldMessenger( - key: scaffoldMessengerKey, - child: Scaffold( - key: scaffoldStateKey, - drawer: selectedPageIndex == 0 - ? CommunityDrawer( - navigateToAccount: () { - Navigator.of(context).pop(); - - if (reduceAnimations) { - widget.pageController.jumpToPage(2); - } else { - widget.pageController.animateToPage(2, duration: const Duration(milliseconds: 500), curve: Curves.ease); - } - }, - ) - : null, - floatingActionButton: thunderBlocState.enableFeedsFab - ? AnimatedOpacity( - opacity: selectedPageIndex == 0 ? 1.0 : 0.0, - duration: const Duration(milliseconds: 150), - curve: Curves.easeIn, - child: IgnorePointer(ignoring: selectedPageIndex != 0, child: FeedFAB(scaffoldMessengerKey: scaffoldMessengerKey)), - ) - : null, - floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling, - bottomNavigationBar: CustomBottomNavigationBar( - selectedPageIndex: selectedPageIndex, - onPageChange: (int index) { - setState(() { - selectedPageIndex = index; - - if (reduceAnimations) { - widget.pageController.jumpToPage(index); - } else { - widget.pageController.animateToPage(index, duration: const Duration(milliseconds: 500), curve: Curves.ease); - } - }); - }, - ), - body: BlocConsumer( - listenWhen: (AuthState previous, AuthState current) { - if (previous.isLoggedIn != current.isLoggedIn || previous.status == AuthStatus.initial) return true; - return false; - }, - buildWhen: (previous, current) => current.status != AuthStatus.failure && current.status != AuthStatus.loading, - listener: (context, state) { - context.read().add(RefreshAccountInformation()); - - // Add a bit of artificial delay to allow preferences to set the proper active profile - Future.delayed(const Duration(milliseconds: 500), () => context.read().add(const GetInboxEvent(reset: true))); - if (context.read().state.status != FeedStatus.initial) { - context.read().add( - FeedFetchedEvent( - feedType: FeedType.general, - postListingType: thunderBlocState.defaultListingType, - sortType: thunderBlocState.defaultSortType, - reset: true, - ), - ); + return Scaffold( + key: scaffoldStateKey, + drawer: selectedPageIndex == 0 + ? CommunityDrawer( + navigateToAccount: () { + Navigator.of(context).pop(); + + if (reduceAnimations) { + widget.pageController.jumpToPage(2); + } else { + widget.pageController.animateToPage(2, duration: const Duration(milliseconds: 500), curve: Curves.ease); + } + }, + ) + : null, + floatingActionButton: thunderBlocState.enableFeedsFab + ? AnimatedOpacity( + opacity: selectedPageIndex == 0 ? 1.0 : 0.0, + duration: const Duration(milliseconds: 150), + curve: Curves.easeIn, + child: IgnorePointer(ignoring: selectedPageIndex != 0, child: FeedFAB()), + ) + : null, + floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling, + bottomNavigationBar: CustomBottomNavigationBar( + selectedPageIndex: selectedPageIndex, + onPageChange: (int index) { + setState(() { + selectedPageIndex = index; + + if (reduceAnimations) { + widget.pageController.jumpToPage(index); + } else { + widget.pageController.animateToPage(index, duration: const Duration(milliseconds: 500), curve: Curves.ease); } - }, - builder: (context, state) { - switch (state.status) { - case AuthStatus.initial: - context.read().add(CheckAuth()); - return Scaffold( - appBar: AppBar(), - body: Center( - child: Container(), + }); + }, + ), + body: BlocConsumer( + listenWhen: (AuthState previous, AuthState current) { + if (previous.isLoggedIn != current.isLoggedIn || previous.status == AuthStatus.initial) return true; + return false; + }, + buildWhen: (previous, current) => current.status != AuthStatus.failure && current.status != AuthStatus.loading, + listener: (context, state) { + context.read().add(RefreshAccountInformation()); + + // Add a bit of artificial delay to allow preferences to set the proper active profile + Future.delayed(const Duration(milliseconds: 500), () => context.read().add(const GetInboxEvent(reset: true))); + if (context.read().state.status != FeedStatus.initial) { + context.read().add( + FeedFetchedEvent( + feedType: FeedType.general, + postListingType: thunderBlocState.defaultListingType, + sortType: thunderBlocState.defaultSortType, + reset: true, ), ); - case AuthStatus.success: - Version? version = thunderBlocState.version; - bool showInAppUpdateNotification = thunderBlocState.showInAppUpdateNotification; - - if (version?.hasUpdate == true && hasShownUpdateDialog == false && showInAppUpdateNotification == true) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showUpdateNotification(context, version); - setState(() => hasShownUpdateDialog = true); - }); - } - - return PageView( - controller: widget.pageController, - onPageChanged: (index) => setState(() => selectedPageIndex = index), - physics: const NeverScrollableScrollPhysics(), - children: [ - Stack( - children: [ - FeedPage( - useGlobalFeedBloc: true, - feedType: FeedType.general, - postListingType: thunderBlocState.defaultListingType, - sortType: thunderBlocState.defaultSortType, - scaffoldStateKey: scaffoldStateKey, - ), - AnimatedOpacity( - opacity: _isFabOpen ? 1.0 : 0.0, - duration: const Duration(milliseconds: 150), - child: _isFabOpen - ? ModalBarrier( - color: theme.colorScheme.background.withOpacity(0.95), - dismissible: true, - onDismiss: () => context.read().add(const OnFabToggle(false)), - ) - : null, - ), - ], - ), - const SearchPage(), - const AccountPage(), - const InboxPage(), - SettingsPage(), - ], - ); + } + }, + builder: (context, state) { + switch (state.status) { + case AuthStatus.initial: + context.read().add(CheckAuth()); + return Scaffold( + appBar: AppBar(), + body: Center( + child: Container(), + ), + ); + case AuthStatus.success: + Version? version = thunderBlocState.version; + bool showInAppUpdateNotification = thunderBlocState.showInAppUpdateNotification; + + if (version?.hasUpdate == true && hasShownUpdateDialog == false && showInAppUpdateNotification == true) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showUpdateNotification(context, version); + setState(() => hasShownUpdateDialog = true); + }); + } - // Should never hit these, they're handled by the login page - case AuthStatus.failure: - case AuthStatus.loading: - return Container(); - case AuthStatus.failureCheckingInstance: - showSnackbar(context, state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage); - return ErrorMessage( - title: AppLocalizations.of(context)!.unableToLoadInstance(LemmyClient.instance.lemmyApiV3.host), - message: AppLocalizations.of(context)!.internetOrInstanceIssues, - actionText: AppLocalizations.of(context)!.accountSettings, - action: () => showProfileModalSheet(context), - ); - } - }, - ), + return PageView( + controller: widget.pageController, + onPageChanged: (index) => setState(() => selectedPageIndex = index), + physics: const NeverScrollableScrollPhysics(), + children: [ + Stack( + children: [ + FeedPage( + useGlobalFeedBloc: true, + feedType: FeedType.general, + postListingType: thunderBlocState.defaultListingType, + sortType: thunderBlocState.defaultSortType, + scaffoldStateKey: scaffoldStateKey, + ), + AnimatedOpacity( + opacity: _isFabOpen ? 1.0 : 0.0, + duration: const Duration(milliseconds: 150), + child: _isFabOpen + ? ModalBarrier( + color: theme.colorScheme.background.withOpacity(0.95), + dismissible: true, + onDismiss: () => context.read().add(const OnFabToggle(false)), + ) + : null, + ), + ], + ), + const SearchPage(), + const AccountPage(), + const InboxPage(), + SettingsPage(), + ], + ); + + // Should never hit these, they're handled by the login page + case AuthStatus.failure: + case AuthStatus.loading: + return Container(); + case AuthStatus.failureCheckingInstance: + showSnackbar(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage); + return ErrorMessage( + title: AppLocalizations.of(context)!.unableToLoadInstance(LemmyClient.instance.lemmyApiV3.host), + message: AppLocalizations.of(context)!.internetOrInstanceIssues, + actionText: AppLocalizations.of(context)!.accountSettings, + action: () => showProfileModalSheet(context), + ); + } + }, ), ); case ThunderStatus.failure: diff --git a/lib/user/pages/user_page.dart b/lib/user/pages/user_page.dart index 5faa86b12..c350bdb0d 100644 --- a/lib/user/pages/user_page.dart +++ b/lib/user/pages/user_page.dart @@ -135,7 +135,7 @@ class _UserPageState extends State { userBloc = context.read(); if (state.status == UserStatus.failedToBlock) { - showSnackbar(context, state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage); + showSnackbar(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage); } switch (state.status) { diff --git a/lib/user/pages/user_page_success.dart b/lib/user/pages/user_page_success.dart index c97b65da4..8830ff1c3 100644 --- a/lib/user/pages/user_page_success.dart +++ b/lib/user/pages/user_page_success.dart @@ -290,7 +290,7 @@ class _UserPageSuccessState extends State with TickerProviderSt onDeleteAction: (int commentId, bool deleted) => context.read().add(DeleteCommentEvent(deleted: deleted, commentId: commentId)), onReportAction: (int commentId) { if (widget.isAccountUser) { - showSnackbar(context, AppLocalizations.of(context)!.cannotReportOwnComment); + showSnackbar(AppLocalizations.of(context)!.cannotReportOwnComment); } else { showReportCommentActionBottomSheet( context, @@ -348,7 +348,7 @@ class _UserPageSuccessState extends State with TickerProviderSt if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true && (!isEdit || commentView.comment.content != newDraftComment?.text)) { await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, AppLocalizations.of(context)!.commentSavedAsDraft); + showSnackbar(AppLocalizations.of(context)!.commentSavedAsDraft); prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { prefs.remove(draftId); @@ -399,7 +399,7 @@ class _UserPageSuccessState extends State with TickerProviderSt onDeleteAction: (int commentId, bool deleted) => context.read().add(DeleteCommentEvent(deleted: deleted, commentId: commentId)), onReportAction: (int commentId) { if (widget.isAccountUser) { - showSnackbar(context, AppLocalizations.of(context)!.cannotReportOwnComment); + showSnackbar(AppLocalizations.of(context)!.cannotReportOwnComment); } else { showReportCommentActionBottomSheet( context, @@ -457,7 +457,7 @@ class _UserPageSuccessState extends State with TickerProviderSt // This delay gives time for the previous page to be dismissed, //so we don't show the snackbar during the transition await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, AppLocalizations.of(context)!.commentSavedAsDraft); + showSnackbar(AppLocalizations.of(context)!.commentSavedAsDraft); prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); } else { prefs.remove(draftId); diff --git a/lib/user/pages/user_settings_page.dart b/lib/user/pages/user_settings_page.dart index d63a712cc..239dd2760 100644 --- a/lib/user/pages/user_settings_page.dart +++ b/lib/user/pages/user_settings_page.dart @@ -60,18 +60,15 @@ class _UserSettingsPageState extends State { if ((state.status == UserSettingsStatus.failure || state.status == UserSettingsStatus.failedRevert) && (state.personBeingBlocked != 0 || state.communityBeingBlocked != 0 || state.instanceBeingBlocked != 0)) { - showSnackbar( - context, - state.status == UserSettingsStatus.failure - ? l10n.failedToUnblock(state.errorMessage ?? l10n.missingErrorMessage) - : l10n.failedToBlock(state.errorMessage ?? l10n.missingErrorMessage)); + showSnackbar(state.status == UserSettingsStatus.failure + ? l10n.failedToUnblock(state.errorMessage ?? l10n.missingErrorMessage) + : l10n.failedToBlock(state.errorMessage ?? l10n.missingErrorMessage)); } else if (state.status == UserSettingsStatus.failure) { - showSnackbar(context, l10n.failedToLoadBlocks(state.errorMessage ?? l10n.missingErrorMessage)); + showSnackbar(l10n.failedToLoadBlocks(state.errorMessage ?? l10n.missingErrorMessage)); } if (state.status == UserSettingsStatus.successBlock && (state.personBeingBlocked != 0 || state.communityBeingBlocked != 0 || state.instanceBeingBlocked != 0)) { showSnackbar( - context, l10n.successfullyUnblocked, trailingIcon: Icons.undo_rounded, trailingAction: () { @@ -87,7 +84,7 @@ class _UserSettingsPageState extends State { } if (state.status == UserSettingsStatus.revert && (state.personBeingBlocked != 0 || state.communityBeingBlocked != 0 || state.instanceBeingBlocked != 0)) { - showSnackbar(context, l10n.successfullyBlocked); + showSnackbar(l10n.successfullyBlocked); } }, builder: (context, state) { diff --git a/lib/utils/global_context.dart b/lib/utils/global_context.dart index 71f429aef..6857c98b1 100644 --- a/lib/utils/global_context.dart +++ b/lib/utils/global_context.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; class GlobalContext { + static GlobalKey overlayKey = GlobalKey(); static GlobalKey scaffoldMessengerKey = GlobalKey(); static BuildContext get context => scaffoldMessengerKey.currentContext!; } diff --git a/lib/utils/image.dart b/lib/utils/image.dart index d9bdea389..087c07888 100644 --- a/lib/utils/image.dart +++ b/lib/utils/image.dart @@ -182,7 +182,7 @@ void uploadImage(BuildContext context, ImageBloc imageBloc, {bool postImage = fa Account? account = await fetchActiveProfileAccount(); imageBloc.add(ImageUploadEvent(imageFile: path, instance: account!.instance!, jwt: account.jwt!, postImage: postImage)); } catch (e) { - showSnackbar(context, AppLocalizations.of(context)!.postUploadImageError, leadingIcon: Icons.warning_rounded, leadingIconColor: Theme.of(context).colorScheme.errorContainer); + showSnackbar(AppLocalizations.of(context)!.postUploadImageError, leadingIcon: Icons.warning_rounded, leadingIconColor: Theme.of(context).colorScheme.errorContainer); } } From 605a6a96e900db8794edc72918698825a9c4ad4b Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Sun, 4 Feb 2024 10:13:38 -0800 Subject: [PATCH 04/11] cleaned up some unused code --- lib/community/pages/create_post_page.dart | 5 ++--- lib/utils/global_context.dart | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/community/pages/create_post_page.dart b/lib/community/pages/create_post_page.dart index 805e831ed..9f0d338d3 100644 --- a/lib/community/pages/create_post_page.dart +++ b/lib/community/pages/create_post_page.dart @@ -13,11 +13,11 @@ import 'package:markdown_editable_textinput/format_markdown.dart'; import 'package:markdown_editable_textinput/markdown_buttons.dart'; import 'package:markdown_editable_textinput/markdown_text_input_field.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:overlay_support/overlay_support.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/community/bloc/image_bloc.dart'; +import 'package:thunder/community/utils/post_card_action_helpers.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/enums/local_settings.dart'; @@ -34,7 +34,6 @@ import 'package:thunder/shared/link_preview_card.dart'; import 'package:thunder/user/widgets/user_indicator.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/utils/debounce.dart'; -import 'package:thunder/utils/global_context.dart'; import 'package:thunder/utils/image.dart'; import 'package:thunder/utils/instance.dart'; import 'package:thunder/post/utils/navigate_post.dart'; @@ -218,7 +217,7 @@ class _CreatePostPageState extends State { sharedPreferences?.setString(draftId, jsonEncode(draftPost.toJson())); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - showSnackbar(AppLocalizations.of(GlobalContext.context)!.postSavedAsDraft); + showSnackbar(l10n.postSavedAsDraft); }); } else { sharedPreferences?.remove(draftId); diff --git a/lib/utils/global_context.dart b/lib/utils/global_context.dart index 6857c98b1..71f429aef 100644 --- a/lib/utils/global_context.dart +++ b/lib/utils/global_context.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; class GlobalContext { - static GlobalKey overlayKey = GlobalKey(); static GlobalKey scaffoldMessengerKey = GlobalKey(); static BuildContext get context => scaffoldMessengerKey.currentContext!; } From 15c624b023c21b041846420ae196f6b5d49a1d24 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Sun, 4 Feb 2024 10:57:42 -0800 Subject: [PATCH 05/11] fixed issue with navigating to post not working on the snackbar due to old context --- lib/community/pages/create_post_page.dart | 14 ++-------- lib/community/widgets/community_sidebar.dart | 29 ++------------------ lib/feed/widgets/feed_fab.dart | 28 ++----------------- lib/post/cubit/create_post_cubit.dart | 2 +- lib/post/utils/navigate_create_post.dart | 29 ++++++++++++++++++-- 5 files changed, 33 insertions(+), 69 deletions(-) diff --git a/lib/community/pages/create_post_page.dart b/lib/community/pages/create_post_page.dart index 9f0d338d3..e23ef7434 100644 --- a/lib/community/pages/create_post_page.dart +++ b/lib/community/pages/create_post_page.dart @@ -341,10 +341,10 @@ class _CreatePostPageState extends State { child: IconButton( onPressed: isSubmitButtonDisabled ? null - : () async { + : () { draftPost.saveAsDraft = false; - final int? postId = await context.read().createOrEditPost( + context.read().createOrEditPost( communityId: communityId!, name: _titleTextController.text, body: _bodyTextController.text, @@ -353,16 +353,6 @@ class _CreatePostPageState extends State { postIdBeingEdited: widget.postView?.post.id, languageId: languageId, ); - - if (widget.postView?.post.id == null && postId != null) { - showSnackbar( - l10n.postCreatedSuccessfully, - trailingIcon: Icons.remove_red_eye_rounded, - trailingAction: () { - navigateToPost(context, postId: postId); - }, - ); - } }, icon: Icon( widget.postView != null ? Icons.edit_rounded : Icons.send_rounded, diff --git a/lib/community/widgets/community_sidebar.dart b/lib/community/widgets/community_sidebar.dart index 1678bc8e0..04407b859 100644 --- a/lib/community/widgets/community_sidebar.dart +++ b/lib/community/widgets/community_sidebar.dart @@ -15,6 +15,7 @@ import 'package:thunder/core/enums/full_name_separator.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/bloc/feed_bloc.dart'; import 'package:thunder/instance/instance_view.dart'; +import 'package:thunder/post/utils/navigate_create_post.dart'; import 'package:thunder/shared/common_markdown_body.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/shared/avatars/user_avatar.dart'; @@ -332,33 +333,7 @@ class CommunityActions extends StatelessWidget { onPressed: isUserLoggedIn ? () async { HapticFeedback.mediumImpact(); - CommunityBloc communityBloc = context.read(); - AccountBloc accountBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - FeedBloc feedBloc = context.read(); - - final ThunderState state = context.read().state; - final bool reduceAnimations = state.reduceAnimations; - - Navigator.of(context).push(SwipeablePageRoute( - transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null, - canOnlySwipeFromEdge: true, - backGestureDetectionWidth: 45, - builder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: communityBloc), - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: feedBloc), - ], - child: CreatePostPage( - communityId: communityView.community.id, - communityView: getCommunityResponse.communityView, - ), - ); - }, - )); + navigateToCreatePostPage(context, communityId: communityView.community.id, communityView: getCommunityResponse.communityView); } : null, style: TextButton.styleFrom( diff --git a/lib/feed/widgets/feed_fab.dart b/lib/feed/widgets/feed_fab.dart index 1b1ad0f48..99ff7ec24 100644 --- a/lib/feed/widgets/feed_fab.dart +++ b/lib/feed/widgets/feed_fab.dart @@ -15,6 +15,7 @@ import 'package:thunder/core/enums/fab_action.dart'; import 'package:thunder/feed/bloc/feed_bloc.dart'; import 'package:thunder/feed/utils/utils.dart'; import 'package:thunder/feed/view/feed_page.dart'; +import 'package:thunder/post/utils/navigate_create_post.dart'; import 'package:thunder/shared/gesture_fab.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/shared/sort_picker.dart'; @@ -288,31 +289,6 @@ class FeedFAB extends StatelessWidget { } FeedBloc feedBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - AccountBloc accountBloc = context.read(); - - final ThunderState thunderState = context.read().state; - final bool reduceAnimations = thunderState.reduceAnimations; - - Navigator.of(context).push( - SwipeablePageRoute( - transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null, - canOnlySwipeFromEdge: true, - backGestureDetectionWidth: 45, - builder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: feedBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: accountBloc), - ], - child: CreatePostPage( - communityId: feedBloc.state.communityId, - communityView: feedBloc.state.fullCommunityView?.communityView, - ), - ); - }, - ), - ); + navigateToCreatePostPage(context, communityId: feedBloc.state.communityId, communityView: feedBloc.state.fullCommunityView?.communityView); } } diff --git a/lib/post/cubit/create_post_cubit.dart b/lib/post/cubit/create_post_cubit.dart index f86b1de4e..8a981d8dd 100644 --- a/lib/post/cubit/create_post_cubit.dart +++ b/lib/post/cubit/create_post_cubit.dart @@ -57,7 +57,7 @@ class CreatePostCubit extends Cubit { // languageId: languageId, // ); - GetPostResponse getPostResponse = await lemmy.run(GetPost(id: 14462486)); + GetPostResponse getPostResponse = await lemmy.run(GetPost(id: 1)); // Parse the newly created post List postViewMedias = await parsePostViews([getPostResponse.postView]); diff --git a/lib/post/utils/navigate_create_post.dart b/lib/post/utils/navigate_create_post.dart index 2be3be52f..b4d600cd4 100644 --- a/lib/post/utils/navigate_create_post.dart +++ b/lib/post/utils/navigate_create_post.dart @@ -2,12 +2,14 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lemmy_api_client/v3.dart'; import 'package:swipeable_page_route/swipeable_page_route.dart'; import 'package:thunder/account/bloc/account_bloc.dart'; import 'package:thunder/community/pages/create_post_page.dart'; import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/feed.dart'; +import 'package:thunder/post/utils/navigate_post.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/thunder/thunder.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -19,19 +21,31 @@ Future navigateToCreatePostPage( File? image, String? url, bool? prePopulated, + int? communityId, + CommunityView? communityView, }) async { try { + final l10n = AppLocalizations.of(context)!; + + FeedBloc? feedBloc; ThunderBloc thunderBloc = context.read(); AccountBloc accountBloc = context.read(); + + try { + feedBloc = context.read(); + } catch (e) { + // Don't need feed block if we're not opening post in the context of a feed. + } + final bool reduceAnimations = thunderBloc.state.reduceAnimations; Navigator.of(context).push(SwipeablePageRoute( transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null, canOnlySwipeFromEdge: true, backGestureDetectionWidth: 45, - builder: (context) { + builder: (navigatorContext) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => FeedBloc(lemmyClient: LemmyClient.instance)), + feedBloc != null ? BlocProvider.value(value: feedBloc) : BlocProvider(create: (context) => FeedBloc(lemmyClient: LemmyClient.instance)), BlocProvider.value(value: thunderBloc), BlocProvider.value(value: accountBloc), ], @@ -41,9 +55,18 @@ Future navigateToCreatePostPage( image: image, url: url, prePopulated: prePopulated, - communityId: null, + communityId: communityId, + communityView: communityView, onPostSuccess: (PostViewMedia postViewMedia) { try { + showSnackbar( + l10n.postCreatedSuccessfully, + trailingIcon: Icons.remove_red_eye_rounded, + trailingAction: () { + navigateToPost(context, postId: postViewMedia.postView.post.id); + }, + ); + context.read().add(FeedItemUpdatedEvent(postViewMedia: postViewMedia)); } catch (e) {} }, From 0ea5ad78023f4d7fd94b92b24456b62afc63a81a Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 5 Feb 2024 08:38:00 -0800 Subject: [PATCH 06/11] remove instances of hiding snackbars --- lib/community/utils/post_card_action_helpers.dart | 3 --- lib/community/widgets/community_sidebar.dart | 1 - lib/shared/snackbar.dart | 4 ---- lib/user/widgets/user_sidebar.dart | 1 - 4 files changed, 9 deletions(-) diff --git a/lib/community/utils/post_card_action_helpers.dart b/lib/community/utils/post_card_action_helpers.dart index 38abfd5d4..1633ee228 100644 --- a/lib/community/utils/post_card_action_helpers.dart +++ b/lib/community/utils/post_card_action_helpers.dart @@ -312,9 +312,6 @@ void onSelected(BuildContext context, PostCardAction postCardAction, PostViewMed // Download mediaFile = await DefaultCacheManager().getSingleFile(postViewMedia.media.first.mediaUrl!); - - // Hide snackbar - hideSnackbar(context); } // Share diff --git a/lib/community/widgets/community_sidebar.dart b/lib/community/widgets/community_sidebar.dart index 04407b859..13823813f 100644 --- a/lib/community/widgets/community_sidebar.dart +++ b/lib/community/widgets/community_sidebar.dart @@ -292,7 +292,6 @@ class BlockCommunityButton extends StatelessWidget { onPressed: isUserLoggedIn ? () { HapticFeedback.heavyImpact(); - hideSnackbar(context); context.read().add(CommunityActionEvent(communityAction: CommunityAction.block, communityId: communityView.community.id, value: !blocked)); } : null, diff --git a/lib/shared/snackbar.dart b/lib/shared/snackbar.dart index 1e8e60b09..2f2e3a9ce 100644 --- a/lib/shared/snackbar.dart +++ b/lib/shared/snackbar.dart @@ -220,7 +220,3 @@ class _ThunderSnackbarState extends State { return snackBar; } } - -void hideSnackbar(BuildContext context) { - OverlaySupportEntry.of(context)?.dismiss(); -} diff --git a/lib/user/widgets/user_sidebar.dart b/lib/user/widgets/user_sidebar.dart index f9c094302..f04bea609 100644 --- a/lib/user/widgets/user_sidebar.dart +++ b/lib/user/widgets/user_sidebar.dart @@ -161,7 +161,6 @@ class _UserSidebarState extends State { onPressed: isLoggedIn ? () { HapticFeedback.heavyImpact(); - hideSnackbar(context); context.read().add( BlockUserEvent( personId: widget.userInfo!.person.id, From 9d5b1139ad3c2ea1b2a402c55206d24ae3530bef Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 5 Feb 2024 09:17:13 -0800 Subject: [PATCH 07/11] added addPostFrameCallback to snackbar --- lib/shared/snackbar.dart | 86 ++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/lib/shared/snackbar.dart b/lib/shared/snackbar.dart index 2f2e3a9ce..4dc898bf2 100644 --- a/lib/shared/snackbar.dart +++ b/lib/shared/snackbar.dart @@ -22,52 +22,54 @@ void showSnackbar( // Allows us to clear the previous overlay before showing the next one const key = TransientKey('transient'); - showOverlay( - (context, progress) { - return SnackbarNotification( - builder: (context) => ThunderSnackbar( - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (leadingIcon != null) - Icon( - leadingIcon, - color: leadingIconColor, - ), - if (leadingIcon != null) const SizedBox(width: 8.0), - Expanded( - child: Text( - text, + WidgetsBinding.instance.addPostFrameCallback((_) { + showOverlay( + (context, progress) { + return SnackbarNotification( + builder: (context) => ThunderSnackbar( + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leadingIcon != null) + Icon( + leadingIcon, + color: leadingIconColor, + ), + if (leadingIcon != null) const SizedBox(width: 8.0), + Expanded( + child: Text( + text, + ), ), - ), - if (trailingIcon != null) - SizedBox( - height: 20, - child: IconButton( - visualDensity: VisualDensity.compact, - onPressed: trailingAction != null - ? () { - OverlaySupportEntry.of(context)?.dismiss(); - trailingAction(); - } - : null, - icon: Icon( - trailingIcon, - color: trailingIconColor ?? Theme.of(context).colorScheme.inversePrimary, + if (trailingIcon != null) + SizedBox( + height: 20, + child: IconButton( + visualDensity: VisualDensity.compact, + onPressed: trailingAction != null + ? () { + OverlaySupportEntry.of(context)?.dismiss(); + trailingAction(); + } + : null, + icon: Icon( + trailingIcon, + color: trailingIconColor ?? Theme.of(context).colorScheme.inversePrimary, + ), ), ), - ), - ], + ], + ), ), - ), - progress: progress, - ); - }, - animationDuration: _snackBarTransitionDuration, - duration: duration ?? Duration(milliseconds: max(kNotificationDuration.inMilliseconds, max(4000, 1000 * wordCount))), // Assuming 60 WPM or 1 WPS - context: GlobalContext.context, - key: key, - ); + progress: progress, + ); + }, + animationDuration: _snackBarTransitionDuration, + duration: duration ?? Duration(milliseconds: max(kNotificationDuration.inMilliseconds, max(4000, 1000 * wordCount))), // Assuming 60 WPM or 1 WPS + context: GlobalContext.context, + key: key, + ); + }); } /// Build the slide translation for the snack bar From 8ce867929fb57fb2bf62cbc4bb4a7eab509ff279 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 5 Feb 2024 09:20:24 -0800 Subject: [PATCH 08/11] removed addPostFrameCallback --- lib/community/pages/create_post_page.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/community/pages/create_post_page.dart b/lib/community/pages/create_post_page.dart index e23ef7434..367d0f95c 100644 --- a/lib/community/pages/create_post_page.dart +++ b/lib/community/pages/create_post_page.dart @@ -215,10 +215,7 @@ class _CreatePostPageState extends State { if (draftPost.isNotEmpty && draftPost.saveAsDraft) { sharedPreferences?.setString(draftId, jsonEncode(draftPost.toJson())); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - showSnackbar(l10n.postSavedAsDraft); - }); + showSnackbar(l10n.postSavedAsDraft); } else { sharedPreferences?.remove(draftId); } From 55f8c760427c42e056558f5ca620f333f3e9e803 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 5 Feb 2024 09:25:28 -0800 Subject: [PATCH 09/11] restored create post function --- lib/post/cubit/create_post_cubit.dart | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/post/cubit/create_post_cubit.dart b/lib/post/cubit/create_post_cubit.dart index 8a981d8dd..573a6c2f9 100644 --- a/lib/post/cubit/create_post_cubit.dart +++ b/lib/post/cubit/create_post_cubit.dart @@ -46,21 +46,18 @@ class CreatePostCubit extends Cubit { emit(state.copyWith(status: CreatePostStatus.submitting)); try { - final lemmy = LemmyClient.instance.lemmyApiV3; - // PostView postView = await createPost( - // communityId: communityId, - // name: name, - // body: body, - // url: url, - // nsfw: nsfw, - // postIdBeingEdited: postIdBeingEdited, - // languageId: languageId, - // ); - - GetPostResponse getPostResponse = await lemmy.run(GetPost(id: 1)); + PostView postView = await createPost( + communityId: communityId, + name: name, + body: body, + url: url, + nsfw: nsfw, + postIdBeingEdited: postIdBeingEdited, + languageId: languageId, + ); // Parse the newly created post - List postViewMedias = await parsePostViews([getPostResponse.postView]); + List postViewMedias = await parsePostViews([postView]); emit(state.copyWith(status: CreatePostStatus.success, postViewMedia: postViewMedias.firstOrNull)); return postViewMedias.firstOrNull?.postView.post.id; From 9cf75b40db5e6a4a76ad5ba5b5afb71c69abd674 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 5 Feb 2024 09:42:38 -0800 Subject: [PATCH 10/11] linting --- lib/post/cubit/create_post_cubit.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/post/cubit/create_post_cubit.dart b/lib/post/cubit/create_post_cubit.dart index 573a6c2f9..3f407f267 100644 --- a/lib/post/cubit/create_post_cubit.dart +++ b/lib/post/cubit/create_post_cubit.dart @@ -6,7 +6,6 @@ import 'package:lemmy_api_client/v3.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/models/post_view_media.dart'; -import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/utils/post.dart'; import 'package:thunder/post/utils/post.dart'; import 'package:thunder/utils/error_messages.dart'; From 417d2b4f33939d9a8f727a23d43ad58ac26ad3d4 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 5 Feb 2024 10:59:16 -0800 Subject: [PATCH 11/11] fixed issue with snackbar reappearing, adjusted padding to account for view insets --- lib/shared/snackbar.dart | 141 ++++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 62 deletions(-) diff --git a/lib/shared/snackbar.dart b/lib/shared/snackbar.dart index 4dc898bf2..15167cebc 100644 --- a/lib/shared/snackbar.dart +++ b/lib/shared/snackbar.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:overlay_support/overlay_support.dart'; + import 'package:thunder/utils/global_context.dart'; const Duration _snackBarTransitionDuration = Duration(milliseconds: 500); @@ -30,17 +31,9 @@ void showSnackbar( content: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (leadingIcon != null) - Icon( - leadingIcon, - color: leadingIconColor, - ), + if (leadingIcon != null) Icon(leadingIcon, color: leadingIconColor), if (leadingIcon != null) const SizedBox(width: 8.0), - Expanded( - child: Text( - text, - ), - ), + Expanded(child: Text(text)), if (trailingIcon != null) SizedBox( height: 20, @@ -72,7 +65,7 @@ void showSnackbar( }); } -/// Build the slide translation for the snack bar +/// Builds a custom snackbar which attempts to match the Material 3 spec as closely as possible. class SnackbarNotification extends StatefulWidget { final WidgetBuilder builder; @@ -156,58 +149,71 @@ class ThunderSnackbar extends StatefulWidget { } class _ThunderSnackbarState extends State { - @override - Widget build(BuildContext context) { - const double horizontalPadding = 16.0; - const double singleLineVerticalPadding = 14.0; + Widget child = Container(); - final ThemeData theme = Theme.of(context); - final SnackBarThemeData snackBarTheme = theme.snackBarTheme; - - final double elevation = snackBarTheme.elevation ?? 6.0; - final Color backgroundColor = theme.colorScheme.inverseSurface; - final ShapeBorder shape = snackBarTheme.shape ?? RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)); + @override + void initState() { + super.initState(); - Widget snackBar = Dismissible( - key: UniqueKey(), - direction: DismissDirection.down, - behavior: HitTestBehavior.deferToChild, - onDismissed: (direction) {}, - child: Container( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom + kBottomNavigationBarHeight + singleLineVerticalPadding), - child: ClipRect( - child: Align( - alignment: AlignmentDirectional.bottomStart, - child: Semantics( - container: true, - liveRegion: true, - child: Padding( - padding: const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0), - child: Material( - shape: shape, - elevation: elevation, - color: backgroundColor, - clipBehavior: Clip.none, - child: Theme( - data: theme, - child: Padding( - padding: const EdgeInsetsDirectional.only(start: horizontalPadding, end: horizontalPadding), - child: Wrap( - children: [ - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(vertical: singleLineVerticalPadding), - child: DefaultTextStyle( - style: theme.textTheme.bodyMedium!.copyWith(color: theme.colorScheme.onInverseSurface), - child: widget.content, + // Initialize the widget here. We do this so that we can change the state of the widget to an empty Container when we dismiss the snackbar. + // Doing so prevents the snackbar from showing back up after it has been dismissed. + WidgetsBinding.instance.addPostFrameCallback((_) { + const double horizontalPadding = 16.0; + const double singleLineVerticalPadding = 14.0; + + final ThemeData theme = Theme.of(context); + final SnackBarThemeData snackBarTheme = theme.snackBarTheme; + + final double elevation = snackBarTheme.elevation ?? 6.0; + final Color backgroundColor = theme.colorScheme.inverseSurface; + final ShapeBorder shape = snackBarTheme.shape ?? RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)); + + double snackbarBottomPadding = 0; + + if (MediaQuery.of(context).viewInsets.bottom == 0) { + // If there is no inset padding, we'll add in some padding for the bottom navigation bar. + snackbarBottomPadding += MediaQuery.of(context).viewPadding.bottom + kBottomNavigationBarHeight + singleLineVerticalPadding; + } else { + snackbarBottomPadding += MediaQuery.of(context).viewInsets.bottom; + } + + child = SafeArea( + child: Container( + padding: EdgeInsets.only(bottom: snackbarBottomPadding), + child: ClipRect( + child: Align( + alignment: AlignmentDirectional.bottomStart, + child: Semantics( + container: true, + liveRegion: true, + child: Padding( + padding: const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0), + child: Material( + shape: shape, + elevation: elevation, + color: backgroundColor, + clipBehavior: Clip.none, + child: Theme( + data: theme, + child: Padding( + padding: const EdgeInsetsDirectional.only(start: horizontalPadding, end: horizontalPadding), + child: Wrap( + children: [ + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: singleLineVerticalPadding), + child: DefaultTextStyle( + style: theme.textTheme.bodyMedium!.copyWith(color: theme.colorScheme.onInverseSurface), + child: widget.content, + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), @@ -216,9 +222,20 @@ class _ThunderSnackbarState extends State { ), ), ), - ), - ); + ); + }); + } - return snackBar; + @override + Widget build(BuildContext context) { + return Dismissible( + key: UniqueKey(), + direction: DismissDirection.down, + behavior: HitTestBehavior.deferToChild, + onDismissed: (direction) { + setState(() => child = Container()); + }, + child: child, + ); } }