mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-10 00:23:58 +02:00
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:
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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))])
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user