From 43469a7ef2406b254e5a09b6482810cc8bc0d4a1 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 6 Apr 2026 02:55:03 +0700 Subject: [PATCH] feat: add configurable extension download fallback --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 7 + go_backend/exports.go | 24 ++ go_backend/exports_test.go | 15 ++ go_backend/extension_providers.go | 67 +++++ go_backend/extension_providers_test.go | 49 ++++ ios/Runner/AppDelegate.swift | 7 + lib/l10n/app_localizations.dart | 30 +++ lib/l10n/app_localizations_de.dart | 18 ++ lib/l10n/app_localizations_en.dart | 18 ++ lib/l10n/app_localizations_es.dart | 18 ++ lib/l10n/app_localizations_fr.dart | 18 ++ lib/l10n/app_localizations_hi.dart | 18 ++ lib/l10n/app_localizations_id.dart | 18 ++ lib/l10n/app_localizations_ja.dart | 18 ++ lib/l10n/app_localizations_ko.dart | 18 ++ lib/l10n/app_localizations_nl.dart | 18 ++ lib/l10n/app_localizations_pt.dart | 18 ++ lib/l10n/app_localizations_ru.dart | 18 ++ lib/l10n/app_localizations_tr.dart | 18 ++ lib/l10n/app_localizations_zh.dart | 18 ++ lib/l10n/arb/app_en.arb | 20 ++ lib/l10n/arb/app_id.arb | 20 ++ lib/models/settings.dart | 7 + lib/models/settings.g.dart | 5 + lib/providers/settings_provider.dart | 50 +++- .../download_fallback_extensions_page.dart | 250 ++++++++++++++++++ lib/screens/settings/extensions_page.dart | 71 ++++- lib/services/platform_bridge.dart | 9 + 28 files changed, 863 insertions(+), 2 deletions(-) create mode 100644 lib/screens/settings/download_fallback_extensions_page.dart diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index ccfb4500..bb415242 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2965,6 +2965,13 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + "setDownloadFallbackExtensionIds" -> { + val extensionIdsJson = call.argument("extension_ids") ?: "" + withContext(Dispatchers.IO) { + Gobackend.setExtensionFallbackProviderIDsJSON(extensionIdsJson) + } + result.success(null) + } "setMetadataProviderPriority" -> { val priorityJson = call.argument("priority") ?: "[]" withContext(Dispatchers.IO) { diff --git a/go_backend/exports.go b/go_backend/exports.go index 51392007..bb13a886 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -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 { diff --git a/go_backend/exports_test.go b/go_backend/exports_test.go index 5e6e32b5..d4243006 100644 --- a/go_backend/exports_test.go +++ b/go_backend/exports_test.go @@ -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", diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 1821d8f3..d00bd886 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -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) { diff --git a/go_backend/extension_providers_test.go b/go_backend/extension_providers_test.go index 5c59f962..b9468897 100644 --- a/go_backend/extension_providers_test.go +++ b/go_backend/extension_providers_test.go @@ -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 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index d89ffcfa..cc5ef880 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -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] diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 4b0e665b..a7806009 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 2a2bf390..b1d4007e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0b589b64..2d0c90fb 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 96051fdd..05882846 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 63884624..b9e5720b 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 723e03a0..7a7a1cdd 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -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'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 77cfef69..f1ddf98d 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 091ada88..8d285bfd 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -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 => 'ダウンロードプロバイダーの拡張はありません'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 3a94834e..2049cd0a 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -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'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index df016151..5a27c088 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 7044bf0f..b9869d31 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 937ffc90..c8b48090 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -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 => 'Нет расширений с провайдером загрузки'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 243312f9..b9215b93 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 0028c509..d5348147 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -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'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index d182a6e3..c041db5b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -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" diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 714dc0ef..6afb1b13 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -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" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index fe172cb1..10c8f05d 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -33,6 +33,7 @@ class AppSettings { final bool askQualityBeforeDownload; final bool enableLogging; final bool useExtensionProviders; + final List? 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? 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), diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 70c5f9f9..efa2cb6d 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -35,6 +35,10 @@ AppSettings _$AppSettingsFromJson(Map 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?) + ?.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 _$AppSettingsToJson( 'askQualityBeforeDownload': instance.askQualityBeforeDownload, 'enableLogging': instance.enableLogging, 'useExtensionProviders': instance.useExtensionProviders, + 'downloadFallbackExtensionIds': instance.downloadFallbackExtensionIds, 'searchProvider': instance.searchProvider, 'homeFeedProvider': instance.homeFeedProvider, 'separateSingles': instance.separateSingles, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index bd54b188..f9da22cf 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -35,9 +35,19 @@ class SettingsNotifier extends Notifier { final prefs = await _prefs; final json = prefs.getString(_settingsKey); if (json != null) { - state = AppSettings.fromJson( + final loaded = AppSettings.fromJson( Map.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 { _syncLyricsSettingsToBackend(); _syncNetworkCompatibilitySettingsToBackend(); + _syncExtensionFallbackSettingsToBackend(); } void _syncLyricsSettingsToBackend() { @@ -83,6 +94,16 @@ class SettingsNotifier extends Notifier { }); } + 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 _runMigrations(SharedPreferences prefs) async { final lastMigration = prefs.getInt(_migrationVersionKey) ?? 0; @@ -172,6 +193,22 @@ class SettingsNotifier extends Notifier { await _saveSettings(); } + List? _sanitizeDownloadFallbackExtensionIds(List? ids) { + if (ids == null) { + return null; + } + + final result = []; + for (final id in ids) { + final normalized = id.trim(); + if (normalized.isEmpty || result.contains(normalized)) { + continue; + } + result.add(normalized); + } + return result; + } + Future _cleanupRetiredSpotifySettings() async { final storedSecret = await _secureStorage.read( key: _spotifyClientSecretKey, @@ -390,6 +427,17 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setDownloadFallbackExtensionIds(List? 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(); diff --git a/lib/screens/settings/download_fallback_extensions_page.dart b/lib/screens/settings/download_fallback_extensions_page.dart new file mode 100644 index 00000000..5805c9d9 --- /dev/null +++ b/lib/screens/settings/download_fallback_extensions_page.dart @@ -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 createState() => + _DownloadFallbackExtensionsPageState(); +} + +class _DownloadFallbackExtensionsPageState + extends ConsumerState { + late List _extensions; + late Set _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 _confirmDiscard(BuildContext context) async { + final result = await showDialog( + 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)), + ); + } +} diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index b782954f..7e3c0017 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -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 { 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( + 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(); diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 5a269e84..19a96a5a 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -781,6 +781,15 @@ class PlatformBridge { return list.map((e) => e as String).toList(); } + static Future setDownloadFallbackExtensionIds( + List? extensionIds, + ) async { + _log.d('setDownloadFallbackExtensionIds: $extensionIds'); + await _channel.invokeMethod('setDownloadFallbackExtensionIds', { + 'extension_ids': extensionIds == null ? '' : jsonEncode(extensionIds), + }); + } + static Future setMetadataProviderPriority( List providerIds, ) async {