Simplify theme management, integrate ToggleButtons for theme selection, and enhance settings view structure.

This commit is contained in:
2025-09-26 20:11:10 +02:00
parent 4a4f10d533
commit ece3c333eb
6 changed files with 204 additions and 141 deletions

View File

@@ -25,10 +25,10 @@ class ThemeController extends ChangeNotifier {
}
/// Sets theme and persists it in Preferences.
Future<void> setTheme(ThemeMode mode) async {
void setTheme(ThemeMode mode) {
_themeMode = mode;
notifyListeners();
await _prefs.setString('theme', _toString(mode));
_prefs.setString('theme', mode.name);
}
ThemeMode _fromString(String value) {
@@ -43,16 +43,5 @@ class ThemeController extends ChangeNotifier {
}
}
String _toString(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return 'light';
case ThemeMode.dark:
return 'dark';
case ThemeMode.system:
return 'system';
}
}
ThemeMode get themeMode => _themeMode;
}

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:fluttery/fluttery.dart';
import 'package:fluttery/logger.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
Future<void> main() async {
// Ensures that the Flutter engine and widget binding
@@ -25,9 +26,12 @@ Future<void> main() async {
await themeController.init();
runApp(
FinlogApp(
router: buildAppRouter(startRoute),
themeController: themeController,
MultiProvider(
providers: [ChangeNotifierProvider(create: (context) => themeController)],
child: FinlogApp(
router: buildAppRouter(startRoute),
themeController: themeController,
),
),
);
}

View File

@@ -1,5 +1,5 @@
import 'package:app/core/app/theme.dart';
import 'package:app/modules/settings/modules/app/model/app_settings_view_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -8,8 +8,9 @@ class AppSettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeModel = context.watch<ThemeController>();
return ChangeNotifierProvider(
create: (_) => AppSettingsViewModel()..load(),
create: (_) => AppSettingsViewModel(themeModel: themeModel)..load(),
child: const _AppSettingsContent(),
);
}
@@ -38,8 +39,6 @@ class _AppSettingsContent extends StatelessWidget {
_TextSizeSection(),
const SizedBox(height: 16),
_LanguageSection(),
const SizedBox(height: 24),
_SaveButton(),
],
),
),
@@ -49,106 +48,78 @@ class _AppSettingsContent extends StatelessWidget {
}
class _SystemBackgroundSection extends StatelessWidget {
const _SystemBackgroundSection();
@override
Widget build(BuildContext context) {
final vm = context.watch<AppSettingsViewModel>();
final scheme = Theme.of(context).colorScheme;
// A few pleasant presets; extend as needed.
final presets = <Color>[
const Color(0xFFFFFFFF),
const Color(0xFFF5F5F5),
const Color(0xFF121212),
const Color(0xFF1E1E1E),
Colors.blueGrey.shade50,
Colors.blueGrey.shade900,
];
final isSystem = vm.backgroundColorSystem == null;
final selected = vm.backgroundColorSystem;
Widget chip({
required Widget child,
required bool selected,
required VoidCallback onTap,
}) {
return InkWell(
borderRadius: BorderRadius.circular(22),
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: selected ? scheme.primary : scheme.outlineVariant,
),
),
child: child,
),
);
}
final selected = vm.themeMode;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
const Text(
'System-Hintergrundfarbe',
style: Theme.of(context).textTheme.titleMedium,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
chip(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.phone_android, size: 18),
const SizedBox(width: 6),
Text(
'Systemstandard',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
selected: isSystem,
onTap: () => vm.setSystemBackgroundColor(null),
),
...presets.map((c) {
final bool sel = selected == c;
return GestureDetector(
onTap: () => vm.setSystemBackgroundColor(c),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: c,
shape: BoxShape.circle,
border: Border.all(
color: sel ? scheme.primary : scheme.outlineVariant,
width: sel ? 2 : 1,
),
),
),
);
}),
const SizedBox(height: 10),
ToggleButtons(
isSelected: [
selected == ThemeMode.system,
selected == ThemeMode.dark,
selected == ThemeMode.light,
],
onPressed: (i) {
switch (i) {
case 0:
vm.setBackgroundPref(ThemeMode.system);
break;
case 1:
vm.setBackgroundPref(ThemeMode.dark);
break;
case 2:
vm.setBackgroundPref(ThemeMode.light);
break;
}
},
borderRadius: BorderRadius.circular(24),
constraints: const BoxConstraints(minHeight: 44, minWidth: 140),
children: const [
_SegItem(icon: Icons.phone_iphone, label: 'Systemstandard'),
_SegItem(emoji: '🌙', label: 'Dark Mode'),
_SegItem(emoji: '☀️', label: 'White Mode'),
],
),
const SizedBox(height: 4),
Text(
isSystem
? 'Aktuell: Systemstandard'
: 'Aktuell: Benutzerdefinierte Farbe',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: scheme.outline),
),
],
);
}
}
class _SegItem extends StatelessWidget {
final IconData? icon;
final String? emoji;
final String label;
const _SegItem({this.icon, this.emoji, required this.label});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) Icon(icon, size: 18),
if (emoji != null) Text(emoji!, style: const TextStyle(fontSize: 18)),
const SizedBox(width: 8),
Text(label),
],
),
);
}
}
class _TextSizeSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
@@ -212,31 +183,3 @@ class _LanguageSection extends StatelessWidget {
);
}
}
class _SaveButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = context.watch<AppSettingsViewModel>();
return FilledButton.icon(
onPressed: vm.isSaving
? null
: () async {
await vm.save();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Einstellungen gespeichert')),
);
}
},
icon: vm.isSaving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.save_outlined),
label: const Text('Speichern'),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:app/core/app/theme.dart';
import 'package:flutter/material.dart';
/// Domain-ish enums for clarity and easy (de)serialization.
@@ -14,15 +15,10 @@ class AppSettingsViewModel extends ChangeNotifier {
bool _isLoading = false;
bool _isSaving = false;
Color? get backgroundColorSystem => _backgroundColorSystem;
final ThemeController _theme;
AppTextSize get textSize => _textSize;
AppLanguage get language => _language;
bool get isLoading => _isLoading;
bool get isSaving => _isSaving;
AppSettingsViewModel({required ThemeController themeModel})
: _theme = themeModel;
/// Pretend to load from backend. Plug your repository here later.
Future<void> load() async {
@@ -41,6 +37,11 @@ class AppSettingsViewModel extends ChangeNotifier {
notifyListeners();
}
void setBackgroundPref(ThemeMode mode) {
_theme.setTheme(mode);
notifyListeners();
}
/// Save to backend (stub).
Future<void> save() async {
if (_isSaving) return;
@@ -71,7 +72,6 @@ class AppSettingsViewModel extends ChangeNotifier {
Map<String, dynamic> toJson() => {
'backgroundColorSystem': _backgroundColorSystem?.value,
// null => system
'textSize': _textSize.name,
'language': _language.name,
};
@@ -94,4 +94,16 @@ class AppSettingsViewModel extends ChangeNotifier {
notifyListeners();
}
ThemeMode get themeMode => _theme.themeMode;
Color? get backgroundColorSystem => _backgroundColorSystem;
AppTextSize get textSize => _textSize;
AppLanguage get language => _language;
bool get isLoading => _isLoading;
bool get isSaving => _isSaving;
}