feat: show 'Internal' version in debug builds, optimize download timeouts, and fix navigation safety

- Add displayVersion getter using kDebugMode: debug shows 'Internal', release shows actual version
- Defer Spotify URL resolution in Deezer downloader until fallback is actually needed
- Unify download timeouts to 24h constant (connection-level timeouts still protect hung connections)
- Fix context shadowing in track metadata options menu and delete dialog
- Use addPostFrameCallback + mounted guards for safer sheet/dialog navigation
This commit is contained in:
zarzet
2026-03-12 04:02:14 +07:00
parent f1eef47600
commit 16669d8b7a
10 changed files with 99 additions and 62 deletions
+11 -5
View File
@@ -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) {
+1 -1
View File
@@ -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() {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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))])
+1 -2
View File
@@ -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",
}
})
+5
View File
@@ -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';
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
),
+75 -48
View File
@@ -2455,21 +2455,21 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
}
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<TrackMetadataScreen> {
.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<TrackMetadataScreen> {
);
}
void _closeOptionsMenuAndRun(
BuildContext sheetContext,
VoidCallback action,
) {
Navigator.pop(sheetContext);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
action();
});
}
Future<void> _openFile(BuildContext context, String filePath) async {
if (isCueVirtualPath(filePath)) {
_showCueVirtualTrackSnackBar(context);
+1 -1
View File
@@ -157,7 +157,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
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),