From 3e04b9cbe3437cff42afa0d938ec623353ff096a Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Fri, 26 Sep 2025 22:55:18 +0200 Subject: [PATCH] Refactor: Introduce `LocaleController` and `ScaleController`, unify text and language settings, and simplify `AppSettingsView` structure. --- .../core/ui/controller/locale_controller.dart | 42 +++++---- .../core/ui/controller/scale_controller.dart | 49 +++++----- finlog_app/app/lib/main.dart | 39 +++++--- .../modules/app/app_settings_view.dart | 92 +++++++++++++------ .../app/model/app_settings_view_model.dart | 87 ++++-------------- 5 files changed, 156 insertions(+), 153 deletions(-) diff --git a/finlog_app/app/lib/core/ui/controller/locale_controller.dart b/finlog_app/app/lib/core/ui/controller/locale_controller.dart index 127b163..44e129a 100644 --- a/finlog_app/app/lib/core/ui/controller/locale_controller.dart +++ b/finlog_app/app/lib/core/ui/controller/locale_controller.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fluttery/fluttery.dart'; import 'package:fluttery/preferences.dart'; -/// Controls the current app language (system, de, en, ...). -/// Loads the locale from Preferences or falls back to system default. class LocaleController extends ChangeNotifier { final Preferences _prefs; @@ -11,32 +9,36 @@ class LocaleController extends ChangeNotifier { static const _key = 'language'; - String _current = 'system'; + LanguagePref _current = LanguagePref.en; // Default = Englisch Future init() async { final saved = await _prefs.getString(_key); - _current = saved ?? 'system'; + _current = switch (saved) { + 'de' => LanguagePref.de, + 'system' => LanguagePref.system, + 'en' || _ => LanguagePref.en, // default fallback = en + }; notifyListeners(); } - void setLanguage(String lang) { - _current = lang; + void setLanguage(LanguagePref pref) { + if (_current == pref) return; + _current = pref; notifyListeners(); - _prefs.setString(_key, lang); + _prefs.setString(_key, pref.name); // fire-and-forget } - /// Returns the actual Locale or null = system. - Locale? get locale { - switch (_current) { - case 'de': - return const Locale('de'); - case 'en': - return const Locale('en'); - case 'system': - default: - return null; - } - } + LanguagePref get language => _current; - String get language => _current; + Locale? get locale => _current.locale; +} + +enum LanguagePref { + system(null), // folgt System + de(Locale('de')), // Deutsch + en(Locale('en')); // Englisch (default) + + final Locale? locale; + + const LanguagePref(this.locale); } diff --git a/finlog_app/app/lib/core/ui/controller/scale_controller.dart b/finlog_app/app/lib/core/ui/controller/scale_controller.dart index 241bb36..63dae1a 100644 --- a/finlog_app/app/lib/core/ui/controller/scale_controller.dart +++ b/finlog_app/app/lib/core/ui/controller/scale_controller.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:fluttery/fluttery.dart'; import 'package:fluttery/preferences.dart'; -/// Controls the current text scale (system, small, medium, large). -/// Loads the value from Preferences or falls back to system default. class ScaleController extends ChangeNotifier { final Preferences _prefs; @@ -11,37 +9,40 @@ class ScaleController extends ChangeNotifier { static const _key = 'textScale'; - /// supported sizes - static const Map _factors = { - 'system': 1.0, - 'small': 0.9, - 'medium': 1.1, - 'large': 1.25, - }; + TextScalePref _current = TextScalePref.system; - String _current = 'system'; + /// Faktor direkt vom Enum + double get factor => _current.factor; - /// Loads text scale from Preferences. Future init() async { final saved = await _prefs.getString(_key); - _current = saved ?? 'system'; - if (!_factors.containsKey(_current)) { - _current = 'system'; - } + _current = switch (saved) { + 'small' => TextScalePref.small, + 'medium' => TextScalePref.medium, + 'large' => TextScalePref.large, + _ => TextScalePref.system, + }; notifyListeners(); } /// Set text scale and persist. - void setScale(String scale) { - if (!_factors.containsKey(scale)) return; - _current = scale; + void setScale(TextScalePref pref) { + if (_current == pref) return; + _current = pref; notifyListeners(); - _prefs.setString(_key, scale); + _prefs.setString(_key, pref.name); // fire-and-forget } - /// get current factor (e.g. for MediaQuery.textScaler) - double get factor => _factors[_current]!; - - /// get current string (system/small/medium/large) - String get scale => _current; + TextScalePref get scale => _current; +} + +enum TextScalePref { + system(1.0), + small(0.9), + medium(1.1), + large(1.4); + + final double factor; + + const TextScalePref(this.factor); } diff --git a/finlog_app/app/lib/main.dart b/finlog_app/app/lib/main.dart index 3ea7a71..0e9e6b6 100644 --- a/finlog_app/app/lib/main.dart +++ b/finlog_app/app/lib/main.dart @@ -1,5 +1,7 @@ import 'package:app/core/app/router.dart'; import 'package:app/core/app/startup/domain/initialize_app.dart'; +import 'package:app/core/ui/controller/locale_controller.dart'; +import 'package:app/core/ui/controller/scale_controller.dart'; import 'package:app/core/ui/controller/theme.dart'; import 'package:flutter/material.dart'; import 'package:fluttery/fluttery.dart'; @@ -25,37 +27,48 @@ Future main() async { final themeController = ThemeController(); await themeController.init(); + final scaleController = ScaleController(); + final localeController = LocaleController(); + runApp( MultiProvider( - providers: [ChangeNotifierProvider(create: (context) => themeController)], - child: FinlogApp( - router: buildAppRouter(startRoute), - themeController: themeController, - ), + providers: [ + ChangeNotifierProvider(create: (context) => themeController), + ChangeNotifierProvider(create: (context) => scaleController), + ChangeNotifierProvider(create: (context) => localeController), + ], + child: FinlogApp(router: buildAppRouter(startRoute)), ), ); } class FinlogApp extends StatelessWidget { final GoRouter router; - final ThemeController themeController; - const FinlogApp({ - super.key, - required this.router, - required this.themeController, - }); + const FinlogApp({super.key, required this.router}); @override Widget build(BuildContext context) { + final theme = context.watch(); + final textScale = context.watch(); + final localeCtrl = context.watch(); + return AnimatedBuilder( - animation: themeController, + animation: theme, builder: (context, _) => MaterialApp.router( title: 'Finlog', + locale: localeCtrl.locale, + supportedLocales: const [Locale('de'), Locale('en')], theme: ThemeData.light(), darkTheme: ThemeData.dark(), - themeMode: themeController.themeMode, + themeMode: theme.themeMode, routerConfig: router, + builder: (context, child) => MediaQuery( + data: MediaQuery.of( + context, + ).copyWith(textScaler: TextScaler.linear(textScale.factor)), + child: child ?? Container(), + ), ), ); } diff --git a/finlog_app/app/lib/modules/settings/modules/app/app_settings_view.dart b/finlog_app/app/lib/modules/settings/modules/app/app_settings_view.dart index e251f20..5db8ff1 100644 --- a/finlog_app/app/lib/modules/settings/modules/app/app_settings_view.dart +++ b/finlog_app/app/lib/modules/settings/modules/app/app_settings_view.dart @@ -1,3 +1,5 @@ +import 'package:app/core/ui/controller/locale_controller.dart'; +import 'package:app/core/ui/controller/scale_controller.dart'; import 'package:app/core/ui/controller/theme.dart'; import 'package:app/modules/settings/modules/app/model/app_settings_view_model.dart'; import 'package:flutter/material.dart'; @@ -8,9 +10,16 @@ class AppSettingsView extends StatelessWidget { @override Widget build(BuildContext context) { - final themeModel = context.watch(); + final themeModel = context.read(); + final scaleModel = context.read(); + final localeModel = context.read(); + return ChangeNotifierProvider( - create: (_) => AppSettingsViewModel(themeModel: themeModel)..load(), + create: (_) => AppSettingsViewModel( + themeModel: themeModel, + scaleModel: scaleModel, + localeModel: localeModel, + )..load(), child: const _AppSettingsContent(), ); } @@ -36,7 +45,7 @@ class _AppSettingsContent extends StatelessWidget { children: [ _SystemBackgroundSection(), const SizedBox(height: 16), - _TextSizeSection(), + _TextScaleSection(), const SizedBox(height: 16), _LanguageSection(), ], @@ -73,13 +82,13 @@ class _SystemBackgroundSection extends StatelessWidget { onPressed: (i) { switch (i) { case 0: - vm.setBackgroundPref(ThemeMode.system); + vm.setThemeMode(ThemeMode.system); break; case 1: - vm.setBackgroundPref(ThemeMode.dark); + vm.setThemeMode(ThemeMode.dark); break; case 2: - vm.setBackgroundPref(ThemeMode.light); + vm.setThemeMode(ThemeMode.light); break; } }, @@ -120,36 +129,59 @@ class _SegItem extends StatelessWidget { } } -class _TextSizeSection extends StatelessWidget { +class _TextScaleSection extends StatelessWidget { + const _TextScaleSection(); + @override Widget build(BuildContext context) { final vm = context.watch(); - final selected = vm.textSize; - - Widget radio(AppTextSize size, String label, String sub) { - return RadioListTile( - value: size, - groupValue: selected, - onChanged: (v) => vm.setTextSize(v ?? AppTextSize.normal), - title: Text(label), - subtitle: Text(sub), - ); - } + final selected = vm.textScale; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Textgröße', style: Theme.of(context).textTheme.titleMedium), + const Text('Textgröße', style: TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 8), - radio(AppTextSize.small, 'Klein', 'Kompakte Darstellung'), - radio(AppTextSize.normal, 'Standard', 'Empfohlene Einstellung'), - radio(AppTextSize.large, 'Groß', 'Größere, besser lesbare Texte'), + ToggleButtons( + isSelected: [ + selected == TextScalePref.system, + selected == TextScalePref.small, + selected == TextScalePref.medium, + selected == TextScalePref.large, + ], + onPressed: (i) { + switch (i) { + case 0: + vm.setTextScale(TextScalePref.system); + break; + case 1: + vm.setTextScale(TextScalePref.small); + break; + case 2: + vm.setTextScale(TextScalePref.medium); + break; + case 3: + vm.setTextScale(TextScalePref.large); + break; + } + }, + borderRadius: BorderRadius.circular(24), + constraints: const BoxConstraints(minHeight: 44, minWidth: 120), + children: const [ + _SegItem(icon: Icons.phone_android, label: 'System'), + _SegItem(icon: Icons.text_fields, label: 'Klein'), + _SegItem(icon: Icons.text_fields, label: 'Mittel'), + _SegItem(icon: Icons.text_fields, label: 'Groß'), + ], + ), ], ); } } class _LanguageSection extends StatelessWidget { + const _LanguageSection(); + @override Widget build(BuildContext context) { final vm = context.watch(); @@ -160,18 +192,24 @@ class _LanguageSection extends StatelessWidget { children: [ Text('Sprache', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), - DropdownButtonFormField( - value: vm.language, - onChanged: (v) => vm.setLanguage(v ?? AppLanguage.de), + + DropdownButtonFormField( + initialValue: vm.language, + onChanged: (v) => vm.setLanguage(v ?? LanguagePref.en), items: const [ - DropdownMenuItem(value: AppLanguage.de, child: Text('Deutsch')), - DropdownMenuItem(value: AppLanguage.en, child: Text('Englisch')), + DropdownMenuItem( + value: LanguagePref.system, + child: Text('Systemstandard'), + ), + DropdownMenuItem(value: LanguagePref.de, child: Text('Deutsch')), + DropdownMenuItem(value: LanguagePref.en, child: Text('Englisch')), ], decoration: InputDecoration( isDense: true, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), + const SizedBox(height: 4), Text( 'Änderungen wirken sich nach dem Speichern aus.', diff --git a/finlog_app/app/lib/modules/settings/modules/app/model/app_settings_view_model.dart b/finlog_app/app/lib/modules/settings/modules/app/model/app_settings_view_model.dart index 842e5c2..4b3594a 100644 --- a/finlog_app/app/lib/modules/settings/modules/app/model/app_settings_view_model.dart +++ b/finlog_app/app/lib/modules/settings/modules/app/model/app_settings_view_model.dart @@ -1,24 +1,23 @@ +import 'package:app/core/ui/controller/locale_controller.dart'; +import 'package:app/core/ui/controller/scale_controller.dart'; import 'package:app/core/ui/controller/theme.dart'; import 'package:flutter/material.dart'; -/// Domain-ish enums for clarity and easy (de)serialization. -enum AppTextSize { small, normal, large } - -enum AppLanguage { de, en } - class AppSettingsViewModel extends ChangeNotifier { - /// Use `null` to represent "system default background". - Color? _backgroundColorSystem; - AppTextSize _textSize = AppTextSize.normal; - AppLanguage _language = AppLanguage.de; - bool _isLoading = false; bool _isSaving = false; final ThemeController _theme; + final ScaleController _scale; + final LocaleController _locale; - AppSettingsViewModel({required ThemeController themeModel}) - : _theme = themeModel; + AppSettingsViewModel({ + required ThemeController themeModel, + required ScaleController scaleModel, + required LocaleController localeModel, + }) : _theme = themeModel, + _scale = scaleModel, + _locale = localeModel; /// Pretend to load from backend. Plug your repository here later. Future load() async { @@ -28,80 +27,30 @@ class AppSettingsViewModel extends ChangeNotifier { // TODO: Replace with real backend call. await Future.delayed(const Duration(milliseconds: 200)); - // Example defaults (could come from server response). - _backgroundColorSystem = null; // null => Systemstandard - _textSize = AppTextSize.normal; - _language = AppLanguage.de; - _isLoading = false; notifyListeners(); } - void setBackgroundPref(ThemeMode mode) { + void setThemeMode(ThemeMode mode) { _theme.setTheme(mode); notifyListeners(); } - /// Save to backend (stub). - Future save() async { - if (_isSaving) return; - _isSaving = true; - notifyListeners(); - - // TODO: Replace with real backend update. - await Future.delayed(const Duration(milliseconds: 300)); - - _isSaving = false; + void setTextScale(TextScalePref pref) { + _scale.setScale(pref); notifyListeners(); } - void setSystemBackgroundColor(Color? colorOrNullForSystem) { - _backgroundColorSystem = colorOrNullForSystem; - notifyListeners(); - } - - void setTextSize(AppTextSize size) { - _textSize = size; - notifyListeners(); - } - - void setLanguage(AppLanguage lang) { - _language = lang; - notifyListeners(); - } - - Map toJson() => { - 'backgroundColorSystem': _backgroundColorSystem?.value, - 'textSize': _textSize.name, - 'language': _language.name, - }; - - void fromJson(Map json) { - final int? colorValue = json['backgroundColorSystem'] as int?; - _backgroundColorSystem = colorValue != null ? Color(colorValue) : null; - - final ts = json['textSize'] as String?; - _textSize = AppTextSize.values.firstWhere( - (e) => e.name == ts, - orElse: () => AppTextSize.normal, - ); - - final lng = json['language'] as String?; - _language = AppLanguage.values.firstWhere( - (e) => e.name == lng, - orElse: () => AppLanguage.de, - ); - + void setLanguage(LanguagePref lang) { + _locale.setLanguage(lang); notifyListeners(); } ThemeMode get themeMode => _theme.themeMode; - Color? get backgroundColorSystem => _backgroundColorSystem; + LanguagePref? get language => _locale.language; - AppTextSize get textSize => _textSize; - - AppLanguage get language => _language; + TextScalePref get textScale => _scale.scale; bool get isLoading => _isLoading;