Compare commits

39 Commits

Author SHA1 Message Date
e8132cee01 Feature: Refactor _DesktopDrawer to support expandable/collapsible states, introduce glass effect panel design, and refine navigation UI. 2025-09-28 13:14:47 +02:00
6532add0d6 dart fix --apply 2025-09-27 20:56:58 +02:00
530476e33d Cleanup 2025-09-27 16:09:56 +02:00
2bdb094819 Feature: Introduce GlassBottomBar for enhanced navigation with a glass effect and integrate it into AppShell for improved UI aesthetics. 2025-09-27 15:39:32 +02:00
ff357d4a4a Remove Logo Header 2025-09-27 15:30:06 +02:00
e30cb54e59 Unused code 2025-09-27 15:20:11 +02:00
329b216876 Feature: Add and implement i18n support for theme settings in German and English, refactor AppSettings to DesignSettings, and improve settings UI structure. 2025-09-27 15:19:47 +02:00
465f7153a4 Cleanup 2025-09-27 14:21:48 +02:00
8fa071e565 Feature: Add feature toggles and settings for modular features (e.g., Car, Inventory), enhance navigation for mobile/desktop, and improve i18n integration. 2025-09-27 13:37:43 +02:00
8ca98d4720 Fixed an issue where selected locale was not used 2025-09-27 12:49:38 +02:00
140e3a7328 Add i18n support and integrate localized strings across modules (Login, AppShell, etc.) 2025-09-27 12:15:57 +02:00
0a0e421158 Feature: Add support for localization, introduce slang for translations, and integrate German and English locale support throughout the app 2025-09-27 11:58:25 +02:00
3e04b9cbe3 Refactor: Introduce LocaleController and ScaleController, unify text and language settings, and simplify AppSettingsView structure. 2025-09-26 22:55:18 +02:00
d5f85c2f41 Refactor: Move theme management to ThemeController, add LocaleController and ScaleController for improved UI customization, and update related imports. 2025-09-26 20:27:45 +02:00
ece3c333eb Simplify theme management, integrate ToggleButtons for theme selection, and enhance settings view structure. 2025-09-26 20:11:10 +02:00
4a4f10d533 Some Code Cleanup 2025-09-25 21:28:21 +02:00
bf5dc6b69c Feature: Add Settings module with PanelNavigator, AppSettingsView, and related components for nested navigation + theming 2025-09-25 21:21:01 +02:00
cfa5ceb393 Code Cleanup 2025-09-25 19:47:29 +02:00
3f515045b2 Feature: Implement AppShell with navigation, add Dashboard, Budget, and Settings views, and integrate GoRouter for routing 2025-09-25 17:29:58 +02:00
25e07aef1e Refactor: Rename features to modules, update imports and README accordingly 2025-09-25 15:53:17 +02:00
3f1b295b65 Implement Login Page, Add App Initialization, Router, and Theme Management 2025-09-23 22:45:47 +02:00
c867133c6b Update README with project structure and Clean Architecture overview 2025-09-23 22:06:45 +02:00
GitLab CI
0789d4408c Merge remote-tracking branch 'origin/production' into dev 2025-09-23 19:13:20 +00:00
ab3c3b674b Merge branch 'fluttery-framework' into 'dev'
Feature #2: Fluttery Framework - Base Essentials

See merge request rhein-software/finlog/finlog!4
2025-09-23 19:01:18 +00:00
c7eafc4bd7 Add Environment service with platform, build mode, and app info support 2025-09-22 22:24:29 +02:00
a7470fc962 Fix test: use dummy test for now 2025-09-22 21:57:33 +02:00
3a4b360f42 Refactor WorkerImpl to integrate logging, enhance testing with mocks, improve timeout and error handling, and add worker ID generation. 2025-09-22 20:04:37 +02:00
64343bbb80 Refactor preferences service initialization to lazy load SharedPreferences and make registerDefaultServices synchronous 2025-09-22 19:34:04 +02:00
d374ff6bf9 Add Worker service with isolated task management and integrate into app 2025-09-22 19:30:18 +02:00
cfd38211a2 Convert registerDefaultServices to async and update service initialization 2025-09-22 19:26:39 +02:00
f3bee63893 Update service registration to use singleton instead of factory 2025-09-22 18:52:07 +02:00
eb8b40c4cd Simplify import paths 2025-09-22 17:39:14 +02:00
f286d7bd0f Fix dart warnings 2025-09-21 11:54:45 +02:00
2df5b6ec62 Integrate secure storage service 2025-09-21 11:53:32 +02:00
fc888f9c1b Register preferences service as a default service 2025-09-21 11:38:33 +02:00
a637becac0 Integrate shared preferences for persistent key-value storage. 2025-09-21 11:37:02 +02:00
5572c66b10 Integrate logging system into Flutter app with service registration and testing setup 2025-09-21 11:03:06 +02:00
daaaed47c4 Add gateway and discovery modules to backend with CI/CD updates 2025-09-21 10:18:32 +02:00
GitLab CI
a4664f894d Merge remote-tracking branch 'origin/production' into dev 2025-09-21 06:59:14 +00:00
38 changed files with 3545 additions and 87 deletions

View File

@@ -1,16 +1,46 @@
# app # Project Structure
A new Flutter project. The project follows a **Clean Architecture inspired** structure with clear separation into **app**, **core**, and *
*features**.
## Getting Started ```text
lib/
app/ → app-wide modules (theme, router, DI, env)
router.dart → global routing (go_router)
theme.dart → app theme and styling
di.dart → dependency injection (providers, locator)
This project is a starting point for a Flutter application. core/ → shared, generic & stable components
network/ → Dio client, interceptors
storage/ → secure/local storage abstractions
error/ → Failure, AppException, guards
utils/ → formatters, validators, helpers
result.dart → Either/Result type
A few resources to get you started if this is your first Flutter project: modules/
inventory/
data/
models/ → DTOs (Json)
sources/ → remote/local data sources
repositories/ → implementations (InventoryRepositoryImpl)
domain/
entities/ → pure Dart objects (business entities)
repositories/ → abstract definitions (InventoryRepository)
usecases/ → business logic (AddItem, ScanBarcode, …)
presentation/
pages/
inventory_page.dart
add_item_page.dart
controllers/ → state management (Riverpod/BLoC) & presenters
widgets/ → feature-specific UI components
inventory_routes.dart → go_router routes for this feature
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) budget/ → same structure as inventory
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) expenses/ → same structure as inventory
auth/ → same structure as inventory
For help getting started with Flutter development, view the main.dart → application entry point
[online documentation](https://docs.flutter.dev/), which offers tutorials, ```
samples, guidance on mobile development, and a full API reference. # Layered Flow
Data Source → Repository Impl → Repository Abstraction → UseCase → Controller → UI (Pages/Widgets)

View File

@@ -0,0 +1,108 @@
{
"hello": "Hallo $name",
"login": {
"title": "Login",
"pleaseSignIn": "Bitte melden Sie sich an",
"signingIn": "Melde Sie an…",
"success": "Login erfolgreich"
},
"dashboard": {
"welcome": "Dashboard Willkommen bei Finlog"
},
"budget": {
"title": "Budgets"
},
"app": {
"navigationCar": "Auto",
"navigationSettings": "Einstellungen",
"navigationDashboard": "Dashboard",
"navigationBudgets": "Budgets",
"navigationHousehold": "Haushalt",
"navigationInventory": "Inventar",
"navigationReports": "Berichte",
"tooltipMenu": "Menü",
"tooltipNotifications": "Benachrichtigungen",
"tooltipUserSettings": "Benutzer-Einstellungen",
"tooltipCollapseRail": "Leiste verkleinern",
"tooltipExpandRail": "Leiste erweitern",
"drawerSettings": "Einstellungen",
"settings": {
"theme": {
"system": "System",
"light": "Hell",
"dark": "Dunkel"
}
}
},
"settings": {
"title": "Einstellungen",
"sections": {
"account": "Konto & Daten",
"app": "App",
"help": "Hilfe & Rechtliches"
},
"items": {
"appSettings": "App-Einstellungen",
"designSettings": "Design-Einstellungen",
"personalData": "Persönliche Daten",
"accountManagement": "Kontoverwaltung",
"helpCenter": "Hilfe",
"feedback": "Feedback",
"legalPrivacy": "Rechtliches & Datenschutz",
"logout": "Abmelden"
},
"messages": {
"logoutNotImplemented": "Logout… (noch nicht implementiert)"
},
"app": {
"systemBackground": "System-Hintergrundfarbe",
"systemDefault": "Systemstandard",
"darkMode": "Dark Mode",
"lightMode": "Light Mode",
"textSize": "Textgröße",
"system": "System",
"small": "Klein",
"medium": "Mittel",
"large": "Groß",
"language": "Sprache",
"german": "Deutsch",
"english": "Englisch"
},
"personalData": {
"name": "Name",
"maxMustermann": "Max Mustermann",
"changePassword": "Passwort ändern",
"twoFactor": "2-Faktor-Authentifizierung",
"off": "Aus"
},
"accountManagement": {
"email": "E-Mail"
},
"help": {
"faq": "FAQ",
"sendFeedback": "Feedback senden"
},
"legal": {
"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"
}
}
}

View File

@@ -0,0 +1,108 @@
{
"hello": "Hello $name",
"login": {
"title": "Login",
"pleaseSignIn": "Please sign in",
"signingIn": "Signing you in…",
"success": "Logged in successfully"
},
"dashboard": {
"welcome": "Dashboard Welcome to Finlog"
},
"budget": {
"title": "Budgets"
},
"app": {
"navigationCar": "Car",
"navigationSettings": "Settings",
"navigationDashboard": "Dashboard",
"navigationHousehold": "Household",
"navigationBudgets": "Budgets",
"navigationInventory": "Inventory",
"navigationReports": "Reports",
"tooltipMenu": "Menu",
"tooltipNotifications": "Notifications",
"tooltipUserSettings": "User Settings",
"tooltipCollapseRail": "Collapse Rail",
"tooltipExpandRail": "Expand Rail",
"drawerSettings": "Settings",
"settings": {
"theme": {
"system": "System",
"light": "Light",
"dark": "Dark"
}
}
},
"settings": {
"title": "Settings",
"sections": {
"account": "Account & Data",
"app": "App",
"help": "Help & Legal"
},
"items": {
"appSettings": "App settings",
"designSettings": "Design settings",
"personalData": "Personal data",
"accountManagement": "Account management",
"helpCenter": "Help",
"feedback": "Feedback",
"legalPrivacy": "Legal & Privacy",
"logout": "Sign out"
},
"messages": {
"logoutNotImplemented": "Logout… (not implemented yet)"
},
"app": {
"systemBackground": "System Background Color",
"systemDefault": "System Default",
"darkMode": "Dark Mode",
"lightMode": "Light Mode",
"textSize": "Text Size",
"system": "System",
"small": "Small",
"medium": "Medium",
"large": "Large",
"language": "Language",
"german": "German",
"english": "English"
},
"personalData": {
"name": "Name",
"maxMustermann": "Max Mustermann",
"changePassword": "Change Password",
"twoFactor": "Two-Factor Authentication",
"off": "Off"
},
"accountManagement": {
"email": "Email"
},
"help": {
"faq": "FAQ",
"sendFeedback": "Send Feedback"
},
"legal": {
"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"
}
}
}

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

View File

@@ -14,6 +14,8 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B4938A305FA6CC3F11C2B360 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98DFFE772CFE93B4BC46C9AD /* Pods_Runner.framework */; };
F56F832F7C6FCA441762B03C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EF9C1B14BC3FAD925D13606 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -45,9 +47,13 @@
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
6A9A17806BE49FD019B0DB41 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
6ACEEED8BD84FC00007A9E9B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7081090C6E22D91B12A63D65 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7EF9C1B14BC3FAD925D13606 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -55,13 +61,26 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
98DFFE772CFE93B4BC46C9AD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A4BF9D95DC3AEA839D03722B /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
F43503EB41EB8364AAA6165B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
FA7CB1383A647C2D24B46844 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
0D70F078EEFD111D42BA3C5C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F56F832F7C6FCA441762B03C /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
B4938A305FA6CC3F11C2B360 /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -94,6 +113,8 @@
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
DA4F95BDE713E57F5B145DB1 /* Pods */,
CA36834C8F3CDFB7D8CB9171 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -121,6 +142,29 @@
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CA36834C8F3CDFB7D8CB9171 /* Frameworks */ = {
isa = PBXGroup;
children = (
98DFFE772CFE93B4BC46C9AD /* Pods_Runner.framework */,
7EF9C1B14BC3FAD925D13606 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
DA4F95BDE713E57F5B145DB1 /* Pods */ = {
isa = PBXGroup;
children = (
6ACEEED8BD84FC00007A9E9B /* Pods-Runner.debug.xcconfig */,
F43503EB41EB8364AAA6165B /* Pods-Runner.release.xcconfig */,
7081090C6E22D91B12A63D65 /* Pods-Runner.profile.xcconfig */,
6A9A17806BE49FD019B0DB41 /* Pods-RunnerTests.debug.xcconfig */,
FA7CB1383A647C2D24B46844 /* Pods-RunnerTests.release.xcconfig */,
A4BF9D95DC3AEA839D03722B /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -128,8 +172,10 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
522EC1B060427CA49FA9A954 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
0D70F078EEFD111D42BA3C5C /* Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -145,12 +191,14 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
6E3C45D3F1E98809BC13B1B5 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
8F7BBF6E33A45DC857BA490D /* [CP] Embed Pods Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -238,6 +286,67 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
522EC1B060427CA49FA9A954 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
6E3C45D3F1E98809BC13B1B5 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
8F7BBF6E33A45DC857BA490D /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@@ -378,6 +487,7 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 6A9A17806BE49FD019B0DB41 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -395,6 +505,7 @@
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = FA7CB1383A647C2D24B46844 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -410,6 +521,7 @@
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A4BF9D95DC3AEA839D03722B /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;

View File

@@ -4,4 +4,7 @@
<FileRef <FileRef
location = "group:Runner.xcodeproj"> location = "group:Runner.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace> </Workspace>

View File

@@ -45,5 +45,10 @@
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>CFBundleLocalizations</key>
<array>
<string>de</string>
<string>en</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,115 @@
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<Preferences>();
/// In-memory cache of feature states
final Map<AppFeature, bool> _enabled = {
for (final f in AppFeature.values) f: f.defaultEnabled,
};
/// Call during app bootstrap (similar to other controllers).
Future<void> 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<AppFeature, bool> get allStates => Map.unmodifiable(_enabled);
Future<void> 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);
}

View File

@@ -0,0 +1,105 @@
import 'package:app/core/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';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:animations/animations.dart';
enum AppRoute {
login('/login'),
home('/home'),
inventory('/inventory'),
inventoryAdd('/inventory/add'),
budget('/budget'),
expenses('/expenses'),
reports('/reports'),
settings('/settings'),
car('/car');
const AppRoute(this.path);
final String path;
}
class AnimatedPage<T> extends CustomTransitionPage<T> {
AnimatedPage({
required LocalKey super.key,
required super.child,
Duration duration = const Duration(milliseconds: 400),
SharedAxisTransitionType transitionType =
SharedAxisTransitionType.horizontal,
}) : super(
transitionDuration: duration,
transitionsBuilder: (context, animation, secondary, child) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondary,
transitionType: transitionType,
child: child,
);
},
);
}
GoRoute _r(
AppRoute route,
Widget Function(BuildContext, GoRouterState) builder, {
List<RouteBase> routes = const [],
SharedAxisTransitionType transitionType = SharedAxisTransitionType.horizontal,
}) {
return GoRoute(
path: route.path,
name: route.name,
pageBuilder: (context, state) => AnimatedPage<void>(
key: state.pageKey,
child: builder(context, state),
transitionType: transitionType,
),
routes: routes,
);
}
GoRouter buildAppRouter(AppRoute initialRoute) {
return GoRouter(
initialLocation: initialRoute.path,
routes: [
// Login separat, ohne Shell
_r(AppRoute.login, (ctx, st) => const LoginPage()),
// App-Inhalte innerhalb der Shell (AppBar + Drawer bleiben stehen)
ShellRoute(
builder: (context, state, child) => AppShell(child: child),
routes: [
_r(AppRoute.home, (ctx, st) => const DashboardView()),
_r(AppRoute.budget, (ctx, st) => const BudgetView()),
_r(AppRoute.settings, (ctx, st) => const SettingsView()),
// Stubs aktualisiere hier, wenn deine Seiten fertig sind:
_r(
AppRoute.inventory,
(ctx, st) => const Center(child: Text('Inventar')),
routes: [
_r(
AppRoute.inventoryAdd,
(ctx, st) => const Center(child: Text('Inventar: Hinzufügen')),
),
],
),
_r(
AppRoute.expenses,
(ctx, st) => const Center(child: Text('Ausgaben')),
),
_r(
AppRoute.reports,
(ctx, st) =>
const Center(child: Text('Reports Auswertungen & Diagramme')),
),
_r(AppRoute.car, (ctx, st) => CarView()),
],
),
],
);
}

View File

@@ -0,0 +1,12 @@
import 'package:app/core/app/router.dart';
class InitializeAppUseCase {
Future<AppRoute> call() async {
// Beispiel: ggf. weitere Init-Schritte
// await _migrateIfNeeded(prefs);
// await _warmupCaches();
// return auth.isLoggedIn ? AppRoute.home : AppRoute.login;
return AppRoute.login;
}
}

View File

@@ -0,0 +1,489 @@
import 'dart:ui' show ImageFilter;
import 'package:app/core/app/router.dart';
import 'package:app/core/app/features/feature_controller.dart';
import 'package:app/core/i18n/translations.g.dart';
import 'package:app/core/ui/glas_bottom_bar.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class AppShell extends StatefulWidget {
final Widget child;
const AppShell({super.key, required this.child});
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
static const double _breakpoint = 800;
final FocusNode _contentFocus = FocusNode(debugLabel: 'contentFocus');
@override
void dispose() {
_contentFocus.dispose();
super.dispose();
}
// --- NAV ITEMS -------------------------------------------------------------
// Desktop/Tablet drawer items
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: AppRoute.home.path,
),
(
icon: Icons.account_balance_wallet_outlined,
selectedIcon: Icons.account_balance_wallet,
label: t.app.navigationBudgets, // Haushaltsbereich inkl. Budget
route: AppRoute.budget.path,
),
(
icon: Icons.inventory_2_outlined,
selectedIcon: Icons.inventory_2,
label: t.app.navigationInventory,
route: AppRoute.inventory.path,
),
(
icon: Icons.bar_chart_outlined,
selectedIcon: Icons.bar_chart,
label: t.app.navigationReports,
route: AppRoute.reports.path,
),
(
icon: Icons.settings_outlined,
selectedIcon: Icons.settings,
label: t.app.navigationSettings,
route: AppRoute.settings.path,
),
];
}
// Mobile bottom bar items (4 Tabs)
List<({IconData icon, String label, String route})> _getMobileTabs(
BuildContext context,
) {
final t = Translations.of(context);
return [
(
icon: Icons.dashboard,
label: t.app.navigationDashboard,
route: AppRoute.home.path,
),
(
icon: Icons.home,
label: (t.app.navigationHousehold),
route: AppRoute.budget.path,
),
(
icon: Icons.inventory_2,
label: t.app.navigationInventory,
route: AppRoute.inventory.path,
),
(
icon: Icons.directions_car,
label: (t.app.navigationCar),
route: AppRoute.car.path,
),
];
}
int _indexForPath<T>(String path, List<T> items, String Function(T) routeOf) {
for (var i = 0; i < items.length; i++) {
if (path.startsWith(routeOf(items[i]))) return i;
}
return 0;
}
// Route→Feature-Guard
bool _routeEnabled(String route, FeatureController fc) {
if (route.startsWith(AppRoute.home.path)) return true;
if (route.startsWith(AppRoute.settings.path)) return true;
if (route.startsWith(AppRoute.budget.path)) return fc.hasHousehold;
if (route.startsWith(AppRoute.inventory.path)) return fc.hasInventory;
if (route.startsWith(AppRoute.car.path)) return fc.hasCar;
if (route.startsWith(AppRoute.reports.path)) return fc.hasReports;
// Default: sichtbar
return true;
}
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final width = MediaQuery.of(context).size.width;
final isDesktop = width >= _breakpoint;
final currentPath = GoRouterState.of(context).matchedLocation;
// keep focus on right/content pane on wide layouts
if (isDesktop) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && !_contentFocus.hasFocus) _contentFocus.requestFocus();
});
}
final appBar = AppBar(
leading: null,
actions: [
IconButton(
tooltip: t.app.tooltipNotifications,
onPressed: () {},
icon: const Icon(Icons.notifications_none),
),
IconButton(
tooltip: t.app.tooltipUserSettings,
onPressed: () => context.push(AppRoute.settings.path),
icon: const Icon(Icons.account_circle_outlined),
),
],
);
if (!isDesktop) {
// ------------------- MOBILE: Bottom Navigation -------------------
final fc = context.read<FeatureController>();
// Wenn aktuelle Route deaktiviert ist, sanft nach Home umleiten
WidgetsBinding.instance.addPostFrameCallback((_) {
final p = GoRouterState.of(context).matchedLocation;
if (!_routeEnabled(p, fc)) {
if (mounted) context.go(AppRoute.home.path);
}
});
return Scaffold(
appBar: appBar,
// Let content flow under the floating bar for the glass effect
extendBody: true,
body: SafeArea(child: widget.child),
bottomNavigationBar: AnimatedBuilder(
animation: fc,
builder: (context, _) {
final baseTabs = _getMobileTabs(context);
// Filter allowed tabs (your existing guard)
var tabs = <({IconData icon, String label, String route})>[
for (final it in baseTabs)
if (_routeEnabled(it.route, fc)) it,
];
// Fallback to ensure min. 2 items
if (tabs.length < 2) {
tabs = [
(
icon: Icons.dashboard,
label: Translations.of(context).app.navigationDashboard,
route: AppRoute.home.path,
),
(
icon: Icons.settings,
label: Translations.of(context).app.navigationSettings,
route: AppRoute.settings.path,
),
];
}
final currentPath = GoRouterState.of(context).matchedLocation;
return GlassBottomBar(
currentPath: currentPath,
onSelect: (route) => context.go(route),
items: [
for (final it in tabs)
GlassBottomBarItem(
icon: it.icon,
label: it.label,
route: it.route,
),
],
);
},
),
);
}
// ------------------- TABLET/DESKTOP: Persistent Drawer -------------------
final items = _getDesktopItems(context);
final selected = _indexForPath(currentPath, items, (it) => it.route);
return Scaffold(
appBar: appBar,
body: Row(
children: [
_DesktopDrawer(items: items, selectedIndex: selected),
const VerticalDivider(width: 1),
// Content
Expanded(
child: SafeArea(
child: Focus(
focusNode: _contentFocus,
autofocus: true,
child: widget.child,
),
),
),
],
),
);
}
}
class _DesktopDrawer extends StatefulWidget {
final List<
({IconData icon, IconData? selectedIcon, String label, String route})
>
items;
final int selectedIndex;
const _DesktopDrawer({required this.items, required this.selectedIndex});
@override
State<_DesktopDrawer> createState() => _DesktopDrawerState();
}
class _DesktopDrawerState extends State<_DesktopDrawer> {
bool _expanded = true; // start expanded
// You can tweak this threshold if needed.
static const double _collapseWidth = 88.0; // collapsed width
static const double _expandWidth = 280.0; // expanded width
static const double _switchAtWidth = 160.0; // below this, use icon-only mode
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final fc = context.read<FeatureController>();
bool routeEnabled(String route) {
if (route.startsWith(AppRoute.home.path)) return true;
if (route.startsWith(AppRoute.settings.path)) return true;
if (route.startsWith(AppRoute.budget.path)) return fc.hasHousehold;
if (route.startsWith(AppRoute.inventory.path)) return fc.hasInventory;
if (route.startsWith(AppRoute.car.path)) return fc.hasCar;
if (route.startsWith(AppRoute.reports.path)) return fc.hasReports;
return true;
}
return AnimatedBuilder(
animation: fc,
builder: (context, _) {
final visibleItems = [
for (final it in widget.items)
if (routeEnabled(it.route)) it,
];
final currentPath = GoRouterState.of(context).matchedLocation;
int safeSelected = 0;
for (var i = 0; i < visibleItems.length; i++) {
if (currentPath.startsWith(visibleItems[i].route)) {
safeSelected = i;
break;
}
}
final double targetWidth = _expanded ? _expandWidth : _collapseWidth;
// Use the same tint as mobile glass bar: cs.surface.withOpacity(0.65)
final Color panelColor = cs.surface.withOpacity(0.65);
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(12),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14),
child: AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
width: targetWidth,
decoration: BoxDecoration(
color: panelColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: cs.outlineVariant.withOpacity(0.40),
),
boxShadow: [
BoxShadow(
color: cs.shadow.withOpacity(0.04),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: LayoutBuilder(
builder: (context, constraints) {
final bool narrow = constraints.maxWidth < _switchAtWidth;
return Column(
children: [
// --------- CONTENT ----------
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
for (var i = 0; i < visibleItems.length; i++)
narrow
? _CollapsedIconItem(
icon:
visibleItems[i].selectedIcon ??
visibleItems[i].icon,
tooltip: visibleItems[i].label,
selected: i == safeSelected,
onTap: () =>
context.go(visibleItems[i].route),
)
: _ExpandedDrawerItem(
icon: visibleItems[i].icon,
selectedIcon:
visibleItems[i].selectedIcon ??
visibleItems[i].icon,
label: visibleItems[i].label,
selected: i == safeSelected,
onTap: () =>
context.go(visibleItems[i].route),
),
],
),
),
const Divider(height: 1),
// Expand/Collapse button (same behavior as before)
IconButton(
tooltip: _expanded
? Translations.of(
context,
).app.tooltipCollapseRail
: Translations.of(
context,
).app.tooltipExpandRail,
onPressed: () =>
setState(() => _expanded = !_expanded),
icon: Icon(
_expanded
? Icons.keyboard_double_arrow_left
: Icons.keyboard_double_arrow_right,
),
),
],
);
},
),
),
),
),
),
);
},
);
}
}
class _ExpandedDrawerItem extends StatelessWidget {
const _ExpandedDrawerItem({
required this.icon,
required this.selectedIcon,
required this.label,
required this.selected,
required this.onTap,
});
final IconData icon;
final IconData selectedIcon;
final String label;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: selected
? cs.primaryContainer.withOpacity(0.35)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
selected ? selectedIcon : icon,
size: 22,
color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant,
),
const SizedBox(width: 16),
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
);
}
}
class _CollapsedIconItem extends StatelessWidget {
const _CollapsedIconItem({
required this.icon,
required this.tooltip,
required this.selected,
required this.onTap,
});
final IconData icon;
final String tooltip;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Tooltip(
message: tooltip,
waitDuration: const Duration(milliseconds: 400),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: selected
? cs.primaryContainer.withOpacity(0.35)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.center,
child: Icon(
icon,
size: 22,
color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant,
),
),
),
);
}
}

View File

@@ -0,0 +1,182 @@
/// Generated file. Do not edit.
///
/// Source: assets/i18n
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 132 (66 per locale)
///
/// Built on 2025-09-27 at 12:40 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:slang/generated.dart';
import 'package:slang_flutter/slang_flutter.dart';
export 'package:slang_flutter/slang_flutter.dart';
import 'translations_de.g.dart' deferred as l_de;
part 'translations_en.g.dart';
/// Supported locales.
///
/// Usage:
/// - LocaleSettings.setLocale(AppLocale.en) // set locale
/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
enum AppLocale with BaseAppLocale<AppLocale, Translations> {
en(languageCode: 'en'),
de(languageCode: 'de');
const AppLocale({
required this.languageCode,
this.scriptCode, // ignore: unused_element, unused_element_parameter
this.countryCode, // ignore: unused_element, unused_element_parameter
});
@override final String languageCode;
@override final String? scriptCode;
@override final String? countryCode;
@override
Future<Translations> build({
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
}) async {
switch (this) {
case AppLocale.en:
return TranslationsEn(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.de:
await l_de.loadLibrary();
return l_de.TranslationsDe(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
}
@override
Translations buildSync({
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
}) {
switch (this) {
case AppLocale.en:
return TranslationsEn(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.de:
return l_de.TranslationsDe(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
}
/// Gets current instance managed by [LocaleSettings].
Translations get translations => LocaleSettings.instance.getTranslations(this);
}
/// Method A: Simple
///
/// No rebuild after locale change.
/// Translation happens during initialization of the widget (call of t).
/// Configurable via 'translate_var'.
///
/// Usage:
/// String a = t.someKey.anotherKey;
/// String b = t['someKey.anotherKey']; // Only for edge cases!
Translations get t => LocaleSettings.instance.currentTranslations;
/// Method B: Advanced
///
/// All widgets using this method will trigger a rebuild when locale changes.
/// Use this if you have e.g. a settings page where the user can select the locale during runtime.
///
/// Step 1:
/// wrap your App with
/// TranslationProvider(
/// child: MyApp()
/// );
///
/// Step 2:
/// final t = Translations.of(context); // Get t variable.
/// String a = t.someKey.anotherKey; // Use t variable.
/// String b = t['someKey.anotherKey']; // Only for edge cases!
class TranslationProvider extends BaseTranslationProvider<AppLocale, Translations> {
TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance);
static InheritedLocaleData<AppLocale, Translations> of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(context);
}
/// Method B shorthand via [BuildContext] extension method.
/// Configurable via 'translate_var'.
///
/// Usage (e.g. in a widget's build method):
/// context.t.someKey.anotherKey
extension BuildContextTranslationsExtension on BuildContext {
Translations get t => TranslationProvider.of(this).translations;
}
/// Manages all translation instances and the current locale
class LocaleSettings extends BaseFlutterLocaleSettings<AppLocale, Translations> {
LocaleSettings._() : super(
utils: AppLocaleUtils.instance,
lazy: true,
);
static final instance = LocaleSettings._();
// static aliases (checkout base methods for documentation)
static AppLocale get currentLocale => instance.currentLocale;
static Stream<AppLocale> getLocaleStream() => instance.getLocaleStream();
static Future<AppLocale> setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);
static Future<AppLocale> setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static Future<AppLocale> useDeviceLocale() => instance.useDeviceLocale();
static Future<void> setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
// synchronous versions
static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync();
static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
/// Provides utility functions without any side effects.
class AppLocaleUtils extends BaseAppLocaleUtils<AppLocale, Translations> {
AppLocaleUtils._() : super(
baseLocale: AppLocale.en,
locales: AppLocale.values,
);
static final instance = AppLocaleUtils._();
// static aliases (checkout base methods for documentation)
static AppLocale parse(String rawLocale) => instance.parse(rawLocale);
static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode);
static AppLocale findDeviceLocale() => instance.findDeviceLocale();
static List<Locale> get supportedLocales => instance.supportedLocales;
static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
}

View File

@@ -0,0 +1,382 @@
///
/// Generated file. Do not edit.
///
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:slang/generated.dart';
import 'translations.g.dart';
// Path: <root>
class TranslationsDe implements Translations {
/// You can call this constructor and build your own translation instance of this locale.
/// Constructing via the enum [AppLocale.build] is preferred.
TranslationsDe({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata<AppLocale, Translations>? meta})
: assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'),
$meta = meta ?? TranslationMetadata(
locale: AppLocale.de,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
) {
$meta.setFlatMapFunction(_flatMapFunction);
}
/// Metadata for the translations of <de>.
@override final TranslationMetadata<AppLocale, Translations> $meta;
/// Access flat map
@override dynamic operator[](String key) => $meta.getTranslation(key);
late final TranslationsDe _root = this; // ignore: unused_field
@override
TranslationsDe $copyWith({TranslationMetadata<AppLocale, Translations>? meta}) => TranslationsDe(meta: meta ?? this.$meta);
// Translations
@override String hello({required Object name}) => 'Hallo ${name}';
@override late final _TranslationsLoginDe login = _TranslationsLoginDe._(_root);
@override late final _TranslationsDashboardDe dashboard = _TranslationsDashboardDe._(_root);
@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
class _TranslationsLoginDe implements TranslationsLoginEn {
_TranslationsLoginDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get title => 'Login';
@override String get pleaseSignIn => 'Bitte melden Sie sich an';
@override String get signingIn => 'Melde Sie an…';
@override String get success => 'Login erfolgreich';
}
// Path: dashboard
class _TranslationsDashboardDe implements TranslationsDashboardEn {
_TranslationsDashboardDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get welcome => 'Dashboard Willkommen bei Finlog';
}
// Path: budget
class _TranslationsBudgetDe implements TranslationsBudgetEn {
_TranslationsBudgetDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get title => 'Budgets';
}
// Path: app
class _TranslationsAppDe implements TranslationsAppEn {
_TranslationsAppDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get navigationCar => 'Auto';
@override String get navigationSettings => 'Einstellungen';
@override String get navigationDashboard => 'Dashboard';
@override String get navigationBudgets => 'Budgets';
@override String get navigationHousehold => 'Haushalt';
@override String get navigationInventory => 'Inventar';
@override String get navigationReports => 'Berichte';
@override String get tooltipMenu => 'Menü';
@override String get tooltipNotifications => 'Benachrichtigungen';
@override String get tooltipUserSettings => 'Benutzer-Einstellungen';
@override String get tooltipCollapseRail => 'Leiste verkleinern';
@override String get tooltipExpandRail => 'Leiste erweitern';
@override String get drawerSettings => 'Einstellungen';
@override late final _TranslationsAppSettingsDe settings = _TranslationsAppSettingsDe._(_root);
}
// Path: settings
class _TranslationsSettingsDe implements TranslationsSettingsEn {
_TranslationsSettingsDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get title => 'Einstellungen';
@override late final _TranslationsSettingsSectionsDe sections = _TranslationsSettingsSectionsDe._(_root);
@override late final _TranslationsSettingsItemsDe items = _TranslationsSettingsItemsDe._(_root);
@override late final _TranslationsSettingsMessagesDe messages = _TranslationsSettingsMessagesDe._(_root);
@override late final _TranslationsSettingsAppDe app = _TranslationsSettingsAppDe._(_root);
@override late final _TranslationsSettingsPersonalDataDe personalData = _TranslationsSettingsPersonalDataDe._(_root);
@override late final _TranslationsSettingsAccountManagementDe accountManagement = _TranslationsSettingsAccountManagementDe._(_root);
@override late final _TranslationsSettingsHelpDe help = _TranslationsSettingsHelpDe._(_root);
@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: app.settings
class _TranslationsAppSettingsDe implements TranslationsAppSettingsEn {
_TranslationsAppSettingsDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override late final _TranslationsAppSettingsThemeDe theme = _TranslationsAppSettingsThemeDe._(_root);
}
// Path: settings.sections
class _TranslationsSettingsSectionsDe implements TranslationsSettingsSectionsEn {
_TranslationsSettingsSectionsDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get account => 'Konto & Daten';
@override String get app => 'App';
@override String get help => 'Hilfe & Rechtliches';
}
// Path: settings.items
class _TranslationsSettingsItemsDe implements TranslationsSettingsItemsEn {
_TranslationsSettingsItemsDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get appSettings => 'App-Einstellungen';
@override String get designSettings => 'Design-Einstellungen';
@override String get personalData => 'Persönliche Daten';
@override String get accountManagement => 'Kontoverwaltung';
@override String get helpCenter => 'Hilfe';
@override String get feedback => 'Feedback';
@override String get legalPrivacy => 'Rechtliches & Datenschutz';
@override String get logout => 'Abmelden';
}
// Path: settings.messages
class _TranslationsSettingsMessagesDe implements TranslationsSettingsMessagesEn {
_TranslationsSettingsMessagesDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get logoutNotImplemented => 'Logout… (noch nicht implementiert)';
}
// Path: settings.app
class _TranslationsSettingsAppDe implements TranslationsSettingsAppEn {
_TranslationsSettingsAppDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get systemBackground => 'System-Hintergrundfarbe';
@override String get systemDefault => 'Systemstandard';
@override String get darkMode => 'Dark Mode';
@override String get lightMode => 'Light Mode';
@override String get textSize => 'Textgröße';
@override String get system => 'System';
@override String get small => 'Klein';
@override String get medium => 'Mittel';
@override String get large => 'Groß';
@override String get language => 'Sprache';
@override String get german => 'Deutsch';
@override String get english => 'Englisch';
}
// Path: settings.personalData
class _TranslationsSettingsPersonalDataDe implements TranslationsSettingsPersonalDataEn {
_TranslationsSettingsPersonalDataDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get name => 'Name';
@override String get maxMustermann => 'Max Mustermann';
@override String get changePassword => 'Passwort ändern';
@override String get twoFactor => '2-Faktor-Authentifizierung';
@override String get off => 'Aus';
}
// Path: settings.accountManagement
class _TranslationsSettingsAccountManagementDe implements TranslationsSettingsAccountManagementEn {
_TranslationsSettingsAccountManagementDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get email => 'E-Mail';
}
// Path: settings.help
class _TranslationsSettingsHelpDe implements TranslationsSettingsHelpEn {
_TranslationsSettingsHelpDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get faq => 'FAQ';
@override String get sendFeedback => 'Feedback senden';
}
// Path: settings.legal
class _TranslationsSettingsLegalDe implements TranslationsSettingsLegalEn {
_TranslationsSettingsLegalDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get privacy => 'Datenschutz';
@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';
}
// Path: app.settings.theme
class _TranslationsAppSettingsThemeDe implements TranslationsAppSettingsThemeEn {
_TranslationsAppSettingsThemeDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get system => 'System';
@override String get light => 'Hell';
@override String get dark => 'Dunkel';
}
/// Flat map(s) containing all translations.
/// Only for edge cases! For simple maps, use the map function of this library.
extension on TranslationsDe {
dynamic _flatMapFunction(String path) {
switch (path) {
case 'hello': return ({required Object name}) => 'Hallo ${name}';
case 'login.title': return 'Login';
case 'login.pleaseSignIn': return 'Bitte melden Sie sich an';
case 'login.signingIn': return 'Melde Sie an…';
case 'login.success': return 'Login erfolgreich';
case 'dashboard.welcome': return 'Dashboard Willkommen bei Finlog';
case 'budget.title': return 'Budgets';
case 'app.navigationCar': return 'Auto';
case 'app.navigationSettings': return 'Einstellungen';
case 'app.navigationDashboard': return 'Dashboard';
case 'app.navigationBudgets': return 'Budgets';
case 'app.navigationHousehold': return 'Haushalt';
case 'app.navigationInventory': return 'Inventar';
case 'app.navigationReports': return 'Berichte';
case 'app.tooltipMenu': return 'Menü';
case 'app.tooltipNotifications': return 'Benachrichtigungen';
case 'app.tooltipUserSettings': return 'Benutzer-Einstellungen';
case 'app.tooltipCollapseRail': return 'Leiste verkleinern';
case 'app.tooltipExpandRail': return 'Leiste erweitern';
case 'app.drawerSettings': return 'Einstellungen';
case 'app.settings.theme.system': return 'System';
case 'app.settings.theme.light': return 'Hell';
case 'app.settings.theme.dark': return 'Dunkel';
case 'settings.title': return 'Einstellungen';
case 'settings.sections.account': return 'Konto & Daten';
case 'settings.sections.app': return 'App';
case 'settings.sections.help': return 'Hilfe & Rechtliches';
case 'settings.items.appSettings': return 'App-Einstellungen';
case 'settings.items.designSettings': return 'Design-Einstellungen';
case 'settings.items.personalData': return 'Persönliche Daten';
case 'settings.items.accountManagement': return 'Kontoverwaltung';
case 'settings.items.helpCenter': return 'Hilfe';
case 'settings.items.feedback': return 'Feedback';
case 'settings.items.legalPrivacy': return 'Rechtliches & Datenschutz';
case 'settings.items.logout': return 'Abmelden';
case 'settings.messages.logoutNotImplemented': return 'Logout… (noch nicht implementiert)';
case 'settings.app.systemBackground': return 'System-Hintergrundfarbe';
case 'settings.app.systemDefault': return 'Systemstandard';
case 'settings.app.darkMode': return 'Dark Mode';
case 'settings.app.lightMode': return 'Light Mode';
case 'settings.app.textSize': return 'Textgröße';
case 'settings.app.system': return 'System';
case 'settings.app.small': return 'Klein';
case 'settings.app.medium': return 'Mittel';
case 'settings.app.large': return 'Groß';
case 'settings.app.language': return 'Sprache';
case 'settings.app.german': return 'Deutsch';
case 'settings.app.english': return 'Englisch';
case 'settings.personalData.name': return 'Name';
case 'settings.personalData.maxMustermann': return 'Max Mustermann';
case 'settings.personalData.changePassword': return 'Passwort ändern';
case 'settings.personalData.twoFactor': return '2-Faktor-Authentifizierung';
case 'settings.personalData.off': return 'Aus';
case 'settings.accountManagement.email': return 'E-Mail';
case 'settings.help.faq': return 'FAQ';
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;
}
}
}

View File

@@ -0,0 +1,520 @@
///
/// Generated file. Do not edit.
///
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
part of 'translations.g.dart';
// Path: <root>
typedef TranslationsEn = Translations; // ignore: unused_element
class Translations implements BaseTranslations<AppLocale, Translations> {
/// Returns the current translations of the given [context].
///
/// Usage:
/// final t = Translations.of(context);
static Translations of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(context).translations;
/// You can call this constructor and build your own translation instance of this locale.
/// Constructing via the enum [AppLocale.build] is preferred.
Translations({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata<AppLocale, Translations>? meta})
: assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'),
$meta = meta ?? TranslationMetadata(
locale: AppLocale.en,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
) {
$meta.setFlatMapFunction(_flatMapFunction);
}
/// Metadata for the translations of <en>.
@override final TranslationMetadata<AppLocale, Translations> $meta;
/// Access flat map
dynamic operator[](String key) => $meta.getTranslation(key);
late final Translations _root = this; // ignore: unused_field
Translations $copyWith({TranslationMetadata<AppLocale, Translations>? meta}) => Translations(meta: meta ?? this.$meta);
// Translations
/// en: 'Hello $name'
String hello({required Object name}) => 'Hello ${name}';
late final TranslationsLoginEn login = TranslationsLoginEn._(_root);
late final TranslationsDashboardEn dashboard = TranslationsDashboardEn._(_root);
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
class TranslationsLoginEn {
TranslationsLoginEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Login'
String get title => 'Login';
/// en: 'Please sign in'
String get pleaseSignIn => 'Please sign in';
/// en: 'Signing you in…'
String get signingIn => 'Signing you in…';
/// en: 'Logged in successfully'
String get success => 'Logged in successfully';
}
// Path: dashboard
class TranslationsDashboardEn {
TranslationsDashboardEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Dashboard Welcome to Finlog'
String get welcome => 'Dashboard Welcome to Finlog';
}
// Path: budget
class TranslationsBudgetEn {
TranslationsBudgetEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Budgets'
String get title => 'Budgets';
}
// Path: app
class TranslationsAppEn {
TranslationsAppEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Car'
String get navigationCar => 'Car';
/// en: 'Settings'
String get navigationSettings => 'Settings';
/// en: 'Dashboard'
String get navigationDashboard => 'Dashboard';
/// en: 'Household'
String get navigationHousehold => 'Household';
/// en: 'Budgets'
String get navigationBudgets => 'Budgets';
/// en: 'Inventory'
String get navigationInventory => 'Inventory';
/// en: 'Reports'
String get navigationReports => 'Reports';
/// en: 'Menu'
String get tooltipMenu => 'Menu';
/// en: 'Notifications'
String get tooltipNotifications => 'Notifications';
/// en: 'User Settings'
String get tooltipUserSettings => 'User Settings';
/// en: 'Collapse Rail'
String get tooltipCollapseRail => 'Collapse Rail';
/// en: 'Expand Rail'
String get tooltipExpandRail => 'Expand Rail';
/// en: 'Settings'
String get drawerSettings => 'Settings';
late final TranslationsAppSettingsEn settings = TranslationsAppSettingsEn._(_root);
}
// Path: settings
class TranslationsSettingsEn {
TranslationsSettingsEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Settings'
String get title => 'Settings';
late final TranslationsSettingsSectionsEn sections = TranslationsSettingsSectionsEn._(_root);
late final TranslationsSettingsItemsEn items = TranslationsSettingsItemsEn._(_root);
late final TranslationsSettingsMessagesEn messages = TranslationsSettingsMessagesEn._(_root);
late final TranslationsSettingsAppEn app = TranslationsSettingsAppEn._(_root);
late final TranslationsSettingsPersonalDataEn personalData = TranslationsSettingsPersonalDataEn._(_root);
late final TranslationsSettingsAccountManagementEn accountManagement = TranslationsSettingsAccountManagementEn._(_root);
late final TranslationsSettingsHelpEn help = TranslationsSettingsHelpEn._(_root);
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: app.settings
class TranslationsAppSettingsEn {
TranslationsAppSettingsEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
late final TranslationsAppSettingsThemeEn theme = TranslationsAppSettingsThemeEn._(_root);
}
// Path: settings.sections
class TranslationsSettingsSectionsEn {
TranslationsSettingsSectionsEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Account & Data'
String get account => 'Account & Data';
/// en: 'App'
String get app => 'App';
/// en: 'Help & Legal'
String get help => 'Help & Legal';
}
// Path: settings.items
class TranslationsSettingsItemsEn {
TranslationsSettingsItemsEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'App settings'
String get appSettings => 'App settings';
/// en: 'Design settings'
String get designSettings => 'Design settings';
/// en: 'Personal data'
String get personalData => 'Personal data';
/// en: 'Account management'
String get accountManagement => 'Account management';
/// en: 'Help'
String get helpCenter => 'Help';
/// en: 'Feedback'
String get feedback => 'Feedback';
/// en: 'Legal & Privacy'
String get legalPrivacy => 'Legal & Privacy';
/// en: 'Sign out'
String get logout => 'Sign out';
}
// Path: settings.messages
class TranslationsSettingsMessagesEn {
TranslationsSettingsMessagesEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Logout… (not implemented yet)'
String get logoutNotImplemented => 'Logout… (not implemented yet)';
}
// Path: settings.app
class TranslationsSettingsAppEn {
TranslationsSettingsAppEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'System Background Color'
String get systemBackground => 'System Background Color';
/// en: 'System Default'
String get systemDefault => 'System Default';
/// en: 'Dark Mode'
String get darkMode => 'Dark Mode';
/// en: 'Light Mode'
String get lightMode => 'Light Mode';
/// en: 'Text Size'
String get textSize => 'Text Size';
/// en: 'System'
String get system => 'System';
/// en: 'Small'
String get small => 'Small';
/// en: 'Medium'
String get medium => 'Medium';
/// en: 'Large'
String get large => 'Large';
/// en: 'Language'
String get language => 'Language';
/// en: 'German'
String get german => 'German';
/// en: 'English'
String get english => 'English';
}
// Path: settings.personalData
class TranslationsSettingsPersonalDataEn {
TranslationsSettingsPersonalDataEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Name'
String get name => 'Name';
/// en: 'Max Mustermann'
String get maxMustermann => 'Max Mustermann';
/// en: 'Change Password'
String get changePassword => 'Change Password';
/// en: 'Two-Factor Authentication'
String get twoFactor => 'Two-Factor Authentication';
/// en: 'Off'
String get off => 'Off';
}
// Path: settings.accountManagement
class TranslationsSettingsAccountManagementEn {
TranslationsSettingsAccountManagementEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Email'
String get email => 'Email';
}
// Path: settings.help
class TranslationsSettingsHelpEn {
TranslationsSettingsHelpEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'FAQ'
String get faq => 'FAQ';
/// en: 'Send Feedback'
String get sendFeedback => 'Send Feedback';
}
// Path: settings.legal
class TranslationsSettingsLegalEn {
TranslationsSettingsLegalEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Privacy'
String get privacy => 'Privacy';
/// en: 'Terms of Service'
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';
}
// Path: app.settings.theme
class TranslationsAppSettingsThemeEn {
TranslationsAppSettingsThemeEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'System'
String get system => 'System';
/// en: 'Light'
String get light => 'Light';
/// en: 'Dark'
String get dark => 'Dark';
}
/// Flat map(s) containing all translations.
/// Only for edge cases! For simple maps, use the map function of this library.
extension on Translations {
dynamic _flatMapFunction(String path) {
switch (path) {
case 'hello': return ({required Object name}) => 'Hello ${name}';
case 'login.title': return 'Login';
case 'login.pleaseSignIn': return 'Please sign in';
case 'login.signingIn': return 'Signing you in…';
case 'login.success': return 'Logged in successfully';
case 'dashboard.welcome': return 'Dashboard Welcome to Finlog';
case 'budget.title': return 'Budgets';
case 'app.navigationCar': return 'Car';
case 'app.navigationSettings': return 'Settings';
case 'app.navigationDashboard': return 'Dashboard';
case 'app.navigationHousehold': return 'Household';
case 'app.navigationBudgets': return 'Budgets';
case 'app.navigationInventory': return 'Inventory';
case 'app.navigationReports': return 'Reports';
case 'app.tooltipMenu': return 'Menu';
case 'app.tooltipNotifications': return 'Notifications';
case 'app.tooltipUserSettings': return 'User Settings';
case 'app.tooltipCollapseRail': return 'Collapse Rail';
case 'app.tooltipExpandRail': return 'Expand Rail';
case 'app.drawerSettings': return 'Settings';
case 'app.settings.theme.system': return 'System';
case 'app.settings.theme.light': return 'Light';
case 'app.settings.theme.dark': return 'Dark';
case 'settings.title': return 'Settings';
case 'settings.sections.account': return 'Account & Data';
case 'settings.sections.app': return 'App';
case 'settings.sections.help': return 'Help & Legal';
case 'settings.items.appSettings': return 'App settings';
case 'settings.items.designSettings': return 'Design settings';
case 'settings.items.personalData': return 'Personal data';
case 'settings.items.accountManagement': return 'Account management';
case 'settings.items.helpCenter': return 'Help';
case 'settings.items.feedback': return 'Feedback';
case 'settings.items.legalPrivacy': return 'Legal & Privacy';
case 'settings.items.logout': return 'Sign out';
case 'settings.messages.logoutNotImplemented': return 'Logout… (not implemented yet)';
case 'settings.app.systemBackground': return 'System Background Color';
case 'settings.app.systemDefault': return 'System Default';
case 'settings.app.darkMode': return 'Dark Mode';
case 'settings.app.lightMode': return 'Light Mode';
case 'settings.app.textSize': return 'Text Size';
case 'settings.app.system': return 'System';
case 'settings.app.small': return 'Small';
case 'settings.app.medium': return 'Medium';
case 'settings.app.large': return 'Large';
case 'settings.app.language': return 'Language';
case 'settings.app.german': return 'German';
case 'settings.app.english': return 'English';
case 'settings.personalData.name': return 'Name';
case 'settings.personalData.maxMustermann': return 'Max Mustermann';
case 'settings.personalData.changePassword': return 'Change Password';
case 'settings.personalData.twoFactor': return 'Two-Factor Authentication';
case 'settings.personalData.off': return 'Off';
case 'settings.accountManagement.email': return 'Email';
case 'settings.help.faq': return 'FAQ';
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;
}
}
}

View File

@@ -0,0 +1,70 @@
import 'package:app/core/i18n/translations.g.dart';
import 'package:flutter/material.dart';
import 'package:fluttery/fluttery.dart';
import 'package:fluttery/preferences.dart';
class LocaleController extends ChangeNotifier {
final Preferences _prefs = App.service<Preferences>();
static const _key = 'language';
LanguagePref _current = LanguagePref.system;
/// Einmal beim App-Start aufrufen
Future<void> init() async {
final saved = await _prefs.getString(_key);
_current = _fromString(saved) ?? LanguagePref.system;
_applyToSlang(_current);
}
/// Sprache ändern (persistieren + sofort anwenden)
Future<void> setLanguage(LanguagePref lang) async {
_current = lang;
await _prefs.setString(_key, lang.name);
print(lang);
_applyToSlang(lang);
notifyListeners();
}
void _applyToSlang(LanguagePref pref) {
final code = pref.code;
if (pref == LanguagePref.system || code == null) {
LocaleSettings.useDeviceLocale();
return;
}
if (AppLocaleUtils.supportedLocalesRaw.contains(code)) {
LocaleSettings.setLocaleRaw(code);
} else {
LocaleSettings.setLocale(AppLocale.en);
}
}
LanguagePref? _fromString(String? value) {
if (value == null || value.isEmpty) return null;
return LanguagePref.values.firstWhere(
(e) => e.name == value,
orElse: () => LanguagePref.system,
);
}
LanguagePref get language => _current;
Locale? get locale => _current.locale;
bool get isSystem => _current == LanguagePref.system;
}
enum LanguagePref {
system(null),
de(Locale('de')),
en(Locale('en'));
final Locale? locale;
const LanguagePref(this.locale);
String? get code => locale?.languageCode;
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:fluttery/fluttery.dart';
import 'package:fluttery/preferences.dart';
class ScaleController extends ChangeNotifier {
final Preferences _prefs;
ScaleController() : _prefs = App.service<Preferences>();
static const _key = 'textScale';
TextScalePref _current = TextScalePref.system;
/// Faktor direkt vom Enum
double get factor => _current.factor;
Future<void> init() async {
final saved = await _prefs.getString(_key);
_current = switch (saved) {
'small' => TextScalePref.small,
'medium' => TextScalePref.medium,
'large' => TextScalePref.large,
_ => TextScalePref.system,
};
notifyListeners();
}
/// Set text scale and persist.
void setScale(TextScalePref pref) {
if (_current == pref) return;
_current = pref;
notifyListeners();
_prefs.setString(_key, pref.name); // fire-and-forget
}
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);
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:fluttery/fluttery.dart';
import 'package:fluttery/preferences.dart';
/// Controls the current theme of the application.
/// Loads the theme from Preferences or falls back to system default.
class ThemeController extends ChangeNotifier {
final Preferences _prefs;
/// themeMode (system, dark or white)
ThemeMode _themeMode = ThemeMode.system;
/// Constructor
ThemeController() : _prefs = App.service<Preferences>();
/// Loads theme from Preferences (or defaults to system).
Future<void> init() async {
final saved = await _prefs.getString('theme');
if (saved == null) {
_themeMode = ThemeMode.system;
} else {
_themeMode = _fromString(saved);
}
notifyListeners();
}
/// Sets theme and persists it in Preferences.
void setTheme(ThemeMode mode) {
_themeMode = mode;
notifyListeners();
_prefs.setString('theme', mode.name);
}
ThemeMode _fromString(String value) {
switch (value) {
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
case 'system':
default:
return ThemeMode.system;
}
}
/// get current ThemeMode
ThemeMode get themeMode => _themeMode;
}

View File

@@ -0,0 +1,129 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class GlassBottomBarItem {
final IconData icon;
final String label;
final String route;
const GlassBottomBarItem({
required this.icon,
required this.label,
required this.route,
});
}
class GlassBottomBar extends StatelessWidget {
const GlassBottomBar({
super.key,
required this.items,
required this.currentPath,
required this.onSelect,
});
final List<GlassBottomBarItem> items;
final String currentPath;
final void Function(String route) onSelect;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
bool isSelected(String route) => currentPath.startsWith(route);
return SafeArea(
minimum: const EdgeInsets.only(left: 12, right: 12, bottom: 12),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: cs.surface.withOpacity(0.65), // glass tint
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline.withOpacity(0.18)),
boxShadow: [
BoxShadow(
color: cs.shadow.withOpacity(0.08),
blurRadius: 18,
offset: const Offset(0, 6),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (final it in items)
_GlassItem(
icon: it.icon,
label: it.label,
selected: isSelected(it.route),
onTap: () => onSelect(it.route),
),
],
),
),
),
),
);
}
}
class _GlassItem extends StatelessWidget {
const _GlassItem({
required this.icon,
required this.label,
required this.selected,
required this.onTap,
});
final IconData icon;
final String label;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOut,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: selected
? cs.primaryContainer.withOpacity(0.35)
: Colors.transparent,
borderRadius: BorderRadius.circular(14),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 22,
color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant,
),
const SizedBox(height: 4),
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant,
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
/// Ermöglicht verschachtelte "Panels" ähnlich einem StackNavigator.
/// Beispiel:
/// ```dart
/// PanelNavigator(
/// rootBuilder: (_) => MyRootPanel(),
/// )
/// ```
class PanelNavigator extends StatefulWidget {
final WidgetBuilder rootBuilder;
const PanelNavigator({super.key, required this.rootBuilder});
@override
State<PanelNavigator> createState() => _PanelNavigatorState();
/// Zugriff per InheritedWidget / Extension
static PanelController of(BuildContext context) {
final inherited = context
.dependOnInheritedWidgetOfExactType<_PanelInherited>();
assert(inherited != null, 'Kein PanelNavigator im Widget-Tree gefunden');
return inherited!.controller;
}
}
class _PanelNavigatorState extends State<PanelNavigator> {
late final PanelController _controller;
@override
void initState() {
super.initState();
_controller = PanelController(
rootBuilder: widget.rootBuilder,
onUpdate: () => setState(() {}),
);
}
@override
Widget build(BuildContext context) {
return _PanelInherited(
controller: _controller,
child: PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
reverse: _controller.navDirection == _NavDirection.back,
transitionBuilder: (child, primary, secondary) {
return SharedAxisTransition(
animation: primary,
secondaryAnimation: secondary,
transitionType: SharedAxisTransitionType.horizontal,
fillColor: Colors.transparent,
child: child,
);
},
child: _controller.getCurrentPanel(context),
),
);
}
}
class PanelController {
final WidgetBuilder rootBuilder;
final VoidCallback onUpdate;
final List<_PanelEntry> _stack = [];
_NavDirection _navDirection = _NavDirection.forward;
int _version = 0;
PanelController({required this.rootBuilder, required this.onUpdate}) {
_stack.add(_PanelEntry(title: null, builder: rootBuilder));
}
_NavDirection get navDirection => _navDirection;
Widget getCurrentPanel(BuildContext context) {
final entry = _stack.last;
return KeyedSubtree(
key: ValueKey('panel_v${_version}_${_stack.length}'),
child: _PanelScaffold(
title: entry.title,
body: entry.builder(context),
onBack: canPop ? pop : null,
),
);
}
bool get canPop => _stack.length > 1;
void push(WidgetBuilder builder, {String? title}) {
_navDirection = _NavDirection.forward;
_stack.add(_PanelEntry(title: title, builder: builder));
_version++;
onUpdate();
}
void pop() {
if (canPop) {
_navDirection = _NavDirection.back;
_stack.removeLast();
_version++;
onUpdate();
}
}
}
enum _NavDirection { forward, back }
class _PanelEntry {
final String? title;
final WidgetBuilder builder;
_PanelEntry({this.title, required this.builder});
}
class _PanelScaffold extends StatelessWidget {
final String? title;
final Widget body;
final VoidCallback? onBack;
const _PanelScaffold({required this.title, required this.body, this.onBack});
@override
Widget build(BuildContext context) {
return Column(
children: [
if (title != null) PanelHeader(title: title!, onBack: onBack),
Expanded(
child: Padding(padding: const EdgeInsets.all(15), child: body),
),
],
);
}
}
class _PanelInherited extends InheritedWidget {
final PanelController controller;
const _PanelInherited({required super.child, required this.controller});
@override
bool updateShouldNotify(_PanelInherited oldWidget) =>
controller != oldWidget.controller;
}
/// ---------- PUBLIC UI COMPONENTS ----------
class PanelHeader extends StatelessWidget {
final String title;
final VoidCallback? onBack;
const PanelHeader({super.key, required this.title, this.onBack});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Material(
color: scheme.surface,
elevation: 1,
child: SizedBox(
height: 56,
child: Row(
children: [
if (onBack != null)
IconButton(icon: const Icon(Icons.arrow_back), onPressed: onBack),
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
),
);
}
}
/// ---------- EXTENSIONS ----------
extension PanelContext on BuildContext {
PanelController get panels => PanelNavigator.of(this);
}

View File

@@ -1,7 +1,16 @@
import 'package:app/core/app/router.dart';
import 'package:app/core/app/startup/domain/initialize_app.dart';
import 'package:app/core/app/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';
import 'package:app/core/ui/controller/theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:fluttery/fluttery.dart'; import 'package:fluttery/fluttery.dart';
import 'package:fluttery/logger.dart'; import 'package:fluttery/logger.dart';
import 'package:fluttery/worker.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
Future<void> main() async { Future<void> main() async {
// Ensures that the Flutter engine and widget binding // Ensures that the Flutter engine and widget binding
@@ -14,81 +23,71 @@ Future<void> main() async {
final logger = App.service<Logger>(); final logger = App.service<Logger>();
logger.debug("[MAIN] Registered all default services"); logger.debug("[MAIN] Registered all default services");
runApp(const MyApp()); // Run initialization before building router
} final init = InitializeAppUseCase();
final startRoute = await init();
class MyApp extends StatelessWidget { final themeController = ThemeController();
const MyApp({super.key}); await themeController.init();
// This widget is the root of your application. final scaleController = ScaleController();
@override final localeController = LocaleController();
Widget build(BuildContext context) { await localeController.init();
App.service<Logger>().info("test");
return MaterialApp( final features = FeatureController();
title: 'Flutter Demo', await features.init();
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<ThemeController>(
create: (context) => themeController,
),
ChangeNotifierProvider<ScaleController>(
create: (context) => scaleController,
),
ChangeNotifierProvider<LocaleController>(
create: (context) => localeController,
),
ChangeNotifierProvider<FeatureController>(
create: (context) => features,
),
],
child: TranslationProvider(
child: FinlogApp(router: buildAppRouter(startRoute)),
), ),
home: const MyHomePage(title: 'Flutter Demo Home Page'), ),
); );
}
} }
class MyHomePage extends StatefulWidget { class FinlogApp extends StatelessWidget {
const MyHomePage({super.key, required this.title}); final GoRouter router;
final String title; const FinlogApp({super.key, required this.router});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
App.service<Worker>().spawn("worker-$_counter", () async {
App.service<Logger>().info("test");
await Future.delayed(const Duration(seconds: 10));
App.service<Logger>().info("end worker");
});
setState(() {
_counter++;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final theme = context.watch<ThemeController>();
appBar: AppBar( final textScale = context.watch<ScaleController>();
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title), return AnimatedBuilder(
), animation: theme,
body: Center( builder: (context, _) => MaterialApp.router(
child: Column( title: 'Finlog',
mainAxisAlignment: MainAxisAlignment.center, locale: TranslationProvider.of(context).flutterLocale,
children: <Widget>[ supportedLocales: AppLocaleUtils.supportedLocales,
const Text('You have pushed the button this many times:'), localizationsDelegates: GlobalMaterialLocalizations.delegates,
Text( theme: ThemeData.light(),
'$_counter', darkTheme: ThemeData.dark(),
style: Theme.of(context).textTheme.headlineMedium, themeMode: theme.themeMode,
), routerConfig: router,
TextButton( builder: (context, child) => MediaQuery(
onPressed: () { data: MediaQuery.of(
}, context,
child: Text("Print workers"), ).copyWith(textScaler: TextScaler.linear(textScale.factor)),
), child: child ?? Container(),
],
), ),
), ),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
); );
} }
} }

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class BudgetView extends StatelessWidget {
const BudgetView({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Budgets'));
}
}

View File

@@ -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 '));
}
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class DashboardView extends StatelessWidget {
const DashboardView({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Dashboard Willkommen bei Finlog'));
}
}

View File

@@ -0,0 +1,84 @@
import 'package:app/core/app/router.dart';
import 'package:app/core/i18n/translations.g.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
bool _loading = false;
Future<void> _simulateLogin() async {
if (_loading) return;
setState(() => _loading = true);
// Simulate latency, perform demo login, then go home.
await Future<void>.delayed(const Duration(milliseconds: 900));
// await App.service<Auth>().login('demo', 'demo');
if (!mounted) return;
context.go(AppRoute.home.path);
setState(() => _loading = false);
}
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: Text(t.login.title)),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlutterLogo(size: 64),
const SizedBox(height: 24),
Text(t.login.pleaseSignIn, style: theme.textTheme.titleMedium),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _loading ? null : _simulateLogin,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
transitionBuilder: (child, anim) =>
FadeTransition(opacity: anim, child: child),
child: _loading
? const SizedBox(
key: ValueKey('loading'),
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(t.login.title, key: const ValueKey('text')),
),
),
),
const SizedBox(height: 12),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: _loading
? Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
t.login.signingIn,
style: theme.textTheme.bodySmall,
),
)
: const SizedBox.shrink(),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,263 @@
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';
import 'package:app/core/ui/controller/theme.dart';
import 'package:app/modules/settings/modules/app/model/design_settings_view_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class DesignSettingsView extends StatelessWidget {
const DesignSettingsView({super.key});
@override
Widget build(BuildContext context) {
final themeModel = context.read<ThemeController>();
final scaleModel = context.read<ScaleController>();
final localeModel = context.read<LocaleController>();
return ChangeNotifierProvider(
create: (_) => DesignSettingsViewModel(
themeModel: themeModel,
scaleModel: scaleModel,
localeModel: localeModel,
)..load(),
child: const _AppSettingsContent(),
);
}
}
class _AppSettingsContent extends StatelessWidget {
const _AppSettingsContent();
@override
Widget build(BuildContext context) {
final vm = context.watch<DesignSettingsViewModel>();
final isLoading = vm.isLoading;
return Column(
children: [
if (isLoading)
const Expanded(child: Center(child: CircularProgressIndicator()))
else
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_SystemBackgroundSection(),
const SizedBox(height: 16),
// _TextScaleSection(),
// const SizedBox(height: 16),
_LanguageSection(),
],
),
),
],
);
}
}
class _SystemBackgroundSection extends StatelessWidget {
const _SystemBackgroundSection();
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final vm = context.watch<DesignSettingsViewModel>();
final selected = vm.themeMode;
final cs = Theme.of(context).colorScheme;
final dividerColor = Theme.of(context).dividerColor;
Widget radioTile({
required bool isSelected,
required String label,
required VoidCallback onTap,
}) {
return ColoredBox(
color: cs.surface,
child: ListTile(
// contentPadding: const EdgeInsets.symmetric(
// // horizontal: 7,
// vertical: 0,
// ),
leading: Icon(
isSelected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
color: isSelected ? cs.primary : null,
),
title: Text(
label,
style: isSelected
? const TextStyle(fontWeight: FontWeight.w600)
: null,
),
onTap: onTap,
selected: isSelected,
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.settings.app.systemBackground,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
radioTile(
isSelected: selected == ThemeMode.system,
label: t.settings.app.systemDefault,
onTap: () => vm.setThemeMode(ThemeMode.system),
),
Divider(height: 1, thickness: 1, color: dividerColor),
radioTile(
isSelected: selected == ThemeMode.dark,
label: t.settings.app.darkMode,
onTap: () => vm.setThemeMode(ThemeMode.dark),
),
Divider(height: 1, thickness: 1, color: dividerColor),
radioTile(
isSelected: selected == ThemeMode.light,
label: t.settings.app.lightMode,
onTap: () => vm.setThemeMode(ThemeMode.light),
),
],
);
}
}
// 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 _TextScaleSection extends StatelessWidget {
// const _TextScaleSection();
//
// @override
// Widget build(BuildContext context) {
// final t = Translations.of(context);
// final vm = context.watch<DesignSettingsViewModel>();
// final selected = vm.textScale;
//
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// t.settings.app.textSize,
// style: const TextStyle(fontWeight: FontWeight.w600),
// ),
// const SizedBox(height: 8),
// 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: [
// _SegItem(icon: Icons.phone_android, label: t.settings.app.system),
// _SegItem(icon: Icons.text_fields, label: t.settings.app.small),
// _SegItem(icon: Icons.text_fields, label: t.settings.app.medium),
// _SegItem(icon: Icons.text_fields, label: t.settings.app.large),
// ],
// ),
// ],
// );
// }
// }
class _LanguageSection extends StatelessWidget {
const _LanguageSection();
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final vm = context.watch<DesignSettingsViewModel>();
final scheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.settings.app.language,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
DropdownButtonFormField<LanguagePref>(
initialValue: vm.language,
onChanged: (v) => vm.setLanguage(v ?? LanguagePref.en),
items: [
DropdownMenuItem(
value: LanguagePref.system,
child: Text(t.settings.app.systemDefault),
),
DropdownMenuItem(
value: LanguagePref.de,
child: Text(t.settings.app.german),
),
DropdownMenuItem(
value: LanguagePref.en,
child: Text(t.settings.app.english),
),
],
decoration: InputDecoration(
isDense: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 4),
Text(
'Änderungen wirken sich nach dem Speichern aus.',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: scheme.outline),
),
],
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:app/core/app/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';
/// 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 AppSettingsView extends StatelessWidget {
const AppSettingsView({super.key});
@override
Widget build(BuildContext context) {
final featureController = context.read<FeatureController>();
final model = FeatureSettingsViewModel(featureController);
return ChangeNotifierProvider(
create: (BuildContext context) => model,
child: Scaffold(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<FeatureController>();
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;
}
}
}

View File

@@ -0,0 +1,55 @@
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';
class DesignSettingsViewModel extends ChangeNotifier {
bool _isLoading = false;
final ThemeController _theme;
final ScaleController _scale;
final LocaleController _locale;
DesignSettingsViewModel({
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<void> load() async {
_isLoading = true;
notifyListeners();
// TODO: Replace with real backend call.
await Future<void>.delayed(const Duration(milliseconds: 200));
_isLoading = false;
notifyListeners();
}
void setThemeMode(ThemeMode mode) {
_theme.setTheme(mode);
notifyListeners();
}
void setTextScale(TextScalePref pref) {
_scale.setScale(pref);
notifyListeners();
}
void setLanguage(LanguagePref lang) {
_locale.setLanguage(lang);
notifyListeners();
}
ThemeMode get themeMode => _theme.themeMode;
LanguagePref? get language => _locale.language;
TextScalePref get textScale => _scale.scale;
bool get isLoading => _isLoading;
}

View File

@@ -0,0 +1,22 @@
import 'package:app/core/app/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<AppFeature, bool> get states => _featureController.allStates;
bool isEnabled(AppFeature f) => _featureController.isEnabled(f);
Future<void> 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;
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class FeedbackPanel extends StatelessWidget {
const FeedbackPanel({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// const PanelHeader(title: 'Feedback'),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(
leading: Icon(Icons.feedback_outlined),
title: Text('Feedback senden'),
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class HelpPanel extends StatelessWidget {
const HelpPanel({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// const PanelHeader(title: 'Hilfe'),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(leading: Icon(Icons.help_outline), title: Text('FAQ')),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class LegalPanel extends StatelessWidget {
const LegalPanel({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// const PanelHeader(title: 'Rechtliches & Datenschutz'),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(
leading: Icon(Icons.privacy_tip_outlined),
title: Text('Datenschutz'),
),
ListTile(
leading: Icon(Icons.article_outlined),
title: Text('Nutzungsbedingungen'),
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class AccountPanel extends StatelessWidget {
const AccountPanel({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// const PanelHeader(title: 'Kontoverwaltung'),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(
leading: Icon(Icons.alternate_email),
title: Text('E-Mail'),
subtitle: Text('max@example.com'),
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class PersonalPanel extends StatelessWidget {
const PersonalPanel({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(
leading: Icon(Icons.person_outline),
title: Text('Name'),
subtitle: Text('Max Mustermann'),
),
ListTile(
leading: Icon(Icons.lock_outline),
title: Text('Passwort ändern'),
),
ListTile(
leading: Icon(Icons.phonelink_lock),
title: Text('2-Faktor-Authentifizierung'),
subtitle: Text('Aus'),
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,138 @@
import 'package:app/core/i18n/translations.g.dart';
import 'package:app/core/ui/panel.dart';
import 'package:app/modules/settings/modules/app/design_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';
import 'package:app/modules/settings/modules/user_data/account_management_view.dart';
import 'package:app/modules/settings/modules/user_data/personal_data_view.dart';
import 'package:flutter/material.dart';
class SettingsView extends StatelessWidget {
const SettingsView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: PanelNavigator(rootBuilder: (ctx) => _CategoryList()),
);
}
}
/// ----------------- Root panel: Category list -----------------
class _CategoryList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final t = Translations.of(context);
Widget tile(IconData icon, String label, Widget Function() detail) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () => context.panels.push((_) => detail(), title: label),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
Icon(icon),
const SizedBox(width: 12),
Expanded(child: Text(label)),
Icon(Icons.chevron_right, color: scheme.outline),
],
),
),
),
);
}
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SectionHeader(t.settings.sections.app),
tile(
Icons.tune,
t.settings.items.appSettings,
() => const AppSettingsView(),
),
const SizedBox(height: 12),
tile(
Icons.phone_iphone,
t.settings.items.designSettings,
() => const DesignSettingsView(),
),
const SizedBox(height: 12),
_SectionHeader(t.settings.sections.account),
tile(
Icons.badge_outlined,
t.settings.items.personalData,
() => const PersonalPanel(),
),
tile(
Icons.manage_accounts_outlined,
t.settings.items.accountManagement,
() => const AccountPanel(),
),
const SizedBox(height: 12),
_SectionHeader(t.settings.sections.help),
tile(
Icons.help_outline,
t.settings.items.helpCenter,
() => const HelpPanel(),
),
tile(
Icons.feedback_outlined,
t.settings.items.feedback,
() => const FeedbackPanel(),
),
tile(
Icons.gavel_outlined,
t.settings.items.legalPrivacy, // "Rechtliches & Datenschutz"
() => const LegalPanel(),
),
const SizedBox(height: 24),
const Divider(),
ListTile(
leading: const Icon(Icons.logout),
title: Text(t.settings.items.logout),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(t.settings.messages.logoutNotImplemented),
),
);
},
),
],
),
);
}
}
class _SectionHeader extends StatelessWidget {
final String text;
const _SectionHeader(this.text);
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 6),
child: Text(
text,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: .2,
),
),
);
}

View File

@@ -31,23 +31,23 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
fluttery: fluttery:
path: ../fluttery path: ../fluttery
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
go_router: ^16.2.2
animations: ^2.0.11
provider: ^6.1.5+1
slang: ^4.8.1
slang_flutter: ^4.8.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
build_runner: ^2.8.0
slang_build_runner: ^4.8.1
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

10
finlog_app/app/slang.yaml Normal file
View File

@@ -0,0 +1,10 @@
base_locale: en
input_directory: assets/i18n
input_file_pattern: .i18n.json
output_directory: lib/core/i18n
output_file_name: translations.g.dart
flutter_integration: true
locale_handling: true
lazy: true
class_name: Translations
enum_name: AppLocale

View File

@@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:fluttery/logger.dart'; import 'package:fluttery/logger.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@@ -8,16 +9,18 @@ class MockUtils {
final logger = MockLogger(); final logger = MockLogger();
when(() => logger.debug(any())).thenAnswer((a) { when(() => logger.debug(any())).thenAnswer((a) {
print("[DEBUG] ${a.positionalArguments[0]}"); debugPrint("[DEBUG] ${a.positionalArguments[0]}");
}); });
when(() => logger.info(any())).thenAnswer((a) { when(() => logger.info(any())).thenAnswer((a) {
print("[INFO] ${a.positionalArguments[0]}"); debugPrint("[INFO] ${a.positionalArguments[0]}");
}); });
when(() => logger.warning(any())).thenAnswer((a) { when(() => logger.warning(any())).thenAnswer((a) {
print("[WARN] ${a.positionalArguments[0]}"); debugPrint("[WARN] ${a.positionalArguments[0]}");
}); });
when(() => logger.error(any(), any(), any())).thenAnswer((a) { when(() => logger.error(any(), any(), any())).thenAnswer((a) {
print("[ERROR] ${a.positionalArguments[0]}\n${a.positionalArguments[2]}"); debugPrint(
"[ERROR] ${a.positionalArguments[0]}\n${a.positionalArguments[2]}",
);
}); });
return logger; return logger;