From 8fa071e565e636458a9aa60e4fe60fd2ec578c6d Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Sat, 27 Sep 2025 13:37:43 +0200 Subject: [PATCH] Feature: Add feature toggles and settings for modular features (e.g., Car, Inventory), enhance navigation for mobile/desktop, and improve i18n integration. --- finlog_app/app/assets/i18n/de.i18n.json | 19 ++ finlog_app/app/assets/i18n/en.i18n.json | 19 ++ finlog_app/app/lib/core/app/router.dart | 5 +- .../lib/core/features/feature_controller.dart | 116 ++++++++++++ .../app/lib/core/i18n/translations.g.dart | 4 +- .../app/lib/core/i18n/translations_de.g.dart | 68 +++++++ .../app/lib/core/i18n/translations_en.g.dart | 86 +++++++++ finlog_app/app/lib/main.dart | 19 +- finlog_app/app/lib/modules/app_shell.dart | 176 ++++++++++-------- finlog_app/app/lib/modules/car/car_view.dart | 10 + .../modules/app/features_settings_view.dart | 73 ++++++++ .../model/feature_settings_view_model.dart | 22 +++ .../lib/modules/settings/settings_view.dart | 9 +- 13 files changed, 545 insertions(+), 81 deletions(-) create mode 100644 finlog_app/app/lib/core/features/feature_controller.dart create mode 100644 finlog_app/app/lib/modules/car/car_view.dart create mode 100644 finlog_app/app/lib/modules/settings/modules/app/features_settings_view.dart create mode 100644 finlog_app/app/lib/modules/settings/modules/app/model/feature_settings_view_model.dart diff --git a/finlog_app/app/assets/i18n/de.i18n.json b/finlog_app/app/assets/i18n/de.i18n.json index f249c69..e0ed9cf 100644 --- a/finlog_app/app/assets/i18n/de.i18n.json +++ b/finlog_app/app/assets/i18n/de.i18n.json @@ -27,6 +27,7 @@ }, "settings": { "title": "Einstellungen", + "featureSettings": "Funktionseinstellungen", "sections": { "account": "Konto & Daten", "app": "App", @@ -76,5 +77,23 @@ "privacy": "Datenschutz", "termsOfService": "Nutzungsbedingungen" } + }, + "features": { + "inventory": { + "displayName": "Inventar", + "description": "Verwaltet Gegenstände, Kategorien und Lagerorte." + }, + "car": { + "displayName": "Auto", + "description": "KFZ-Tracking (Tanken, Wartung, Kosten, Kilometer)." + }, + "household": { + "displayName": "Haushalt (inkl. Budget)", + "description": "Haushaltsfunktionen inkl. Budgetplanung." + }, + "reports": { + "displayName": "Berichte", + "description": "Statistiken von Ausgaben" + } } } diff --git a/finlog_app/app/assets/i18n/en.i18n.json b/finlog_app/app/assets/i18n/en.i18n.json index 4eff33a..6386f66 100644 --- a/finlog_app/app/assets/i18n/en.i18n.json +++ b/finlog_app/app/assets/i18n/en.i18n.json @@ -27,6 +27,7 @@ }, "settings": { "title": "Settings", + "featureSettings": "Feature Settings", "sections": { "account": "Account & Data", "app": "App", @@ -76,5 +77,23 @@ "privacy": "Privacy", "termsOfService": "Terms of Service" } + }, + "features": { + "inventory": { + "displayName": "Inventory", + "description": "Manages items, categories and storage locations." + }, + "car": { + "displayName": "Car", + "description": "Vehicle tracking (fuel, maintenance, costs, mileage)." + }, + "household": { + "displayName": "Household (incl. Budget)", + "description": "Household functions including budget planning." + }, + "reports": { + "displayName": "Reports", + "description": "Statistics of expenses" + } } } diff --git a/finlog_app/app/lib/core/app/router.dart b/finlog_app/app/lib/core/app/router.dart index 00256ed..0f0ca54 100644 --- a/finlog_app/app/lib/core/app/router.dart +++ b/finlog_app/app/lib/core/app/router.dart @@ -1,5 +1,6 @@ import 'package:app/modules/app_shell.dart'; import 'package:app/modules/budget/budget_view.dart'; +import 'package:app/modules/car/car_view.dart'; import 'package:app/modules/dashboard/dashboard_view.dart'; import 'package:app/modules/login/pages/login_page.dart'; import 'package:app/modules/settings/settings_view.dart'; @@ -15,7 +16,8 @@ enum AppRoute { budget('/budget'), expenses('/expenses'), reports('/reports'), - settings('/settings'); + settings('/settings'), + car('/car'); const AppRoute(this.path); @@ -95,6 +97,7 @@ GoRouter buildAppRouter(AppRoute initialRoute) { (ctx, st) => const Center(child: Text('Reports – Auswertungen & Diagramme')), ), + _r(AppRoute.car, (ctx, st) => CarView()), ], ), ], diff --git a/finlog_app/app/lib/core/features/feature_controller.dart b/finlog_app/app/lib/core/features/feature_controller.dart new file mode 100644 index 0000000..739c9c5 --- /dev/null +++ b/finlog_app/app/lib/core/features/feature_controller.dart @@ -0,0 +1,116 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttery/fluttery.dart'; +import 'package:fluttery/preferences.dart'; +import 'package:app/core/i18n/translations.g.dart'; + +/// Define all toggleable features here. +/// You can freely add more later. +enum AppFeature { + inventory, + car, + household, // incl. budget + reports, +} + +extension AppFeatureKey on AppFeature { + String get prefKey { + switch (this) { + case AppFeature.inventory: + return 'feature.inventory.enabled'; + case AppFeature.car: + return 'feature.car.enabled'; + case AppFeature.household: + return 'feature.household.enabled'; + case AppFeature.reports: + return 'feature.reports.enabled'; + } + } + + /// Optional: default state if the pref isn't set yet + bool get defaultEnabled { + switch (this) { + case AppFeature.inventory: + return true; + case AppFeature.car: + return false; + case AppFeature.household: + return true; + case AppFeature.reports: + return true; + } + } + + /// Human-readable name for UI using translations + String displayName(BuildContext context) { + final t = Translations.of(context); + switch (this) { + case AppFeature.inventory: + return t.features.inventory.displayName; + case AppFeature.car: + return t.features.car.displayName; + case AppFeature.household: + return t.features.household.displayName; + case AppFeature.reports: + return t.features.reports.displayName; + } + } + + /// Description/help text shown below the switch using translations + String description(BuildContext context) { + final t = Translations.of(context); + switch (this) { + case AppFeature.inventory: + return t.features.inventory.description; + case AppFeature.car: + return t.features.car.description; + case AppFeature.household: + return t.features.household.description; + case AppFeature.reports: + return t.features.reports.description; + } + } + + /// Optional: an icon to show in list tiles (import material in the view) +} + +/// Controller pattern like your other controllers (ChangeNotifier + init) +class FeatureController extends ChangeNotifier { + final Preferences _prefs; + + FeatureController() : _prefs = App.service(); + + /// In-memory cache of feature states + final Map _enabled = { + for (final f in AppFeature.values) f: f.defaultEnabled, + }; + + /// Call during app bootstrap (similar to other controllers). + Future init() async { + for (final f in AppFeature.values) { + final v = await _prefs.getBool(f.prefKey); + _enabled[f] = v ?? true; + } + } + + bool isEnabled(AppFeature feature) => + _enabled[feature] ?? feature.defaultEnabled; + + /// Convenience map for UI bindings + Map get allStates => Map.unmodifiable(_enabled); + + Future setEnabled(AppFeature feature, bool value) async { + _enabled[feature] = value; + await _prefs.setBool(feature.prefKey, value); + notifyListeners(); + } + + /// Optional helper: filter routes/features in the app shell, etc. + bool get hasInventory => isEnabled(AppFeature.inventory); + + bool get hasCar => isEnabled(AppFeature.car); + + bool get hasHousehold => isEnabled(AppFeature.household); + + bool get hasReports => isEnabled(AppFeature.reports); +} diff --git a/finlog_app/app/lib/core/i18n/translations.g.dart b/finlog_app/app/lib/core/i18n/translations.g.dart index 7b2f310..266a8c0 100644 --- a/finlog_app/app/lib/core/i18n/translations.g.dart +++ b/finlog_app/app/lib/core/i18n/translations.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 104 (52 per locale) +/// Strings: 122 (61 per locale) /// -/// Built on 2025-09-27 at 10:12 UTC +/// Built on 2025-09-27 at 11:35 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/finlog_app/app/lib/core/i18n/translations_de.g.dart b/finlog_app/app/lib/core/i18n/translations_de.g.dart index 145b647..210bd88 100644 --- a/finlog_app/app/lib/core/i18n/translations_de.g.dart +++ b/finlog_app/app/lib/core/i18n/translations_de.g.dart @@ -42,6 +42,7 @@ class TranslationsDe implements Translations { @override late final _TranslationsBudgetDe budget = _TranslationsBudgetDe._(_root); @override late final _TranslationsAppDe app = _TranslationsAppDe._(_root); @override late final _TranslationsSettingsDe settings = _TranslationsSettingsDe._(_root); + @override late final _TranslationsFeaturesDe features = _TranslationsFeaturesDe._(_root); } // Path: login @@ -105,6 +106,7 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn { // Translations @override String get title => 'Einstellungen'; + @override String get featureSettings => 'Funktionseinstellungen'; @override late final _TranslationsSettingsSectionsDe sections = _TranslationsSettingsSectionsDe._(_root); @override late final _TranslationsSettingsItemsDe items = _TranslationsSettingsItemsDe._(_root); @override late final _TranslationsSettingsMessagesDe messages = _TranslationsSettingsMessagesDe._(_root); @@ -115,6 +117,19 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn { @override late final _TranslationsSettingsLegalDe legal = _TranslationsSettingsLegalDe._(_root); } +// Path: features +class _TranslationsFeaturesDe implements TranslationsFeaturesEn { + _TranslationsFeaturesDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override late final _TranslationsFeaturesInventoryDe inventory = _TranslationsFeaturesInventoryDe._(_root); + @override late final _TranslationsFeaturesCarDe car = _TranslationsFeaturesCarDe._(_root); + @override late final _TranslationsFeaturesHouseholdDe household = _TranslationsFeaturesHouseholdDe._(_root); + @override late final _TranslationsFeaturesReportsDe reports = _TranslationsFeaturesReportsDe._(_root); +} + // Path: settings.sections class _TranslationsSettingsSectionsDe implements TranslationsSettingsSectionsEn { _TranslationsSettingsSectionsDe._(this._root); @@ -220,6 +235,50 @@ class _TranslationsSettingsLegalDe implements TranslationsSettingsLegalEn { @override String get termsOfService => 'Nutzungsbedingungen'; } +// Path: features.inventory +class _TranslationsFeaturesInventoryDe implements TranslationsFeaturesInventoryEn { + _TranslationsFeaturesInventoryDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get displayName => 'Inventar'; + @override String get description => 'Verwaltet Gegenstände, Kategorien und Lagerorte.'; +} + +// Path: features.car +class _TranslationsFeaturesCarDe implements TranslationsFeaturesCarEn { + _TranslationsFeaturesCarDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get displayName => 'Auto'; + @override String get description => 'KFZ-Tracking (Tanken, Wartung, Kosten, Kilometer).'; +} + +// Path: features.household +class _TranslationsFeaturesHouseholdDe implements TranslationsFeaturesHouseholdEn { + _TranslationsFeaturesHouseholdDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get displayName => 'Haushalt (inkl. Budget)'; + @override String get description => 'Haushaltsfunktionen inkl. Budgetplanung.'; +} + +// Path: features.reports +class _TranslationsFeaturesReportsDe implements TranslationsFeaturesReportsEn { + _TranslationsFeaturesReportsDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get displayName => 'Berichte'; + @override String get description => 'Statistiken von Ausgaben'; +} + /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. extension on TranslationsDe { @@ -244,6 +303,7 @@ extension on TranslationsDe { case 'app.tooltipExpandRail': return 'Leiste erweitern'; case 'app.drawerSettings': return 'Einstellungen'; case 'settings.title': return 'Einstellungen'; + case 'settings.featureSettings': return 'Funktionseinstellungen'; case 'settings.sections.account': return 'Konto & Daten'; case 'settings.sections.app': return 'App'; case 'settings.sections.help': return 'Hilfe & Rechtliches'; @@ -277,6 +337,14 @@ extension on TranslationsDe { case 'settings.help.sendFeedback': return 'Feedback senden'; case 'settings.legal.privacy': return 'Datenschutz'; case 'settings.legal.termsOfService': return 'Nutzungsbedingungen'; + case 'features.inventory.displayName': return 'Inventar'; + case 'features.inventory.description': return 'Verwaltet Gegenstände, Kategorien und Lagerorte.'; + case 'features.car.displayName': return 'Auto'; + case 'features.car.description': return 'KFZ-Tracking (Tanken, Wartung, Kosten, Kilometer).'; + case 'features.household.displayName': return 'Haushalt (inkl. Budget)'; + case 'features.household.description': return 'Haushaltsfunktionen inkl. Budgetplanung.'; + case 'features.reports.displayName': return 'Berichte'; + case 'features.reports.description': return 'Statistiken von Ausgaben'; default: return null; } } diff --git a/finlog_app/app/lib/core/i18n/translations_en.g.dart b/finlog_app/app/lib/core/i18n/translations_en.g.dart index 881707d..6fbca5d 100644 --- a/finlog_app/app/lib/core/i18n/translations_en.g.dart +++ b/finlog_app/app/lib/core/i18n/translations_en.g.dart @@ -48,6 +48,7 @@ class Translations implements BaseTranslations { late final TranslationsBudgetEn budget = TranslationsBudgetEn._(_root); late final TranslationsAppEn app = TranslationsAppEn._(_root); late final TranslationsSettingsEn settings = TranslationsSettingsEn._(_root); + late final TranslationsFeaturesEn features = TranslationsFeaturesEn._(_root); } // Path: login @@ -148,6 +149,9 @@ class TranslationsSettingsEn { /// en: 'Settings' String get title => 'Settings'; + /// en: 'Feature Settings' + String get featureSettings => 'Feature Settings'; + late final TranslationsSettingsSectionsEn sections = TranslationsSettingsSectionsEn._(_root); late final TranslationsSettingsItemsEn items = TranslationsSettingsItemsEn._(_root); late final TranslationsSettingsMessagesEn messages = TranslationsSettingsMessagesEn._(_root); @@ -158,6 +162,19 @@ class TranslationsSettingsEn { late final TranslationsSettingsLegalEn legal = TranslationsSettingsLegalEn._(_root); } +// Path: features +class TranslationsFeaturesEn { + TranslationsFeaturesEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsFeaturesInventoryEn inventory = TranslationsFeaturesInventoryEn._(_root); + late final TranslationsFeaturesCarEn car = TranslationsFeaturesCarEn._(_root); + late final TranslationsFeaturesHouseholdEn household = TranslationsFeaturesHouseholdEn._(_root); + late final TranslationsFeaturesReportsEn reports = TranslationsFeaturesReportsEn._(_root); +} + // Path: settings.sections class TranslationsSettingsSectionsEn { TranslationsSettingsSectionsEn._(this._root); @@ -329,6 +346,66 @@ class TranslationsSettingsLegalEn { String get termsOfService => 'Terms of Service'; } +// Path: features.inventory +class TranslationsFeaturesInventoryEn { + TranslationsFeaturesInventoryEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Inventory' + String get displayName => 'Inventory'; + + /// en: 'Manages items, categories and storage locations.' + String get description => 'Manages items, categories and storage locations.'; +} + +// Path: features.car +class TranslationsFeaturesCarEn { + TranslationsFeaturesCarEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Car' + String get displayName => 'Car'; + + /// en: 'Vehicle tracking (fuel, maintenance, costs, mileage).' + String get description => 'Vehicle tracking (fuel, maintenance, costs, mileage).'; +} + +// Path: features.household +class TranslationsFeaturesHouseholdEn { + TranslationsFeaturesHouseholdEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Household (incl. Budget)' + String get displayName => 'Household (incl. Budget)'; + + /// en: 'Household functions including budget planning.' + String get description => 'Household functions including budget planning.'; +} + +// Path: features.reports +class TranslationsFeaturesReportsEn { + TranslationsFeaturesReportsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Reports' + String get displayName => 'Reports'; + + /// en: 'Statistics of expenses' + String get description => 'Statistics of expenses'; +} + /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. extension on Translations { @@ -353,6 +430,7 @@ extension on Translations { case 'app.tooltipExpandRail': return 'Expand Rail'; case 'app.drawerSettings': return 'Settings'; case 'settings.title': return 'Settings'; + case 'settings.featureSettings': return 'Feature Settings'; case 'settings.sections.account': return 'Account & Data'; case 'settings.sections.app': return 'App'; case 'settings.sections.help': return 'Help & Legal'; @@ -386,6 +464,14 @@ extension on Translations { case 'settings.help.sendFeedback': return 'Send Feedback'; case 'settings.legal.privacy': return 'Privacy'; case 'settings.legal.termsOfService': return 'Terms of Service'; + case 'features.inventory.displayName': return 'Inventory'; + case 'features.inventory.description': return 'Manages items, categories and storage locations.'; + case 'features.car.displayName': return 'Car'; + case 'features.car.description': return 'Vehicle tracking (fuel, maintenance, costs, mileage).'; + case 'features.household.displayName': return 'Household (incl. Budget)'; + case 'features.household.description': return 'Household functions including budget planning.'; + case 'features.reports.displayName': return 'Reports'; + case 'features.reports.description': return 'Statistics of expenses'; default: return null; } } diff --git a/finlog_app/app/lib/main.dart b/finlog_app/app/lib/main.dart index 4949aa2..8d11952 100644 --- a/finlog_app/app/lib/main.dart +++ b/finlog_app/app/lib/main.dart @@ -1,5 +1,6 @@ import 'package:app/core/app/router.dart'; import 'package:app/core/app/startup/domain/initialize_app.dart'; +import 'package:app/core/features/feature_controller.dart'; import 'package:app/core/i18n/translations.g.dart'; import 'package:app/core/ui/controller/locale_controller.dart'; import 'package:app/core/ui/controller/scale_controller.dart'; @@ -33,12 +34,24 @@ Future main() async { final localeController = LocaleController(); await localeController.init(); + final features = FeatureController(); + await features.init(); + runApp( MultiProvider( providers: [ - ChangeNotifierProvider(create: (context) => themeController), - ChangeNotifierProvider(create: (context) => scaleController), - ChangeNotifierProvider(create: (context) => localeController), + ChangeNotifierProvider( + create: (context) => themeController, + ), + ChangeNotifierProvider( + create: (context) => scaleController, + ), + ChangeNotifierProvider( + create: (context) => localeController, + ), + ChangeNotifierProvider( + create: (context) => features, + ), ], child: TranslationProvider( child: FinlogApp(router: buildAppRouter(startRoute)), diff --git a/finlog_app/app/lib/modules/app_shell.dart b/finlog_app/app/lib/modules/app_shell.dart index f69da0e..dc3764c 100644 --- a/finlog_app/app/lib/modules/app_shell.dart +++ b/finlog_app/app/lib/modules/app_shell.dart @@ -12,8 +12,7 @@ class AppShell extends StatefulWidget { } class _AppShellState extends State { - static const double _railBreakpoint = 800; // tablet and up - bool _railExtended = true; // start "open" on tablet/desktop + static const double _breakpoint = 800; // Tablet/Desktop threshold final FocusNode _contentFocus = FocusNode(debugLabel: 'contentFocus'); @override @@ -24,14 +23,16 @@ class _AppShellState extends State { // --- NAV ITEMS ------------------------------------------------------------- - List<({IconData icon, IconData? selectedIcon, String label, String route})> _getItems(BuildContext context) { + // Desktop/Tablet drawer items (you can keep them rich/longer here) + List<({IconData icon, IconData? selectedIcon, String label, String route})> + _getDesktopItems(BuildContext context) { final t = Translations.of(context); return [ ( icon: Icons.dashboard_outlined, selectedIcon: Icons.dashboard, label: t.app.navigationDashboard, - route: '/home', + route: '/', ), ( icon: Icons.account_balance_wallet_outlined, @@ -52,41 +53,62 @@ class _AppShellState extends State { route: '/reports', ), ( - icon: Icons.settings, - selectedIcon: Icons.settings_outlined, + icon: Icons.settings_outlined, + selectedIcon: Icons.settings, label: t.app.navigationSettings, route: '/settings', ), ]; } - int _indexForPath(String p, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) { + // Mobile bottom bar items (exactly the four you asked for) + List<({IconData icon, String label, String route})> _getMobileTabs( + BuildContext context, + ) { + final t = Translations.of(context); + return [ + (icon: Icons.dashboard, label: t.app.navigationDashboard, route: '/home'), + // “Haushalt (inkl. Budget)” → map to /budget for now + (icon: Icons.home, label: 'Haushalt', route: '/budget'), + (icon: Icons.inventory_2, label: 'Inventar', route: '/inventory'), + (icon: Icons.directions_car, label: 'Auto', route: '/car'), + ]; + } + + int _indexForPath(String path, List items, String Function(T) routeOf) { for (var i = 0; i < items.length; i++) { - if (p.startsWith(items[i].route)) return i; + if (path.startsWith(routeOf(items[i]))) return i; } return 0; } - void _goForIndex(BuildContext ctx, int i, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) => ctx.go(items[i].route); + void _goForIndex( + BuildContext ctx, + int i, + List items, + String Function(T) routeOf, + ) { + ctx.go(routeOf(items[i])); + } @override Widget build(BuildContext context) { final t = Translations.of(context); - final items = _getItems(context); final width = MediaQuery.of(context).size.width; - final isRail = width >= _railBreakpoint; + final isDesktop = width >= _breakpoint; final currentPath = GoRouterState.of(context).matchedLocation; // keep focus on right/content pane on wide layouts - if (isRail) { + if (isDesktop) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && !_contentFocus.hasFocus) _contentFocus.requestFocus(); }); } final appBar = AppBar( - title: _LogoHeader(), - leading: isRail + title: const _LogoHeader(), + // On desktop we use a persistent drawer, so no burger button. + leading: isDesktop ? null : Builder( builder: (ctx) => IconButton( @@ -109,79 +131,42 @@ class _AppShellState extends State { ], ); - if (!isRail) { - // ------------------- MOBILE: Drawer ------------------- - final selectedIndex = _indexForPath(currentPath, items); + if (!isDesktop) { + // ------------------- MOBILE: Bottom Navigation ------------------- + final tabs = _getMobileTabs(context); + final selected = _indexForPath(currentPath, tabs, (it) => it.route); + return Scaffold( appBar: appBar, - drawer: _AppDrawer(items: items, selectedIndex: selectedIndex), + // Keep drawer for mobile? You asked for bottom bar instead — remove drawer. body: SafeArea(child: widget.child), + bottomNavigationBar: NavigationBar( + selectedIndex: selected, + onDestinationSelected: (i) => + _goForIndex(context, i, tabs, (it) => it.route), + destinations: [ + for (final it in tabs) + NavigationDestination(icon: Icon(it.icon), label: it.label), + ], + ), ); } - // ------------------- TABLET/DESKTOP: NavigationRail ------------------- - final selected = _indexForPath(currentPath, items); - final scheme = Theme.of(context).colorScheme; + // ------------------- TABLET/DESKTOP: Persistent Drawer ------------------- + final items = _getDesktopItems(context); + final selected = _indexForPath(currentPath, items, (it) => it.route); return Scaffold( appBar: appBar, body: Row( children: [ - NavigationRailTheme( - data: NavigationRailThemeData( - groupAlignment: -1.0, - // align to top - useIndicator: true, - indicatorColor: scheme.secondaryContainer, - // background for selected "button" - indicatorShape: const StadiumBorder(), - selectedIconTheme: IconThemeData( - color: scheme.onSecondaryContainer, - ), - selectedLabelTextStyle: TextStyle( - color: scheme.onSecondaryContainer, - fontWeight: FontWeight.w600, - ), - ), - child: NavigationRail( - extended: _railExtended, - selectedIndex: selected, - onDestinationSelected: (i) => _goForIndex(context, i, items), - // leading: const Padding( - // padding: EdgeInsets.only(top: 8), - // child: _LogoHeader(), - // ), - destinations: [ - for (final it in items) - NavigationRailDestination( - icon: Icon(it.icon), - selectedIcon: Icon(it.selectedIcon ?? it.icon), - label: Text(it.label), - ), - ], - trailingAtBottom: true, - trailing: Column( - children: [ - const SizedBox(height: 8), - const Divider(height: 1), - // toggle lives at the very bottom so layout doesn't jump - IconButton( - tooltip: _railExtended - ? t.app.tooltipCollapseRail - : t.app.tooltipExpandRail, - onPressed: () => - setState(() => _railExtended = !_railExtended), - icon: Icon( - _railExtended - ? Icons.keyboard_double_arrow_left - : Icons.keyboard_double_arrow_right, - ), - ), - ], - ), - ), + // Persistent drawer area + SizedBox( + width: 300, + child: _DesktopDrawer(items: items, selectedIndex: selected), ), const VerticalDivider(width: 1), + // Content Expanded( child: SafeArea( child: Focus( @@ -197,6 +182,49 @@ class _AppShellState extends State { } } +class _DesktopDrawer extends StatelessWidget { + final List< + ({IconData icon, IconData? selectedIcon, String label, String route}) + > + items; + final int selectedIndex; + + const _DesktopDrawer({required this.items, required this.selectedIndex}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + + return Material( + elevation: 0, + child: SafeArea( + child: ListTileTheme( + selectedColor: scheme.onSecondaryContainer, + selectedTileColor: scheme.secondaryContainer, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 8), + children: [ + const _LogoHeader(), + const Divider(), + for (var i = 0; i < items.length; i++) + ListTile( + selected: i == selectedIndex, + leading: Icon( + i == selectedIndex + ? (items[i].selectedIcon ?? items[i].icon) + : items[i].icon, + ), + title: Text(items[i].label), + onTap: () => context.go(items[i].route), + ), + ], + ), + ), + ), + ); + } +} + class _AppDrawer extends StatelessWidget { final List< ({IconData icon, IconData? selectedIcon, String label, String route}) diff --git a/finlog_app/app/lib/modules/car/car_view.dart b/finlog_app/app/lib/modules/car/car_view.dart new file mode 100644 index 0000000..5da2c59 --- /dev/null +++ b/finlog_app/app/lib/modules/car/car_view.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class CarView extends StatelessWidget { + const CarView({super.key}); + + @override + Widget build(BuildContext context) { + return const Center(child: Text('Auto-Manager ')); + } +} diff --git a/finlog_app/app/lib/modules/settings/modules/app/features_settings_view.dart b/finlog_app/app/lib/modules/settings/modules/app/features_settings_view.dart new file mode 100644 index 0000000..ad9918b --- /dev/null +++ b/finlog_app/app/lib/modules/settings/modules/app/features_settings_view.dart @@ -0,0 +1,73 @@ +import 'package:app/core/features/feature_controller.dart'; +import 'package:app/modules/settings/modules/app/model/feature_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:app/core/i18n/translations.g.dart'; + +/// A dedicated section under "Einstellungen" to enable/disable features. +/// You can link this screen from your existing Settings list. +/// If you keep a single Settings page, render _FeatureSettingsSection in-place. +class FeatureSettingsView extends StatelessWidget { + const FeatureSettingsView({super.key}); + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final featureController = context.read(); + final model = FeatureSettingsViewModel(featureController); + + return ChangeNotifierProvider( + create: (BuildContext context) => model, + child: Scaffold( + appBar: AppBar(title: Text(t.settings.featureSettings)), + body: const _FeatureSettingsSection(), + ), + ); + } +} + +/// If you prefer to embed this into your existing AppSettingsView, +/// use this widget directly inside your Settings ListView/CustomScrollView. +class _FeatureSettingsSection extends StatelessWidget { + const _FeatureSettingsSection(); + + @override + Widget build(BuildContext context) { + final controller = context.watch(); + final states = controller.allStates; + + return ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: states.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (ctx, i) { + final feature = states.keys.elementAt(i); + final enabled = states[feature] ?? feature.defaultEnabled; + + final icon = _iconFor(feature); + + return SwitchListTile( + value: enabled, + secondary: Icon(icon), + title: Text(feature.displayName(context)), + subtitle: Text(feature.description(context)), + onChanged: (v) => controller.setEnabled(feature, v), + ); + }, + ); + } + + IconData _iconFor(AppFeature f) { + switch (f) { + case AppFeature.inventory: + return Icons.inventory_2_outlined; + case AppFeature.car: + return Icons.directions_car; + case AppFeature.household: + return Icons.home_outlined; + case AppFeature.reports: + return Icons.bar_chart_outlined; + } + } +} diff --git a/finlog_app/app/lib/modules/settings/modules/app/model/feature_settings_view_model.dart b/finlog_app/app/lib/modules/settings/modules/app/model/feature_settings_view_model.dart new file mode 100644 index 0000000..be83d20 --- /dev/null +++ b/finlog_app/app/lib/modules/settings/modules/app/model/feature_settings_view_model.dart @@ -0,0 +1,22 @@ +import 'package:app/core/features/feature_controller.dart'; +import 'package:flutter/foundation.dart'; + +/// Lightweight VM that wraps FeatureController for the Settings screen. +/// Mirrors your other *ViewModel classes’ init pattern. +class FeatureSettingsViewModel extends ChangeNotifier { + FeatureSettingsViewModel(this._featureController); + + final FeatureController _featureController; + + Map get states => _featureController.allStates; + + bool isEnabled(AppFeature f) => _featureController.isEnabled(f); + + Future toggle(AppFeature f, bool value) async { + await _featureController.setEnabled(f, value); + notifyListeners(); + } + + /// Expose the controller to listen from the view if needed + FeatureController get controller => _featureController; +} diff --git a/finlog_app/app/lib/modules/settings/settings_view.dart b/finlog_app/app/lib/modules/settings/settings_view.dart index d474ced..a473f3a 100644 --- a/finlog_app/app/lib/modules/settings/settings_view.dart +++ b/finlog_app/app/lib/modules/settings/settings_view.dart @@ -1,6 +1,7 @@ import 'package:app/core/i18n/translations.g.dart'; import 'package:app/core/ui/panel.dart'; import 'package:app/modules/settings/modules/app/app_settings_view.dart'; +import 'package:app/modules/settings/modules/app/features_settings_view.dart'; import 'package:app/modules/settings/modules/help/feedback_view.dart'; import 'package:app/modules/settings/modules/help/help_view.dart'; import 'package:app/modules/settings/modules/help/legal_view.dart'; @@ -16,7 +17,7 @@ class SettingsView extends StatelessWidget { final t = Translations.of(context); return Scaffold( - appBar: AppBar(title: Text(t.settings.title)), + // appBar: AppBar(title: Text(t.settings.title)), body: PanelNavigator(rootBuilder: (ctx) => _CategoryList()), ); } @@ -64,6 +65,12 @@ class _CategoryList extends StatelessWidget { () => const AppSettingsView(), ), const SizedBox(height: 12), + tile( + Icons.tune, + t.settings.featureSettings, + () => const FeatureSettingsView(), + ), + const SizedBox(height: 12), _SectionHeader(t.settings.sections.account), tile(