introduced setting to request disable animations

This commit is contained in:
Fabian Freund
2026-03-09 16:17:46 +01:00
parent 0fa526b1cb
commit 9700b3868a
8 changed files with 164 additions and 16 deletions
@@ -65,6 +65,7 @@ class _VisualSection extends StatelessWidget {
children: [ children: [
SettingSection(name: 'Visual'), SettingSection(name: 'Visual'),
_UiZoomSection(), _UiZoomSection(),
_DisableAnimationsTile(),
_ThemeSection(), _ThemeSection(),
], ],
); );
@@ -147,6 +148,32 @@ double _normalizeUiScale(double value) {
return normalized.clamp(minUiScaleFactor, maxUiScaleFactor); return normalized.clamp(minUiScaleFactor, maxUiScaleFactor);
} }
class _DisableAnimationsTile extends HookConsumerWidget {
const _DisableAnimationsTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final disableAnimations = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.disableAnimations),
);
return SwitchListTile.adaptive(
title: const Text('Disable Animations'),
subtitle: const Text('Reduce motion and turn off app animations'),
secondary: const Icon(Icons.animation),
value: disableAnimations,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.disableAnimations(value),
);
},
);
}
}
class _TabBarSection extends StatelessWidget { class _TabBarSection extends StatelessWidget {
const _TabBarSection(); const _TabBarSection();
@@ -68,6 +68,7 @@ enum DeleteBrowsingDataType {
class GeneralSettings with FastEquatable { class GeneralSettings with FastEquatable {
final ThemeMode themeMode; final ThemeMode themeMode;
final double uiScaleFactor; final double uiScaleFactor;
final bool disableAnimations;
final bool enableReadability; final bool enableReadability;
final bool enforceReadability; final bool enforceReadability;
final Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit; final Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit;
@@ -117,6 +118,7 @@ class GeneralSettings with FastEquatable {
GeneralSettings({ GeneralSettings({
required this.themeMode, required this.themeMode,
required this.uiScaleFactor, required this.uiScaleFactor,
required this.disableAnimations,
required this.enableReadability, required this.enableReadability,
required this.enforceReadability, required this.enforceReadability,
required this.deleteBrowsingDataOnQuit, required this.deleteBrowsingDataOnQuit,
@@ -165,6 +167,7 @@ class GeneralSettings with FastEquatable {
GeneralSettings.withDefaults({ GeneralSettings.withDefaults({
ThemeMode? themeMode, ThemeMode? themeMode,
double? uiScaleFactor, double? uiScaleFactor,
bool? disableAnimations,
bool? enableReadability, bool? enableReadability,
bool? enforceReadability, bool? enforceReadability,
this.deleteBrowsingDataOnQuit, this.deleteBrowsingDataOnQuit,
@@ -210,6 +213,7 @@ class GeneralSettings with FastEquatable {
String? unshortenerToken, String? unshortenerToken,
}) : themeMode = themeMode ?? ThemeMode.dark, }) : themeMode = themeMode ?? ThemeMode.dark,
uiScaleFactor = uiScaleFactor ?? defaultUiScaleFactor, uiScaleFactor = uiScaleFactor ?? defaultUiScaleFactor,
disableAnimations = disableAnimations ?? false,
enableReadability = enableReadability ?? true, enableReadability = enableReadability ?? true,
enforceReadability = enforceReadability ?? false, enforceReadability = enforceReadability ?? false,
defaultSearchProvider = defaultSearchProvider ?? _fallbackSearchProvider, defaultSearchProvider = defaultSearchProvider ?? _fallbackSearchProvider,
@@ -288,6 +292,7 @@ class GeneralSettings with FastEquatable {
List<Object?> get hashParameters => [ List<Object?> get hashParameters => [
themeMode, themeMode,
uiScaleFactor, uiScaleFactor,
disableAnimations,
enableReadability, enableReadability,
enforceReadability, enforceReadability,
deleteBrowsingDataOnQuit, deleteBrowsingDataOnQuit,
@@ -11,6 +11,8 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings uiScaleFactor(double uiScaleFactor); GeneralSettings uiScaleFactor(double uiScaleFactor);
GeneralSettings disableAnimations(bool disableAnimations);
GeneralSettings enableReadability(bool enableReadability); GeneralSettings enableReadability(bool enableReadability);
GeneralSettings enforceReadability(bool enforceReadability); GeneralSettings enforceReadability(bool enforceReadability);
@@ -125,6 +127,7 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings call({ GeneralSettings call({
ThemeMode themeMode, ThemeMode themeMode,
double uiScaleFactor, double uiScaleFactor,
bool disableAnimations,
bool enableReadability, bool enableReadability,
bool enforceReadability, bool enforceReadability,
Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit, Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit,
@@ -185,6 +188,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
GeneralSettings uiScaleFactor(double uiScaleFactor) => GeneralSettings uiScaleFactor(double uiScaleFactor) =>
call(uiScaleFactor: uiScaleFactor); call(uiScaleFactor: uiScaleFactor);
@override
GeneralSettings disableAnimations(bool disableAnimations) =>
call(disableAnimations: disableAnimations);
@override @override
GeneralSettings enableReadability(bool enableReadability) => GeneralSettings enableReadability(bool enableReadability) =>
call(enableReadability: enableReadability); call(enableReadability: enableReadability);
@@ -381,6 +388,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
GeneralSettings call({ GeneralSettings call({
Object? themeMode = const $CopyWithPlaceholder(), Object? themeMode = const $CopyWithPlaceholder(),
Object? uiScaleFactor = const $CopyWithPlaceholder(), Object? uiScaleFactor = const $CopyWithPlaceholder(),
Object? disableAnimations = const $CopyWithPlaceholder(),
Object? enableReadability = const $CopyWithPlaceholder(), Object? enableReadability = const $CopyWithPlaceholder(),
Object? enforceReadability = const $CopyWithPlaceholder(), Object? enforceReadability = const $CopyWithPlaceholder(),
Object? deleteBrowsingDataOnQuit = const $CopyWithPlaceholder(), Object? deleteBrowsingDataOnQuit = const $CopyWithPlaceholder(),
@@ -436,6 +444,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.uiScaleFactor ? _value.uiScaleFactor
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: uiScaleFactor as double, : uiScaleFactor as double,
disableAnimations:
disableAnimations == const $CopyWithPlaceholder() ||
disableAnimations == null
? _value.disableAnimations
// ignore: cast_nullable_to_non_nullable
: disableAnimations as bool,
enableReadability: enableReadability:
enableReadability == const $CopyWithPlaceholder() || enableReadability == const $CopyWithPlaceholder() ||
enableReadability == null enableReadability == null
@@ -711,6 +725,7 @@ GeneralSettings _$GeneralSettingsFromJson(
) => GeneralSettings.withDefaults( ) => GeneralSettings.withDefaults(
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']), themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']),
uiScaleFactor: (json['uiScaleFactor'] as num?)?.toDouble(), uiScaleFactor: (json['uiScaleFactor'] as num?)?.toDouble(),
disableAnimations: json['disableAnimations'] as bool?,
enableReadability: json['enableReadability'] as bool?, enableReadability: json['enableReadability'] as bool?,
enforceReadability: json['enforceReadability'] as bool?, enforceReadability: json['enforceReadability'] as bool?,
deleteBrowsingDataOnQuit: (json['deleteBrowsingDataOnQuit'] as List<dynamic>?) deleteBrowsingDataOnQuit: (json['deleteBrowsingDataOnQuit'] as List<dynamic>?)
@@ -799,6 +814,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
) => <String, dynamic>{ ) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'uiScaleFactor': instance.uiScaleFactor, 'uiScaleFactor': instance.uiScaleFactor,
'disableAnimations': instance.disableAnimations,
'enableReadability': instance.enableReadability, 'enableReadability': instance.enableReadability,
'enforceReadability': instance.enforceReadability, 'enforceReadability': instance.enforceReadability,
'deleteBrowsingDataOnQuit': instance.deleteBrowsingDataOnQuit 'deleteBrowsingDataOnQuit': instance.deleteBrowsingDataOnQuit
@@ -52,6 +52,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.double, DriftSqlType.double,
db.typeMapping, db.typeMapping,
), ),
'disableAnimations': settings['disableAnimations']?.readAs(
DriftSqlType.bool,
db.typeMapping,
),
'enableReadability': settings['enableReadability']?.readAs( 'enableReadability': settings['enableReadability']?.readAs(
DriftSqlType.bool, DriftSqlType.bool,
db.typeMapping, db.typeMapping,
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
} }
String _$generalSettingsRepositoryHash() => String _$generalSettingsRepositoryHash() =>
r'2098a12825b94a9b39d2e24f90a6afaab47fe3c7'; r'12e49ad38ff25c6bad1680b3eb3a01274ebfb35f';
abstract class _$GeneralSettingsRepository abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> { extends $StreamNotifier<GeneralSettings> {
+6
View File
@@ -133,6 +133,11 @@ class _MainWidget extends HookConsumerWidget {
(value) => value.uiScaleFactor, (value) => value.uiScaleFactor,
), ),
); );
final disableAnimations = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.disableAnimations,
),
);
useOnInitialization(() async { useOnInitialization(() async {
await CountryCodes.init(); await CountryCodes.init();
@@ -261,6 +266,7 @@ class _MainWidget extends HookConsumerWidget {
), ),
themeMode: themeMode, themeMode: themeMode,
uiScaleFactor: uiScaleFactor, uiScaleFactor: uiScaleFactor,
disableAnimations: disableAnimations,
); );
}, },
); );
+45 -15
View File
@@ -31,12 +31,14 @@ class MainApp extends HookConsumerWidget {
final ThemeData? darkTheme; final ThemeData? darkTheme;
final ThemeMode? themeMode; final ThemeMode? themeMode;
final double uiScaleFactor; final double uiScaleFactor;
final bool disableAnimations;
const MainApp({ const MainApp({
required this.theme, required this.theme,
required this.darkTheme, required this.darkTheme,
required this.themeMode, required this.themeMode,
required this.uiScaleFactor, required this.uiScaleFactor,
required this.disableAnimations,
super.key, super.key,
}); });
@@ -54,8 +56,9 @@ class MainApp extends HookConsumerWidget {
darkTheme: darkTheme, darkTheme: darkTheme,
themeMode: themeMode, themeMode: themeMode,
builder: (context, child) { builder: (context, child) {
return _AppUiScale( return _AppMediaQueryOverrides(
uiScaleFactor: uiScaleFactor, uiScaleFactor: uiScaleFactor,
disableAnimations: disableAnimations,
child: child ?? const SizedBox.shrink(), child: child ?? const SizedBox.shrink(),
); );
}, },
@@ -84,8 +87,9 @@ class MainApp extends HookConsumerWidget {
themeMode: themeMode, themeMode: themeMode,
routerConfig: router.value, routerConfig: router.value,
builder: (context, child) { builder: (context, child) {
return _AppUiScale( return _AppMediaQueryOverrides(
uiScaleFactor: uiScaleFactor, uiScaleFactor: uiScaleFactor,
disableAnimations: disableAnimations,
child: _SyncEventListener( child: _SyncEventListener(
child: child ?? const SizedBox.shrink(), child: child ?? const SizedBox.shrink(),
), ),
@@ -100,8 +104,9 @@ class MainApp extends HookConsumerWidget {
darkTheme: darkTheme, darkTheme: darkTheme,
themeMode: themeMode, themeMode: themeMode,
builder: (context, child) { builder: (context, child) {
return _AppUiScale( return _AppMediaQueryOverrides(
uiScaleFactor: uiScaleFactor, uiScaleFactor: uiScaleFactor,
disableAnimations: disableAnimations,
child: child ?? const SizedBox.shrink(), child: child ?? const SizedBox.shrink(),
); );
}, },
@@ -125,29 +130,54 @@ class MainApp extends HookConsumerWidget {
} }
} }
class _AppUiScale extends StatelessWidget { MediaQueryData applyAppMediaQueryOverrides({
required MediaQueryData mediaQuery,
required double uiScaleFactor,
required bool disableAnimations,
}) {
final textScaler = uiScaleFactor == 1.0
? mediaQuery.textScaler
: _AppTextScaler(
baseTextScaler: mediaQuery.textScaler,
uiScaleFactor: uiScaleFactor,
);
if (disableAnimations) {
return mediaQuery.copyWith(textScaler: textScaler, disableAnimations: true);
}
if (uiScaleFactor == 1.0) {
return mediaQuery;
}
return mediaQuery.copyWith(textScaler: textScaler);
}
class _AppMediaQueryOverrides extends StatelessWidget {
final double uiScaleFactor; final double uiScaleFactor;
final bool disableAnimations;
final Widget child; final Widget child;
const _AppUiScale({required this.uiScaleFactor, required this.child}); const _AppMediaQueryOverrides({
required this.uiScaleFactor,
required this.disableAnimations,
required this.child,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (uiScaleFactor == 1.0) { if (uiScaleFactor == 1.0 && !disableAnimations) {
return child; return child;
} }
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final overriddenMediaQuery = applyAppMediaQueryOverrides(
return MediaQuery( mediaQuery: mediaQuery,
data: mediaQuery.copyWith( uiScaleFactor: uiScaleFactor,
textScaler: _AppTextScaler( disableAnimations: disableAnimations,
baseTextScaler: mediaQuery.textScaler,
uiScaleFactor: uiScaleFactor,
),
),
child: child,
); );
return MediaQuery(data: overriddenMediaQuery, child: child);
} }
} }
+60
View File
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:weblibre/presentation/main_app.dart';
void main() {
group('applyAppMediaQueryOverrides', () {
test('returns the original media query when no overrides are enabled', () {
const mediaQuery = MediaQueryData();
final result = applyAppMediaQueryOverrides(
mediaQuery: mediaQuery,
uiScaleFactor: 1.0,
disableAnimations: false,
);
expect(result, same(mediaQuery));
});
test(
'preserves the system animation preference when app override is off',
() {
const mediaQuery = MediaQueryData(disableAnimations: true);
final result = applyAppMediaQueryOverrides(
mediaQuery: mediaQuery,
uiScaleFactor: 1.0,
disableAnimations: false,
);
expect(result.disableAnimations, isTrue);
expect(result, same(mediaQuery));
},
);
test('forces animations off when the app setting is enabled', () {
const mediaQuery = MediaQueryData(disableAnimations: false);
final result = applyAppMediaQueryOverrides(
mediaQuery: mediaQuery,
uiScaleFactor: 1.0,
disableAnimations: true,
);
expect(result.disableAnimations, isTrue);
});
test('applies ui scale on top of the base text scaler', () {
const mediaQuery = MediaQueryData();
final result = applyAppMediaQueryOverrides(
mediaQuery: mediaQuery,
uiScaleFactor: 1.25,
disableAnimations: false,
);
expect(result.textScaler.scale(20), 25);
});
});
}