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.
This commit is contained in:
zarzet
2026-07-14 09:10:16 +07:00
parent 1893f3faa8
commit b3cad6e526
22 changed files with 166 additions and 1 deletions
+9 -1
View File
@@ -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,
+12
View File
@@ -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:
+7
View File
@@ -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 => 'مسح الكل';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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 => 'すべて消去';
+7
View File
@@ -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 => '모두 지우기';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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 => 'Очистить всё';
+7
View File
@@ -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';
+7
View File
@@ -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 => 'Усі';
+7
View File
@@ -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';
+8
View File
@@ -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"
+8
View File
@@ -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,
+2
View File
@@ -48,6 +48,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> 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<String, dynamic> _$AppSettingsToJson(
'singleFilenameFormat': instance.singleFilenameFormat,
'albumFolderStructure': instance.albumFolderStructure,
'showExtensionStore': instance.showExtensionStore,
'heroAnimationsEnabled': instance.heroAnimationsEnabled,
'extensionVerificationBrowserMode': instance.extensionVerificationBrowserMode,
'locale': instance.locale,
'lyricsMode': instance.lyricsMode,
+5
View File
@@ -580,6 +580,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings();
}
void setHeroAnimationsEnabled(bool enabled) {
state = state.copyWith(heroAnimationsEnabled: enabled);
_saveSettings();
}
void setExtensionVerificationBrowserMode(String mode) {
state = state.copyWith(
extensionVerificationBrowserMode:
@@ -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),
),