feat(appearance): manual toggle to force backdrop blur on lower device tiers

This commit is contained in:
zarzet
2026-07-27 01:39:16 +07:00
parent 9da3607932
commit ac56e76b62
20 changed files with 137 additions and 6 deletions
+12
View File
@@ -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:
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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 => 'すべて消去';
+7
View File
@@ -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 => '모두 지우기';
+7
View File
@@ -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';
+7
View File
@@ -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 => 'Очистить всё';
+7
View File
@@ -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';
+7
View File
@@ -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 => 'Усі';
+8
View File
@@ -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"
+1 -1
View File
@@ -56,7 +56,7 @@ void main() {
lowEndDeviceProvider.overrideWithValue(
runtimeProfile.disableOverscrollEffects,
),
backdropBlurEnabledProvider.overrideWithValue(
deviceSupportsBackdropBlurProvider.overrideWithValue(
runtimeProfile.enableBackdropBlur,
),
initialSettingsProvider.overrideWithValue(bootstrapSettings),
+8
View File
@@ -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,
+2
View File
@@ -50,6 +50,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> 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<String, dynamic> _$AppSettingsToJson(
'albumFolderStructure': instance.albumFolderStructure,
'showExtensionStore': instance.showExtensionStore,
'heroAnimationsEnabled': instance.heroAnimationsEnabled,
'forceBackdropBlur': instance.forceBackdropBlur,
'extensionVerificationBrowserMode': instance.extensionVerificationBrowserMode,
'locale': instance.locale,
'lyricsMode': instance.lyricsMode,
+12 -3
View File
@@ -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<bool>((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<bool>((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<bool>((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<bool>((ref) {
return ref.watch(deviceSupportsBackdropBlurProvider) ||
ref.watch(settingsProvider.select((s) => s.forceBackdropBlur));
});
+5
View File
@@ -658,6 +658,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings();
}
void setForceBackdropBlur(bool enabled) {
state = state.copyWith(forceBackdropBlur: enabled);
_saveSettings();
}
void setExtensionVerificationBrowserMode(String mode) {
state = state.copyWith(
extensionVerificationBrowserMode:
+3 -2
View File
@@ -891,8 +891,9 @@ class _MainShellState extends ConsumerState<MainShell>
],
);
// 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,
@@ -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,
),
],