diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go index ca8dad5d..3dda18c3 100644 --- a/go_backend/extension_manager.go +++ b/go_backend/extension_manager.go @@ -1000,6 +1000,7 @@ func (m *extensionManager) GetInstalledExtensionsJSON() (string, error) { HasLyricsProvider bool `json:"has_lyrics_provider"` SkipMetadataEnrichment bool `json:"skip_metadata_enrichment"` SkipLyrics bool `json:"skip_lyrics"` + StopProviderFallback bool `json:"stop_provider_fallback"` SearchBehavior *SearchBehaviorConfig `json:"search_behavior,omitempty"` TrackMatching *TrackMatchingConfig `json:"track_matching,omitempty"` PostProcessing *PostProcessingConfig `json:"post_processing,omitempty"` @@ -1057,6 +1058,7 @@ func (m *extensionManager) GetInstalledExtensionsJSON() (string, error) { HasLyricsProvider: ext.Manifest.IsLyricsProvider(), SkipMetadataEnrichment: ext.Manifest.SkipMetadataEnrichment, SkipLyrics: ext.Manifest.SkipLyrics, + StopProviderFallback: ext.Manifest.StopsProviderFallback(), SearchBehavior: ext.Manifest.SearchBehavior, TrackMatching: ext.Manifest.TrackMatching, PostProcessing: ext.Manifest.PostProcessing, diff --git a/go_backend/extension_manifest.go b/go_backend/extension_manifest.go index fedc4312..e1b92141 100644 --- a/go_backend/extension_manifest.go +++ b/go_backend/extension_manifest.go @@ -115,6 +115,7 @@ type ExtensionManifest struct { MinAppVersion string `json:"minAppVersion,omitempty"` SkipMetadataEnrichment bool `json:"skipMetadataEnrichment,omitempty"` SkipLyrics bool `json:"skipLyrics,omitempty"` + StopProviderFallback bool `json:"stopProviderFallback,omitempty"` SkipBuiltInFallback bool `json:"skipBuiltInFallback,omitempty"` SearchBehavior *SearchBehaviorConfig `json:"searchBehavior,omitempty"` URLHandler *URLHandlerConfig `json:"urlHandler,omitempty"` @@ -226,6 +227,13 @@ func (m *ExtensionManifest) IsLyricsProvider() bool { return m.HasType(ExtensionTypeLyricsProvider) } +func (m *ExtensionManifest) StopsProviderFallback() bool { + if m == nil { + return false + } + return m.StopProviderFallback || m.SkipBuiltInFallback +} + func (m *ExtensionManifest) IsDomainAllowed(domain string) bool { domain = strings.ToLower(strings.TrimSpace(domain)) for _, allowed := range m.Permissions.Network { diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 31974377..74709e75 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1664,7 +1664,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } var lastErr error - var skipBuiltIn bool + var stopProviderFallback bool var sourceExtensionLocked bool var sourceExtensionAvailability *ExtAvailabilityResult var sourceExtensionTrackID string @@ -1882,13 +1882,13 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro ext, err := extManager.GetExtension(req.Source) if err == nil && ext.Enabled && ext.Error == "" && ext.Manifest.IsDownloadProvider() { - skipBuiltIn = ext.Manifest.SkipBuiltInFallback + stopProviderFallback = ext.Manifest.StopsProviderFallback() provider := newExtensionProviderWrapper(ext) trackID := resolvePreferredTrackIDForExtension(ext, req, sourceExtensionTrackID) - GoLog("[DownloadWithExtensionFallback] Downloading from source extension with trackID: %s (skipBuiltInFallback: %v)\n", trackID, skipBuiltIn) + GoLog("[DownloadWithExtensionFallback] Downloading from source extension with trackID: %s (stopProviderFallback: %v)\n", trackID, stopProviderFallback) outputPath := buildOutputPathForExtension(req, ext) if req.ItemID != "" { @@ -1987,12 +1987,12 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } GoLog("[DownloadWithExtensionFallback] Source extension %s failed: %v\n", req.Source, lastErr) - if skipBuiltIn || sourceExtensionLocked { + if stopProviderFallback || sourceExtensionLocked { if sourceExtensionLocked { GoLog("[DownloadWithExtensionFallback] Source extension %s requested skip_fallback, not trying other providers\n", req.Source) return buildExtensionFallbackStoppedResponse(req.Source, sourceExtensionAvailability, lastErr), nil } - GoLog("[DownloadWithExtensionFallback] skipBuiltInFallback is true, not trying other providers\n") + GoLog("[DownloadWithExtensionFallback] stopProviderFallback is true, not trying other providers\n") return &DownloadResponse{ Success: false, Error: "Download failed: " + lastErr.Error(), diff --git a/go_backend/extension_test.go b/go_backend/extension_test.go index e80b14e3..10bdca60 100644 --- a/go_backend/extension_test.go +++ b/go_backend/extension_test.go @@ -44,6 +44,23 @@ func TestParseManifest_Valid(t *testing.T) { } } +func TestExtensionManifestStopsProviderFallback(t *testing.T) { + modernManifest := &ExtensionManifest{StopProviderFallback: true} + if !modernManifest.StopsProviderFallback() { + t.Fatal("expected stopProviderFallback to stop provider fallback") + } + + legacyManifest := &ExtensionManifest{SkipBuiltInFallback: true} + if !legacyManifest.StopsProviderFallback() { + t.Fatal("expected legacy skipBuiltInFallback to stop provider fallback") + } + + defaultManifest := &ExtensionManifest{} + if defaultManifest.StopsProviderFallback() { + t.Fatal("expected default manifest to allow provider fallback") + } +} + func TestParseManifest_MissingName(t *testing.T) { invalidManifest := `{ "version": "1.0.0", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e189f367..99ef3bda 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1795,7 +1795,7 @@ abstract class AppLocalizations { /// 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.'** + /// **'Choose which installed download extensions can be used during automatic fallback.'** String get providerPriorityFallbackExtensionsDescription; /// Hint below the extension fallback selection list @@ -1804,10 +1804,10 @@ abstract class AppLocalizations { /// **'Only enabled extensions with download-provider capability are listed here.'** String get providerPriorityFallbackExtensionsHint; - /// Label for built-in providers (Tidal/Qobuz) + /// Label for legacy providers kept for compatibility /// /// In en, this message translates to: - /// **'Built-in'** + /// **'Legacy'** String get providerBuiltIn; /// Label for extension-provided providers diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1c288c6f..26d74ef5 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -971,7 +971,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3a7f321f..7715abe4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -957,14 +957,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => 'Only enabled extensions with download-provider capability are listed here.'; @override - String get providerBuiltIn => 'Built-in'; + String get providerBuiltIn => 'Legacy'; @override String get providerExtension => 'Extension'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 1b3ba395..d5e5e86e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -957,7 +957,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index febc27f3..3c49607d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -959,7 +959,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index bf1d49ed..c6936cc5 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -957,7 +957,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index c0af574b..bdcd1de6 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -960,14 +960,14 @@ class AppLocalizationsId extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Pilih ekstensi unduhan terpasang mana yang boleh dipakai saat fallback otomatis. Provider bawaan tetap mengikuti urutan prioritas di atas.'; + 'Pilih ekstensi unduhan terpasang mana yang boleh dipakai saat fallback otomatis.'; @override String get providerPriorityFallbackExtensionsHint => 'Hanya ekstensi aktif dengan kemampuan download provider yang ditampilkan di sini.'; @override - String get providerBuiltIn => 'Bawaan'; + String get providerBuiltIn => 'Legacy'; @override String get providerExtension => 'Ekstensi'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 0cd8b269..0d90562c 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -950,7 +950,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index d5ef074c..b51d5c10 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -939,7 +939,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 91a9a831..1f705cc8 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -957,7 +957,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index a86eef76..8a48305f 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -957,7 +957,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index fba0594c..6d9df619 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -971,7 +971,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index a83ccb41..9ef834b5 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -969,7 +969,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index a68ed687..437cea45 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -957,7 +957,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b4635b68..514a5597 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1239,7 +1239,7 @@ "@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": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, @@ -1247,9 +1247,9 @@ "@providerPriorityFallbackExtensionsHint": { "description": "Hint below the extension fallback selection list" }, - "providerBuiltIn": "Built-in", + "providerBuiltIn": "Legacy", "@providerBuiltIn": { - "description": "Label for built-in providers (Tidal/Qobuz)" + "description": "Label for legacy providers kept for compatibility" }, "providerExtension": "Extension", "@providerExtension": { diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 830b74ad..11b11050 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -1143,7 +1143,7 @@ "@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": "Pilih ekstensi unduhan terpasang mana yang boleh dipakai saat fallback otomatis.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, @@ -1151,9 +1151,9 @@ "@providerPriorityFallbackExtensionsHint": { "description": "Hint below the extension fallback selection list" }, - "providerBuiltIn": "Bawaan", + "providerBuiltIn": "Legacy", "@providerBuiltIn": { - "description": "Label for built-in providers (Tidal/Qobuz)" + "description": "Label for legacy providers kept for compatibility" }, "providerExtension": "Ekstensi", "@providerExtension": { diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index f349ddaf..6ea261e9 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -71,6 +71,7 @@ class Extension { final bool hasLyricsProvider; final bool skipMetadataEnrichment; final bool skipLyrics; + final bool stopProviderFallback; final SearchBehavior? searchBehavior; final URLHandler? urlHandler; final TrackMatching? trackMatching; @@ -95,6 +96,7 @@ class Extension { this.hasLyricsProvider = false, this.skipMetadataEnrichment = false, this.skipLyrics = false, + this.stopProviderFallback = false, this.searchBehavior, this.urlHandler, this.trackMatching, @@ -132,6 +134,7 @@ class Extension { skipMetadataEnrichment: json['skip_metadata_enrichment'] as bool? ?? false, skipLyrics: json['skip_lyrics'] as bool? ?? false, + stopProviderFallback: json['stop_provider_fallback'] as bool? ?? false, searchBehavior: json['search_behavior'] != null ? SearchBehavior.fromJson( json['search_behavior'] as Map, @@ -172,6 +175,7 @@ class Extension { bool? hasLyricsProvider, bool? skipMetadataEnrichment, bool? skipLyrics, + bool? stopProviderFallback, SearchBehavior? searchBehavior, URLHandler? urlHandler, TrackMatching? trackMatching, @@ -197,6 +201,7 @@ class Extension { skipMetadataEnrichment: skipMetadataEnrichment ?? this.skipMetadataEnrichment, skipLyrics: skipLyrics ?? this.skipLyrics, + stopProviderFallback: stopProviderFallback ?? this.stopProviderFallback, searchBehavior: searchBehavior ?? this.searchBehavior, urlHandler: urlHandler ?? this.urlHandler, trackMatching: trackMatching ?? this.trackMatching, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index fc254b4e..ac810c2e 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -52,6 +52,7 @@ class SettingsNotifier extends Notifier { loaded.defaultSearchTab, ); state = loaded.copyWith( + useExtensionProviders: true, downloadFallbackExtensionIds: sanitizedDownloadFallbackExtensionIds, clearDownloadFallbackExtensionIds: loaded.downloadFallbackExtensionIds != null && @@ -148,6 +149,9 @@ class SettingsNotifier extends Notifier { state.defaultService == 'deezer') { state = state.copyWith(defaultService: ''); } + if (!state.useExtensionProviders) { + state = state.copyWith(useExtensionProviders: true); + } await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); await _saveSettings(); } @@ -446,7 +450,7 @@ class SettingsNotifier extends Notifier { } void setUseExtensionProviders(bool enabled) { - state = state.copyWith(useExtensionProviders: enabled); + state = state.copyWith(useExtensionProviders: true); _saveSettings(); } diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index 52b419d1..d2828382 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,15 +164,7 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = [ - 'Ldav', - 'Nico', - 'Feuerstern', - 'R4ND0MIZ3D', - 'Isra', - 'bigJr48', - 'Mick', - ]; + const donorNames = []; // Match SettingsGroup color logic final cardColor = isDark diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 21265387..91e269ce 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -323,7 +323,11 @@ class _DownloadSettingsPageState extends ConsumerState { final effectiveBuiltInServiceId = isBuiltInService ? settings.defaultService : replacedBuiltInServiceId; - final isBuiltInCompatibleService = effectiveBuiltInServiceId != null; + final extensionService = extensionDownloadProviders + .where((e) => e.id == settings.defaultService) + .firstOrNull; + final hasQualityOptions = extensionService?.qualityOptions.isNotEmpty ?? false; + final canAskQuality = effectiveBuiltInServiceId != null || hasQualityOptions; final isTidalService = effectiveBuiltInServiceId == 'tidal'; return PopScope( @@ -400,17 +404,17 @@ class _DownloadSettingsPageState extends ConsumerState { title: context.l10n.downloadAskBeforeDownload, subtitle: !hasDownloadProviders ? context.l10n.extensionsNoDownloadProvider - : isBuiltInCompatibleService + : canAskQuality ? context.l10n.downloadAskQualitySubtitle : context.l10n.downloadSelectServiceToEnable, value: settings.askQualityBeforeDownload, - enabled: hasDownloadProviders && isBuiltInCompatibleService, + enabled: hasDownloadProviders && canAskQuality, onChanged: (value) => ref .read(settingsProvider.notifier) .setAskQualityBeforeDownload(value), ), if (!settings.askQualityBeforeDownload && - isBuiltInCompatibleService) ...[ + canAskQuality) ...[ _QualityOption( title: context.l10n.qualityFlacLossless, subtitle: context.l10n.qualityFlacLosslessSubtitle, @@ -472,7 +476,7 @@ class _DownloadSettingsPageState extends ConsumerState { text: context.l10n.extensionsNoDownloadProvider, secondaryText: context.l10n.storeAddRepoDescription, ), - ] else if (!isBuiltInCompatibleService) ...[ + ] else if (!canAskQuality) ...[ Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Row( @@ -2110,37 +2114,59 @@ class _ServiceSelector extends ConsumerWidget { text: context.l10n.extensionsNoDownloadProvider, secondaryText: context.l10n.storeAddRepoDescription, ) - else ...[ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (final provider in builtInProviders) - _ServiceChip( - icon: resolveProviderIcon(provider.id), - label: provider.displayName, - isSelected: effectiveService == provider.id, - onTap: () => onChanged(provider.id), - ), - ], - ), - if (extensionProviders.isNotEmpty) ...[ - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (final extension in extensionProviders) + else + Builder( + builder: (context) { + final allItems = [ + for (final provider in builtInProviders) + _ServiceChip( + icon: resolveProviderIcon(provider.id), + label: provider.displayName, + isSelected: effectiveService == provider.id, + onTap: () => onChanged(provider.id), + ), + for (final ext in extensionProviders) _ServiceChip( icon: Icons.extension, - label: extension.displayName, - isSelected: effectiveService == extension.id, - onTap: () => onChanged(extension.id), + label: ext.displayName, + isSelected: effectiveService == ext.id, + onTap: () => onChanged(ext.id), ), - ], - ), - ], - ], + ]; + const perRow = 4; + final rows = []; + for (var i = 0; i < allItems.length; i += perRow) { + final chunk = allItems.sublist( + i, + (i + perRow > allItems.length) + ? allItems.length + : i + perRow, + ); + rows.add( + Row( + children: [ + for (var j = 0; j < perRow; j++) ...[ + if (j > 0) const SizedBox(width: 8), + Expanded( + child: j < chunk.length + ? chunk[j] + : const SizedBox.shrink(), + ), + ], + ], + ), + ); + } + return Column( + children: [ + for (var i = 0; i < rows.length; i++) ...[ + if (i > 0) const SizedBox(height: 8), + rows[i], + ], + ], + ); + }, + ), ], ), ); @@ -2247,6 +2273,9 @@ class _ServiceChip extends StatelessWidget { const SizedBox(height: 6), Text( label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, @@ -2254,7 +2283,6 @@ class _ServiceChip extends StatelessWidget { ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant, ), - overflow: TextOverflow.ellipsis, ), ], ), diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index 48d621eb..03ab215f 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -724,6 +724,9 @@ class _MetadataSourceSelector extends ConsumerWidget { final settings = ref.watch(settingsProvider); final extState = ref.watch(extensionProvider); final builtInProviders = builtInSearchProviderSpecs; + final extensionSearchProviders = extState.extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .toList(); final rawSearchProvider = settings.searchProvider?.trim() ?? ''; final isValidBuiltIn = isBuiltInSearchProvider(rawSearchProvider); @@ -757,6 +760,9 @@ class _MetadataSourceSelector extends ConsumerWidget { subtitle = context.l10n.optionsPrimaryProviderSubtitle; } + final hasAnyProvider = + builtInProviders.isNotEmpty || extensionSearchProviders.isNotEmpty; + return Padding( padding: const EdgeInsets.all(16), child: Column( @@ -778,28 +784,7 @@ class _MetadataSourceSelector extends ConsumerWidget { ), ), const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (final provider in builtInProviders) - _SourceChip( - icon: resolveProviderIcon( - provider.id, - tidalIcon: Icons.waves, - ), - label: provider.displayName, - isSelected: searchProvider == provider.id, - onTap: () { - ref - .read(settingsProvider.notifier) - .setSearchProvider(provider.id); - }, - ), - ], - ), - if (activeExtension != null) ...[ - const SizedBox(height: 12), + if (!hasAnyProvider) Row( children: [ Icon( @@ -810,15 +795,78 @@ class _MetadataSourceSelector extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - context.l10n.optionsSwitchBack, + context.l10n.optionsPrimaryProviderSubtitle, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ), ], + ) + else + Builder( + builder: (context) { + final allItems = [ + for (final provider in builtInProviders) + _SourceChip( + icon: resolveProviderIcon( + provider.id, + tidalIcon: Icons.waves, + ), + label: provider.displayName, + isSelected: searchProvider == provider.id, + onTap: () { + ref + .read(settingsProvider.notifier) + .setSearchProvider(provider.id); + }, + ), + for (final ext in extensionSearchProviders) + _SourceChip( + icon: Icons.extension, + label: ext.displayName, + isSelected: searchProvider == ext.id, + onTap: () { + ref + .read(settingsProvider.notifier) + .setSearchProvider(ext.id); + }, + ), + ]; + const perRow = 4; + final rows = []; + for (var i = 0; i < allItems.length; i += perRow) { + final chunk = allItems.sublist( + i, + (i + perRow > allItems.length) + ? allItems.length + : i + perRow, + ); + rows.add( + Row( + children: [ + for (var j = 0; j < perRow; j++) ...[ + if (j > 0) const SizedBox(width: 8), + Expanded( + child: j < chunk.length + ? chunk[j] + : const SizedBox.shrink(), + ), + ], + ], + ), + ); + } + return Column( + children: [ + for (var i = 0; i < rows.length; i++) ...[ + if (i > 0) const SizedBox(height: 8), + rows[i], + ], + ], + ); + }, ), - ], ], ), ); @@ -939,7 +987,7 @@ class _SourceChip extends StatelessWidget { onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18), + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -953,6 +1001,9 @@ class _SourceChip extends StatelessWidget { const SizedBox(height: 6), Text( label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,