refactor(ui): consolidate file feedback and shared display helpers

This commit is contained in:
zarzet committed 2026-09-30 21:43:39 +07:00
1 parent c5789df87d
commit 377710ec22
17 files changed
+77 -164

No files matched your search

+2 -9
View File
@@ -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<DownloadedAlbumScreen>
startItem: track,
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.snackbarCannotOpenFile(context.friendlyError(e)),
),
),
);
}
if (mounted) showCannotOpenFileSnackBar(context, e);
}
}
@@ -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);
}
}
+2 -9
View File
@@ -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<LocalAlbumScreen>
.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);
}
}
+2 -9
View File
@@ -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<MainShell>
'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) {
+2 -7
View File
@@ -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<NowPlayingScreen> {
await openFile(source);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.snackbarCannotOpenFile(context.friendlyError(e)),
),
),
);
showCannotOpenFileSnackBar(context, e);
}
}
+1 -15
View File
@@ -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<QueueTab> {
final List<String> _filterModes = ['all', 'albums', 'singles', 'playlists'];
bool _isPageControllerInitialized = false;
bool _wasTabVisible = false;
static const List<String> _months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
String _searchQuery = '';
+1 -1
View File
@@ -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);
+2 -18
View File
@@ -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);
}
}
+8 -16
View File
@@ -262,11 +262,11 @@ class AboutPage extends StatelessWidget {
),
);
}
}
static Future<void> _launchUrl(String url) async {
final uri = Uri.parse(url);
await launchUrl(uri, mode: LaunchMode.inAppBrowserView);
}
Future<void> _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<void> _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<void> _launchCrowdin(String username) async {
final uri = Uri.parse('https://crowdin.com/profile/$username');
await launchUrl(uri, mode: LaunchMode.inAppBrowserView);
}
}
class _AboutSettingsItem extends StatelessWidget {
+2 -9
View File
@@ -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<SetupScreen> {
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? ?? '';
+2 -10
View File
@@ -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')}';
}
+20 -14
View File
@@ -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<dynamic>? ?? [];
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) {
+1 -15
View File
@@ -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<TrackMetadataScreen>
static final RegExp _invalidFileNameChars = RegExp(r'[<>:"/\\|?*\x00-\x1f]');
static final RegExp _multiUnderscore = RegExp(r'_+');
static final RegExp _leadingOrTrailingDots = RegExp(r'^\.+|\.+$');
static const List<String> _months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
String get _coverCacheKey => _itemId;
@override
@@ -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,
+2 -10
View File
@@ -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<bool> 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;
}
+16
View File
@@ -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<String> 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();
+12
View File
@@ -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<SnackBar, SnackBarClosedReason> showAppSnackBar(
BuildContext context, {