mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-18 17:07:19 +02:00
feat: move Amazon Music to extension, fix Deezer download timeout
This commit is contained in:
@@ -17,7 +17,6 @@ final _routerProvider = Provider<GoRouter>((ref) {
|
||||
settingsProvider.select((s) => s.hasCompletedTutorial),
|
||||
);
|
||||
|
||||
// Determine initial location based on app state
|
||||
String initialLocation;
|
||||
if (isFirstLaunch) {
|
||||
initialLocation = '/setup';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/// App version and info constants
|
||||
/// Update version here only - all other files will reference this
|
||||
class AppInfo {
|
||||
static const String version = '3.7.1';
|
||||
static const String buildNumber = '104';
|
||||
static const String version = '3.7.2';
|
||||
static const String buildNumber = '105';
|
||||
static const String fullVersion = '$version+$buildNumber';
|
||||
|
||||
|
||||
|
||||
@@ -763,7 +763,7 @@ abstract class AppLocalizations {
|
||||
/// App description in header card
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'**
|
||||
/// **'Download Spotify tracks in lossless quality from Tidal and Qobuz.'**
|
||||
String get aboutAppDescription;
|
||||
|
||||
/// Section header for artist albums
|
||||
@@ -1576,7 +1576,7 @@ abstract class AppLocalizations {
|
||||
/// **'If a track is not available on the first provider, the app will automatically try the next one.'**
|
||||
String get providerPriorityInfo;
|
||||
|
||||
/// Label for built-in providers (Tidal/Qobuz/Amazon)
|
||||
/// Label for built-in providers (Tidal/Qobuz)
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Built-in'**
|
||||
@@ -3271,7 +3271,7 @@ abstract class AppLocalizations {
|
||||
/// Tutorial welcome tip 2
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'**
|
||||
/// **'Get FLAC quality audio from Tidal, Qobuz, or Deezer'**
|
||||
String get tutorialWelcomeTip2;
|
||||
|
||||
/// Tutorial welcome tip 3
|
||||
|
||||
@@ -365,7 +365,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Lade Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.';
|
||||
'Lade Spotify-Titel in verlustfreier Qualität von Tidal und Qobuz herunter.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Alben';
|
||||
@@ -1826,7 +1826,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'FLAC-Qualität von Tidal, Qobuz oder Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -356,7 +356,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -1809,7 +1809,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -356,7 +356,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -1809,7 +1809,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
@@ -2705,7 +2705,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Descarga pistas de Spotify con calidad sin pérdida de Tidal, Qobuz y Amazon Music.';
|
||||
'Descarga pistas de Spotify con calidad sin pérdida de Tidal y Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Álbumes';
|
||||
@@ -4150,7 +4150,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Obtén audio en calidad FLAC de Tidal, Qobuz o Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -358,7 +358,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -1811,7 +1811,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Audio en qualité FLAC depuis Tidal, Qobuz ou Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -356,7 +356,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -1809,7 +1809,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Tidal, Qobuz, या Deezer से FLAC गुणवत्ता ऑडियो प्राप्त करें';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -359,7 +359,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.';
|
||||
'Unduh lagu Spotify dalam kualitas lossless dari Tidal dan Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Album';
|
||||
@@ -1816,7 +1816,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Dapatkan audio kualitas FLAC dari Tidal, Qobuz, atau Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -352,7 +352,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Tidal、Qobuz、Amazon Music から Spotify のトラックをロスレス品質でダウンロードします。';
|
||||
'Tidal、Qobuz から Spotify のトラックをロスレス品質でダウンロードします。';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'アルバム';
|
||||
@@ -1795,8 +1795,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
'Download music from Spotify, Deezer, or paste any supported URL';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
String get tutorialWelcomeTip2 => 'Tidal、Qobuz、Deezer から FLAC 品質のオーディオを取得';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -355,7 +355,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -1807,8 +1807,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
'Download music from Spotify, Deezer, or paste any supported URL';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
String get tutorialWelcomeTip2 => 'Tidal, Qobuz 또는 Deezer에서 FLAC 품질 오디오 받기';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -356,7 +356,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -1809,7 +1809,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Krijg FLAC-kwaliteit audio van Tidal, Qobuz of Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -356,7 +356,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -1809,7 +1809,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
@@ -2705,7 +2705,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Baixe faixas do Spotify em qualidade sem perdas do Tidal, Qobuz e Amazon Music.';
|
||||
'Baixe faixas do Spotify em qualidade sem perdas do Tidal e Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Álbuns';
|
||||
@@ -4147,7 +4147,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Obtenha áudio em qualidade FLAC do Tidal, Qobuz ou Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -363,7 +363,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Скачайте треки Spotify в Lossless качестве из Tidal, Qobuz и Amazon Music.';
|
||||
'Скачайте треки Spotify в Lossless качестве из Tidal и Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Альбомы';
|
||||
@@ -1855,8 +1855,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Скачивайте музыку из Spotify, Deezer, или вставьте любой поддерживаемый URL';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Скачайте FLAC с Tidal, Qobuz или Amazon Music';
|
||||
String get tutorialWelcomeTip2 => 'Скачайте FLAC с Tidal, Qobuz или Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -361,7 +361,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Spotify şarkılarını Tidal, Qobuz ve Amazon Music\'den yüksek kalitede indir.';
|
||||
'Spotify şarkılarını Tidal ve Qobuz\'den yüksek kalitede indir.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albümler';
|
||||
@@ -1821,7 +1821,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Tidal, Qobuz veya Deezer\'den FLAC kalitesinde ses alın';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -356,7 +356,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -1809,7 +1809,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Deezer';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
@@ -2696,7 +2696,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -4124,8 +4124,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh {
|
||||
'Download music from Spotify, Deezer, or paste any supported URL';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
String get tutorialWelcomeTip2 => '从 Tidal、Qobuz 或 Deezer 获取 FLAC 品质音频';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
@@ -4808,7 +4807,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
|
||||
|
||||
@override
|
||||
String get artistAlbums => 'Albums';
|
||||
@@ -6236,8 +6235,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
'Download music from Spotify, Deezer, or paste any supported URL';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
String get tutorialWelcomeTip2 => '從 Tidal、Qobuz 或 Deezer 取得 FLAC 品質音訊';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Lade Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.",
|
||||
"aboutAppDescription": "Lade Spotify-Titel in verlustfreier Qualität von Tidal und Qobuz herunter.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Integriert",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Erweiterung",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "FLAC-Qualität von Tidal, Qobuz oder Deezer",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1097,7 +1097,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
@@ -2383,7 +2383,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Deezer",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -402,7 +402,7 @@
|
||||
"@aboutDabMusicDesc": {
|
||||
"description": "Credit for DAB Music API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1005,7 +1005,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Descarga pistas de Spotify con calidad sin pérdida de Tidal, Qobuz y Amazon Music.",
|
||||
"aboutAppDescription": "Descarga pistas de Spotify con calidad sin pérdida de Tidal y Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Integrado",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extensión",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Obtén audio en calidad FLAC de Tidal, Qobuz o Deezer",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Audio en qualité FLAC depuis Tidal, Qobuz ou Deezer",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Tidal, Qobuz, या Deezer से FLAC गुणवत्ता ऑडियो प्राप्त करें",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.",
|
||||
"aboutAppDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal dan Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1097,7 +1097,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Bawaan",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Ekstensi",
|
||||
"@providerExtension": {
|
||||
@@ -2383,7 +2383,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Dapatkan audio kualitas FLAC dari Tidal, Qobuz, atau Deezer",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Tidal、Qobuz、Amazon Music から Spotify のトラックをロスレス品質でダウンロードします。",
|
||||
"aboutAppDescription": "Tidal、Qobuz から Spotify のトラックをロスレス品質でダウンロードします。",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "内蔵",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "拡張",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Tidal、Qobuz、Deezer から FLAC 品質のオーディオを取得",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Tidal, Qobuz 또는 Deezer에서 FLAC 품질 오디오 받기",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Krijg FLAC-kwaliteit audio van Tidal, Qobuz of Deezer",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -402,7 +402,7 @@
|
||||
"@aboutDabMusicDesc": {
|
||||
"description": "Credit for DAB Music API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1005,7 +1005,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Baixe faixas do Spotify em qualidade sem perdas do Tidal, Qobuz e Amazon Music.",
|
||||
"aboutAppDescription": "Baixe faixas do Spotify em qualidade sem perdas do Tidal e Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Embutido",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extensão",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Obtenha áudio em qualidade FLAC do Tidal, Qobuz ou Deezer",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Скачайте треки Spotify в Lossless качестве из Tidal, Qobuz и Amazon Music.",
|
||||
"aboutAppDescription": "Скачайте треки Spotify в Lossless качестве из Tidal и Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Встроенные",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Расширение",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Скачайте FLAC с Tidal, Qobuz или Amazon Music",
|
||||
"tutorialWelcomeTip2": "Скачайте FLAC с Tidal, Qobuz или Deezer",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Spotify şarkılarını Tidal, Qobuz ve Amazon Music'den yüksek kalitede indir.",
|
||||
"aboutAppDescription": "Spotify şarkılarını Tidal ve Qobuz'den yüksek kalitede indir.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Dahili",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Eklenti",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "Tidal, Qobuz veya Deezer'den FLAC kalitesinde ses alın",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -402,7 +402,7 @@
|
||||
"@aboutDabMusicDesc": {
|
||||
"description": "Credit for DAB Music API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1005,7 +1005,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "从 Tidal、Qobuz 或 Deezer 获取 FLAC 品质音频",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
"@aboutSpotiSaverDesc": {
|
||||
"description": "Credit for SpotiSaver API"
|
||||
},
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
|
||||
"@aboutAppDescription": {
|
||||
"description": "App description in header card"
|
||||
},
|
||||
@@ -1089,7 +1089,7 @@
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
},
|
||||
"providerExtension": "Extension",
|
||||
"@providerExtension": {
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"@tutorialWelcomeTip1": {
|
||||
"description": "Tutorial welcome tip 1"
|
||||
},
|
||||
"tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
|
||||
"tutorialWelcomeTip2": "從 Tidal、Qobuz 或 Deezer 取得 FLAC 品質音訊",
|
||||
"@tutorialWelcomeTip2": {
|
||||
"description": "Tutorial welcome tip 2"
|
||||
},
|
||||
|
||||
@@ -55,17 +55,14 @@ class AppSettings {
|
||||
final String
|
||||
songLinkRegion; // SongLink userCountry region code used for platform lookup
|
||||
|
||||
// Local Library Settings
|
||||
final bool localLibraryEnabled; // Enable local library scanning
|
||||
final String localLibraryPath; // Path to scan for audio files
|
||||
final bool
|
||||
localLibraryShowDuplicates; // Show indicator when searching for existing tracks
|
||||
|
||||
// Tutorial/Onboarding
|
||||
final bool
|
||||
hasCompletedTutorial; // Track if user has completed the app tutorial
|
||||
|
||||
// Lyrics Provider Settings
|
||||
final List<String>
|
||||
lyricsProviders; // Ordered list of enabled lyrics provider IDs
|
||||
final bool
|
||||
@@ -77,7 +74,6 @@ class AppSettings {
|
||||
final String
|
||||
musixmatchLanguage; // Optional ISO language code for Musixmatch localized lyrics
|
||||
|
||||
// Version upgrade tracking
|
||||
final String
|
||||
lastSeenVersion; // Last app version the user has acknowledged (e.g. '3.7.0')
|
||||
|
||||
@@ -124,13 +120,10 @@ class AppSettings {
|
||||
this.downloadNetworkMode = 'any',
|
||||
this.networkCompatibilityMode = false,
|
||||
this.songLinkRegion = 'US',
|
||||
// Local Library defaults
|
||||
this.localLibraryEnabled = false,
|
||||
this.localLibraryPath = '',
|
||||
this.localLibraryShowDuplicates = true,
|
||||
// Tutorial default
|
||||
this.hasCompletedTutorial = false,
|
||||
// Lyrics providers default order
|
||||
this.lyricsProviders = const [
|
||||
'lrclib',
|
||||
'spotify_api',
|
||||
@@ -143,7 +136,6 @@ class AppSettings {
|
||||
this.lyricsIncludeRomanizationNetease = false,
|
||||
this.lyricsMultiPersonWordByWord = false,
|
||||
this.musixmatchLanguage = '',
|
||||
// Version upgrade tracking
|
||||
this.lastSeenVersion = '',
|
||||
});
|
||||
|
||||
@@ -191,19 +183,15 @@ class AppSettings {
|
||||
String? downloadNetworkMode,
|
||||
bool? networkCompatibilityMode,
|
||||
String? songLinkRegion,
|
||||
// Local Library
|
||||
bool? localLibraryEnabled,
|
||||
String? localLibraryPath,
|
||||
bool? localLibraryShowDuplicates,
|
||||
// Tutorial
|
||||
bool? hasCompletedTutorial,
|
||||
// Lyrics providers
|
||||
List<String>? lyricsProviders,
|
||||
bool? lyricsIncludeTranslationNetease,
|
||||
bool? lyricsIncludeRomanizationNetease,
|
||||
bool? lyricsMultiPersonWordByWord,
|
||||
String? musixmatchLanguage,
|
||||
// Version upgrade tracking
|
||||
String? lastSeenVersion,
|
||||
}) {
|
||||
return AppSettings(
|
||||
@@ -259,14 +247,11 @@ class AppSettings {
|
||||
networkCompatibilityMode:
|
||||
networkCompatibilityMode ?? this.networkCompatibilityMode,
|
||||
songLinkRegion: songLinkRegion ?? this.songLinkRegion,
|
||||
// Local Library
|
||||
localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled,
|
||||
localLibraryPath: localLibraryPath ?? this.localLibraryPath,
|
||||
localLibraryShowDuplicates:
|
||||
localLibraryShowDuplicates ?? this.localLibraryShowDuplicates,
|
||||
// Tutorial
|
||||
hasCompletedTutorial: hasCompletedTutorial ?? this.hasCompletedTutorial,
|
||||
// Lyrics providers
|
||||
lyricsProviders: lyricsProviders ?? this.lyricsProviders,
|
||||
lyricsIncludeTranslationNetease:
|
||||
lyricsIncludeTranslationNetease ??
|
||||
@@ -277,7 +262,6 @@ class AppSettings {
|
||||
lyricsMultiPersonWordByWord:
|
||||
lyricsMultiPersonWordByWord ?? this.lyricsMultiPersonWordByWord,
|
||||
musixmatchLanguage: musixmatchLanguage ?? this.musixmatchLanguage,
|
||||
// Version upgrade tracking
|
||||
lastSeenVersion: lastSeenVersion ?? this.lastSeenVersion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -592,10 +592,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
final deletedCount = await _db.deleteByIds(orphanedIds);
|
||||
|
||||
// Update in-memory state
|
||||
final orphanedSet = orphanedIds.toSet();
|
||||
state = state.copyWith(
|
||||
items: state.items
|
||||
@@ -1596,18 +1594,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
String _determineOutputExt(String quality, String service) {
|
||||
// YouTube provider - lossy only (Opus or MP3)
|
||||
if (service.toLowerCase() == 'youtube') {
|
||||
if (quality.toLowerCase().contains('mp3')) {
|
||||
return '.mp3';
|
||||
}
|
||||
return '.opus';
|
||||
}
|
||||
// Amazon stream is delivered as MP4/M4A container (may contain FLAC audio),
|
||||
// so SAF should keep .m4a before decrypt/convert pipeline.
|
||||
if (service.toLowerCase() == 'amazon') {
|
||||
return '.m4a';
|
||||
}
|
||||
if (service.toLowerCase() == 'tidal' && quality == 'HIGH') {
|
||||
return '.m4a';
|
||||
}
|
||||
@@ -2903,7 +2895,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
failedCount: _failedInSession,
|
||||
);
|
||||
|
||||
// Auto-export failed downloads if enabled
|
||||
final settings = ref.read(settingsProvider);
|
||||
if (settings.autoExportFailedDownloads && _failedInSession > 0) {
|
||||
final exportPath = await exportFailedDownloads();
|
||||
@@ -3206,7 +3197,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
!trackToDownload.id.startsWith('deezer:') &&
|
||||
!trackToDownload.id.startsWith('extension:')) {
|
||||
try {
|
||||
// Extract clean Spotify ID (remove spotify: prefix if present)
|
||||
String spotifyId = trackToDownload.id;
|
||||
if (spotifyId.startsWith('spotify:track:')) {
|
||||
spotifyId = spotifyId.split(':').last;
|
||||
@@ -3508,9 +3498,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
|
||||
if (!wasExisting &&
|
||||
decryptionKey.isNotEmpty &&
|
||||
filePath != null &&
|
||||
actualService == 'amazon') {
|
||||
_log.i('Amazon encrypted stream detected, decrypting via FFmpeg...');
|
||||
filePath != null) {
|
||||
_log.i('Encrypted stream detected, decrypting via FFmpeg...');
|
||||
updateItemStatus(item.id, DownloadStatus.downloading, progress: 0.9);
|
||||
|
||||
if (effectiveSafMode && isContentUri(filePath)) {
|
||||
@@ -3539,7 +3528,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
DownloadStatus.failed,
|
||||
error: 'Failed to decrypt Amazon stream',
|
||||
error: 'Failed to decrypt encrypted stream',
|
||||
errorType: DownloadErrorType.unknown,
|
||||
);
|
||||
return;
|
||||
@@ -3564,7 +3553,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
);
|
||||
|
||||
if (newUri == null) {
|
||||
_log.e('Failed to write decrypted Amazon stream back to SAF');
|
||||
_log.e('Failed to write decrypted stream back to SAF');
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
DownloadStatus.failed,
|
||||
@@ -3579,7 +3568,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
filePath = newUri;
|
||||
finalSafFileName = newFileName;
|
||||
_log.i('Amazon SAF decryption completed');
|
||||
_log.i('SAF decryption completed');
|
||||
} finally {
|
||||
try {
|
||||
await File(tempPath).delete();
|
||||
@@ -3601,7 +3590,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
DownloadStatus.failed,
|
||||
error: 'Failed to decrypt Amazon stream',
|
||||
error: 'Failed to decrypt encrypted stream',
|
||||
errorType: DownloadErrorType.unknown,
|
||||
);
|
||||
try {
|
||||
@@ -3610,7 +3599,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return;
|
||||
}
|
||||
filePath = decryptedPath;
|
||||
_log.i('Amazon local decryption completed');
|
||||
_log.i('Local decryption completed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3832,7 +3821,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local file path flow (original)
|
||||
if (quality == 'HIGH') {
|
||||
final tidalHighFormat = settings.tidalHighFormat;
|
||||
_log.i(
|
||||
@@ -4049,10 +4037,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
!effectiveSafMode &&
|
||||
isFlacFile &&
|
||||
!wasExisting &&
|
||||
actualService == 'amazon' &&
|
||||
decryptionKey.isNotEmpty) {
|
||||
_log.d(
|
||||
'Local FLAC after Amazon decrypt detected, embedding metadata and cover...',
|
||||
'Local FLAC after decrypt detected, embedding metadata and cover...',
|
||||
);
|
||||
try {
|
||||
updateItemStatus(
|
||||
@@ -4112,7 +4099,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
|
||||
final isContentUriPath = isContentUri(filePath);
|
||||
if (isContentUriPath && effectiveSafMode) {
|
||||
// SAF mode: copy to temp, embed, write back
|
||||
final tempPath = await _copySafToTemp(filePath);
|
||||
if (tempPath != null) {
|
||||
try {
|
||||
@@ -4133,7 +4119,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
copyright: backendCopyright,
|
||||
);
|
||||
}
|
||||
// Write back to SAF
|
||||
final ext = isMp3File ? '.mp3' : '.opus';
|
||||
final newFileName = '${safBaseName ?? 'track'}$ext';
|
||||
final newUri = await _writeTempToSaf(
|
||||
@@ -4162,7 +4147,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-SAF mode: embed directly
|
||||
try {
|
||||
if (isMp3File) {
|
||||
await _embedMetadataToMp3(
|
||||
|
||||
@@ -757,7 +757,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
Future<void> loadProviderPriority() async {
|
||||
try {
|
||||
// Load from SharedPreferences first (persisted)
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedJson = prefs.getString(_providerPriorityKey);
|
||||
|
||||
@@ -768,10 +767,8 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
priority = _sanitizeDownloadProviderPriority(priority);
|
||||
_log.d('Loaded provider priority from prefs: $priority');
|
||||
await prefs.setString(_providerPriorityKey, jsonEncode(priority));
|
||||
// Sync to Go backend
|
||||
await PlatformBridge.setProviderPriority(priority);
|
||||
} else {
|
||||
// Fallback to Go backend default
|
||||
priority = await PlatformBridge.getProviderPriority();
|
||||
priority = _sanitizeDownloadProviderPriority(priority);
|
||||
await PlatformBridge.setProviderPriority(priority);
|
||||
@@ -787,11 +784,9 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
Future<void> setProviderPriority(List<String> priority) async {
|
||||
try {
|
||||
final sanitized = _sanitizeDownloadProviderPriority(priority);
|
||||
// Save to SharedPreferences for persistence
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_providerPriorityKey, jsonEncode(sanitized));
|
||||
|
||||
// Sync to Go backend
|
||||
await PlatformBridge.setProviderPriority(sanitized);
|
||||
state = state.copyWith(providerPriority: sanitized);
|
||||
_log.d('Saved provider priority: $sanitized');
|
||||
@@ -811,7 +806,7 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
for (final provider in const ['tidal', 'qobuz', 'amazon', 'deezer']) {
|
||||
for (final provider in const ['tidal', 'qobuz', 'deezer']) {
|
||||
if (!result.contains(provider)) {
|
||||
result.add(provider);
|
||||
}
|
||||
@@ -822,7 +817,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
Future<void> loadMetadataProviderPriority() async {
|
||||
try {
|
||||
// Load from SharedPreferences first (persisted)
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedJson = prefs.getString(_metadataProviderPriorityKey);
|
||||
|
||||
@@ -831,10 +825,8 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
final saved = jsonDecode(savedJson) as List<dynamic>;
|
||||
priority = saved.map((e) => e as String).toList();
|
||||
_log.d('Loaded metadata provider priority from prefs: $priority');
|
||||
// Sync to Go backend
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
} else {
|
||||
// Fallback to Go backend default
|
||||
priority = await PlatformBridge.getMetadataProviderPriority();
|
||||
_log.d('Using default metadata provider priority: $priority');
|
||||
}
|
||||
@@ -847,11 +839,9 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
Future<void> setMetadataProviderPriority(List<String> priority) async {
|
||||
try {
|
||||
// Save to SharedPreferences for persistence
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_metadataProviderPriorityKey, jsonEncode(priority));
|
||||
|
||||
// Sync to Go backend
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
state = state.copyWith(metadataProviderPriority: priority);
|
||||
_log.d('Saved metadata provider priority: $priority');
|
||||
@@ -880,7 +870,7 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
|
||||
List<String> getAllDownloadProviders() {
|
||||
final providers = ['tidal', 'qobuz', 'amazon', 'deezer'];
|
||||
final providers = ['tidal', 'qobuz', 'deezer'];
|
||||
for (final ext in state.extensions) {
|
||||
if (ext.enabled && ext.hasDownloadProvider) {
|
||||
providers.add(ext.id);
|
||||
|
||||
@@ -204,7 +204,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
state = TrackState(isLoading: true, hasSearchText: state.hasSearchText);
|
||||
|
||||
try {
|
||||
// Step 1: Check for extension URL handlers first (handles YT Music, etc.)
|
||||
final extensionHandler = await PlatformBridge.findURLHandler(url);
|
||||
if (extensionHandler != null) {
|
||||
_log.i('Found extension URL handler: $extensionHandler for URL: $url');
|
||||
@@ -215,7 +214,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
result = await PlatformBridge.handleURLWithExtension(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
// Check if we got valid data
|
||||
if (result != null &&
|
||||
result['type'] == 'track' &&
|
||||
result['track'] != null) {
|
||||
@@ -321,7 +319,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Try Deezer URL parsing
|
||||
if (url.contains('deezer.com') || url.contains('deezer.page.link')) {
|
||||
_log.i('Detected Deezer URL, parsing...');
|
||||
final parsed = await PlatformBridge.parseDeezerUrl(url);
|
||||
@@ -387,7 +384,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Try Tidal URL parsing
|
||||
if (url.contains('tidal.com')) {
|
||||
_log.i('Detected Tidal URL, parsing...');
|
||||
final parsed = await PlatformBridge.parseTidalUrl(url);
|
||||
@@ -461,7 +457,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 4: Fall back to Spotify parsing
|
||||
final parsed = await PlatformBridge.parseSpotifyUrl(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
|
||||
@@ -305,7 +305,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Full-screen cover background (no blur, full resolution)
|
||||
if (widget.coverUrl != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl:
|
||||
@@ -326,7 +325,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// Bottom gradient for readability
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -345,7 +343,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Album info overlay at bottom
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
|
||||
@@ -21,7 +21,6 @@ import 'package:spotiflac_android/widgets/download_service_picker.dart';
|
||||
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
|
||||
/// Simple in-memory cache for artist data
|
||||
class _ArtistCache {
|
||||
static final Map<String, _CacheEntry> _cache = {};
|
||||
static const Duration _ttl = Duration(minutes: 10);
|
||||
@@ -69,7 +68,6 @@ class _CacheEntry {
|
||||
});
|
||||
}
|
||||
|
||||
/// Artist screen with Spotify-like design
|
||||
class ArtistScreen extends ConsumerStatefulWidget {
|
||||
final String artistId;
|
||||
final String artistName;
|
||||
@@ -717,7 +715,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
// Options
|
||||
if (albums.isNotEmpty)
|
||||
_DiscographyOptionTile(
|
||||
icon: Icons.library_music,
|
||||
@@ -830,7 +827,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
int failedCount = 0;
|
||||
|
||||
for (final album in albums) {
|
||||
if (!_isFetchingDiscography) break; // Cancelled
|
||||
if (!_isFetchingDiscography) break;
|
||||
|
||||
try {
|
||||
final tracks = await _fetchAlbumTracks(album);
|
||||
@@ -1066,7 +1063,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter, // Show top of image (faces)
|
||||
alignment: Alignment.topCenter,
|
||||
memCacheWidth: 800,
|
||||
cacheManager: CoverCacheManager.instance,
|
||||
placeholder: (context, url) =>
|
||||
@@ -1155,7 +1152,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Download Discography button (icon only, right-aligned)
|
||||
if (hasDiscography && !_isSelectionMode) ...[
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
@@ -1201,7 +1197,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build Popular tracks section like Spotify
|
||||
Widget _buildPopularSection(ColorScheme colorScheme) {
|
||||
if (_topTracks == null || _topTracks!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -1416,7 +1411,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle tap on popular track item
|
||||
void _handlePopularTrackTap(Track track, {required bool isQueued}) async {
|
||||
if (isQueued) return;
|
||||
|
||||
@@ -1636,7 +1630,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Selection overlay
|
||||
if (_isSelectionMode)
|
||||
Positioned.fill(
|
||||
child: AnimatedContainer(
|
||||
@@ -1652,7 +1645,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Checkbox
|
||||
if (_isSelectionMode)
|
||||
Positioned(
|
||||
top: 8,
|
||||
|
||||
@@ -18,7 +18,6 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
|
||||
import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart';
|
||||
|
||||
/// Screen to display downloaded tracks from a specific album
|
||||
class DownloadedAlbumScreen extends ConsumerStatefulWidget {
|
||||
final String albumName;
|
||||
final String artistName;
|
||||
@@ -361,7 +360,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
);
|
||||
final tracks = _getAlbumTracks(allHistoryItems);
|
||||
|
||||
// Show empty state if no tracks found
|
||||
if (tracks.isEmpty) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(widget.albumName)),
|
||||
@@ -480,7 +478,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Full-screen cover background
|
||||
if (embeddedCoverPath != null)
|
||||
Image.file(
|
||||
File(embeddedCoverPath),
|
||||
@@ -508,7 +505,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// Bottom gradient for readability
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -527,7 +523,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Album info overlay at bottom
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
@@ -711,10 +706,8 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
final discTracks = discMap[discNumber];
|
||||
if (discTracks == null || discTracks.isEmpty) continue;
|
||||
|
||||
// Add disc separator
|
||||
children.add(_buildDiscSeparator(context, colorScheme, discNumber));
|
||||
|
||||
// Add tracks for this disc
|
||||
for (final track in discTracks) {
|
||||
children.add(
|
||||
KeyedSubtree(
|
||||
@@ -897,7 +890,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Share SAF content URIs via native intent
|
||||
if (safUris.isNotEmpty) {
|
||||
try {
|
||||
if (safUris.length == 1) {
|
||||
@@ -908,13 +900,11 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Share regular files via SharePlus
|
||||
if (filesToShare.isNotEmpty) {
|
||||
await SharePlus.instance.share(ShareParams(files: filesToShare));
|
||||
}
|
||||
}
|
||||
|
||||
/// Show batch convert bottom sheet
|
||||
void _showBatchConvertSheet(
|
||||
BuildContext context,
|
||||
List<DownloadHistoryItem> allTracks,
|
||||
@@ -1388,7 +1378,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Action buttons row: Share, Convert
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
||||
@@ -520,7 +520,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extState = ref.read(extensionProvider);
|
||||
final searchProvider = settings.searchProvider;
|
||||
// Use filterOverride if provided, otherwise read from state
|
||||
final selectedFilter =
|
||||
filterOverride ?? ref.read(trackProvider).selectedSearchFilter;
|
||||
|
||||
@@ -535,7 +534,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
extState.extensions.any((e) => e.id == searchProvider && e.enabled);
|
||||
|
||||
if (isExtensionEnabled) {
|
||||
// Build options with filter if selected
|
||||
Map<String, dynamic>? options;
|
||||
if (selectedFilter != null) {
|
||||
options = {'filter': selectedFilter};
|
||||
@@ -1116,7 +1114,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
),
|
||||
|
||||
// Search filter bar (only shown when has search results)
|
||||
if (hasActualResults && !showRecentAccess)
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
@@ -1286,8 +1283,8 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
],
|
||||
),
|
||||
), // Close RefreshIndicator
|
||||
), // Close GestureDetector
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1434,7 +1431,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
return _buildExploreSection(sections[sectionIndex], colorScheme);
|
||||
}
|
||||
|
||||
// Bottom padding
|
||||
return const SizedBox(height: 16);
|
||||
}, childCount: totalCount),
|
||||
),
|
||||
@@ -2705,7 +2701,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
// "All" chip (no filter)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
@@ -2728,7 +2723,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
),
|
||||
),
|
||||
// Filter chips from extension
|
||||
...filters.map((filter) {
|
||||
final isSelected = selectedFilter == filter.id;
|
||||
return Padding(
|
||||
@@ -2830,7 +2824,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
prefixIcon: _SearchProviderDropdown(
|
||||
onProviderChanged: () {
|
||||
_lastSearchQuery = null;
|
||||
// Reset filter when provider changes
|
||||
ref.read(trackProvider.notifier).setSearchFilter(null);
|
||||
setState(() {});
|
||||
final text = _urlController.text.trim();
|
||||
|
||||
@@ -158,7 +158,6 @@ class LibraryPlaylistsScreen extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header: drag handle + thumbnail + playlist info
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
@@ -210,7 +209,6 @@ class LibraryPlaylistsScreen extends ConsumerWidget {
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
|
||||
// Rename
|
||||
_PlaylistOptionTile(
|
||||
icon: Icons.edit_outlined,
|
||||
title: context.l10n.collectionRenamePlaylist,
|
||||
@@ -225,7 +223,6 @@ class LibraryPlaylistsScreen extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
|
||||
// Change cover
|
||||
_PlaylistOptionTile(
|
||||
icon: Icons.image_outlined,
|
||||
title: context.l10n.collectionPlaylistChangeCover,
|
||||
@@ -235,7 +232,6 @@ class LibraryPlaylistsScreen extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
|
||||
// Delete
|
||||
_PlaylistOptionTile(
|
||||
icon: Icons.delete_outline,
|
||||
iconColor: colorScheme.error,
|
||||
|
||||
@@ -37,7 +37,6 @@ class _LibraryTracksFolderScreenState
|
||||
bool _showTitleInAppBar = false;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
// ── Multi-select state ──
|
||||
bool _isSelectionMode = false;
|
||||
final Set<String> _selectedKeys = {};
|
||||
|
||||
@@ -145,8 +144,6 @@ class _LibraryTracksFolderScreenState
|
||||
return url;
|
||||
}
|
||||
|
||||
// ── Selection helpers ──
|
||||
|
||||
void _enterSelectionMode(String key) {
|
||||
HapticFeedback.mediumImpact();
|
||||
setState(() {
|
||||
@@ -181,8 +178,6 @@ class _LibraryTracksFolderScreenState
|
||||
});
|
||||
}
|
||||
|
||||
// ── Batch actions ──
|
||||
|
||||
Future<void> _removeSelected(List<CollectionTrackEntry> entries) async {
|
||||
final keysToRemove = _selectedKeys.toSet();
|
||||
if (keysToRemove.isEmpty) return;
|
||||
@@ -426,7 +421,6 @@ class _LibraryTracksFolderScreenState
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Drag handle
|
||||
Container(
|
||||
width: 32,
|
||||
height: 4,
|
||||
@@ -437,7 +431,6 @@ class _LibraryTracksFolderScreenState
|
||||
),
|
||||
),
|
||||
|
||||
// Header: [X close] [count] [Select All / Deselect]
|
||||
Row(
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
@@ -493,7 +486,6 @@ class _LibraryTracksFolderScreenState
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Action buttons row
|
||||
Row(
|
||||
children: [
|
||||
if (isWishlist)
|
||||
@@ -525,7 +517,6 @@ class _LibraryTracksFolderScreenState
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Remove button (full width, red)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
@@ -714,7 +705,6 @@ class _LibraryTracksFolderScreenState
|
||||
)
|
||||
else
|
||||
coverFallback,
|
||||
// Bottom gradient for readability
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -733,7 +723,6 @@ class _LibraryTracksFolderScreenState
|
||||
),
|
||||
),
|
||||
),
|
||||
// Title and track count overlay
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
@@ -829,8 +818,6 @@ class _LibraryTracksFolderScreenState
|
||||
);
|
||||
}
|
||||
|
||||
// ── Header actions ──
|
||||
|
||||
Widget _buildHeaderActionPlaceholder() => const SizedBox(width: 48, height: 48);
|
||||
|
||||
Widget _buildDownloadAllCenterButton(List<CollectionTrackEntry> entries) {
|
||||
@@ -1263,7 +1250,6 @@ class _CollectionTrackTile extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header: drag handle + cover + track info
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/providers/local_library_provider.dart';
|
||||
import 'package:spotiflac_android/providers/playback_provider.dart';
|
||||
|
||||
/// Screen to display tracks from a local library album
|
||||
class LocalAlbumScreen extends ConsumerStatefulWidget {
|
||||
final String albumName;
|
||||
final String artistName;
|
||||
@@ -83,7 +82,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
List<LocalLibraryItem> _buildSortedTracks() {
|
||||
final tracks = List<LocalLibraryItem>.from(widget.tracks);
|
||||
tracks.sort((a, b) {
|
||||
// Sort by disc number first, then by track number
|
||||
final aDisc = a.discNumber ?? 1;
|
||||
final bDisc = b.discNumber ?? 1;
|
||||
if (aDisc != bDisc) return aDisc.compareTo(bDisc);
|
||||
@@ -197,7 +195,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
),
|
||||
);
|
||||
|
||||
// Go back if all tracks were deleted
|
||||
if (deletedCount == currentTracks.length) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
@@ -233,7 +230,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
final tracks = _sortedTracksCache;
|
||||
|
||||
// Show empty state if no tracks found
|
||||
if (tracks.isEmpty) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(widget.albumName)),
|
||||
@@ -326,7 +322,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Full-screen cover background
|
||||
if (widget.coverPath != null)
|
||||
Image.file(
|
||||
File(widget.coverPath!),
|
||||
@@ -343,7 +338,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// Bottom gradient for readability
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -362,7 +356,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Album info overlay at bottom
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
@@ -888,7 +881,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Batch re-enrich selected local tracks
|
||||
Future<void> _reEnrichSelected(List<LocalLibraryItem> allTracks) async {
|
||||
final tracksById = {for (final t in allTracks) t.id: t};
|
||||
final selected = <LocalLibraryItem>[];
|
||||
@@ -988,7 +980,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
).showSnackBar(SnackBar(content: Text(summary)));
|
||||
}
|
||||
|
||||
/// Show batch convert bottom sheet
|
||||
void _showBatchConvertSheet(
|
||||
BuildContext context,
|
||||
List<LocalLibraryItem> allTracks,
|
||||
@@ -1261,7 +1252,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
String? safTempPath;
|
||||
|
||||
if (isSaf) {
|
||||
// Copy SAF file to temp for conversion
|
||||
safTempPath = await PlatformBridge.copyContentUriToTemp(
|
||||
item.filePath,
|
||||
);
|
||||
@@ -1296,7 +1286,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
if (isSaf) {
|
||||
// For SAF: derive the parent tree URI and relative dir from the content URI,
|
||||
// then create new SAF file and delete old one
|
||||
//
|
||||
// Parse the SAF URI to get the tree document path:
|
||||
// content://...tree/...document/.../oldName.flac
|
||||
// We need tree URI and relative dir to create the new file
|
||||
@@ -1375,14 +1364,12 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete old SAF file
|
||||
try {
|
||||
await PlatformBridge.safDelete(item.filePath);
|
||||
} catch (_) {}
|
||||
await localDb.deleteByPath(item.filePath);
|
||||
}
|
||||
|
||||
// Clean up temp files
|
||||
try {
|
||||
await File(newPath).delete();
|
||||
} catch (_) {}
|
||||
@@ -1400,7 +1387,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Reload local library to pick up converted files
|
||||
ref.read(localLibraryProvider.notifier).reloadFromStorage();
|
||||
_exitSelectionMode();
|
||||
|
||||
@@ -1513,7 +1499,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Action buttons row: Re-enrich, Convert
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
||||
@@ -206,7 +206,6 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Full-screen cover background
|
||||
if (widget.coverUrl != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl:
|
||||
@@ -227,7 +226,6 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// Bottom gradient for readability
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -246,7 +244,6 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Playlist info overlay at bottom
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
@@ -436,8 +433,6 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shuffle / Love / Download buttons ──
|
||||
|
||||
Widget _buildCircleButton({
|
||||
required IconData icon,
|
||||
required String tooltip,
|
||||
|
||||
@@ -211,7 +211,7 @@ class _GroupedAlbum {
|
||||
class _GroupedLocalAlbum {
|
||||
final String albumName;
|
||||
final String artistName;
|
||||
final String? coverPath; // Local cover file path
|
||||
final String? coverPath;
|
||||
final List<LocalLibraryItem> tracks;
|
||||
final DateTime latestScanned;
|
||||
final String searchKey;
|
||||
@@ -229,12 +229,11 @@ class _GroupedLocalAlbum {
|
||||
|
||||
class _HistoryStats {
|
||||
final Map<String, int> albumCounts;
|
||||
final Map<String, int> localAlbumCounts; // For identifying local singles
|
||||
final Map<String, int> localAlbumCounts;
|
||||
final List<_GroupedAlbum> groupedAlbums;
|
||||
final List<_GroupedLocalAlbum> groupedLocalAlbums; // Local library albums
|
||||
final List<_GroupedLocalAlbum> groupedLocalAlbums;
|
||||
final int albumCount;
|
||||
final int singleTracks;
|
||||
// Local library stats
|
||||
final int localAlbumCount;
|
||||
final int localSingleTracks;
|
||||
|
||||
@@ -933,8 +932,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
overlay.insert(_playlistSelectionOverlayEntry!);
|
||||
}
|
||||
|
||||
// --- Playlist selection mode ---
|
||||
|
||||
void _enterPlaylistSelectionMode(String playlistId) {
|
||||
HapticFeedback.mediumImpact();
|
||||
setState(() {
|
||||
@@ -1202,11 +1199,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
await deleteFile(cleanPath);
|
||||
} catch (_) {}
|
||||
|
||||
// Remove from appropriate database
|
||||
if (item.source == LibraryItemSource.downloaded) {
|
||||
historyNotifier.removeFromHistory(item.historyItem!.id);
|
||||
} else {
|
||||
// Remove from local library database
|
||||
await localLibraryDb.deleteByPath(item.filePath);
|
||||
}
|
||||
deletedCount++;
|
||||
@@ -2024,7 +2019,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
Map<String, int> albumCounts, [
|
||||
String searchQuery = '',
|
||||
]) {
|
||||
// First apply search filter
|
||||
var filteredItems = items;
|
||||
if (searchQuery.isNotEmpty) {
|
||||
final query = searchQuery;
|
||||
@@ -2034,7 +2028,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Then apply filter mode
|
||||
if (filterMode == 'all') return filteredItems;
|
||||
|
||||
switch (filterMode) {
|
||||
@@ -2639,7 +2632,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
),
|
||||
|
||||
// Search bar - always at top
|
||||
if (allHistoryItems.isNotEmpty ||
|
||||
hasQueueItems ||
|
||||
localLibraryItems.isNotEmpty)
|
||||
|
||||
@@ -49,7 +49,7 @@ class AboutPage extends StatelessWidget {
|
||||
title: Text(
|
||||
context.l10n.aboutTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio), // 20 -> 28
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
@@ -462,7 +462,6 @@ class _ContributorItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Translator data model
|
||||
class _Translator {
|
||||
final String name;
|
||||
final String crowdinUsername;
|
||||
@@ -477,7 +476,6 @@ class _Translator {
|
||||
});
|
||||
}
|
||||
|
||||
/// Translators section with compact chip-style layout
|
||||
class _TranslatorsSection extends StatelessWidget {
|
||||
const _TranslatorsSection();
|
||||
|
||||
@@ -558,7 +556,6 @@ class _TranslatorsSection extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Individual translator chip
|
||||
class _TranslatorChip extends StatelessWidget {
|
||||
final _Translator translator;
|
||||
|
||||
|
||||
@@ -148,7 +148,6 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// A simplified preview of how the app looks with current settings
|
||||
class _ThemePreviewCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -423,7 +422,6 @@ class _ColorPaletteItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimized app bar title with animation
|
||||
class _AppBarTitle extends StatelessWidget {
|
||||
final String title;
|
||||
final double topPadding;
|
||||
@@ -440,14 +438,14 @@ class _AppBarTitle extends StatelessWidget {
|
||||
final expandRatio =
|
||||
((constraints.maxHeight - minHeight) / (maxHeight - minHeight))
|
||||
.clamp(0.0, 1.0);
|
||||
final leftPadding = 56 - (32 * expandRatio); // 56 -> 24
|
||||
final leftPadding = 56 - (32 * expandRatio);
|
||||
return FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio), // 20 -> 28
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
|
||||
@@ -56,17 +56,14 @@ class DonatePage extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Donate links card
|
||||
_DonateLinksCard(colorScheme: colorScheme),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Recent donors section
|
||||
_RecentDonorsCard(colorScheme: colorScheme),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Combined notice card
|
||||
Card(
|
||||
elevation: 0,
|
||||
color: colorScheme.secondaryContainer.withValues(
|
||||
|
||||
@@ -23,7 +23,7 @@ class DownloadSettingsPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
static const _builtInServices = ['tidal', 'qobuz', 'amazon', 'deezer'];
|
||||
static const _builtInServices = ['tidal', 'qobuz', 'deezer'];
|
||||
static const _songLinkRegions = [
|
||||
'AD',
|
||||
'AE',
|
||||
@@ -326,7 +326,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
((constraints.maxHeight - minHeight) /
|
||||
(maxHeight - minHeight))
|
||||
.clamp(0.0, 1.0);
|
||||
final leftPadding = 56 - (32 * expandRatio); // 56 -> 24
|
||||
final leftPadding = 56 - (32 * expandRatio);
|
||||
return FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(
|
||||
@@ -336,7 +336,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
title: Text(
|
||||
context.l10n.downloadTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio), // 20 -> 28
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
@@ -450,7 +450,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Select Tidal, Qobuz, or Amazon above to configure quality',
|
||||
'Select Tidal or Qobuz above to configure quality',
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -720,7 +720,6 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Download Network Mode
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionDownload),
|
||||
),
|
||||
@@ -778,7 +777,6 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// All Files Access section (Android 13+ only)
|
||||
if (Platform.isAndroid && _androidSdkVersion >= 33) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(
|
||||
@@ -2039,7 +2037,7 @@ class _ServiceSelector extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final extState = ref.watch(extensionProvider);
|
||||
final builtInServiceIds = ['tidal', 'qobuz', 'amazon', 'deezer', 'youtube'];
|
||||
final builtInServiceIds = ['tidal', 'qobuz', 'deezer', 'youtube'];
|
||||
|
||||
final extensionProviders = extState.extensions
|
||||
.where((e) => e.enabled && e.hasDownloadProvider)
|
||||
@@ -2076,15 +2074,6 @@ class _ServiceSelector extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _ServiceChip(
|
||||
icon: Icons.shopping_bag_outlined,
|
||||
label: 'Amazon',
|
||||
isSelected: effectiveService == 'amazon',
|
||||
onTap: () => onChanged('amazon'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _ServiceChip(
|
||||
icon: Icons.smart_display,
|
||||
|
||||
@@ -658,7 +658,6 @@ class _SettingItemState extends State<_SettingItem> {
|
||||
);
|
||||
}
|
||||
|
||||
// For button type, show a different layout
|
||||
if (widget.setting.type == 'button') {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -271,7 +271,6 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Settings Section
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(
|
||||
title: context.l10n.libraryScanSettings,
|
||||
@@ -442,7 +441,6 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
),
|
||||
],
|
||||
|
||||
// Info Section
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
@@ -558,7 +556,6 @@ class _LibraryHeroCard extends StatelessWidget {
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background decorative elements
|
||||
Positioned(
|
||||
right: -20,
|
||||
top: -20,
|
||||
@@ -581,7 +578,6 @@ class _LibraryHeroCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Content
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
|
||||
@@ -122,8 +122,6 @@ class _LyricsProviderPriorityPageState
|
||||
);
|
||||
}
|
||||
|
||||
// ── State mutations ──
|
||||
|
||||
void _enableProvider(String id) {
|
||||
setState(() => _enabledProviders.add(id));
|
||||
_markChanged();
|
||||
@@ -142,8 +140,6 @@ class _LyricsProviderPriorityPageState
|
||||
_markChanged();
|
||||
}
|
||||
|
||||
// ── Save / Discard ──
|
||||
|
||||
Future<void> _saveChanges() async {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
@@ -180,8 +176,6 @@ class _LyricsProviderPriorityPageState
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
// ── Provider metadata ──
|
||||
|
||||
static _LyricsProviderInfo _getLyricsProviderInfo(String id) {
|
||||
switch (id) {
|
||||
case 'spotify_api':
|
||||
@@ -230,10 +224,6 @@ class _LyricsProviderPriorityPageState
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Enabled provider card (reorderable)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class _EnabledProviderItem extends StatelessWidget {
|
||||
final String providerId;
|
||||
final _LyricsProviderInfo info;
|
||||
@@ -273,7 +263,6 @@ class _EnabledProviderItem extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// Numbered badge
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
@@ -296,10 +285,8 @@ class _EnabledProviderItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Icon
|
||||
Icon(info.icon, color: colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
// Name + description
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -319,7 +306,6 @@ class _EnabledProviderItem extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Enable/disable switch
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: FittedBox(
|
||||
@@ -327,7 +313,6 @@ class _EnabledProviderItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// Drag handle
|
||||
Icon(Icons.drag_handle, color: colorScheme.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
@@ -338,10 +323,6 @@ class _EnabledProviderItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Disabled provider card
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class _DisabledProviderItem extends StatelessWidget {
|
||||
final String providerId;
|
||||
final _LyricsProviderInfo info;
|
||||
@@ -383,10 +364,8 @@ class _DisabledProviderItem extends StatelessWidget {
|
||||
// Empty space aligned with numbered badge
|
||||
const SizedBox(width: 28),
|
||||
const SizedBox(width: 16),
|
||||
// Icon (muted)
|
||||
Icon(info.icon, color: colorScheme.outline),
|
||||
const SizedBox(width: 12),
|
||||
// Name + description
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -407,7 +386,6 @@ class _DisabledProviderItem extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Switch
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: FittedBox(
|
||||
@@ -424,10 +402,6 @@ class _DisabledProviderItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Provider info model
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class _LyricsProviderInfo {
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
@@ -43,7 +43,7 @@ class OptionsSettingsPage extends ConsumerWidget {
|
||||
((constraints.maxHeight - minHeight) /
|
||||
(maxHeight - minHeight))
|
||||
.clamp(0.0, 1.0);
|
||||
final leftPadding = 56 - (32 * expandRatio); // 56 -> 24
|
||||
final leftPadding = 56 - (32 * expandRatio);
|
||||
return FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(
|
||||
@@ -53,7 +53,7 @@ class OptionsSettingsPage extends ConsumerWidget {
|
||||
title: Text(
|
||||
context.l10n.optionsTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio), // 20 -> 28
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
@@ -331,7 +331,6 @@ class OptionsSettingsPage extends ConsumerWidget {
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
// Show loading indicator
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
|
||||
@@ -333,12 +333,6 @@ class _ProviderItem extends StatelessWidget {
|
||||
);
|
||||
case 'qobuz':
|
||||
return _ProviderInfo(name: 'Qobuz', icon: Icons.album, isBuiltIn: true);
|
||||
case 'amazon':
|
||||
return _ProviderInfo(
|
||||
name: 'Amazon Music',
|
||||
icon: Icons.shopping_bag,
|
||||
isBuiltIn: true,
|
||||
);
|
||||
case 'youtube':
|
||||
return _ProviderInfo(
|
||||
name: 'YouTube',
|
||||
|
||||
@@ -22,7 +22,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentStep = 0;
|
||||
|
||||
// State variables
|
||||
bool _storagePermissionGranted = false;
|
||||
bool _notificationPermissionGranted = false;
|
||||
String? _selectedDirectory;
|
||||
@@ -474,7 +473,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Calculate progress
|
||||
final progress = (_currentStep + 1) / _totalSteps;
|
||||
|
||||
return Scaffold(
|
||||
@@ -482,7 +480,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Top Bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
@@ -497,9 +494,8 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 48), // Spacer
|
||||
const SizedBox(width: 48),
|
||||
const Spacer(),
|
||||
// Progress Indicator
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
@@ -530,7 +526,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
|
||||
@@ -627,7 +627,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Full-screen cover background
|
||||
if (_hasPath(_embeddedCoverPreviewPath))
|
||||
Image.file(
|
||||
File(_embeddedCoverPreviewPath!),
|
||||
@@ -657,7 +656,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// Bottom gradient for readability
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -676,7 +674,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Track info overlay at bottom
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
@@ -2606,7 +2603,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Target format
|
||||
Text(
|
||||
context.l10n.trackConvertTargetFormat,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
@@ -2639,7 +2635,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Bitrate
|
||||
Text(
|
||||
context.l10n.trackConvertBitrate,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
@@ -2664,7 +2659,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Convert button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
@@ -2750,7 +2744,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
SnackBar(content: Text(context.l10n.trackConvertConverting)),
|
||||
);
|
||||
|
||||
// Step 1: Read metadata from file (fallback to known item metadata).
|
||||
final metadata = _buildFallbackMetadata();
|
||||
try {
|
||||
final result = await PlatformBridge.readFileMetadata(cleanFilePath);
|
||||
@@ -2768,7 +2761,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
_log.w('readFileMetadata threw, using fallback metadata: $e');
|
||||
}
|
||||
|
||||
// Step 2: Extract cover art to temp file
|
||||
String? coverPath;
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
@@ -2783,7 +2775,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Step 3: Handle SAF vs regular file
|
||||
String workingPath = cleanFilePath;
|
||||
final isSaf = _isSafFile;
|
||||
String? safTempPath;
|
||||
@@ -2803,7 +2794,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
workingPath = safTempPath;
|
||||
}
|
||||
|
||||
// Step 4: Convert
|
||||
final newPath = await FFmpegService.convertAudioFormat(
|
||||
inputPath: workingPath,
|
||||
targetFormat: targetFormat.toLowerCase(),
|
||||
@@ -2838,7 +2828,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
|
||||
final newQuality = _buildConvertedQualityLabel(targetFormat, bitrate);
|
||||
|
||||
// Step 5: Handle SAF write-back
|
||||
if (isSaf) {
|
||||
final treeUri = _downloadItem?.downloadTreeUri;
|
||||
final relativeDir = _downloadItem?.safRelativeDir ?? '';
|
||||
@@ -3064,7 +3053,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
// Also remove from local library database
|
||||
// ref.read(localLibraryProvider.notifier).removeItem(_localLibraryItem!.id);
|
||||
} else {
|
||||
// Existing download history deletion logic
|
||||
try {
|
||||
await deleteFile(cleanFilePath);
|
||||
} catch (e) {
|
||||
@@ -3661,7 +3649,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
expand: false,
|
||||
builder: (context, scrollController) => Column(
|
||||
children: [
|
||||
// Handle bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
child: Container(
|
||||
@@ -3673,7 +3660,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Title row
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
@@ -3698,7 +3684,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Fields
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
|
||||
@@ -90,7 +90,6 @@ class _TutorialScreenState extends ConsumerState<TutorialScreen> {
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Top Navigation Bar
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: topBarPaddingH,
|
||||
@@ -112,7 +111,6 @@ class _TutorialScreenState extends ConsumerState<TutorialScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Skip button
|
||||
TextButton(
|
||||
onPressed: _skipTutorial,
|
||||
style: TextButton.styleFrom(
|
||||
@@ -131,7 +129,6 @@ class _TutorialScreenState extends ConsumerState<TutorialScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Main Content Area
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
@@ -218,12 +215,10 @@ class _TutorialScreenState extends ConsumerState<TutorialScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Control Area
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
// Expressive Page Indicators
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(_totalPages, (index) {
|
||||
@@ -246,7 +241,6 @@ class _TutorialScreenState extends ConsumerState<TutorialScreen> {
|
||||
}),
|
||||
),
|
||||
SizedBox(height: bottomGap),
|
||||
// Action Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: actionButtonHeight,
|
||||
@@ -402,7 +396,6 @@ class _InteractiveSearchExampleState extends State<_InteractiveSearchExample> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Search Input
|
||||
TextField(
|
||||
controller: _controller,
|
||||
onChanged: (value) {
|
||||
@@ -428,7 +421,6 @@ class _InteractiveSearchExampleState extends State<_InteractiveSearchExample> {
|
||||
),
|
||||
),
|
||||
|
||||
// Result Placeholder
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeOutBack,
|
||||
@@ -541,7 +533,6 @@ class _InteractiveDownloadExampleState
|
||||
_isCompleted = true;
|
||||
});
|
||||
|
||||
// Reset after a delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
||||
@@ -198,7 +198,7 @@ class FFmpegService {
|
||||
final trimmedKey = decryptionKey.trim();
|
||||
if (trimmedKey.isEmpty) return inputPath;
|
||||
|
||||
// Amazon encrypted streams are commonly MP4 container with FLAC audio.
|
||||
// Encrypted streams are commonly MP4 container with FLAC audio.
|
||||
// Prefer FLAC output to avoid MP4 muxing errors during decrypt copy.
|
||||
final preferredExt = inputPath.toLowerCase().endsWith('.m4a')
|
||||
? '.flac'
|
||||
@@ -217,7 +217,10 @@ class FFmpegService {
|
||||
required String key,
|
||||
}) {
|
||||
final audioMap = mapAudioOnly ? '-map 0:a ' : '';
|
||||
return '-v error -decryption_key "$key" -i "$inputPath" $audioMap-c copy "$outputPath" -y';
|
||||
// Force MOV demuxer: -decryption_key is only supported by the MOV/MP4
|
||||
// demuxer. The input may carry a .flac extension (SAF mode) while actually
|
||||
// containing an encrypted M4A stream, so we must override auto-detection.
|
||||
return '-v error -decryption_key "$key" -f mov -i "$inputPath" $audioMap-c copy "$outputPath" -y';
|
||||
}
|
||||
|
||||
final keyCandidates = _buildDecryptionKeyCandidates(trimmedKey);
|
||||
@@ -627,7 +630,7 @@ class FFmpegService {
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<LiveDecryptedStreamResult?> startAmazonLiveDecryptedStream({
|
||||
static Future<LiveDecryptedStreamResult?> startEncryptedLiveDecryptedStream({
|
||||
required String encryptedStreamUrl,
|
||||
required String decryptionKey,
|
||||
String preferredFormat = 'flac',
|
||||
@@ -1225,7 +1228,6 @@ class FFmpegService {
|
||||
final extension = format == 'opus' ? '.opus' : '.mp3';
|
||||
final outputPath = _buildOutputPath(inputPath, extension);
|
||||
|
||||
// Step 1: Convert audio
|
||||
String command;
|
||||
if (format == 'opus') {
|
||||
command =
|
||||
@@ -1245,7 +1247,6 @@ class FFmpegService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 2: Embed metadata + cover into the converted file.
|
||||
// Treat embed failure as conversion failure when metadata/cover was requested.
|
||||
final hasMetadata = metadata.values.any((v) => v.trim().isNotEmpty);
|
||||
final hasCover = coverPath != null && coverPath.trim().isNotEmpty;
|
||||
@@ -1281,7 +1282,6 @@ class FFmpegService {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Delete original if requested
|
||||
if (deleteOriginal) {
|
||||
try {
|
||||
await File(inputPath).delete();
|
||||
|
||||
@@ -104,8 +104,6 @@ class HistoryDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== iOS Path Normalization ====================
|
||||
|
||||
/// Pattern to match iOS container paths
|
||||
/// Example: /var/mobile/Containers/Data/Application/UUID-HERE/Documents/...
|
||||
static final _iosContainerPattern = RegExp(
|
||||
@@ -325,8 +323,6 @@ class HistoryDatabase {
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== CRUD Operations ====================
|
||||
|
||||
/// Insert or update a history item
|
||||
Future<void> upsert(Map<String, dynamic> json) async {
|
||||
final db = await database;
|
||||
|
||||
@@ -403,8 +403,6 @@ class PlatformBridge {
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
// ==================== LYRICS PROVIDER SETTINGS ====================
|
||||
|
||||
/// Sets the lyrics provider order. Providers not in the list are disabled.
|
||||
static Future<void> setLyricsProviders(List<String> providers) async {
|
||||
final providersJSON = jsonEncode(providers);
|
||||
@@ -1060,8 +1058,6 @@ class PlatformBridge {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== LOCAL LIBRARY SCANNING ====================
|
||||
|
||||
/// Set the directory for caching extracted cover art
|
||||
static Future<void> setLibraryCoverCacheDir(String cacheDir) async {
|
||||
_log.i('setLibraryCoverCacheDir: $cacheDir');
|
||||
@@ -1261,5 +1257,4 @@ class PlatformBridge {
|
||||
await _channel.invokeMethod('clearStoreCache');
|
||||
}
|
||||
|
||||
// ==================== YOUTUBE / COBALT ====================
|
||||
}
|
||||
|
||||
@@ -28,14 +28,12 @@ class _KofiPainter extends CustomPainter {
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// Cup body
|
||||
final cup = RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(s * 0.08, s * 0.28, s * 0.62, s * 0.52),
|
||||
Radius.circular(s * 0.12),
|
||||
);
|
||||
canvas.drawRRect(cup, paint);
|
||||
|
||||
// Handle
|
||||
final handlePaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
|
||||
@@ -67,17 +67,6 @@ const _builtInServices = [
|
||||
),
|
||||
],
|
||||
),
|
||||
BuiltInService(
|
||||
id: 'amazon',
|
||||
label: 'Amazon',
|
||||
qualityOptions: [
|
||||
QualityOption(
|
||||
id: 'LOSSLESS',
|
||||
label: 'FLAC Best Available',
|
||||
description: 'Amazon API delivers the best available lossless quality',
|
||||
),
|
||||
],
|
||||
),
|
||||
BuiltInService(
|
||||
id: 'deezer',
|
||||
label: 'Deezer',
|
||||
@@ -209,7 +198,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
||||
return ext.qualityOptions;
|
||||
}
|
||||
|
||||
// Default fallback options
|
||||
return [
|
||||
const QualityOption(
|
||||
id: 'DEFAULT',
|
||||
|
||||
Reference in New Issue
Block a user