add scale setting

This commit is contained in:
Fabian Freund
2026-03-05 17:59:59 +01:00
parent d4265fc8c8
commit 66183ec075
7 changed files with 191 additions and 2 deletions
@@ -19,6 +19,7 @@
*/
import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/routing/routes.dart';
@@ -63,12 +64,89 @@ class _VisualSection extends StatelessWidget {
return const Column(
children: [
SettingSection(name: 'Visual'),
_UiZoomSection(),
_ThemeSection(),
],
);
}
}
class _UiZoomSection extends HookConsumerWidget {
const _UiZoomSection();
static final _sliderDivisions =
((maxUiScaleFactor - minUiScaleFactor) / uiScaleFactorStep).round();
@override
Widget build(BuildContext context, WidgetRef ref) {
final uiScaleFactor = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.uiScaleFactor),
);
final sliderValue = useState(uiScaleFactor);
useEffect(() {
sliderValue.value = uiScaleFactor;
return null;
}, [uiScaleFactor]);
final sliderLabel = '${(sliderValue.value * 100).round()}%';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('User Interface Zoom'),
subtitle: Text('Make the user interface smaller or larger'),
leading: Icon(Icons.zoom_in),
contentPadding: EdgeInsets.zero,
),
Row(
children: [
Text(sliderLabel, style: Theme.of(context).textTheme.titleLarge),
Expanded(
child: Slider(
min: minUiScaleFactor,
max: maxUiScaleFactor,
divisions: _sliderDivisions,
label: sliderLabel,
value: sliderValue.value.clamp(
minUiScaleFactor,
maxUiScaleFactor,
),
onChanged: (value) {
sliderValue.value = value;
},
onChangeEnd: (value) async {
final normalized = _normalizeUiScale(value);
sliderValue.value = normalized;
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.uiScaleFactor(normalized),
);
},
),
),
],
),
],
),
);
}
}
double _normalizeUiScale(double value) {
final clampedValue = value.clamp(minUiScaleFactor, maxUiScaleFactor);
final stepIndex = ((clampedValue - minUiScaleFactor) / uiScaleFactorStep)
.round();
final normalized = minUiScaleFactor + (stepIndex * uiScaleFactorStep);
return normalized.clamp(minUiScaleFactor, maxUiScaleFactor);
}
class _TabBarSection extends StatelessWidget {
const _TabBarSection();
@@ -34,6 +34,11 @@ const _fallbackSearchProvider = BangKey(
);
const _fallbackAutocompleteProvider = SearchSuggestionProviders.none;
const defaultUiScaleFactor = 1.0;
const minUiScaleFactor = 0.5;
const maxUiScaleFactor = 1.5;
const uiScaleFactorStep = 0.05;
enum TabBarSwipeAction { switchLastOpened, navigateOrderedTabs }
enum QuickTabSwitcherMode { lastUsedTabs, containerTabs }
@@ -62,6 +67,7 @@ enum DeleteBrowsingDataType {
@JsonSerializable(includeIfNull: true, constructor: 'withDefaults')
class GeneralSettings with FastEquatable {
final ThemeMode themeMode;
final double uiScaleFactor;
final bool enableReadability;
final bool enforceReadability;
final Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit;
@@ -110,6 +116,7 @@ class GeneralSettings with FastEquatable {
GeneralSettings({
required this.themeMode,
required this.uiScaleFactor,
required this.enableReadability,
required this.enforceReadability,
required this.deleteBrowsingDataOnQuit,
@@ -157,6 +164,7 @@ class GeneralSettings with FastEquatable {
GeneralSettings.withDefaults({
ThemeMode? themeMode,
double? uiScaleFactor,
bool? enableReadability,
bool? enforceReadability,
this.deleteBrowsingDataOnQuit,
@@ -201,6 +209,7 @@ class GeneralSettings with FastEquatable {
bool? unshortenerEnabled,
String? unshortenerToken,
}) : themeMode = themeMode ?? ThemeMode.dark,
uiScaleFactor = uiScaleFactor ?? defaultUiScaleFactor,
enableReadability = enableReadability ?? true,
enforceReadability = enforceReadability ?? false,
defaultSearchProvider = defaultSearchProvider ?? _fallbackSearchProvider,
@@ -278,6 +287,7 @@ class GeneralSettings with FastEquatable {
@override
List<Object?> get hashParameters => [
themeMode,
uiScaleFactor,
enableReadability,
enforceReadability,
deleteBrowsingDataOnQuit,
@@ -9,6 +9,8 @@ part of 'general_settings.dart';
abstract class _$GeneralSettingsCWProxy {
GeneralSettings themeMode(ThemeMode themeMode);
GeneralSettings uiScaleFactor(double uiScaleFactor);
GeneralSettings enableReadability(bool enableReadability);
GeneralSettings enforceReadability(bool enforceReadability);
@@ -122,6 +124,7 @@ abstract class _$GeneralSettingsCWProxy {
/// ```
GeneralSettings call({
ThemeMode themeMode,
double uiScaleFactor,
bool enableReadability,
bool enforceReadability,
Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit,
@@ -178,6 +181,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
@override
GeneralSettings themeMode(ThemeMode themeMode) => call(themeMode: themeMode);
@override
GeneralSettings uiScaleFactor(double uiScaleFactor) =>
call(uiScaleFactor: uiScaleFactor);
@override
GeneralSettings enableReadability(bool enableReadability) =>
call(enableReadability: enableReadability);
@@ -373,6 +380,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
/// ```
GeneralSettings call({
Object? themeMode = const $CopyWithPlaceholder(),
Object? uiScaleFactor = const $CopyWithPlaceholder(),
Object? enableReadability = const $CopyWithPlaceholder(),
Object? enforceReadability = const $CopyWithPlaceholder(),
Object? deleteBrowsingDataOnQuit = const $CopyWithPlaceholder(),
@@ -423,6 +431,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.themeMode
// ignore: cast_nullable_to_non_nullable
: themeMode as ThemeMode,
uiScaleFactor:
uiScaleFactor == const $CopyWithPlaceholder() || uiScaleFactor == null
? _value.uiScaleFactor
// ignore: cast_nullable_to_non_nullable
: uiScaleFactor as double,
enableReadability:
enableReadability == const $CopyWithPlaceholder() ||
enableReadability == null
@@ -697,6 +710,7 @@ GeneralSettings _$GeneralSettingsFromJson(
Map<String, dynamic> json,
) => GeneralSettings.withDefaults(
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']),
uiScaleFactor: (json['uiScaleFactor'] as num?)?.toDouble(),
enableReadability: json['enableReadability'] as bool?,
enforceReadability: json['enforceReadability'] as bool?,
deleteBrowsingDataOnQuit: (json['deleteBrowsingDataOnQuit'] as List<dynamic>?)
@@ -784,6 +798,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
GeneralSettings instance,
) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'uiScaleFactor': instance.uiScaleFactor,
'enableReadability': instance.enableReadability,
'enforceReadability': instance.enforceReadability,
'deleteBrowsingDataOnQuit': instance.deleteBrowsingDataOnQuit
@@ -48,6 +48,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.string,
db.typeMapping,
),
'uiScaleFactor': settings['uiScaleFactor']?.readAs(
DriftSqlType.double,
db.typeMapping,
),
'enableReadability': settings['enableReadability']?.readAs(
DriftSqlType.bool,
db.typeMapping,
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
}
String _$generalSettingsRepositoryHash() =>
r'23b15af79f0f41771c20f7c0f7e94f93659ee1e8';
r'2098a12825b94a9b39d2e24f90a6afaab47fe3c7';
abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> {
+6
View File
@@ -128,6 +128,11 @@ class _MainWidget extends HookConsumerWidget {
final themeMode = ref.watch(
generalSettingsWithDefaultsProvider.select((value) => value.themeMode),
);
final uiScaleFactor = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.uiScaleFactor,
),
);
useOnInitialization(() async {
await CountryCodes.init();
@@ -255,6 +260,7 @@ class _MainWidget extends HookConsumerWidget {
extensions: const <ThemeExtension<dynamic>>[AppColors.dark],
),
themeMode: themeMode,
uiScaleFactor: uiScaleFactor,
);
},
);
+77 -1
View File
@@ -30,11 +30,13 @@ class MainApp extends HookConsumerWidget {
final ThemeData? theme;
final ThemeData? darkTheme;
final ThemeMode? themeMode;
final double uiScaleFactor;
const MainApp({
required this.theme,
required this.darkTheme,
required this.themeMode,
required this.uiScaleFactor,
super.key,
});
@@ -51,6 +53,12 @@ class MainApp extends HookConsumerWidget {
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
builder: (context, child) {
return _AppUiScale(
uiScaleFactor: uiScaleFactor,
child: child ?? const SizedBox.shrink(),
);
},
home: Scaffold(
body: Center(
child: Column(
@@ -76,7 +84,12 @@ class MainApp extends HookConsumerWidget {
themeMode: themeMode,
routerConfig: router.value,
builder: (context, child) {
return _SyncEventListener(child: child ?? const SizedBox.shrink());
return _AppUiScale(
uiScaleFactor: uiScaleFactor,
child: _SyncEventListener(
child: child ?? const SizedBox.shrink(),
),
);
},
);
},
@@ -86,6 +99,12 @@ class MainApp extends HookConsumerWidget {
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
builder: (context, child) {
return _AppUiScale(
uiScaleFactor: uiScaleFactor,
child: child ?? const SizedBox.shrink(),
);
},
home: Scaffold(
appBar: AppBar(title: const Text('Initiallization Error')),
body: Center(
@@ -106,6 +125,63 @@ class MainApp extends HookConsumerWidget {
}
}
class _AppUiScale extends StatelessWidget {
final double uiScaleFactor;
final Widget child;
const _AppUiScale({required this.uiScaleFactor, required this.child});
@override
Widget build(BuildContext context) {
if (uiScaleFactor == 1.0) {
return child;
}
final mediaQuery = MediaQuery.of(context);
return MediaQuery(
data: mediaQuery.copyWith(
textScaler: _AppTextScaler(
baseTextScaler: mediaQuery.textScaler,
uiScaleFactor: uiScaleFactor,
),
),
child: child,
);
}
}
class _AppTextScaler extends TextScaler {
final TextScaler baseTextScaler;
final double uiScaleFactor;
const _AppTextScaler({
required this.baseTextScaler,
required this.uiScaleFactor,
}) : assert(uiScaleFactor > 0);
@override
double scale(double fontSize) =>
baseTextScaler.scale(fontSize) * uiScaleFactor;
@override
double get textScaleFactor => scale(1.0);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is _AppTextScaler &&
baseTextScaler == other.baseTextScaler &&
uiScaleFactor == other.uiScaleFactor;
}
@override
int get hashCode => Object.hash(baseTextScaler, uiScaleFactor);
}
class _SyncEventListener extends ConsumerWidget {
final Widget child;