diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5968b9c0..ca09b7ed 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2914,6 +2914,18 @@ abstract class AppLocalizations { /// **'Fly covers between screens, e.g. when opening the player'** String get appearanceHeroAnimationsSubtitle; + /// Toggle that forces backdrop blur on even when the device profile disabled it + /// + /// In en, this message translates to: + /// **'Always use blur effects'** + String get appearanceForceBlur; + + /// Subtitle for the force-blur toggle + /// + /// In en, this message translates to: + /// **'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'** + String get appearanceForceBlurSubtitle; + /// Button - clear all queue items /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e9af1365..b93bb920 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1598,6 +1598,13 @@ class AppLocalizationsDe extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Alles löschen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 04e48e85..47ea2c63 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1576,6 +1576,13 @@ class AppLocalizationsEn extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 894a26c4..6cd39fe1 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1576,6 +1576,13 @@ class AppLocalizationsEs extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 81c94e7b..43804375 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1620,6 +1620,13 @@ class AppLocalizationsFr extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Des fenêtres contextuelles apparaissent entre les écrans, par exemple lors de l\'ouverture du lecteur'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Tout effacer'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 63864856..2a157315 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1585,6 +1585,13 @@ class AppLocalizationsId extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Hapus Semua'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index c6af13a6..3857bc99 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1567,6 +1567,13 @@ class AppLocalizationsJa extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'すべて消去'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index c258d825..52af31e9 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1543,6 +1543,13 @@ class AppLocalizationsKo extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => '화면 간 이동 시 표지 이미지가 날아가는 애니메이션을 표시합니다 (예시: 플레이어를 실행할 경우)'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => '모두 지우기'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 0dbf7153..2e44cf73 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1576,6 +1576,13 @@ class AppLocalizationsPt extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 813e2050..86bfd1f9 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1591,6 +1591,13 @@ class AppLocalizationsRu extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Очистить всё'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index bd531119..5d3495bc 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1593,6 +1593,13 @@ class AppLocalizationsTr extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Tümünü Temizle'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index e7ea5f89..51f217f0 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1599,6 +1599,13 @@ class AppLocalizationsUk extends AppLocalizations { String get appearanceHeroAnimationsSubtitle => 'Fly covers between screens, e.g. when opening the player'; + @override + String get appearanceForceBlur => 'Always use blur effects'; + + @override + String get appearanceForceBlurSubtitle => + 'Enable the navigation bar blur even on devices where it is off by default. May cost performance.'; + @override String get queueClearAll => 'Усі'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 717b1917..f851a710 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2091,6 +2091,14 @@ "@appearanceHeroAnimationsSubtitle": { "description": "Subtitle for the Hero animations toggle" }, + "appearanceForceBlur": "Always use blur effects", + "@appearanceForceBlur": { + "description": "Toggle that forces backdrop blur on even when the device profile disabled it" + }, + "appearanceForceBlurSubtitle": "Enable the navigation bar blur even on devices where it is off by default. May cost performance.", + "@appearanceForceBlurSubtitle": { + "description": "Subtitle for the force-blur toggle" + }, "queueClearAll": "Clear All", "@queueClearAll": { "description": "Button - clear all queue items" diff --git a/lib/main.dart b/lib/main.dart index 8a7a3a95..25e83812 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -56,7 +56,7 @@ void main() { lowEndDeviceProvider.overrideWithValue( runtimeProfile.disableOverscrollEffects, ), - backdropBlurEnabledProvider.overrideWithValue( + deviceSupportsBackdropBlurProvider.overrideWithValue( runtimeProfile.enableBackdropBlur, ), initialSettingsProvider.overrideWithValue(bootstrapSettings), diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 0bca7018..17ec1ff0 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -49,6 +49,11 @@ class AppSettings { /// Shared-element (Hero) flights, e.g. the mini player artwork expanding /// into the full player. Off skips the flights entirely. final bool heroAnimationsEnabled; + + /// Forces the shell's backdrop blur on even when the startup runtime + /// profile disabled it for this device tier. Off means "follow the + /// device default". + final bool forceBackdropBlur; final String extensionVerificationBrowserMode; // 'external_first' or 'in_app_first' final String locale; @@ -139,6 +144,7 @@ class AppSettings { this.albumFolderStructure = 'artist_album', this.showExtensionStore = true, this.heroAnimationsEnabled = true, + this.forceBackdropBlur = false, this.extensionVerificationBrowserMode = 'in_app_first', this.locale = 'system', this.lyricsMode = 'embed', @@ -211,6 +217,7 @@ class AppSettings { String? albumFolderStructure, bool? showExtensionStore, bool? heroAnimationsEnabled, + bool? forceBackdropBlur, String? extensionVerificationBrowserMode, String? locale, String? lyricsMode, @@ -293,6 +300,7 @@ class AppSettings { showExtensionStore: showExtensionStore ?? this.showExtensionStore, heroAnimationsEnabled: heroAnimationsEnabled ?? this.heroAnimationsEnabled, + forceBackdropBlur: forceBackdropBlur ?? this.forceBackdropBlur, extensionVerificationBrowserMode: extensionVerificationBrowserMode ?? this.extensionVerificationBrowserMode, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index a70a4842..38550d00 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -50,6 +50,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( json['albumFolderStructure'] as String? ?? 'artist_album', showExtensionStore: json['showExtensionStore'] as bool? ?? true, heroAnimationsEnabled: json['heroAnimationsEnabled'] as bool? ?? true, + forceBackdropBlur: json['forceBackdropBlur'] as bool? ?? false, extensionVerificationBrowserMode: json['extensionVerificationBrowserMode'] as String? ?? 'in_app_first', locale: json['locale'] as String? ?? 'system', @@ -133,6 +134,7 @@ Map _$AppSettingsToJson( 'albumFolderStructure': instance.albumFolderStructure, 'showExtensionStore': instance.showExtensionStore, 'heroAnimationsEnabled': instance.heroAnimationsEnabled, + 'forceBackdropBlur': instance.forceBackdropBlur, 'extensionVerificationBrowserMode': instance.extensionVerificationBrowserMode, 'locale': instance.locale, 'lyricsMode': instance.lyricsMode, diff --git a/lib/providers/runtime_profile_provider.dart b/lib/providers/runtime_profile_provider.dart index d580a68e..2bbf5cf0 100644 --- a/lib/providers/runtime_profile_provider.dart +++ b/lib/providers/runtime_profile_provider.dart @@ -1,10 +1,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/providers/settings_provider.dart'; /// True on low-end hardware (arm32-only or low-RAM, resolved at startup in /// main.dart via a ProviderScope override). UI uses it to skip expensive /// effects like the shell's backdrop blur. final lowEndDeviceProvider = Provider((ref) => false); -/// Generic visual capability decided by the startup runtime profile. Effects -/// can consume this without hardcoding a device model or platform source. -final backdropBlurEnabledProvider = Provider((ref) => false); +/// Raw device capability decided by the startup runtime profile (overridden +/// in main.dart). Only the `high` tier enables blur by default. +final deviceSupportsBackdropBlurProvider = Provider((ref) => false); + +/// Whether backdrop blur effects should render: the device default, or the +/// user's manual override from appearance settings (issue #488 — let lower +/// tiers opt back in). +final backdropBlurEnabledProvider = Provider((ref) { + return ref.watch(deviceSupportsBackdropBlurProvider) || + ref.watch(settingsProvider.select((s) => s.forceBackdropBlur)); +}); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 029c71aa..4c6473d9 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -658,6 +658,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setForceBackdropBlur(bool enabled) { + state = state.copyWith(forceBackdropBlur: enabled); + _saveSettings(); + } + void setExtensionVerificationBrowserMode(String mode) { state = state.copyWith( extensionVerificationBrowserMode: diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 51d3ce95..57a9b374 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -891,8 +891,9 @@ class _MainShellState extends ConsumerState ], ); // The backdrop blur re-filters everything scrolling underneath on - // every frame; low-end devices get an opaque base instead. - if (!ref.read(backdropBlurEnabledProvider)) { + // every frame; low-end devices get an opaque base instead unless + // the user forces blur on in appearance settings. + if (!ref.watch(backdropBlurEnabledProvider)) { return ColoredBox( color: settingsGroupColor(context), child: bottomBar, diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index f1dc1db9..d89c1c26 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -101,6 +101,15 @@ class AppearanceSettingsPage extends ConsumerWidget { onChanged: (value) => ref .read(settingsProvider.notifier) .setHeroAnimationsEnabled(value), + ), + SettingsSwitchItem( + icon: Icons.blur_on, + title: context.l10n.appearanceForceBlur, + subtitle: context.l10n.appearanceForceBlurSubtitle, + value: settings.forceBackdropBlur, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setForceBackdropBlur(value), showDivider: false, ), ],