Compare commits
39 Commits
production
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| e8132cee01 | |||
| 6532add0d6 | |||
| 530476e33d | |||
| 2bdb094819 | |||
| ff357d4a4a | |||
| e30cb54e59 | |||
| 329b216876 | |||
| 465f7153a4 | |||
| 8fa071e565 | |||
| 8ca98d4720 | |||
| 140e3a7328 | |||
| 0a0e421158 | |||
| 3e04b9cbe3 | |||
| d5f85c2f41 | |||
| ece3c333eb | |||
| 4a4f10d533 | |||
| bf5dc6b69c | |||
| cfa5ceb393 | |||
| 3f515045b2 | |||
| 25e07aef1e | |||
| 3f1b295b65 | |||
| c867133c6b | |||
|
|
0789d4408c | ||
| ab3c3b674b | |||
| c7eafc4bd7 | |||
| a7470fc962 | |||
| 3a4b360f42 | |||
| 64343bbb80 | |||
| d374ff6bf9 | |||
| cfd38211a2 | |||
| f3bee63893 | |||
| eb8b40c4cd | |||
| f286d7bd0f | |||
| 2df5b6ec62 | |||
| fc888f9c1b | |||
| a637becac0 | |||
| 5572c66b10 | |||
| daaaed47c4 | |||
|
|
a4664f894d |
@@ -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)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
budget/ → same structure as inventory
|
||||
expenses/ → same structure as inventory
|
||||
auth/ → same structure as inventory
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
main.dart → application entry point
|
||||
```
|
||||
# Layered Flow
|
||||
|
||||
Data Source → Repository Impl → Repository Abstraction → UseCase → Controller → UI (Pages/Widgets)
|
||||
|
||||
108
finlog_app/app/assets/i18n/de.i18n.json
Normal file
108
finlog_app/app/assets/i18n/de.i18n.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
108
finlog_app/app/assets/i18n/en.i18n.json
Normal file
108
finlog_app/app/assets/i18n/en.i18n.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -45,9 +47,13 @@
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
@@ -55,13 +61,26 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
0D70F078EEFD111D42BA3C5C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F56F832F7C6FCA441762B03C /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B4938A305FA6CC3F11C2B360 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -94,6 +113,8 @@
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
DA4F95BDE713E57F5B145DB1 /* Pods */,
|
||||
CA36834C8F3CDFB7D8CB9171 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -121,6 +142,29 @@
|
||||
path = Runner;
|
||||
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 */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -128,8 +172,10 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
522EC1B060427CA49FA9A954 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
0D70F078EEFD111D42BA3C5C /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -145,12 +191,14 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
6E3C45D3F1E98809BC13B1B5 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
8F7BBF6E33A45DC857BA490D /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -238,6 +286,67 @@
|
||||
shellPath = /bin/sh;
|
||||
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 */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -378,6 +487,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6A9A17806BE49FD019B0DB41 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -395,6 +505,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FA7CB1383A647C2D24B46844 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -410,6 +521,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A4BF9D95DC3AEA839D03722B /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
||||
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -45,5 +45,10 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>de</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
115
finlog_app/app/lib/core/app/features/feature_controller.dart
Normal file
115
finlog_app/app/lib/core/app/features/feature_controller.dart
Normal 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);
|
||||
}
|
||||
105
finlog_app/app/lib/core/app/router.dart
Normal file
105
finlog_app/app/lib/core/app/router.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
489
finlog_app/app/lib/core/app_shell.dart
Normal file
489
finlog_app/app/lib/core/app_shell.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
182
finlog_app/app/lib/core/i18n/translations.g.dart
Normal file
182
finlog_app/app/lib/core/i18n/translations.g.dart
Normal 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;
|
||||
}
|
||||
382
finlog_app/app/lib/core/i18n/translations_de.g.dart
Normal file
382
finlog_app/app/lib/core/i18n/translations_de.g.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
520
finlog_app/app/lib/core/i18n/translations_en.g.dart
Normal file
520
finlog_app/app/lib/core/i18n/translations_en.g.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
70
finlog_app/app/lib/core/ui/controller/locale_controller.dart
Normal file
70
finlog_app/app/lib/core/ui/controller/locale_controller.dart
Normal 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;
|
||||
}
|
||||
48
finlog_app/app/lib/core/ui/controller/scale_controller.dart
Normal file
48
finlog_app/app/lib/core/ui/controller/scale_controller.dart
Normal 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);
|
||||
}
|
||||
48
finlog_app/app/lib/core/ui/controller/theme.dart
Normal file
48
finlog_app/app/lib/core/ui/controller/theme.dart
Normal 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;
|
||||
}
|
||||
129
finlog_app/app/lib/core/ui/glas_bottom_bar.dart
Normal file
129
finlog_app/app/lib/core/ui/glas_bottom_bar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
183
finlog_app/app/lib/core/ui/panel.dart
Normal file
183
finlog_app/app/lib/core/ui/panel.dart
Normal 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);
|
||||
}
|
||||
@@ -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_localizations/flutter_localizations.dart';
|
||||
import 'package:fluttery/fluttery.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 {
|
||||
// Ensures that the Flutter engine and widget binding
|
||||
@@ -14,81 +23,71 @@ Future<void> main() async {
|
||||
final logger = App.service<Logger>();
|
||||
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 {
|
||||
const MyApp({super.key});
|
||||
final themeController = ThemeController();
|
||||
await themeController.init();
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
App.service<Logger>().info("test");
|
||||
final scaleController = ScaleController();
|
||||
final localeController = LocaleController();
|
||||
await localeController.init();
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
final features = FeatureController();
|
||||
await features.init();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ThemeController>(
|
||||
create: (context) => themeController,
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
ChangeNotifierProvider<ScaleController>(
|
||||
create: (context) => scaleController,
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text('You have pushed the button this many times:'),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
ChangeNotifierProvider<LocaleController>(
|
||||
create: (context) => localeController,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
},
|
||||
child: Text("Print workers"),
|
||||
ChangeNotifierProvider<FeatureController>(
|
||||
create: (context) => features,
|
||||
),
|
||||
],
|
||||
child: TranslationProvider(
|
||||
child: FinlogApp(router: buildAppRouter(startRoute)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class FinlogApp extends StatelessWidget {
|
||||
final GoRouter router;
|
||||
|
||||
const FinlogApp({super.key, required this.router});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<ThemeController>();
|
||||
final textScale = context.watch<ScaleController>();
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: theme,
|
||||
builder: (context, _) => MaterialApp.router(
|
||||
title: 'Finlog',
|
||||
locale: TranslationProvider.of(context).flutterLocale,
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
themeMode: theme.themeMode,
|
||||
routerConfig: router,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(
|
||||
context,
|
||||
).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.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
10
finlog_app/app/lib/modules/budget/budget_view.dart
Normal file
10
finlog_app/app/lib/modules/budget/budget_view.dart
Normal 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'));
|
||||
}
|
||||
}
|
||||
10
finlog_app/app/lib/modules/car/car_view.dart
Normal file
10
finlog_app/app/lib/modules/car/car_view.dart
Normal 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 '));
|
||||
}
|
||||
}
|
||||
10
finlog_app/app/lib/modules/dashboard/dashboard_view.dart
Normal file
10
finlog_app/app/lib/modules/dashboard/dashboard_view.dart
Normal 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'));
|
||||
}
|
||||
}
|
||||
84
finlog_app/app/lib/modules/login/pages/login_page.dart
Normal file
84
finlog_app/app/lib/modules/login/pages/login_page.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
138
finlog_app/app/lib/modules/settings/settings_view.dart
Normal file
138
finlog_app/app/lib/modules/settings/settings_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -31,23 +31,23 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
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
|
||||
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:
|
||||
flutter_test:
|
||||
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
|
||||
build_runner: ^2.8.0
|
||||
slang_build_runner: ^4.8.1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
10
finlog_app/app/slang.yaml
Normal file
10
finlog_app/app/slang.yaml
Normal 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
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fluttery/logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
@@ -8,16 +9,18 @@ class MockUtils {
|
||||
final logger = MockLogger();
|
||||
|
||||
when(() => logger.debug(any())).thenAnswer((a) {
|
||||
print("[DEBUG] ${a.positionalArguments[0]}");
|
||||
debugPrint("[DEBUG] ${a.positionalArguments[0]}");
|
||||
});
|
||||
when(() => logger.info(any())).thenAnswer((a) {
|
||||
print("[INFO] ${a.positionalArguments[0]}");
|
||||
debugPrint("[INFO] ${a.positionalArguments[0]}");
|
||||
});
|
||||
when(() => logger.warning(any())).thenAnswer((a) {
|
||||
print("[WARN] ${a.positionalArguments[0]}");
|
||||
debugPrint("[WARN] ${a.positionalArguments[0]}");
|
||||
});
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user