From 377710ec221e3394d9a5d03ac379343bbd37b03b Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:43:39 +0700 Subject: [PATCH] refactor(ui): consolidate file feedback and shared display helpers --- lib/screens/downloaded_album_screen.dart | 11 ++---- lib/screens/library_tracks_folder_screen.dart | 9 ++--- lib/screens/local_album_screen.dart | 11 ++---- lib/screens/main_shell.dart | 11 ++---- lib/screens/now_playing_screen.dart | 9 ++--- lib/screens/queue_tab.dart | 16 +-------- lib/screens/queue_tab_item_widgets.dart | 2 +- lib/screens/queue_tab_navigation.dart | 20 ++--------- lib/screens/settings/about_page.dart | 24 +++++-------- lib/screens/setup_screen.dart | 11 ++---- lib/screens/track_metadata_actions.dart | 12 ++----- lib/screens/track_metadata_convert.dart | 34 +++++++++++-------- lib/screens/track_metadata_screen.dart | 16 +-------- .../track_metadata_screen_display.dart | 15 -------- lib/utils/local_playback.dart | 12 ++----- lib/utils/string_utils.dart | 16 +++++++++ lib/widgets/app_snack_bar.dart | 12 +++++++ 17 files changed, 77 insertions(+), 164 deletions(-) diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 6826b45d..b7ed3972 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -29,6 +29,7 @@ import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; import 'package:spotiflac_android/widgets/collection_scaffold.dart'; import 'package:spotiflac_android/widgets/cached_cover_image.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:spotiflac_android/widgets/album_track_tile.dart'; import 'package:spotiflac_android/widgets/animation_utils.dart'; import 'package:spotiflac_android/widgets/destructive_selection_button.dart'; @@ -188,15 +189,7 @@ class _DownloadedAlbumScreenState extends ConsumerState startItem: track, ); } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(e)), - ), - ), - ); - } + if (mounted) showCannotOpenFileSnackBar(context, e); } } diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index 6c5b4673..87222186 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -8,6 +8,7 @@ import 'package:spotiflac_android/theme/app_tokens.dart'; import 'package:spotiflac_android/widgets/track_card.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; import 'package:spotiflac_android/widgets/app_alert_dialog.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/theme/mornye_icons.dart'; import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus, XFile; @@ -1057,13 +1058,7 @@ class _CollectionTrackTile extends ConsumerWidget { .playTrackList(folderTracks, startIndex: trackIndex); } catch (error) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(error)), - ), - ), - ); + showCannotOpenFileSnackBar(context, error); } } diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 35d4634d..27200391 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -26,6 +26,7 @@ import 'package:spotiflac_android/screens/selection_mode_mixin.dart'; import 'package:spotiflac_android/widgets/collection_scaffold.dart'; import 'package:spotiflac_android/widgets/album_track_tile.dart'; import 'package:spotiflac_android/widgets/animation_utils.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:spotiflac_android/widgets/destructive_selection_button.dart'; import 'package:spotiflac_android/widgets/selection_action_button.dart'; import 'package:spotiflac_android/widgets/selection_bottom_bar.dart'; @@ -154,15 +155,7 @@ class _LocalAlbumScreenState extends ConsumerState .read(playbackProvider.notifier) .playLocalLibraryQueue(_sortedTracksCache, startItem: track); } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(e)), - ), - ), - ); - } + if (mounted) showCannotOpenFileSnackBar(context, e); } } diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 2e8a56eb..34f4939d 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -32,6 +32,7 @@ import 'package:spotiflac_android/services/notification_service.dart'; import 'package:spotiflac_android/services/app_remote_config_service.dart'; import 'package:spotiflac_android/services/update_checker.dart'; import 'package:spotiflac_android/widgets/app_announcement_dialog.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:spotiflac_android/widgets/update_dialog.dart'; import 'package:spotiflac_android/widgets/animation_utils.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -277,15 +278,7 @@ class _MainShellState extends ConsumerState 'Failed to repair SAF access from startup: $e', ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile( - context.friendlyError(e), - ), - ), - ), - ); + showCannotOpenFileSnackBar(context, e); } } finally { if (dialogContext.mounted) { diff --git a/lib/screens/now_playing_screen.dart b/lib/screens/now_playing_screen.dart index 33104366..4ba80f3b 100644 --- a/lib/screens/now_playing_screen.dart +++ b/lib/screens/now_playing_screen.dart @@ -35,6 +35,7 @@ import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/utils/synced_lyrics_scroll.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; import 'package:spotiflac_android/widgets/app_loading_indicator.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:spotiflac_android/widgets/expressive_button.dart'; import 'package:spotiflac_android/widgets/expressive_icon_button.dart'; import 'package:spotiflac_android/widgets/aligned_lyric_pronunciation.dart'; @@ -2021,13 +2022,7 @@ class _NowPlayingScreenState extends ConsumerState { await openFile(source); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(e)), - ), - ), - ); + showCannotOpenFileSnackBar(context, e); } } diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 06bc95f9..787ed746 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -14,6 +14,7 @@ import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; import 'package:spotiflac_android/widgets/app_sliver_header.dart'; import 'package:spotiflac_android/widgets/app_search_field.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:spotiflac_android/widgets/library_search_results.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart'; import 'package:flutter/services.dart'; @@ -261,21 +262,6 @@ class _QueueTabState extends ConsumerState { final List _filterModes = ['all', 'albums', 'singles', 'playlists']; bool _isPageControllerInitialized = false; bool _wasTabVisible = false; - static const List _months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; - final TextEditingController _searchController = TextEditingController(); final FocusNode _searchFocusNode = FocusNode(); String _searchQuery = ''; diff --git a/lib/screens/queue_tab_item_widgets.dart b/lib/screens/queue_tab_item_widgets.dart index 8e123ed8..f9fab858 100644 --- a/lib/screens/queue_tab_item_widgets.dart +++ b/lib/screens/queue_tab_item_widgets.dart @@ -847,7 +847,7 @@ extension _QueueTabItemWidgets on _QueueTabState { final isSelected = _selectedIds.contains(item.id); final date = item.addedAt; final dateStr = - '${_QueueTabState._months[date.month - 1]} ${date.day}, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; + '${shortMonthNames[date.month - 1]} ${date.day}, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; final isDownloaded = item.source == LibraryItemSource.downloaded; final quality = item.qualityForMode(_libraryQualityLabelMode); diff --git a/lib/screens/queue_tab_navigation.dart b/lib/screens/queue_tab_navigation.dart index 1c157a09..36ed9745 100644 --- a/lib/screens/queue_tab_navigation.dart +++ b/lib/screens/queue_tab_navigation.dart @@ -25,15 +25,7 @@ extension _QueueTabNavigation on _QueueTabState { coverUrl: coverUrl, ); } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(e)), - ), - ), - ); - } + if (mounted) showCannotOpenFileSnackBar(context, e); } } @@ -63,15 +55,7 @@ extension _QueueTabNavigation on _QueueTabState { externalPath: item.filePath, ); } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(e)), - ), - ), - ); - } + if (mounted) showCannotOpenFileSnackBar(context, e); } } diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 7b4a41a0..f9337e72 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -262,11 +262,11 @@ class AboutPage extends StatelessWidget { ), ); } +} - static Future _launchUrl(String url) async { - final uri = Uri.parse(url); - await launchUrl(uri, mode: LaunchMode.inAppBrowserView); - } +Future _launchUrl(String url) async { + final uri = Uri.parse(url); + await launchUrl(uri, mode: LaunchMode.inAppBrowserView); } class _AppHeaderCard extends StatelessWidget { @@ -386,7 +386,7 @@ class _ContributorItem extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ InkWell( - onTap: () => _launchGitHub(githubUsername), + onTap: () => _launchUrl('https://github.com/$githubUsername'), splashColor: colorScheme.primary.withValues(alpha: 0.12), highlightColor: colorScheme.primary.withValues(alpha: 0.08), child: Padding( @@ -457,11 +457,6 @@ class _ContributorItem extends StatelessWidget { ], ); } - - Future _launchGitHub(String username) async { - final uri = Uri.parse('https://github.com/$username'); - await launchUrl(uri, mode: LaunchMode.inAppBrowserView); - } } class _Translator { @@ -619,7 +614,9 @@ class _TranslatorChip extends StatelessWidget { color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(20), child: InkWell( - onTap: () => _launchCrowdin(translator.crowdinUsername), + onTap: () => _launchUrl( + 'https://crowdin.com/profile/${translator.crowdinUsername}', + ), borderRadius: BorderRadius.circular(20), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -656,11 +653,6 @@ class _TranslatorChip extends StatelessWidget { ), ); } - - Future _launchCrowdin(String username) async { - final uri = Uri.parse('https://crowdin.com/profile/$username'); - await launchUrl(uri, mode: LaunchMode.inAppBrowserView); - } } class _AboutSettingsItem extends StatelessWidget { diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index f002f896..840652ab 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:spotiflac_android/widgets/app_alert_dialog.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:path_provider/path_provider.dart'; @@ -257,15 +258,7 @@ class _SetupScreenState extends ConsumerState { result = await PlatformBridge.pickSafTree(); } catch (e) { _log.w('Failed to open Android SAF picker: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(e)), - ), - ), - ); - } + if (mounted) showCannotOpenFileSnackBar(context, e); } if (result != null) { final treeUri = result['tree_uri'] as String? ?? ''; diff --git a/lib/screens/track_metadata_actions.dart b/lib/screens/track_metadata_actions.dart index f54e5993..40f322e0 100644 --- a/lib/screens/track_metadata_actions.dart +++ b/lib/screens/track_metadata_actions.dart @@ -220,15 +220,7 @@ extension _TrackMetadataFileActions on _TrackMetadataScreenState { explicit: isExplicit, ); } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(e)), - ), - ), - ); - } + if (context.mounted) showCannotOpenFileSnackBar(context, e); } } @@ -284,7 +276,7 @@ extension _TrackMetadataFileActions on _TrackMetadataScreenState { } String _formatFullDate(DateTime date) { - return '${date.day} ${_TrackMetadataScreenState._months[date.month - 1]} ${date.year}, ' + return '${date.day} ${shortMonthNames[date.month - 1]} ${date.year}, ' '${date.hour.toString().padLeft(2, '0')}:' '${date.minute.toString().padLeft(2, '0')}'; } diff --git a/lib/screens/track_metadata_convert.dart b/lib/screens/track_metadata_convert.dart index bb1ca8b0..e1a2a752 100644 --- a/lib/screens/track_metadata_convert.dart +++ b/lib/screens/track_metadata_convert.dart @@ -350,10 +350,10 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { final cueInfo = await PlatformBridge.parseCueSheet(cuePath); if (!mounted) return; - _hideCurrentSnackBar(); + ScaffoldMessenger.of(this.context).hideCurrentSnackBar(); if (cueInfo.containsKey('error')) { - _showSnackBarMessage(_l10nCueSplitNoAudioFile); + _showSnackBarMessage(this.context.l10n.cueSplitNoAudioFile); return; } @@ -365,7 +365,7 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { final tracksRaw = cueInfo['tracks'] as List? ?? []; if (audioPath.isEmpty) { - _showSnackBarMessage(_l10nCueSplitNoAudioFile); + _showSnackBarMessage(this.context.l10n.cueSplitNoAudioFile); return; } @@ -374,7 +374,7 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { .toList(); if (tracks.isEmpty) { - _showSnackBarMessage(_l10nCueSplitFailed); + _showSnackBarMessage(this.context.l10n.cueSplitFailed); return; } @@ -514,8 +514,8 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { ); } catch (e) { if (!mounted) return; - _hideCurrentSnackBar(); - _showSnackBarMessage(_l10nCueSplitFailed); + ScaffoldMessenger.of(this.context).hideCurrentSnackBar(); + _showSnackBarMessage(this.context.l10n.cueSplitFailed); _log.e('Failed to parse CUE sheet: $e'); } } @@ -668,7 +668,9 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { } if (!mounted) return; - _showLongSnackBarMessage(_l10nCueSplitSplitting(1, tracks.length)); + _showLongSnackBarMessage( + context.l10n.cueSplitSplitting(1, tracks.length), + ); String? coverPath; try { @@ -699,8 +701,10 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { coverPath: coverPath, onProgress: (current, total) { if (mounted) { - _hideCurrentSnackBar(); - _showLongSnackBarMessage(_l10nCueSplitSplitting(current, total)); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + _showLongSnackBarMessage( + context.l10n.cueSplitSplitting(current, total), + ); } }, ); @@ -741,18 +745,20 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { } if (mounted) { - _hideCurrentSnackBar(); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); if (finalOutputPaths != null && finalOutputPaths.isNotEmpty) { - _showSnackBarMessage(_l10nCueSplitSuccess(finalOutputPaths.length)); + _showSnackBarMessage( + context.l10n.cueSplitSuccess(finalOutputPaths.length), + ); } else { - _showSnackBarMessage(_l10nCueSplitFailed); + _showSnackBarMessage(context.l10n.cueSplitFailed); } } } catch (e) { _log.e('CUE split failed: $e'); if (mounted) { - _hideCurrentSnackBar(); - _showSnackBarMessage(_l10nCueSplitFailed); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + _showSnackBarMessage(context.l10n.cueSplitFailed); } } finally { if (safTempAudioPath != null) { diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index ea67002e..443f3edf 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -13,6 +13,7 @@ import 'package:spotiflac_android/widgets/app_action_button.dart'; import 'package:spotiflac_android/widgets/app_choice_chip.dart'; import 'package:spotiflac_android/widgets/app_switch.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; @@ -183,21 +184,6 @@ class _TrackMetadataScreenState extends ConsumerState static final RegExp _invalidFileNameChars = RegExp(r'[<>:"/\\|?*\x00-\x1f]'); static final RegExp _multiUnderscore = RegExp(r'_+'); static final RegExp _leadingOrTrailingDots = RegExp(r'^\.+|\.+$'); - static const List _months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; - String get _coverCacheKey => _itemId; @override diff --git a/lib/screens/track_metadata_screen_display.dart b/lib/screens/track_metadata_screen_display.dart index 5e6fd76d..c9987ad4 100644 --- a/lib/screens/track_metadata_screen_display.dart +++ b/lib/screens/track_metadata_screen_display.dart @@ -154,21 +154,6 @@ extension _TrackMetadataDisplay on _TrackMetadataScreenState { ).showSnackBar(SnackBar(content: Text(_cueVirtualTrackGuidance(context)))); } - void _hideCurrentSnackBar() { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - } - - String get _l10nCueSplitFailed => context.l10n.cueSplitFailed; - String get _l10nCueSplitNoAudioFile => context.l10n.cueSplitNoAudioFile; - - String _l10nCueSplitSplitting(int current, int total) { - return context.l10n.cueSplitSplitting(current, total); - } - - String _l10nCueSplitSuccess(int count) { - return context.l10n.cueSplitSuccess(count); - } - void _showSnackBarMessage(String message) { ScaffoldMessenger.of( context, diff --git a/lib/utils/local_playback.dart b/lib/utils/local_playback.dart index 8c3c57ea..ec6cc11d 100644 --- a/lib/utils/local_playback.dart +++ b/lib/utils/local_playback.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; import 'package:spotiflac_android/utils/file_access.dart'; +import 'package:spotiflac_android/widgets/app_snack_bar.dart'; /// Plays [track] from download history or the local library when a matching /// file already exists on disk. @@ -71,15 +71,7 @@ Future playLocalIfAvailable( return true; } } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCannotOpenFile(context.friendlyError(e)), - ), - ), - ); - } + if (context.mounted) showCannotOpenFileSnackBar(context, e); return true; } diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index c4e61def..7242749d 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -70,6 +70,22 @@ String formatBytes(int bytes) { return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB'; } +/// English month abbreviations indexed by `DateTime.month - 1`. +const List shortMonthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; + /// "m:ss" clock formatting for track durations. String formatClock(num seconds) { final total = seconds.round(); diff --git a/lib/widgets/app_snack_bar.dart b/lib/widgets/app_snack_bar.dart index dfdb1f25..aba4eae3 100644 --- a/lib/widgets/app_snack_bar.dart +++ b/lib/widgets/app_snack_bar.dart @@ -1,7 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart'; +/// Reports a failure to open a local file with a plain Material snackbar. +void showCannotOpenFileSnackBar(BuildContext context, Object error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.snackbarCannotOpenFile(context.friendlyError(error)), + ), + ), + ); +} + /// Transient messages use the same glass as confirmation dialogs in Mornye. ScaffoldFeatureController showAppSnackBar( BuildContext context, {