diff --git a/go_backend/deezer_download.go b/go_backend/deezer_download.go index 01e5c654..bef0e2ff 100644 --- a/go_backend/deezer_download.go +++ b/go_backend/deezer_download.go @@ -394,11 +394,6 @@ func downloadFromDeezer(req DownloadRequest) (DeezerDownloadResult, error) { } } - spotifyURL, err := resolveSpotifyURLForYoinkify(req) - if err != nil { - return DeezerDownloadResult{}, err - } - filename := buildFilenameFromTemplate(req.FilenameFormat, map[string]interface{}{ "title": req.TrackName, "artist": req.ArtistName, @@ -461,6 +456,17 @@ func downloadFromDeezer(req DownloadRequest) (DeezerDownloadResult, error) { } if downloadErr != nil || deezerURLErr != nil { + spotifyURL, err := resolveSpotifyURLForYoinkify(req) + if err != nil { + if deezerURLErr != nil { + return DeezerDownloadResult{}, fmt.Errorf( + "deezer download failed: direct Deezer resolution error: %v; Yoinkify fallback error: %w", + deezerURLErr, + err, + ) + } + return DeezerDownloadResult{}, err + } downloadErr = deezerClient.DownloadFromYoinkify(spotifyURL, outputPath, req.OutputFD, req.ItemID) if downloadErr != nil { if errors.Is(downloadErr, ErrDownloadCancelled) { diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 89cf02a2..9da7c344 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -484,7 +484,7 @@ func (p *ExtensionProviderWrapper) GetDownloadURL(trackID, quality string) (*Ext return &urlResult, nil } -const ExtDownloadTimeout = 5 * time.Minute +const ExtDownloadTimeout = DownloadTimeout func (p *ExtensionProviderWrapper) Download(trackID, quality, outputPath string, onProgress func(percent int)) (*ExtDownloadResult, error) { if !p.extension.Manifest.IsDownloadProvider() { diff --git a/go_backend/httputil.go b/go_backend/httputil.go index b54ec489..08fac2a8 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -31,7 +31,7 @@ func getRandomUserAgent() string { const ( DefaultTimeout = 60 * time.Second - DownloadTimeout = 120 * time.Second + DownloadTimeout = 24 * time.Hour SongLinkTimeout = 30 * time.Second DefaultMaxRetries = 3 DefaultRetryDelay = 1 * time.Second diff --git a/go_backend/tidal.go b/go_backend/tidal.go index ea9a8886..1e540110 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -583,7 +583,7 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, GoLog("[Tidal] Manifest parsed - directURL: %v, initURL: %v, mediaURLs count: %d\n", directURL != "", initURL != "", len(mediaURLs)) - client := NewHTTPClientWithTimeout(120 * time.Second) + client := NewHTTPClientWithTimeout(DownloadTimeout) if directURL != "" { GoLog("[Tidal] BTS format - downloading from direct URL: %s...\n", directURL[:min(80, len(directURL))]) diff --git a/go_backend/youtube.go b/go_backend/youtube.go index e43d0e39..b8038032 100644 --- a/go_backend/youtube.go +++ b/go_backend/youtube.go @@ -11,7 +11,6 @@ import ( "strconv" "strings" "sync" - "time" ) type YouTubeDownloader struct { @@ -82,7 +81,7 @@ type YouTubeDownloadResult struct { func NewYouTubeDownloader() *YouTubeDownloader { youtubeDownloaderOnce.Do(func() { globalYouTubeDownloader = &YouTubeDownloader{ - client: NewHTTPClientWithTimeout(120 * time.Second), + client: NewHTTPClientWithTimeout(DownloadTimeout), apiURL: "https://api.qwkuns.me", } }) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index d423fac6..911e99fe 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -1,9 +1,14 @@ +import 'package:flutter/foundation.dart'; + /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { static const String version = '3.7.2'; static const String buildNumber = '105'; static const String fullVersion = '$version+$buildNumber'; + + /// Shows "Internal" in debug builds, actual version in release. + static String get displayVersion => kDebugMode ? 'Internal' : version; static const String appName = 'SpotiFLAC'; diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 81fd1aa5..1ed5ce43 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -234,7 +234,7 @@ class AboutPage extends StatelessWidget { icon: Icons.info_outline, title: context.l10n.aboutVersion, subtitle: - 'v${AppInfo.version} (build ${AppInfo.buildNumber})', + 'v${AppInfo.displayVersion} (build ${AppInfo.buildNumber})', showDivider: false, ), ], @@ -341,7 +341,7 @@ class _AppHeaderCard extends StatelessWidget { borderRadius: BorderRadius.circular(12), ), child: Text( - 'v${AppInfo.version}', + 'v${AppInfo.displayVersion}', style: Theme.of(context).textTheme.labelMedium?.copyWith( color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index df5bc8d0..140f933b 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -133,7 +133,7 @@ class SettingsTab extends ConsumerWidget { SettingsItem( icon: Icons.info_outline, title: l10n.settingsAbout, - subtitle: '${l10n.aboutVersion} ${AppInfo.version}', + subtitle: '${l10n.aboutVersion} ${AppInfo.displayVersion}', onTap: () => _navigateTo(context, const AboutPage()), showDivider: false, ), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index d8693612..4394592f 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -2455,21 +2455,21 @@ class _TrackMetadataScreenState extends ConsumerState { } void _showOptionsMenu( - BuildContext context, + BuildContext screenContext, WidgetRef ref, ColorScheme colorScheme, ) { showModalBottomSheet( - context: context, + context: screenContext, useRootNavigator: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), isScrollControlled: true, constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, + maxHeight: MediaQuery.of(screenContext).size.height * 0.7, ), - builder: (context) => SafeArea( + builder: (sheetContext) => SafeArea( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -2486,89 +2486,99 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(height: 16), ListTile( leading: const Icon(Icons.copy), - title: Text(context.l10n.trackCopyFilePath), + title: Text(sheetContext.l10n.trackCopyFilePath), onTap: () { - Navigator.pop(context); - _copyToClipboard(context, cleanFilePath); + _closeOptionsMenuAndRun( + sheetContext, + () => _copyToClipboard(screenContext, cleanFilePath), + ); }, ), if (_fileExists) ListTile( leading: const Icon(Icons.edit_outlined), - title: Text(context.l10n.trackEditMetadata), + title: Text(sheetContext.l10n.trackEditMetadata), onTap: () { - Navigator.pop(context); - _showEditMetadataSheet(context, ref, colorScheme); + _closeOptionsMenuAndRun( + sheetContext, + () => + _showEditMetadataSheet(screenContext, ref, colorScheme), + ); }, ), if (!_isLocalItem && (_coverUrl != null || _fileExists)) ListTile( leading: const Icon(Icons.image_outlined), - title: Text(context.l10n.trackSaveCoverArt), - subtitle: Text(context.l10n.trackSaveCoverArtSubtitle), + title: Text(sheetContext.l10n.trackSaveCoverArt), + subtitle: Text(sheetContext.l10n.trackSaveCoverArtSubtitle), onTap: () { - Navigator.pop(context); - _saveCoverArt(); + _closeOptionsMenuAndRun(sheetContext, _saveCoverArt); }, ), if (!_isLocalItem) ListTile( leading: const Icon(Icons.lyrics_outlined), - title: Text(context.l10n.trackSaveLyrics), - subtitle: Text(context.l10n.trackSaveLyricsSubtitle), + title: Text(sheetContext.l10n.trackSaveLyrics), + subtitle: Text(sheetContext.l10n.trackSaveLyricsSubtitle), onTap: () { - Navigator.pop(context); - _saveLyrics(); + _closeOptionsMenuAndRun(sheetContext, _saveLyrics); }, ), if (_fileExists) ListTile( leading: const Icon(Icons.travel_explore), - title: Text(context.l10n.trackReEnrich), - subtitle: Text(context.l10n.trackReEnrichOnlineSubtitle), + title: Text(sheetContext.l10n.trackReEnrich), + subtitle: Text(sheetContext.l10n.trackReEnrichOnlineSubtitle), onTap: () { - Navigator.pop(context); - _reEnrichMetadata(); + _closeOptionsMenuAndRun(sheetContext, _reEnrichMetadata); }, ), if (_fileExists && _isConvertibleFormat) ListTile( leading: const Icon(Icons.swap_horiz), - title: Text(context.l10n.trackConvertFormat), - subtitle: Text(context.l10n.trackConvertFormatSubtitle), + title: Text(sheetContext.l10n.trackConvertFormat), + subtitle: Text(sheetContext.l10n.trackConvertFormatSubtitle), onTap: () { - Navigator.pop(context); - _showConvertSheet(context); + _closeOptionsMenuAndRun( + sheetContext, + () => _showConvertSheet(screenContext), + ); }, ), if (_fileExists && _isCueFile) ListTile( leading: const Icon(Icons.call_split), - title: Text(context.l10n.cueSplitTitle), - subtitle: Text(context.l10n.cueSplitSubtitle), + title: Text(sheetContext.l10n.cueSplitTitle), + subtitle: Text(sheetContext.l10n.cueSplitSubtitle), onTap: () { - Navigator.pop(context); - _showCueSplitSheet(context); + _closeOptionsMenuAndRun( + sheetContext, + () => _showCueSplitSheet(screenContext), + ); }, ), const Divider(height: 1), ListTile( leading: const Icon(Icons.share), - title: Text(context.l10n.trackMetadataShare), + title: Text(sheetContext.l10n.trackMetadataShare), onTap: () { - Navigator.pop(context); - _shareFile(context); + _closeOptionsMenuAndRun( + sheetContext, + () => _shareFile(screenContext), + ); }, ), ListTile( leading: Icon(Icons.delete, color: colorScheme.error), title: Text( - context.l10n.trackRemoveFromDevice, + sheetContext.l10n.trackRemoveFromDevice, style: TextStyle(color: colorScheme.error), ), onTap: () { - Navigator.pop(context); - _confirmDelete(context, ref, colorScheme); + _closeOptionsMenuAndRun( + sheetContext, + () => _confirmDelete(screenContext, ref, colorScheme), + ); }, ), const SizedBox(height: 16), @@ -3650,20 +3660,20 @@ class _TrackMetadataScreenState extends ConsumerState { } void _confirmDelete( - BuildContext context, + BuildContext screenContext, WidgetRef ref, ColorScheme colorScheme, ) { showDialog( - context: context, - useRootNavigator: false, - builder: (context) => AlertDialog( - title: Text(context.l10n.trackDeleteConfirmTitle), - content: Text(context.l10n.trackDeleteConfirmMessage), + context: screenContext, + useRootNavigator: true, + builder: (dialogContext) => AlertDialog( + title: Text(dialogContext.l10n.trackDeleteConfirmTitle), + content: Text(dialogContext.l10n.trackDeleteConfirmMessage), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.dialogCancel), + onPressed: () => Navigator.pop(dialogContext), + child: Text(dialogContext.l10n.dialogCancel), ), TextButton( onPressed: () async { @@ -3691,13 +3701,19 @@ class _TrackMetadataScreenState extends ConsumerState { .removeFromHistory(_downloadItem!.id); } - if (context.mounted) { - Navigator.pop(context); - Navigator.pop(context); + if (dialogContext.mounted) { + Navigator.pop(dialogContext); } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final navigator = Navigator.of(context); + if (navigator.canPop()) { + navigator.pop(true); + } + }); }, child: Text( - context.l10n.dialogDelete, + dialogContext.l10n.dialogDelete, style: TextStyle(color: colorScheme.error), ), ), @@ -3706,6 +3722,17 @@ class _TrackMetadataScreenState extends ConsumerState { ); } + void _closeOptionsMenuAndRun( + BuildContext sheetContext, + VoidCallback action, + ) { + Navigator.pop(sheetContext); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + action(); + }); + } + Future _openFile(BuildContext context, String filePath) async { if (isCueVirtualPath(filePath)) { _showCueVirtualTrackSnackBar(context); diff --git a/lib/widgets/update_dialog.dart b/lib/widgets/update_dialog.dart index f46c4d04..b015a293 100644 --- a/lib/widgets/update_dialog.dart +++ b/lib/widgets/update_dialog.dart @@ -157,7 +157,7 @@ class _UpdateDialogState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _VersionChip(version: AppInfo.version, label: context.l10n.updateCurrent, colorScheme: colorScheme), + _VersionChip(version: AppInfo.displayVersion, label: context.l10n.updateCurrent, colorScheme: colorScheme), const SizedBox(width: 12), Icon(Icons.arrow_forward_rounded, size: 20, color: colorScheme.primary), const SizedBox(width: 12),