From b3cad6e526025b8b9ae456ad0e36a1f32a86251d Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 14 Jul 2026 08:24:24 +0700 Subject: [PATCH] feat(settings): toggle to disable Hero animations A single HeroMode above the router disables shared-element flights app-wide (mini player expand, track metadata cover) for users who prefer no motion or want to save the overlay rendering cost. On by default; persisted in AppSettings. --- lib/app.dart | 10 +++++++++- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 7 +++++++ lib/l10n/app_localizations_de.dart | 7 +++++++ lib/l10n/app_localizations_en.dart | 7 +++++++ lib/l10n/app_localizations_es.dart | 7 +++++++ lib/l10n/app_localizations_fr.dart | 7 +++++++ lib/l10n/app_localizations_hi.dart | 7 +++++++ lib/l10n/app_localizations_id.dart | 7 +++++++ lib/l10n/app_localizations_ja.dart | 7 +++++++ lib/l10n/app_localizations_ko.dart | 7 +++++++ lib/l10n/app_localizations_nl.dart | 7 +++++++ lib/l10n/app_localizations_pt.dart | 7 +++++++ lib/l10n/app_localizations_ru.dart | 7 +++++++ lib/l10n/app_localizations_tr.dart | 7 +++++++ lib/l10n/app_localizations_uk.dart | 7 +++++++ lib/l10n/app_localizations_zh.dart | 7 +++++++ lib/l10n/arb/app_en.arb | 8 ++++++++ lib/models/settings.dart | 8 ++++++++ lib/models/settings.g.dart | 2 ++ lib/providers/settings_provider.dart | 5 +++++ .../settings/appearance_settings_page.dart | 17 +++++++++++++++++ 22 files changed, 166 insertions(+), 1 deletion(-) diff --git a/lib/app.dart b/lib/app.dart index 3735d911..6384b21c 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -98,6 +98,9 @@ class SpotiFLACApp extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(_routerProvider); final localeString = ref.watch(settingsProvider.select((s) => s.locale)); + final heroAnimationsEnabled = ref.watch( + settingsProvider.select((s) => s.heroAnimationsEnabled), + ); final scrollBehavior = disableOverscrollEffects ? const MaterialScrollBehavior().copyWith(overscroll: false) : const _FluidScrollBehavior(); @@ -133,7 +136,12 @@ class SpotiFLACApp extends ConsumerWidget { final mediaQuery = MediaQuery.of(context); return MediaQuery( data: mediaQuery.copyWith(displayFeatures: const []), - child: child ?? const SizedBox.shrink(), + // Global Hero kill switch: heroes below a disabled HeroMode + // never register, so no flights run on any navigator. + child: HeroMode( + enabled: heroAnimationsEnabled, + child: child ?? const SizedBox.shrink(), + ), ); }, routerConfig: router, diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e1ce3b4d..6976d1db 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2834,6 +2834,18 @@ abstract class AppLocalizations { /// **'Pure black background'** String get appearanceAmoledDarkSubtitle; + /// Toggle for shared-element (Hero) transitions + /// + /// In en, this message translates to: + /// **'Hero animations'** + String get appearanceHeroAnimations; + + /// Subtitle for the Hero animations toggle + /// + /// In en, this message translates to: + /// **'Fly covers between screens, e.g. when opening the player'** + String get appearanceHeroAnimationsSubtitle; + /// Button - clear all queue items /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 0563aec4..3fdcb6c4 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1522,6 +1522,13 @@ class AppLocalizationsAr extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'خلفية سوداء تامة'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'مسح الكل'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9585a914..70f498c9 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1545,6 +1545,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'AMOLED Hintergrund'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Alles löschen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bfed6eae..b660e3dd 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1523,6 +1523,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Pure black background'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ffaf5fb7..e5f76503 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1523,6 +1523,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Pure black background'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ded7268a..dc952f71 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1567,6 +1567,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Fond noir pur'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Tout effacer'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index d46c9632..583f2855 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1523,6 +1523,13 @@ class AppLocalizationsHi extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Pure black background'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 5412f2c5..7cb64bec 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1532,6 +1532,13 @@ class AppLocalizationsId extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Latar belakang hitam murni'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Hapus Semua'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index bc44df1b..df945731 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1514,6 +1514,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'ピュアブラックの背景'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'すべて消去'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 59991ea9..5130c19c 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1490,6 +1490,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => '순수 검정 배경화면'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => '모두 지우기'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 461222f2..9d8c8f8f 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1523,6 +1523,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Pure black background'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 30605216..44871469 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1523,6 +1523,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Pure black background'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index dd0820b6..0260c4c4 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1538,6 +1538,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Глубокий чёрный фон'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Очистить всё'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index d85c998b..008d15fd 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1540,6 +1540,13 @@ class AppLocalizationsTr extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Saf siyah arka plan'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @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 3ee3b343..85b0a8ed 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1546,6 +1546,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Чисто чорний фон'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Усі'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4c7d8418..565ad57f 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1523,6 +1523,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appearanceAmoledDarkSubtitle => 'Pure black background'; + @override + String get appearanceHeroAnimations => 'Hero animations'; + + @override + String get appearanceHeroAnimationsSubtitle => + 'Fly covers between screens, e.g. when opening the player'; + @override String get queueClearAll => 'Clear All'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6e175a6c..d13fad53 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2010,6 +2010,14 @@ "@appearanceAmoledDarkSubtitle": { "description": "Subtitle for AMOLED dark" }, + "appearanceHeroAnimations": "Hero animations", + "@appearanceHeroAnimations": { + "description": "Toggle for shared-element (Hero) transitions" + }, + "appearanceHeroAnimationsSubtitle": "Fly covers between screens, e.g. when opening the player", + "@appearanceHeroAnimationsSubtitle": { + "description": "Subtitle for the Hero animations toggle" + }, "queueClearAll": "Clear All", "@queueClearAll": { "description": "Button - clear all queue items" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index e505bad6..9dc18a00 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -43,6 +43,10 @@ class AppSettings { final String singleFilenameFormat; final String albumFolderStructure; final bool showExtensionStore; + + /// Shared-element (Hero) flights, e.g. the mini player artwork expanding + /// into the full player. Off skips the flights entirely. + final bool heroAnimationsEnabled; final String extensionVerificationBrowserMode; // 'external_first' or 'in_app_first' final String locale; @@ -131,6 +135,7 @@ class AppSettings { this.singleFilenameFormat = '{title} - {artist}', this.albumFolderStructure = 'artist_album', this.showExtensionStore = true, + this.heroAnimationsEnabled = true, this.extensionVerificationBrowserMode = 'in_app_first', this.locale = 'system', this.lyricsMode = 'embed', @@ -200,6 +205,7 @@ class AppSettings { String? singleFilenameFormat, String? albumFolderStructure, bool? showExtensionStore, + bool? heroAnimationsEnabled, String? extensionVerificationBrowserMode, String? locale, String? lyricsMode, @@ -277,6 +283,8 @@ class AppSettings { singleFilenameFormat: singleFilenameFormat ?? this.singleFilenameFormat, albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure, showExtensionStore: showExtensionStore ?? this.showExtensionStore, + heroAnimationsEnabled: + heroAnimationsEnabled ?? this.heroAnimationsEnabled, extensionVerificationBrowserMode: extensionVerificationBrowserMode ?? this.extensionVerificationBrowserMode, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 9ac1f0d8..c405cd80 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -48,6 +48,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( albumFolderStructure: json['albumFolderStructure'] as String? ?? 'artist_album', showExtensionStore: json['showExtensionStore'] as bool? ?? true, + heroAnimationsEnabled: json['heroAnimationsEnabled'] as bool? ?? true, extensionVerificationBrowserMode: json['extensionVerificationBrowserMode'] as String? ?? 'in_app_first', locale: json['locale'] as String? ?? 'system', @@ -128,6 +129,7 @@ Map _$AppSettingsToJson( 'singleFilenameFormat': instance.singleFilenameFormat, 'albumFolderStructure': instance.albumFolderStructure, 'showExtensionStore': instance.showExtensionStore, + 'heroAnimationsEnabled': instance.heroAnimationsEnabled, 'extensionVerificationBrowserMode': instance.extensionVerificationBrowserMode, 'locale': instance.locale, 'lyricsMode': instance.lyricsMode, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 720502ab..c82f27a9 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -580,6 +580,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setHeroAnimationsEnabled(bool enabled) { + state = state.copyWith(heroAnimationsEnabled: enabled); + _saveSettings(); + } + void setExtensionVerificationBrowserMode(String mode) { state = state.copyWith( extensionVerificationBrowserMode: diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 48d7969f..b5565576 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -90,6 +90,23 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.animation, + title: context.l10n.appearanceHeroAnimations, + subtitle: context.l10n.appearanceHeroAnimationsSubtitle, + value: settings.heroAnimationsEnabled, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setHeroAnimationsEnabled(value), + showDivider: false, + ), + ], + ), + ), + SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionLanguage), ),