mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-24 00:34:07 +02:00
feat: add configurable extension download fallback
This commit is contained in:
@@ -2965,6 +2965,13 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"setDownloadFallbackExtensionIds" -> {
|
||||
val extensionIdsJson = call.argument<String>("extension_ids") ?: ""
|
||||
withContext(Dispatchers.IO) {
|
||||
Gobackend.setExtensionFallbackProviderIDsJSON(extensionIdsJson)
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"setMetadataProviderPriority" -> {
|
||||
val priorityJson = call.argument<String>("priority") ?: "[]"
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -2665,6 +2665,30 @@ func GetProviderPriorityJSON() (string, error) {
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func SetExtensionFallbackProviderIDsJSON(providerIDsJSON string) error {
|
||||
if strings.TrimSpace(providerIDsJSON) == "" {
|
||||
SetExtensionFallbackProviderIDs(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
var providerIDs []string
|
||||
if err := json.Unmarshal([]byte(providerIDsJSON), &providerIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
SetExtensionFallbackProviderIDs(providerIDs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetExtensionFallbackProviderIDsJSON() (string, error) {
|
||||
providerIDs := GetExtensionFallbackProviderIDs()
|
||||
jsonBytes, err := json.Marshal(providerIDs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func SetMetadataProviderPriorityJSON(priorityJSON string) error {
|
||||
var priority []string
|
||||
if err := json.Unmarshal([]byte(priorityJSON), &priority); err != nil {
|
||||
|
||||
@@ -2,6 +2,21 @@ package gobackend
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSetExtensionFallbackProviderIDsJSONEmptyStringResetsDefault(t *testing.T) {
|
||||
original := GetExtensionFallbackProviderIDs()
|
||||
defer SetExtensionFallbackProviderIDs(original)
|
||||
|
||||
SetExtensionFallbackProviderIDs([]string{"custom-ext"})
|
||||
|
||||
if err := SetExtensionFallbackProviderIDsJSON(""); err != nil {
|
||||
t.Fatalf("SetExtensionFallbackProviderIDsJSON returned error: %v", err)
|
||||
}
|
||||
|
||||
if got := GetExtensionFallbackProviderIDs(); got != nil {
|
||||
t.Fatalf("expected nil fallback provider list after reset, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDownloadSuccessResponsePrefersRequestedAlbumMetadata(t *testing.T) {
|
||||
req := DownloadRequest{
|
||||
TrackName: "Bonus Track",
|
||||
|
||||
@@ -676,6 +676,9 @@ func (m *extensionManager) SearchTracksWithExtensions(query string, limit int) (
|
||||
var providerPriority []string
|
||||
var providerPriorityMu sync.RWMutex
|
||||
|
||||
var extensionFallbackProviderIDs []string
|
||||
var extensionFallbackProviderIDsMu sync.RWMutex
|
||||
|
||||
var metadataProviderPriority []string
|
||||
var metadataProviderPriorityMu sync.RWMutex
|
||||
|
||||
@@ -701,6 +704,65 @@ func GetProviderPriority() []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func SetExtensionFallbackProviderIDs(providerIDs []string) {
|
||||
extensionFallbackProviderIDsMu.Lock()
|
||||
defer extensionFallbackProviderIDsMu.Unlock()
|
||||
|
||||
if providerIDs == nil {
|
||||
extensionFallbackProviderIDs = nil
|
||||
GoLog("[Extension] Extension fallback providers reset to default (all enabled download extensions)\n")
|
||||
return
|
||||
}
|
||||
|
||||
sanitized := make([]string, 0, len(providerIDs))
|
||||
seen := map[string]struct{}{}
|
||||
for _, providerID := range providerIDs {
|
||||
providerID = strings.TrimSpace(providerID)
|
||||
if providerID == "" || isBuiltInProvider(strings.ToLower(providerID)) {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[providerID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[providerID] = struct{}{}
|
||||
sanitized = append(sanitized, providerID)
|
||||
}
|
||||
|
||||
extensionFallbackProviderIDs = sanitized
|
||||
GoLog("[Extension] Extension fallback providers set: %v\n", sanitized)
|
||||
}
|
||||
|
||||
func GetExtensionFallbackProviderIDs() []string {
|
||||
extensionFallbackProviderIDsMu.RLock()
|
||||
defer extensionFallbackProviderIDsMu.RUnlock()
|
||||
|
||||
if extensionFallbackProviderIDs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]string, len(extensionFallbackProviderIDs))
|
||||
copy(result, extensionFallbackProviderIDs)
|
||||
return result
|
||||
}
|
||||
|
||||
func isExtensionFallbackAllowed(providerID string) bool {
|
||||
if isBuiltInProvider(strings.ToLower(providerID)) {
|
||||
return true
|
||||
}
|
||||
|
||||
allowed := GetExtensionFallbackProviderIDs()
|
||||
if allowed == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, allowedProviderID := range allowed {
|
||||
if allowedProviderID == providerID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SetMetadataProviderPriority(providerIDs []string) {
|
||||
metadataProviderPriorityMu.Lock()
|
||||
defer metadataProviderPriorityMu.Unlock()
|
||||
@@ -1308,6 +1370,11 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
continue
|
||||
}
|
||||
|
||||
if !isBuiltInProvider(providerIDNormalized) && !isExtensionFallbackAllowed(providerID) {
|
||||
GoLog("[DownloadWithExtensionFallback] Skipping extension provider %s (not enabled for fallback)\n", providerID)
|
||||
continue
|
||||
}
|
||||
|
||||
GoLog("[DownloadWithExtensionFallback] Trying provider: %s\n", providerID)
|
||||
|
||||
if isBuiltInProvider(providerIDNormalized) {
|
||||
|
||||
@@ -19,6 +19,55 @@ func TestSetMetadataProviderPriorityAddsBuiltIns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetExtensionFallbackProviderIDsSkipsBuiltInsAndDuplicates(t *testing.T) {
|
||||
original := GetExtensionFallbackProviderIDs()
|
||||
defer SetExtensionFallbackProviderIDs(original)
|
||||
|
||||
SetExtensionFallbackProviderIDs([]string{"ext-a", "tidal", "ext-a", " ext-b "})
|
||||
|
||||
got := GetExtensionFallbackProviderIDs()
|
||||
want := []string{"ext-a", "ext-b"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected fallback provider length: got %v want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("unexpected fallback provider at %d: got %v want %v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsExtensionFallbackAllowedDefaultsToAllExtensions(t *testing.T) {
|
||||
original := GetExtensionFallbackProviderIDs()
|
||||
defer SetExtensionFallbackProviderIDs(original)
|
||||
|
||||
SetExtensionFallbackProviderIDs(nil)
|
||||
|
||||
if !isExtensionFallbackAllowed("custom-ext") {
|
||||
t.Fatal("expected custom extension to be allowed when no fallback allowlist is configured")
|
||||
}
|
||||
if !isExtensionFallbackAllowed("qobuz") {
|
||||
t.Fatal("expected built-in provider to remain allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsExtensionFallbackAllowedRespectsAllowlist(t *testing.T) {
|
||||
original := GetExtensionFallbackProviderIDs()
|
||||
defer SetExtensionFallbackProviderIDs(original)
|
||||
|
||||
SetExtensionFallbackProviderIDs([]string{"allowed-ext"})
|
||||
|
||||
if !isExtensionFallbackAllowed("allowed-ext") {
|
||||
t.Fatal("expected explicitly allowed extension to be permitted")
|
||||
}
|
||||
if isExtensionFallbackAllowed("blocked-ext") {
|
||||
t.Fatal("expected extension outside allowlist to be blocked")
|
||||
}
|
||||
if !isExtensionFallbackAllowed("deezer") {
|
||||
t.Fatal("expected built-in provider to ignore extension allowlist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) {
|
||||
originalPriority := GetMetadataProviderPriority()
|
||||
originalSearch := searchBuiltInMetadataTracksFunc
|
||||
|
||||
@@ -607,6 +607,13 @@ import Gobackend // Import Go framework
|
||||
let response = GobackendGetProviderPriorityJSON(&error)
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "setDownloadFallbackExtensionIds":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let extensionIdsJson = args["extension_ids"] as? String ?? ""
|
||||
GobackendSetExtensionFallbackProviderIDsJSON(extensionIdsJson, &error)
|
||||
if let error = error { throw error }
|
||||
return nil
|
||||
|
||||
case "setMetadataProviderPriority":
|
||||
let args = call.arguments as! [String: Any]
|
||||
|
||||
@@ -1738,6 +1738,24 @@ abstract class AppLocalizations {
|
||||
/// **'If a track is not available on the first provider, the app will automatically try the next one.'**
|
||||
String get providerPriorityInfo;
|
||||
|
||||
/// Section title for choosing which download extensions can be used as fallback providers
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Extension Fallback'**
|
||||
String get providerPriorityFallbackExtensionsTitle;
|
||||
|
||||
/// Section description for extension fallback selection
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'**
|
||||
String get providerPriorityFallbackExtensionsDescription;
|
||||
|
||||
/// Hint below the extension fallback selection list
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Only enabled extensions with download-provider capability are listed here.'**
|
||||
String get providerPriorityFallbackExtensionsHint;
|
||||
|
||||
/// Label for built-in providers (Tidal/Qobuz)
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2644,6 +2662,18 @@ abstract class AppLocalizations {
|
||||
/// **'Set download service order'**
|
||||
String get extensionsDownloadPrioritySubtitle;
|
||||
|
||||
/// Setting and page title for choosing which download extensions can be used during fallback
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fallback Extensions'**
|
||||
String get extensionsFallbackTitle;
|
||||
|
||||
/// Subtitle for download fallback extensions menu
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose which installed download extensions can be used as fallback'**
|
||||
String get extensionsFallbackSubtitle;
|
||||
|
||||
/// Empty state - no download providers
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -940,6 +940,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'Wenn kein Titel bei dem ersten Anbieter nicht verfügbar ist, wird die App automatisch den nächsten versuchen.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Integriert';
|
||||
|
||||
@@ -1438,6 +1449,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get extensionsDownloadPrioritySubtitle =>
|
||||
'Download-Service-Reihenfolge festlegen';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'Keine Erweiterungen mit Download-Provider';
|
||||
|
||||
@@ -926,6 +926,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Built-in';
|
||||
|
||||
@@ -1415,6 +1426,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -926,6 +926,17 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Built-in';
|
||||
|
||||
@@ -1415,6 +1426,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -928,6 +928,17 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Built-in';
|
||||
|
||||
@@ -1417,6 +1428,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -926,6 +926,17 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Built-in';
|
||||
|
||||
@@ -1415,6 +1426,13 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -930,6 +930,17 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Fallback Ekstensi';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Pilih ekstensi unduhan terpasang mana yang boleh dipakai saat fallback otomatis. Provider bawaan tetap mengikuti urutan prioritas di atas.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Hanya ekstensi aktif dengan kemampuan download provider yang ditampilkan di sini.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Bawaan';
|
||||
|
||||
@@ -1423,6 +1434,13 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get extensionsDownloadPrioritySubtitle =>
|
||||
'Atur urutan layanan unduhan';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Pilih ekstensi unduhan terpasang yang boleh dipakai saat fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'Tidak ada ekstensi dengan provider unduhan';
|
||||
|
||||
@@ -920,6 +920,17 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => '内蔵';
|
||||
|
||||
@@ -1409,6 +1420,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'ダウンロードサービスの順序を設定';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider => 'ダウンロードプロバイダーの拡張はありません';
|
||||
|
||||
|
||||
@@ -908,6 +908,17 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Built-in';
|
||||
|
||||
@@ -1395,6 +1406,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -926,6 +926,17 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Built-in';
|
||||
|
||||
@@ -1415,6 +1426,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -926,6 +926,17 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Built-in';
|
||||
|
||||
@@ -1415,6 +1426,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -940,6 +940,17 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'Если трек не доступен у первого провайдера, приложение автоматически попробует следующий.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Встроенные';
|
||||
|
||||
@@ -1439,6 +1450,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get extensionsDownloadPrioritySubtitle =>
|
||||
'Установка порядок сервисов скачивания';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'Нет расширений с провайдером загрузки';
|
||||
|
||||
@@ -931,6 +931,17 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'Eğer bir şarkı ilk hizmette mevcut değilse uygulama otomatik olarak bir sonrakini deneyecektir.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Dahili';
|
||||
|
||||
@@ -1421,6 +1432,13 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -926,6 +926,17 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get providerPriorityInfo =>
|
||||
'If a track is not available on the first provider, the app will automatically try the next one.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsDescription =>
|
||||
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
|
||||
|
||||
@override
|
||||
String get providerPriorityFallbackExtensionsHint =>
|
||||
'Only enabled extensions with download-provider capability are listed here.';
|
||||
|
||||
@override
|
||||
String get providerBuiltIn => 'Built-in';
|
||||
|
||||
@@ -1415,6 +1426,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get extensionsDownloadPrioritySubtitle => 'Set download service order';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackTitle => 'Fallback Extensions';
|
||||
|
||||
@override
|
||||
String get extensionsFallbackSubtitle =>
|
||||
'Choose which installed download extensions can be used as fallback';
|
||||
|
||||
@override
|
||||
String get extensionsNoDownloadProvider =>
|
||||
'No extensions with download provider';
|
||||
|
||||
@@ -1203,6 +1203,18 @@
|
||||
"@providerPriorityInfo": {
|
||||
"description": "Info tip about fallback behavior"
|
||||
},
|
||||
"providerPriorityFallbackExtensionsTitle": "Extension Fallback",
|
||||
"@providerPriorityFallbackExtensionsTitle": {
|
||||
"description": "Section title for choosing which download extensions can be used as fallback providers"
|
||||
},
|
||||
"providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.",
|
||||
"@providerPriorityFallbackExtensionsDescription": {
|
||||
"description": "Section description for extension fallback selection"
|
||||
},
|
||||
"providerPriorityFallbackExtensionsHint": "Only enabled extensions with download-provider capability are listed here.",
|
||||
"@providerPriorityFallbackExtensionsHint": {
|
||||
"description": "Hint below the extension fallback selection list"
|
||||
},
|
||||
"providerBuiltIn": "Built-in",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
@@ -1857,6 +1869,14 @@
|
||||
"@extensionsDownloadPrioritySubtitle": {
|
||||
"description": "Subtitle for download priority"
|
||||
},
|
||||
"extensionsFallbackTitle": "Fallback Extensions",
|
||||
"@extensionsFallbackTitle": {
|
||||
"description": "Setting and page title for choosing which download extensions can be used during fallback"
|
||||
},
|
||||
"extensionsFallbackSubtitle": "Choose which installed download extensions can be used as fallback",
|
||||
"@extensionsFallbackSubtitle": {
|
||||
"description": "Subtitle for download fallback extensions menu"
|
||||
},
|
||||
"extensionsNoDownloadProvider": "No extensions with download provider",
|
||||
"@extensionsNoDownloadProvider": {
|
||||
"description": "Empty state - no download providers"
|
||||
|
||||
@@ -1119,6 +1119,18 @@
|
||||
"@providerPriorityInfo": {
|
||||
"description": "Info tip about fallback behavior"
|
||||
},
|
||||
"providerPriorityFallbackExtensionsTitle": "Fallback Ekstensi",
|
||||
"@providerPriorityFallbackExtensionsTitle": {
|
||||
"description": "Section title for choosing which download extensions can be used as fallback providers"
|
||||
},
|
||||
"providerPriorityFallbackExtensionsDescription": "Pilih ekstensi unduhan terpasang mana yang boleh dipakai saat fallback otomatis. Provider bawaan tetap mengikuti urutan prioritas di atas.",
|
||||
"@providerPriorityFallbackExtensionsDescription": {
|
||||
"description": "Section description for extension fallback selection"
|
||||
},
|
||||
"providerPriorityFallbackExtensionsHint": "Hanya ekstensi aktif dengan kemampuan download provider yang ditampilkan di sini.",
|
||||
"@providerPriorityFallbackExtensionsHint": {
|
||||
"description": "Hint below the extension fallback selection list"
|
||||
},
|
||||
"providerBuiltIn": "Bawaan",
|
||||
"@providerBuiltIn": {
|
||||
"description": "Label for built-in providers (Tidal/Qobuz)"
|
||||
@@ -1713,6 +1725,14 @@
|
||||
"@extensionsDownloadPrioritySubtitle": {
|
||||
"description": "Subtitle for download priority"
|
||||
},
|
||||
"extensionsFallbackTitle": "Fallback Extensions",
|
||||
"@extensionsFallbackTitle": {
|
||||
"description": "Setting and page title for choosing which download extensions can be used during fallback"
|
||||
},
|
||||
"extensionsFallbackSubtitle": "Pilih ekstensi unduhan terpasang yang boleh dipakai saat fallback",
|
||||
"@extensionsFallbackSubtitle": {
|
||||
"description": "Subtitle for download fallback extensions menu"
|
||||
},
|
||||
"extensionsNoDownloadProvider": "Tidak ada ekstensi dengan provider unduhan",
|
||||
"@extensionsNoDownloadProvider": {
|
||||
"description": "Empty state - no download providers"
|
||||
|
||||
@@ -33,6 +33,7 @@ class AppSettings {
|
||||
final bool askQualityBeforeDownload;
|
||||
final bool enableLogging;
|
||||
final bool useExtensionProviders;
|
||||
final List<String>? downloadFallbackExtensionIds;
|
||||
final String? searchProvider;
|
||||
final String? homeFeedProvider;
|
||||
final bool separateSingles;
|
||||
@@ -108,6 +109,7 @@ class AppSettings {
|
||||
this.askQualityBeforeDownload = true,
|
||||
this.enableLogging = false,
|
||||
this.useExtensionProviders = true,
|
||||
this.downloadFallbackExtensionIds,
|
||||
this.searchProvider,
|
||||
this.homeFeedProvider,
|
||||
this.separateSingles = false,
|
||||
@@ -170,6 +172,8 @@ class AppSettings {
|
||||
bool? askQualityBeforeDownload,
|
||||
bool? enableLogging,
|
||||
bool? useExtensionProviders,
|
||||
List<String>? downloadFallbackExtensionIds,
|
||||
bool clearDownloadFallbackExtensionIds = false,
|
||||
String? searchProvider,
|
||||
bool clearSearchProvider = false,
|
||||
String? homeFeedProvider,
|
||||
@@ -232,6 +236,9 @@ class AppSettings {
|
||||
enableLogging: enableLogging ?? this.enableLogging,
|
||||
useExtensionProviders:
|
||||
useExtensionProviders ?? this.useExtensionProviders,
|
||||
downloadFallbackExtensionIds: clearDownloadFallbackExtensionIds
|
||||
? null
|
||||
: (downloadFallbackExtensionIds ?? this.downloadFallbackExtensionIds),
|
||||
searchProvider: clearSearchProvider
|
||||
? null
|
||||
: (searchProvider ?? this.searchProvider),
|
||||
|
||||
@@ -35,6 +35,10 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
askQualityBeforeDownload: json['askQualityBeforeDownload'] as bool? ?? true,
|
||||
enableLogging: json['enableLogging'] as bool? ?? false,
|
||||
useExtensionProviders: json['useExtensionProviders'] as bool? ?? true,
|
||||
downloadFallbackExtensionIds:
|
||||
(json['downloadFallbackExtensionIds'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
searchProvider: json['searchProvider'] as String?,
|
||||
homeFeedProvider: json['homeFeedProvider'] as String?,
|
||||
separateSingles: json['separateSingles'] as bool? ?? false,
|
||||
@@ -105,6 +109,7 @@ Map<String, dynamic> _$AppSettingsToJson(
|
||||
'askQualityBeforeDownload': instance.askQualityBeforeDownload,
|
||||
'enableLogging': instance.enableLogging,
|
||||
'useExtensionProviders': instance.useExtensionProviders,
|
||||
'downloadFallbackExtensionIds': instance.downloadFallbackExtensionIds,
|
||||
'searchProvider': instance.searchProvider,
|
||||
'homeFeedProvider': instance.homeFeedProvider,
|
||||
'separateSingles': instance.separateSingles,
|
||||
|
||||
@@ -35,9 +35,19 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
final prefs = await _prefs;
|
||||
final json = prefs.getString(_settingsKey);
|
||||
if (json != null) {
|
||||
state = AppSettings.fromJson(
|
||||
final loaded = AppSettings.fromJson(
|
||||
Map<String, dynamic>.from(jsonDecode(json) as Map),
|
||||
);
|
||||
final sanitizedDownloadFallbackExtensionIds =
|
||||
_sanitizeDownloadFallbackExtensionIds(
|
||||
loaded.downloadFallbackExtensionIds,
|
||||
);
|
||||
state = loaded.copyWith(
|
||||
downloadFallbackExtensionIds: sanitizedDownloadFallbackExtensionIds,
|
||||
clearDownloadFallbackExtensionIds:
|
||||
loaded.downloadFallbackExtensionIds != null &&
|
||||
sanitizedDownloadFallbackExtensionIds == null,
|
||||
);
|
||||
|
||||
await _runMigrations(prefs);
|
||||
await _normalizeIosDownloadDirectoryIfNeeded();
|
||||
@@ -50,6 +60,7 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
|
||||
_syncLyricsSettingsToBackend();
|
||||
_syncNetworkCompatibilitySettingsToBackend();
|
||||
_syncExtensionFallbackSettingsToBackend();
|
||||
}
|
||||
|
||||
void _syncLyricsSettingsToBackend() {
|
||||
@@ -83,6 +94,16 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
});
|
||||
}
|
||||
|
||||
void _syncExtensionFallbackSettingsToBackend() {
|
||||
if (!PlatformBridge.supportsCoreBackend) return;
|
||||
|
||||
PlatformBridge.setDownloadFallbackExtensionIds(
|
||||
state.downloadFallbackExtensionIds,
|
||||
).catchError((Object e) {
|
||||
_log.w('Failed to sync extension fallback settings to backend: $e');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _runMigrations(SharedPreferences prefs) async {
|
||||
final lastMigration = prefs.getInt(_migrationVersionKey) ?? 0;
|
||||
|
||||
@@ -172,6 +193,22 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
await _saveSettings();
|
||||
}
|
||||
|
||||
List<String>? _sanitizeDownloadFallbackExtensionIds(List<String>? ids) {
|
||||
if (ids == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final result = <String>[];
|
||||
for (final id in ids) {
|
||||
final normalized = id.trim();
|
||||
if (normalized.isEmpty || result.contains(normalized)) {
|
||||
continue;
|
||||
}
|
||||
result.add(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _cleanupRetiredSpotifySettings() async {
|
||||
final storedSecret = await _secureStorage.read(
|
||||
key: _spotifyClientSecretKey,
|
||||
@@ -390,6 +427,17 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setDownloadFallbackExtensionIds(List<String>? extensionIds) {
|
||||
final sanitized = _sanitizeDownloadFallbackExtensionIds(extensionIds);
|
||||
state = state.copyWith(
|
||||
downloadFallbackExtensionIds: sanitized,
|
||||
clearDownloadFallbackExtensionIds:
|
||||
extensionIds == null && state.downloadFallbackExtensionIds != null,
|
||||
);
|
||||
_saveSettings();
|
||||
_syncExtensionFallbackSettingsToBackend();
|
||||
}
|
||||
|
||||
void setSeparateSingles(bool enabled) {
|
||||
state = state.copyWith(separateSingles: enabled);
|
||||
_saveSettings();
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/utils/app_bar_layout.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
||||
class DownloadFallbackExtensionsPage extends ConsumerStatefulWidget {
|
||||
const DownloadFallbackExtensionsPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<DownloadFallbackExtensionsPage> createState() =>
|
||||
_DownloadFallbackExtensionsPageState();
|
||||
}
|
||||
|
||||
class _DownloadFallbackExtensionsPageState
|
||||
extends ConsumerState<DownloadFallbackExtensionsPage> {
|
||||
late List<Extension> _extensions;
|
||||
late Set<String> _selectedExtensionIds;
|
||||
bool _hasChanges = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadExtensions();
|
||||
}
|
||||
|
||||
void _loadExtensions() {
|
||||
final extState = ref.read(extensionProvider);
|
||||
final settings = ref.read(settingsProvider);
|
||||
|
||||
_extensions = extState.extensions
|
||||
.where(
|
||||
(extension) => extension.enabled && extension.hasDownloadProvider,
|
||||
)
|
||||
.toList();
|
||||
|
||||
final savedIds = settings.downloadFallbackExtensionIds;
|
||||
if (savedIds == null) {
|
||||
_selectedExtensionIds = _extensions
|
||||
.map((extension) => extension.id)
|
||||
.toSet();
|
||||
} else {
|
||||
final allowedIds = _extensions.map((extension) => extension.id).toSet();
|
||||
_selectedExtensionIds = savedIds
|
||||
.where((extensionId) => allowedIds.contains(extensionId))
|
||||
.toSet();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final topPadding = normalizedHeaderTopPadding(context);
|
||||
|
||||
return PopScope(
|
||||
canPop: !_hasChanges,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) return;
|
||||
final shouldPop = await _confirmDiscard(context);
|
||||
if (shouldPop && context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 120 + topPadding,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
leading: IconButton(
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () async {
|
||||
if (_hasChanges) {
|
||||
final shouldPop = await _confirmDiscard(context);
|
||||
if (shouldPop && context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (_hasChanges)
|
||||
TextButton(
|
||||
onPressed: _saveChanges,
|
||||
child: Text(context.l10n.dialogSave),
|
||||
),
|
||||
],
|
||||
flexibleSpace: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxHeight = 120 + topPadding;
|
||||
final minHeight = kToolbarHeight + topPadding;
|
||||
final expandRatio =
|
||||
((constraints.maxHeight - minHeight) /
|
||||
(maxHeight - minHeight))
|
||||
.clamp(0.0, 1.0);
|
||||
final leftPadding = 56 - (32 * expandRatio);
|
||||
return FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
bottom: 16,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.extensionsFallbackTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
context.l10n.providerPriorityFallbackExtensionsDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_extensions.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.4,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.extensionsNoDownloadProvider,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_extensions.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
margin: EdgeInsets.zero,
|
||||
children: List.generate(_extensions.length, (index) {
|
||||
final extension = _extensions[index];
|
||||
final isSelected = _selectedExtensionIds.contains(
|
||||
extension.id,
|
||||
);
|
||||
return SettingsSwitchItem(
|
||||
icon: Icons.extension_rounded,
|
||||
title: extension.displayName,
|
||||
subtitle: extension.id,
|
||||
value: isSelected,
|
||||
showDivider: index != _extensions.length - 1,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value) {
|
||||
_selectedExtensionIds.add(extension.id);
|
||||
} else {
|
||||
_selectedExtensionIds.remove(extension.id);
|
||||
}
|
||||
_hasChanges = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_extensions.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Text(
|
||||
context.l10n.providerPriorityFallbackExtensionsHint,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _confirmDiscard(BuildContext context) async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.l10n.dialogDiscardChanges),
|
||||
content: Text(context.l10n.dialogUnsavedChanges),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(context.l10n.dialogDiscard),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
void _saveChanges() {
|
||||
final allExtensionIds = _extensions
|
||||
.map((extension) => extension.id)
|
||||
.toList();
|
||||
final selectedExtensionIds = allExtensionIds
|
||||
.where(_selectedExtensionIds.contains)
|
||||
.toList();
|
||||
final fallbackExtensionIds =
|
||||
selectedExtensionIds.length == allExtensionIds.length
|
||||
? null
|
||||
: selectedExtensionIds;
|
||||
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDownloadFallbackExtensionIds(fallbackExtensionIds);
|
||||
setState(() {
|
||||
_hasChanges = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarProviderPrioritySaved)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/explore_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/screens/settings/download_fallback_extensions_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/extension_detail_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/provider_priority_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/metadata_provider_priority_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/provider_priority_page.dart';
|
||||
import 'package:spotiflac_android/utils/app_bar_layout.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
||||
@@ -151,6 +152,7 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
_DownloadPriorityItem(),
|
||||
_DownloadFallbackItem(),
|
||||
_MetadataPriorityItem(),
|
||||
_SearchProviderSelector(),
|
||||
_HomeFeedProviderSelector(),
|
||||
@@ -588,6 +590,73 @@ class _MetadataPriorityItem extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DownloadFallbackItem extends ConsumerWidget {
|
||||
const _DownloadFallbackItem();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final extState = ref.watch(extensionProvider);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final hasDownloadExtensions = extState.extensions.any(
|
||||
(e) => e.enabled && e.hasDownloadProvider,
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: hasDownloadExtensions
|
||||
? () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const DownloadFallbackExtensionsPage(),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.alt_route,
|
||||
color: hasDownloadExtensions
|
||||
? colorScheme.onSurfaceVariant
|
||||
: colorScheme.outline,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.extensionsFallbackTitle,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: hasDownloadExtensions ? null : colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
hasDownloadExtensions
|
||||
? context.l10n.extensionsFallbackSubtitle
|
||||
: context.l10n.extensionsNoDownloadProvider,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: hasDownloadExtensions
|
||||
? colorScheme.onSurfaceVariant
|
||||
: colorScheme.outline,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchProviderSelector extends ConsumerWidget {
|
||||
const _SearchProviderSelector();
|
||||
|
||||
|
||||
@@ -781,6 +781,15 @@ class PlatformBridge {
|
||||
return list.map((e) => e as String).toList();
|
||||
}
|
||||
|
||||
static Future<void> setDownloadFallbackExtensionIds(
|
||||
List<String>? extensionIds,
|
||||
) async {
|
||||
_log.d('setDownloadFallbackExtensionIds: $extensionIds');
|
||||
await _channel.invokeMethod('setDownloadFallbackExtensionIds', {
|
||||
'extension_ids': extensionIds == null ? '' : jsonEncode(extensionIds),
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> setMetadataProviderPriority(
|
||||
List<String> providerIds,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user