mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 13:22:49 +02:00
feat(download): add automatic lossy conversion
This commit is contained in:
@@ -2764,6 +2764,60 @@ abstract class AppLocalizations {
|
||||
/// **'Lossy Format'**
|
||||
String get downloadLossyFormat;
|
||||
|
||||
/// Toggle for automatic post-download audio conversion
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto-convert after download'**
|
||||
String get downloadAutoConvert;
|
||||
|
||||
/// Explanation of safe automatic post-download conversion
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.'**
|
||||
String get downloadAutoConvertSubtitle;
|
||||
|
||||
/// Automatic conversion output format setting
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Output format'**
|
||||
String get downloadAutoConvertFormat;
|
||||
|
||||
/// Automatic conversion format picker explanation
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose the lossy format used for newly completed downloads.'**
|
||||
String get downloadAutoConvertFormatSubtitle;
|
||||
|
||||
/// Automatic conversion bitrate setting
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Output quality'**
|
||||
String get downloadAutoConvertBitrate;
|
||||
|
||||
/// Automatic conversion bitrate picker explanation
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Higher bitrates preserve more detail but create larger files.'**
|
||||
String get downloadAutoConvertBitrateSubtitle;
|
||||
|
||||
/// MP3 automatic conversion option description
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Best compatibility across players and devices'**
|
||||
String get downloadAutoConvertMp3Subtitle;
|
||||
|
||||
/// M4A AAC automatic conversion option description
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Efficient AAC audio in an M4A container'**
|
||||
String get downloadAutoConvertM4aSubtitle;
|
||||
|
||||
/// Opus automatic conversion option description
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Best efficiency for modern players'**
|
||||
String get downloadAutoConvertOpusSubtitle;
|
||||
|
||||
/// Title of the lossy format picker bottom sheet
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -1515,6 +1515,39 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Verlustbehaftetes Format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Verlustbehaftetes 320kbps-Format';
|
||||
|
||||
|
||||
@@ -1494,6 +1494,39 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Lossy Format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Lossy 320kbps Format';
|
||||
|
||||
|
||||
@@ -1494,6 +1494,39 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Lossy Format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Lossy 320kbps Format';
|
||||
|
||||
|
||||
@@ -1533,6 +1533,39 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Format avec perte';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Format avec perte à 320 kbps';
|
||||
|
||||
|
||||
@@ -1501,6 +1501,39 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Format Lossy';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Konversi otomatis setelah unduh';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Ubah unduhan yang selesai ke format lossy yang lebih kecil. File asli hanya diganti setelah konversi berhasil.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Format keluaran';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Pilih format lossy untuk unduhan yang baru selesai.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Kualitas keluaran';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Bitrate lebih tinggi menjaga lebih banyak detail, tetapi ukuran file juga lebih besar.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Kompatibilitas terbaik di berbagai pemutar dan perangkat';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Audio AAC yang efisien dalam container M4A';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Efisiensi terbaik untuk pemutar modern';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Format Lossy 320kbps';
|
||||
|
||||
|
||||
@@ -1485,6 +1485,39 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Lossy Format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Lossy 320kbps Format';
|
||||
|
||||
|
||||
@@ -1464,6 +1464,39 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => '손실 압축 형식';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => '손실 압축 320kbps 형식';
|
||||
|
||||
|
||||
@@ -1494,6 +1494,39 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Lossy Format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Lossy 320kbps Format';
|
||||
|
||||
|
||||
@@ -1505,6 +1505,39 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Формат с потерями';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Формат с потерями 320 кбит/с';
|
||||
|
||||
|
||||
@@ -1507,6 +1507,39 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Kayıplı Format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Kayıplı 320kbps Formatı';
|
||||
|
||||
|
||||
@@ -1513,6 +1513,39 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get downloadLossyFormat => 'Формат із втратами';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvert => 'Auto-convert after download';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertSubtitle =>
|
||||
'Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormat => 'Output format';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertFormatSubtitle =>
|
||||
'Choose the lossy format used for newly completed downloads.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrate => 'Output quality';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertBitrateSubtitle =>
|
||||
'Higher bitrates preserve more detail but create larger files.';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertMp3Subtitle =>
|
||||
'Best compatibility across players and devices';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertM4aSubtitle =>
|
||||
'Efficient AAC audio in an M4A container';
|
||||
|
||||
@override
|
||||
String get downloadAutoConvertOpusSubtitle =>
|
||||
'Best efficiency for modern players';
|
||||
|
||||
@override
|
||||
String get downloadLossy320Format => 'Формат із втратами 320 кбіт/с';
|
||||
|
||||
|
||||
@@ -1991,6 +1991,42 @@
|
||||
"@downloadLossyFormat": {
|
||||
"description": "Setting title to pick output format for lossy downloads"
|
||||
},
|
||||
"downloadAutoConvert": "Auto-convert after download",
|
||||
"@downloadAutoConvert": {
|
||||
"description": "Toggle for automatic post-download audio conversion"
|
||||
},
|
||||
"downloadAutoConvertSubtitle": "Convert completed downloads to a smaller lossy format. The original is replaced only after conversion succeeds.",
|
||||
"@downloadAutoConvertSubtitle": {
|
||||
"description": "Explanation of safe automatic post-download conversion"
|
||||
},
|
||||
"downloadAutoConvertFormat": "Output format",
|
||||
"@downloadAutoConvertFormat": {
|
||||
"description": "Automatic conversion output format setting"
|
||||
},
|
||||
"downloadAutoConvertFormatSubtitle": "Choose the lossy format used for newly completed downloads.",
|
||||
"@downloadAutoConvertFormatSubtitle": {
|
||||
"description": "Automatic conversion format picker explanation"
|
||||
},
|
||||
"downloadAutoConvertBitrate": "Output quality",
|
||||
"@downloadAutoConvertBitrate": {
|
||||
"description": "Automatic conversion bitrate setting"
|
||||
},
|
||||
"downloadAutoConvertBitrateSubtitle": "Higher bitrates preserve more detail but create larger files.",
|
||||
"@downloadAutoConvertBitrateSubtitle": {
|
||||
"description": "Automatic conversion bitrate picker explanation"
|
||||
},
|
||||
"downloadAutoConvertMp3Subtitle": "Best compatibility across players and devices",
|
||||
"@downloadAutoConvertMp3Subtitle": {
|
||||
"description": "MP3 automatic conversion option description"
|
||||
},
|
||||
"downloadAutoConvertM4aSubtitle": "Efficient AAC audio in an M4A container",
|
||||
"@downloadAutoConvertM4aSubtitle": {
|
||||
"description": "M4A AAC automatic conversion option description"
|
||||
},
|
||||
"downloadAutoConvertOpusSubtitle": "Best efficiency for modern players",
|
||||
"@downloadAutoConvertOpusSubtitle": {
|
||||
"description": "Opus automatic conversion option description"
|
||||
},
|
||||
"downloadLossy320Format": "Lossy 320kbps Format",
|
||||
"@downloadLossy320Format": {
|
||||
"description": "Title of the lossy format picker bottom sheet"
|
||||
|
||||
@@ -4253,6 +4253,42 @@
|
||||
"@downloadLossyFormat": {
|
||||
"description": "Setting title to pick output format for lossy downloads"
|
||||
},
|
||||
"downloadAutoConvert": "Konversi otomatis setelah unduh",
|
||||
"@downloadAutoConvert": {
|
||||
"description": "Toggle for automatic post-download audio conversion"
|
||||
},
|
||||
"downloadAutoConvertSubtitle": "Ubah unduhan yang selesai ke format lossy yang lebih kecil. File asli hanya diganti setelah konversi berhasil.",
|
||||
"@downloadAutoConvertSubtitle": {
|
||||
"description": "Explanation of safe automatic post-download conversion"
|
||||
},
|
||||
"downloadAutoConvertFormat": "Format keluaran",
|
||||
"@downloadAutoConvertFormat": {
|
||||
"description": "Automatic conversion output format setting"
|
||||
},
|
||||
"downloadAutoConvertFormatSubtitle": "Pilih format lossy untuk unduhan yang baru selesai.",
|
||||
"@downloadAutoConvertFormatSubtitle": {
|
||||
"description": "Automatic conversion format picker explanation"
|
||||
},
|
||||
"downloadAutoConvertBitrate": "Kualitas keluaran",
|
||||
"@downloadAutoConvertBitrate": {
|
||||
"description": "Automatic conversion bitrate setting"
|
||||
},
|
||||
"downloadAutoConvertBitrateSubtitle": "Bitrate lebih tinggi menjaga lebih banyak detail, tetapi ukuran file juga lebih besar.",
|
||||
"@downloadAutoConvertBitrateSubtitle": {
|
||||
"description": "Automatic conversion bitrate picker explanation"
|
||||
},
|
||||
"downloadAutoConvertMp3Subtitle": "Kompatibilitas terbaik di berbagai pemutar dan perangkat",
|
||||
"@downloadAutoConvertMp3Subtitle": {
|
||||
"description": "MP3 automatic conversion option description"
|
||||
},
|
||||
"downloadAutoConvertM4aSubtitle": "Audio AAC yang efisien dalam container M4A",
|
||||
"@downloadAutoConvertM4aSubtitle": {
|
||||
"description": "M4A AAC automatic conversion option description"
|
||||
},
|
||||
"downloadAutoConvertOpusSubtitle": "Efisiensi terbaik untuk pemutar modern",
|
||||
"@downloadAutoConvertOpusSubtitle": {
|
||||
"description": "Opus automatic conversion option description"
|
||||
},
|
||||
"snackbarAlreadyInLibrary": "\"{trackName}\" sudah ada di perpustakaan Anda",
|
||||
"@snackbarAlreadyInLibrary": {
|
||||
"description": "Snackbar - track already exists in local library",
|
||||
|
||||
@@ -69,6 +69,9 @@ class AppSettings {
|
||||
final String lyricsMode;
|
||||
final String
|
||||
tidalHighFormat; // Legacy key for 320kbps lossy output format: 'mp3_320', 'aac_320', 'opus_256', or 'opus_128'
|
||||
final bool autoConvertDownloads;
|
||||
final String autoConvertFormat; // 'mp3', 'aac' (M4A), or 'opus'
|
||||
final String autoConvertBitrate; // '128k', '192k', '256k', or '320k'
|
||||
final bool
|
||||
useAllFilesAccess; // Android 13+ only: enable MANAGE_EXTERNAL_STORAGE
|
||||
final bool autoExportFailedDownloads;
|
||||
@@ -142,7 +145,7 @@ class AppSettings {
|
||||
this.historyViewMode = 'grid',
|
||||
this.historyFilterMode = 'all',
|
||||
this.defaultLibraryView = 'last',
|
||||
this.libraryQualityLabelMode = libraryQualityLabelBitrate,
|
||||
this.libraryQualityLabelMode = AppSettings.libraryQualityLabelBitrate,
|
||||
this.askQualityBeforeDownload = true,
|
||||
this.enableLogging = false,
|
||||
this.useExtensionProviders = true,
|
||||
@@ -160,6 +163,9 @@ class AppSettings {
|
||||
this.locale = 'system',
|
||||
this.lyricsMode = 'embed',
|
||||
this.tidalHighFormat = 'mp3_320',
|
||||
this.autoConvertDownloads = false,
|
||||
this.autoConvertFormat = 'mp3',
|
||||
this.autoConvertBitrate = '320k',
|
||||
this.useAllFilesAccess = false,
|
||||
this.autoExportFailedDownloads = false,
|
||||
this.downloadNetworkMode = 'any',
|
||||
@@ -235,6 +241,9 @@ class AppSettings {
|
||||
String? locale,
|
||||
String? lyricsMode,
|
||||
String? tidalHighFormat,
|
||||
bool? autoConvertDownloads,
|
||||
String? autoConvertFormat,
|
||||
String? autoConvertBitrate,
|
||||
bool? useAllFilesAccess,
|
||||
bool? autoExportFailedDownloads,
|
||||
String? downloadNetworkMode,
|
||||
@@ -323,6 +332,9 @@ class AppSettings {
|
||||
locale: locale ?? this.locale,
|
||||
lyricsMode: lyricsMode ?? this.lyricsMode,
|
||||
tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat,
|
||||
autoConvertDownloads: autoConvertDownloads ?? this.autoConvertDownloads,
|
||||
autoConvertFormat: autoConvertFormat ?? this.autoConvertFormat,
|
||||
autoConvertBitrate: autoConvertBitrate ?? this.autoConvertBitrate,
|
||||
useAllFilesAccess: useAllFilesAccess ?? this.useAllFilesAccess,
|
||||
autoExportFailedDownloads:
|
||||
autoExportFailedDownloads ?? this.autoExportFailedDownloads,
|
||||
|
||||
@@ -60,6 +60,9 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
locale: json['locale'] as String? ?? 'system',
|
||||
lyricsMode: json['lyricsMode'] as String? ?? 'embed',
|
||||
tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320',
|
||||
autoConvertDownloads: json['autoConvertDownloads'] as bool? ?? false,
|
||||
autoConvertFormat: json['autoConvertFormat'] as String? ?? 'mp3',
|
||||
autoConvertBitrate: json['autoConvertBitrate'] as String? ?? '320k',
|
||||
useAllFilesAccess: json['useAllFilesAccess'] as bool? ?? false,
|
||||
autoExportFailedDownloads:
|
||||
json['autoExportFailedDownloads'] as bool? ?? false,
|
||||
@@ -145,6 +148,9 @@ Map<String, dynamic> _$AppSettingsToJson(
|
||||
'locale': instance.locale,
|
||||
'lyricsMode': instance.lyricsMode,
|
||||
'tidalHighFormat': instance.tidalHighFormat,
|
||||
'autoConvertDownloads': instance.autoConvertDownloads,
|
||||
'autoConvertFormat': instance.autoConvertFormat,
|
||||
'autoConvertBitrate': instance.autoConvertBitrate,
|
||||
'useAllFilesAccess': instance.useAllFilesAccess,
|
||||
'autoExportFailedDownloads': instance.autoExportFailedDownloads,
|
||||
'downloadNetworkMode': instance.downloadNetworkMode,
|
||||
|
||||
@@ -28,6 +28,7 @@ import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
import 'package:spotiflac_android/utils/artist_utils.dart';
|
||||
import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
||||
import 'package:spotiflac_android/utils/audio_conversion_utils.dart';
|
||||
import 'package:spotiflac_android/utils/int_utils.dart';
|
||||
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
|
||||
import 'package:spotiflac_android/utils/progress_stream_poller.dart';
|
||||
@@ -556,6 +557,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
embedReplayGain: settings.embedReplayGain,
|
||||
postProcessingEnabled: postProcessingEnabled,
|
||||
tidalHighFormat: settings.tidalHighFormat,
|
||||
autoConvertDownloads: settings.autoConvertDownloads,
|
||||
autoConvertFormat: normalizeAutoConvertFormat(settings.autoConvertFormat),
|
||||
autoConvertBitrate: normalizeAutoConvertBitrate(
|
||||
settings.autoConvertBitrate,
|
||||
),
|
||||
trackNumber: normalizedTrackNumber,
|
||||
playlistPosition: _validPlaylistPosition(item),
|
||||
discNumber: normalizedDiscNumber,
|
||||
|
||||
@@ -23,6 +23,20 @@ class _QualityVariantFileOutcome {
|
||||
});
|
||||
}
|
||||
|
||||
class _AutoConversionOutcome {
|
||||
final String filePath;
|
||||
final String? fileName;
|
||||
final String quality;
|
||||
final bool converted;
|
||||
|
||||
const _AutoConversionOutcome({
|
||||
required this.filePath,
|
||||
required this.fileName,
|
||||
required this.quality,
|
||||
required this.converted,
|
||||
});
|
||||
}
|
||||
|
||||
/// AC-4 repair only applies to MP4 containers; decrypt can also emit raw
|
||||
/// FLAC, which the native MP4 box parser would reject as corrupt.
|
||||
bool _isMp4Container(String path) {
|
||||
@@ -31,6 +45,160 @@ bool _isMp4Container(String path) {
|
||||
}
|
||||
|
||||
extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
Future<_AutoConversionOutcome> _autoConvertDownloadedFile({
|
||||
required String itemId,
|
||||
required String filePath,
|
||||
required String? fileName,
|
||||
required String currentQuality,
|
||||
required AppSettings settings,
|
||||
required Track track,
|
||||
required Map<String, dynamic> result,
|
||||
required String downloadService,
|
||||
required String storageMode,
|
||||
String? downloadTreeUri,
|
||||
String? safRelativeDir,
|
||||
}) async {
|
||||
if (!settings.autoConvertDownloads) {
|
||||
return _AutoConversionOutcome(
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
quality: currentQuality,
|
||||
converted: false,
|
||||
);
|
||||
}
|
||||
|
||||
final targetFormat = normalizeAutoConvertFormat(settings.autoConvertFormat);
|
||||
final targetBitrate = normalizeAutoConvertBitrate(
|
||||
settings.autoConvertBitrate,
|
||||
);
|
||||
final targetBitrateKbps = autoConvertBitrateKbps(targetBitrate);
|
||||
if (autoConversionAlreadySatisfied(
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
targetFormat: targetFormat,
|
||||
targetBitrate: targetBitrate,
|
||||
quality: currentQuality,
|
||||
bitrateKbps: readPositiveBitrateKbps(
|
||||
result['bitrate'] ?? result['actual_bitrate'],
|
||||
),
|
||||
)) {
|
||||
return _AutoConversionOutcome(
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
quality: currentQuality,
|
||||
converted: false,
|
||||
);
|
||||
}
|
||||
|
||||
final baseFileName = (fileName?.trim().isNotEmpty == true
|
||||
? fileName!.trim()
|
||||
: File(filePath).uri.pathSegments.last);
|
||||
final convertedFileName = convertedOutputFileName(
|
||||
originalFileName: baseFileName,
|
||||
targetFormat: targetFormat,
|
||||
);
|
||||
final convertedQuality =
|
||||
'${displayFormatForLossyFormat(targetFormat)} ${targetBitrateKbps}kbps';
|
||||
|
||||
Future<void> embedConvertedMetadata(String convertedPath) async {
|
||||
if (!settings.embedMetadata) return;
|
||||
try {
|
||||
await _embedMetadataToFile(
|
||||
convertedPath,
|
||||
track,
|
||||
format: metadataFormatForLossyFormat(targetFormat),
|
||||
genre: result['genre'] as String?,
|
||||
label: result['label'] as String?,
|
||||
copyright: result['copyright'] as String?,
|
||||
downloadService: downloadService,
|
||||
writeExternalLrc: storageMode != 'saf',
|
||||
);
|
||||
} catch (e) {
|
||||
// The audio conversion itself is still valid. Preserve the converted
|
||||
// file if an optional tag/cover write fails.
|
||||
_log.w('Automatic conversion metadata embed failed: $e');
|
||||
result['auto_conversion_metadata_warning'] = e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
updateItemStatus(itemId, DownloadStatus.finalizing, progress: 0.97);
|
||||
String? convertedPath;
|
||||
String? publishedFileName = convertedFileName;
|
||||
if (storageMode == 'saf' && isContentUri(filePath)) {
|
||||
if (downloadTreeUri == null || downloadTreeUri.isEmpty) {
|
||||
throw StateError('Missing SAF tree for automatic conversion');
|
||||
}
|
||||
convertedPath = await _replaceSafFileVia(
|
||||
uri: filePath,
|
||||
treeUri: downloadTreeUri,
|
||||
relativeDir: safRelativeDir ?? '',
|
||||
avoidOverwrite:
|
||||
convertedFileName.toLowerCase() != baseFileName.toLowerCase(),
|
||||
onPublishedFileName: (value) => publishedFileName = value,
|
||||
op: (tempPath, addCleanup) async {
|
||||
final output = await FFmpegService.convertAudioFormat(
|
||||
inputPath: tempPath,
|
||||
targetFormat: targetFormat,
|
||||
bitrate: targetBitrate,
|
||||
metadata: const {},
|
||||
deleteOriginal: false,
|
||||
);
|
||||
if (output == null) return null;
|
||||
addCleanup(output);
|
||||
await embedConvertedMetadata(output);
|
||||
return (output, convertedFileName);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
convertedPath = await FFmpegService.convertAudioFormat(
|
||||
inputPath: filePath,
|
||||
targetFormat: targetFormat,
|
||||
bitrate: targetBitrate,
|
||||
metadata: const {},
|
||||
deleteOriginal: true,
|
||||
);
|
||||
if (convertedPath != null) {
|
||||
publishedFileName = File(convertedPath).uri.pathSegments.last;
|
||||
await embedConvertedMetadata(convertedPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (convertedPath == null || convertedPath.isEmpty) {
|
||||
throw StateError('FFmpeg returned no automatic conversion output');
|
||||
}
|
||||
|
||||
result['file_path'] = convertedPath;
|
||||
result['file_name'] = publishedFileName;
|
||||
result['audio_codec'] = targetFormat;
|
||||
result['format'] = targetFormat;
|
||||
result['bitrate'] = targetBitrateKbps;
|
||||
result.remove('actual_bit_depth');
|
||||
result.remove('actual_sample_rate');
|
||||
_log.i(
|
||||
'Automatic conversion completed: ${autoConvertFormatLabel(targetFormat)} @ $targetBitrate',
|
||||
);
|
||||
return _AutoConversionOutcome(
|
||||
filePath: convertedPath,
|
||||
fileName: publishedFileName,
|
||||
quality: convertedQuality,
|
||||
converted: true,
|
||||
);
|
||||
} catch (e) {
|
||||
// A successful download remains usable when the optional conversion
|
||||
// fails. Conversion helpers only remove the source after atomic output
|
||||
// promotion, so returning the original path is safe here.
|
||||
result['auto_conversion_warning'] = e.toString();
|
||||
_log.w('Automatic conversion failed; keeping downloaded source: $e');
|
||||
return _AutoConversionOutcome(
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
quality: currentQuality,
|
||||
converted: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the [DownloadHistoryItem] shared by the native-worker and inline
|
||||
/// completion paths. Fields whose source/derivation legitimately differs
|
||||
/// between the two callers (SAF location, probed vs. raw audio metadata,
|
||||
@@ -709,7 +877,12 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
final tidalHighFormat = settings.tidalHighFormat;
|
||||
final tidalHighFormat = settings.autoConvertDownloads
|
||||
? autoConvertLossySetting(
|
||||
format: settings.autoConvertFormat,
|
||||
bitrate: settings.autoConvertBitrate,
|
||||
)
|
||||
: settings.tidalHighFormat;
|
||||
final format = lossyFormatForSetting(tidalHighFormat);
|
||||
final newExt = lossyExtensionForFormat(format);
|
||||
final displayFormat = displayFormatForLossyFormat(format);
|
||||
@@ -761,6 +934,11 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
}
|
||||
result['file_name'] = newFileName;
|
||||
result['_native_actual_quality'] = '$displayFormat $bitrateDisplay';
|
||||
result['audio_codec'] = format;
|
||||
result['format'] = format;
|
||||
result['bitrate'] = int.tryParse(tidalHighFormat.split('_').last);
|
||||
result.remove('actual_bit_depth');
|
||||
result.remove('actual_sample_rate');
|
||||
return newUri;
|
||||
}
|
||||
|
||||
@@ -775,6 +953,11 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
}
|
||||
await embedConvertedMetadata(convertedPath);
|
||||
result['_native_actual_quality'] = '$displayFormat $bitrateDisplay';
|
||||
result['audio_codec'] = format;
|
||||
result['format'] = format;
|
||||
result['bitrate'] = int.tryParse(tidalHighFormat.split('_').last);
|
||||
result.remove('actual_bit_depth');
|
||||
result.remove('actual_sample_rate');
|
||||
return convertedPath;
|
||||
}
|
||||
|
||||
|
||||
@@ -1212,6 +1212,30 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
if (postProcessedPath != null && postProcessedPath.isNotEmpty) {
|
||||
filePath = postProcessedPath;
|
||||
}
|
||||
final autoConvertOutcome = await _autoConvertDownloadedFile(
|
||||
itemId: item.id,
|
||||
filePath: filePath,
|
||||
fileName: result['file_name'] as String? ?? context.safFileName,
|
||||
currentQuality: actualQuality,
|
||||
settings: settings,
|
||||
track: trackToDownload,
|
||||
result: result,
|
||||
downloadService: context.item.service,
|
||||
storageMode: context.storageMode,
|
||||
downloadTreeUri: context.downloadTreeUri,
|
||||
safRelativeDir: context.safRelativeDir,
|
||||
);
|
||||
filePath = autoConvertOutcome.filePath;
|
||||
actualQuality = autoConvertOutcome.quality;
|
||||
if (autoConvertOutcome.fileName != null) {
|
||||
result['file_name'] = autoConvertOutcome.fileName;
|
||||
}
|
||||
if (autoConvertOutcome.converted) {
|
||||
actualBitDepth = null;
|
||||
actualSampleRate = null;
|
||||
actualFormat = normalizeAutoConvertFormat(settings.autoConvertFormat);
|
||||
actualBitrate = autoConvertBitrateKbps(settings.autoConvertBitrate);
|
||||
}
|
||||
await _writeNativeWorkerReplayGain(
|
||||
context: context,
|
||||
settings: settings,
|
||||
|
||||
@@ -713,6 +713,27 @@ class _DownloadRun {
|
||||
}
|
||||
}
|
||||
|
||||
final autoConvertInput = filePath;
|
||||
if (!wasExisting && autoConvertInput != null) {
|
||||
final outcome = await n._autoConvertDownloadedFile(
|
||||
itemId: item.id,
|
||||
filePath: autoConvertInput,
|
||||
fileName: finalSafFileName ?? result['file_name'] as String?,
|
||||
currentQuality: actualQuality,
|
||||
settings: settings,
|
||||
track: trackToDownload,
|
||||
result: result,
|
||||
downloadService: item.service,
|
||||
storageMode: effectiveSafMode ? 'saf' : 'app',
|
||||
downloadTreeUri: settings.downloadTreeUri,
|
||||
safRelativeDir: effectiveOutputDir,
|
||||
);
|
||||
filePath = outcome.filePath;
|
||||
finalSafFileName = outcome.fileName;
|
||||
actualQuality = outcome.quality;
|
||||
if (outcome.converted) probedFinalMetadata = null;
|
||||
}
|
||||
|
||||
final variantInput = filePath;
|
||||
if (variantInput != null && item.preserveQualityVariant) {
|
||||
final variantOutcome = await n._finalizeQualityVariantFilename(
|
||||
@@ -911,7 +932,12 @@ class _DownloadRun {
|
||||
}
|
||||
|
||||
Future<void> _convertSafM4aToLossy(String currentFilePath) async {
|
||||
final tidalHighFormat = settings.tidalHighFormat;
|
||||
final tidalHighFormat = settings.autoConvertDownloads
|
||||
? autoConvertLossySetting(
|
||||
format: settings.autoConvertFormat,
|
||||
bitrate: settings.autoConvertBitrate,
|
||||
)
|
||||
: settings.tidalHighFormat;
|
||||
_log.i(
|
||||
'Lossy 320kbps quality (SAF), converting M4A to $tidalHighFormat...',
|
||||
);
|
||||
@@ -972,6 +998,11 @@ class _DownloadRun {
|
||||
? '${tidalHighFormat.split('_').last}kbps'
|
||||
: '320kbps';
|
||||
actualQuality = '$displayFormat $bitrateDisplay';
|
||||
result['audio_codec'] = format;
|
||||
result['format'] = format;
|
||||
result['bitrate'] = int.tryParse(tidalHighFormat.split('_').last);
|
||||
result.remove('actual_bit_depth');
|
||||
result.remove('actual_sample_rate');
|
||||
} else if (convertFailed) {
|
||||
_log.w('M4A to $format conversion failed, keeping M4A file');
|
||||
actualQuality = 'AAC 320kbps';
|
||||
@@ -1115,7 +1146,12 @@ class _DownloadRun {
|
||||
}
|
||||
|
||||
Future<void> _convertLocalM4aToLossy(String currentFilePath) async {
|
||||
final tidalHighFormat = settings.tidalHighFormat;
|
||||
final tidalHighFormat = settings.autoConvertDownloads
|
||||
? autoConvertLossySetting(
|
||||
format: settings.autoConvertFormat,
|
||||
bitrate: settings.autoConvertBitrate,
|
||||
)
|
||||
: settings.tidalHighFormat;
|
||||
_log.i(
|
||||
'Lossy 320kbps quality download, converting M4A to $tidalHighFormat...',
|
||||
);
|
||||
@@ -1138,6 +1174,11 @@ class _DownloadRun {
|
||||
? '${tidalHighFormat.split('_').last}kbps'
|
||||
: '320kbps';
|
||||
actualQuality = '$displayFormat $bitrateDisplay';
|
||||
result['audio_codec'] = format;
|
||||
result['format'] = format;
|
||||
result['bitrate'] = int.tryParse(tidalHighFormat.split('_').last);
|
||||
result.remove('actual_bit_depth');
|
||||
result.remove('actual_sample_rate');
|
||||
_log.i('Successfully converted M4A to $format: $convertedPath');
|
||||
|
||||
_log.i('Embedding metadata to $format...');
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/artist_utils.dart';
|
||||
import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
@@ -162,6 +163,12 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
libraryQualityLabelMode: _normalizeLibraryQualityLabelMode(
|
||||
loaded.libraryQualityLabelMode,
|
||||
),
|
||||
autoConvertFormat: normalizeAutoConvertFormat(
|
||||
loaded.autoConvertFormat,
|
||||
),
|
||||
autoConvertBitrate: normalizeAutoConvertBitrate(
|
||||
loaded.autoConvertBitrate,
|
||||
),
|
||||
defaultService: loaded.defaultService,
|
||||
searchProvider: loaded.searchProvider,
|
||||
extensionVerificationBrowserMode:
|
||||
@@ -735,6 +742,25 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setAutoConvertDownloads(bool enabled) {
|
||||
state = state.copyWith(autoConvertDownloads: enabled);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setAutoConvertFormat(String format) {
|
||||
state = state.copyWith(
|
||||
autoConvertFormat: normalizeAutoConvertFormat(format),
|
||||
);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setAutoConvertBitrate(String bitrate) {
|
||||
state = state.copyWith(
|
||||
autoConvertBitrate: normalizeAutoConvertBitrate(bitrate),
|
||||
);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setUseAllFilesAccess(bool enabled) {
|
||||
state = state.copyWith(useAllFilesAccess: enabled);
|
||||
_saveSettings();
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/screens/settings/download_fallback_extensions_page.dart';
|
||||
import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
||||
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
@@ -109,10 +111,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAudioQuality(quality.id),
|
||||
showDivider:
|
||||
quality != qualityOptions.last ||
|
||||
(usesTidalCompatibilityOptions &&
|
||||
settings.audioQuality == 'HIGH'),
|
||||
showDivider: true,
|
||||
),
|
||||
if (usesTidalCompatibilityOptions &&
|
||||
settings.audioQuality == 'HIGH')
|
||||
@@ -128,9 +127,46 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
ref,
|
||||
settings.tidalHighFormat,
|
||||
),
|
||||
showDivider: false,
|
||||
showDivider: true,
|
||||
),
|
||||
],
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.auto_fix_high_outlined,
|
||||
title: context.l10n.downloadAutoConvert,
|
||||
subtitle: context.l10n.downloadAutoConvertSubtitle,
|
||||
value: settings.autoConvertDownloads,
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAutoConvertDownloads(value),
|
||||
showDivider: settings.autoConvertDownloads,
|
||||
),
|
||||
if (settings.autoConvertDownloads) ...[
|
||||
SettingsItem(
|
||||
icon: Icons.audio_file_outlined,
|
||||
title: context.l10n.downloadAutoConvertFormat,
|
||||
subtitle: autoConvertFormatLabel(
|
||||
settings.autoConvertFormat,
|
||||
),
|
||||
onTap: () => _showAutoConvertFormatPicker(
|
||||
context,
|
||||
ref,
|
||||
settings.autoConvertFormat,
|
||||
),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.speed_outlined,
|
||||
title: context.l10n.downloadAutoConvertBitrate,
|
||||
subtitle: normalizeAutoConvertBitrate(
|
||||
settings.autoConvertBitrate,
|
||||
).replaceAll('k', ' kbps'),
|
||||
onTap: () => _showAutoConvertBitratePicker(
|
||||
context,
|
||||
ref,
|
||||
settings.autoConvertBitrate,
|
||||
),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -497,6 +533,91 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showAutoConvertFormatPicker(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String current,
|
||||
) {
|
||||
final normalizedCurrent = normalizeAutoConvertFormat(current);
|
||||
showAppBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
title: context.l10n.downloadAutoConvertFormat,
|
||||
subtitle: context.l10n.downloadAutoConvertFormatSubtitle,
|
||||
builder: (sheetContext) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final format in autoConvertFormats)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
format == 'opus'
|
||||
? Icons.graphic_eq
|
||||
: format == 'aac'
|
||||
? Icons.album_outlined
|
||||
: Icons.audiotrack,
|
||||
),
|
||||
title: Text(autoConvertFormatLabel(format)),
|
||||
subtitle: Text(switch (format) {
|
||||
'aac' => context.l10n.downloadAutoConvertM4aSubtitle,
|
||||
'opus' => context.l10n.downloadAutoConvertOpusSubtitle,
|
||||
_ => context.l10n.downloadAutoConvertMp3Subtitle,
|
||||
}),
|
||||
trailing: normalizedCurrent == format
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: Theme.of(sheetContext).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAutoConvertFormat(format);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAutoConvertBitratePicker(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String current,
|
||||
) {
|
||||
final normalizedCurrent = normalizeAutoConvertBitrate(current);
|
||||
showAppBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
title: context.l10n.downloadAutoConvertBitrate,
|
||||
subtitle: context.l10n.downloadAutoConvertBitrateSubtitle,
|
||||
builder: (sheetContext) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final bitrate in autoConvertBitrates)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.speed_outlined),
|
||||
title: Text(bitrate.replaceAll('k', ' kbps')),
|
||||
trailing: normalizedCurrent == bitrate
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: Theme.of(sheetContext).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAutoConvertBitrate(bitrate);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showNetworkModePicker(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
|
||||
@@ -321,6 +321,19 @@ class SettingsSearchCatalog {
|
||||
title: l10n.downloadLossyFormat,
|
||||
keywords: const ['mp3', 'aac', 'opus'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.auto_fix_high_outlined,
|
||||
title: l10n.downloadAutoConvert,
|
||||
subtitle: l10n.downloadAutoConvertSubtitle,
|
||||
keywords: const [
|
||||
'automatic conversion',
|
||||
'mp3',
|
||||
'm4a',
|
||||
'aac',
|
||||
'opus',
|
||||
'bitrate',
|
||||
],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.wifi,
|
||||
title: l10n.settingsDownloadNetwork,
|
||||
|
||||
@@ -20,6 +20,9 @@ class DownloadRequestPayload {
|
||||
final bool embedReplayGain;
|
||||
final bool postProcessingEnabled;
|
||||
final String tidalHighFormat;
|
||||
final bool autoConvertDownloads;
|
||||
final String autoConvertFormat;
|
||||
final String autoConvertBitrate;
|
||||
final int trackNumber;
|
||||
final int playlistPosition;
|
||||
final int discNumber;
|
||||
@@ -73,6 +76,9 @@ class DownloadRequestPayload {
|
||||
this.embedReplayGain = false,
|
||||
this.postProcessingEnabled = false,
|
||||
this.tidalHighFormat = 'mp3_320',
|
||||
this.autoConvertDownloads = false,
|
||||
this.autoConvertFormat = 'mp3',
|
||||
this.autoConvertBitrate = '320k',
|
||||
this.trackNumber = 0,
|
||||
this.playlistPosition = 0,
|
||||
this.discNumber = 0,
|
||||
@@ -128,6 +134,9 @@ class DownloadRequestPayload {
|
||||
'embed_replaygain': embedReplayGain,
|
||||
'post_processing_enabled': postProcessingEnabled,
|
||||
'tidal_high_format': tidalHighFormat,
|
||||
'auto_convert_downloads': autoConvertDownloads,
|
||||
'auto_convert_format': autoConvertFormat,
|
||||
'auto_convert_bitrate': autoConvertBitrate,
|
||||
'track_number': trackNumber,
|
||||
'playlist_position': playlistPosition,
|
||||
'disc_number': discNumber,
|
||||
@@ -187,6 +196,9 @@ class DownloadRequestPayload {
|
||||
embedReplayGain: embedReplayGain,
|
||||
postProcessingEnabled: postProcessingEnabled,
|
||||
tidalHighFormat: tidalHighFormat,
|
||||
autoConvertDownloads: autoConvertDownloads,
|
||||
autoConvertFormat: autoConvertFormat,
|
||||
autoConvertBitrate: autoConvertBitrate,
|
||||
trackNumber: trackNumber,
|
||||
playlistPosition: playlistPosition,
|
||||
discNumber: discNumber,
|
||||
|
||||
@@ -285,6 +285,69 @@ String displayFormatForLossyFormat(String format) {
|
||||
return format == 'aac' ? 'AAC' : format.toUpperCase();
|
||||
}
|
||||
|
||||
const List<String> autoConvertFormats = ['mp3', 'aac', 'opus'];
|
||||
const List<String> autoConvertBitrates = ['128k', '192k', '256k', '320k'];
|
||||
|
||||
String normalizeAutoConvertFormat(String value) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (normalized == 'm4a') return 'aac';
|
||||
return autoConvertFormats.contains(normalized) ? normalized : 'mp3';
|
||||
}
|
||||
|
||||
String normalizeAutoConvertBitrate(String value) {
|
||||
final match = RegExp(r'(\d+)').firstMatch(value);
|
||||
final normalized = match == null ? '' : '${match.group(1)}k';
|
||||
return autoConvertBitrates.contains(normalized) ? normalized : '320k';
|
||||
}
|
||||
|
||||
int autoConvertBitrateKbps(String value) {
|
||||
return int.parse(normalizeAutoConvertBitrate(value).replaceAll('k', ''));
|
||||
}
|
||||
|
||||
String autoConvertLossySetting({
|
||||
required String format,
|
||||
required String bitrate,
|
||||
}) {
|
||||
final normalizedFormat = normalizeAutoConvertFormat(format);
|
||||
final normalizedBitrate = autoConvertBitrateKbps(bitrate);
|
||||
return '${normalizedFormat}_$normalizedBitrate';
|
||||
}
|
||||
|
||||
String autoConvertFormatLabel(String format) {
|
||||
return switch (normalizeAutoConvertFormat(format)) {
|
||||
'aac' => 'M4A (AAC)',
|
||||
'opus' => 'Opus',
|
||||
_ => 'MP3',
|
||||
};
|
||||
}
|
||||
|
||||
bool autoConversionAlreadySatisfied({
|
||||
required String? filePath,
|
||||
String? fileName,
|
||||
required String targetFormat,
|
||||
required String targetBitrate,
|
||||
String? quality,
|
||||
int? bitrateKbps,
|
||||
}) {
|
||||
final sourceFormat = audioFormatForPath(filePath, fileName: fileName);
|
||||
final normalizedTarget = normalizeAutoConvertFormat(targetFormat);
|
||||
final matchesFormat = switch (normalizedTarget) {
|
||||
'aac' => sourceFormat == 'AAC' || sourceFormat == 'M4A',
|
||||
'opus' => sourceFormat == 'OPUS',
|
||||
_ => sourceFormat == 'MP3',
|
||||
};
|
||||
if (!matchesFormat) return false;
|
||||
|
||||
final targetKbps = autoConvertBitrateKbps(targetBitrate);
|
||||
if (bitrateKbps != null && bitrateKbps > 0) {
|
||||
return bitrateKbps == targetKbps;
|
||||
}
|
||||
return RegExp(
|
||||
'\\b$targetKbps\\s*kbps\\b',
|
||||
caseSensitive: false,
|
||||
).hasMatch(quality ?? '');
|
||||
}
|
||||
|
||||
String? resolveDisplayQuality({
|
||||
required String? filePath,
|
||||
String? fileName,
|
||||
|
||||
Reference in New Issue
Block a user