diff --git a/finlog_app/app/lib/app/router.dart b/finlog_app/app/lib/app/router.dart new file mode 100644 index 0000000..e3ca04c --- /dev/null +++ b/finlog_app/app/lib/app/router.dart @@ -0,0 +1,79 @@ +import 'package:app/features/login/pages/login_page.dart'; +import 'package:app/main.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:animations/animations.dart'; + +/// Centralized route names and paths. +/// Splash is not routable (handled before router creation). +enum AppRoute { + login('/login'), + home('/home'), + inventory('/inventory'), + inventoryAdd('/inventory/add'), + budget('/budget'), + expenses('/expenses'); + + const AppRoute(this.path); + + final String path; +} + +/// Generic animated page using the `animations` package. +/// Change [transitionType] to vertical / scaled globally if desired. +class AnimatedPage extends CustomTransitionPage { + 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, + ); + }, + ); +} + +/// Helper to create a GoRoute with the global AnimatedPage transition. +/// Use this for EVERY route to keep transitions consistent. +GoRoute _r( + AppRoute route, + Widget Function(BuildContext, GoRouterState) builder, { + List routes = const [], + SharedAxisTransitionType transitionType = SharedAxisTransitionType.horizontal, +}) { + return GoRoute( + path: route.path, + name: route.name, + pageBuilder: (context, state) => AnimatedPage( + key: state.pageKey, + child: builder(context, state), + transitionType: transitionType, + ), + routes: routes, + ); +} + +/// Builds the application-wide router. The initial route is decided before creation. +GoRouter buildAppRouter(AppRoute initialRoute) { + return GoRouter( + initialLocation: initialRoute.path, + routes: [ + _r(AppRoute.login, (ctx, st) => const LoginPage()), + _r(AppRoute.home, (ctx, st) => const MyApp()), + // Add feature routes here, all getting the same animation: + // _r(AppRoute.inventory, (ctx, st) => const InventoryPage(), routes: [ + // _r(AppRoute.inventoryAdd, (ctx, st) => const AddItemPage()), + // ]), + // _r(AppRoute.budget, (ctx, st) => const BudgetPage()), + // _r(AppRoute.expenses, (ctx, st) => const ExpensesPage()), + ], + ); +} diff --git a/finlog_app/app/lib/app/startup/domain/initialize_app.dart b/finlog_app/app/lib/app/startup/domain/initialize_app.dart new file mode 100644 index 0000000..13f5666 --- /dev/null +++ b/finlog_app/app/lib/app/startup/domain/initialize_app.dart @@ -0,0 +1,12 @@ +import 'package:app/app/router.dart'; + +class InitializeAppUseCase { + Future call() async { + // Beispiel: ggf. weitere Init-Schritte + // await _migrateIfNeeded(prefs); + // await _warmupCaches(); + + // return auth.isLoggedIn ? AppRoute.home : AppRoute.login; + return AppRoute.login; + } +} diff --git a/finlog_app/app/lib/app/theme.dart b/finlog_app/app/lib/app/theme.dart new file mode 100644 index 0000000..9d054f8 --- /dev/null +++ b/finlog_app/app/lib/app/theme.dart @@ -0,0 +1,58 @@ +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; + + // vars + ThemeMode _themeMode = ThemeMode.system; + + /// Constructor + ThemeController() : _prefs = App.service(); + + /// Loads theme from Preferences (or defaults to system). + Future 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. + Future setTheme(ThemeMode mode) async { + _themeMode = mode; + notifyListeners(); + await _prefs.setString('theme', _toString(mode)); + } + + ThemeMode _fromString(String value) { + switch (value) { + case 'light': + return ThemeMode.light; + case 'dark': + return ThemeMode.dark; + case 'system': + default: + return ThemeMode.system; + } + } + + String _toString(ThemeMode mode) { + switch (mode) { + case ThemeMode.light: + return 'light'; + case ThemeMode.dark: + return 'dark'; + case ThemeMode.system: + return 'system'; + } + } + + ThemeMode get themeMode => _themeMode; +} diff --git a/finlog_app/app/lib/features/login/pages/login_page.dart b/finlog_app/app/lib/features/login/pages/login_page.dart new file mode 100644 index 0000000..1b13ed1 --- /dev/null +++ b/finlog_app/app/lib/features/login/pages/login_page.dart @@ -0,0 +1,82 @@ +import 'package:app/app/router.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + bool _loading = false; + + Future _simulateLogin() async { + if (_loading) return; + setState(() => _loading = true); + + // Simulate latency, perform demo login, then go home. + await Future.delayed(const Duration(milliseconds: 900)); + // await App.service().login('demo', 'demo'); + + if (!mounted) return; + context.go(AppRoute.home.path); + + setState(() => _loading = false); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar(title: const Text('Login')), + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 360), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlutterLogo(size: 64), + const SizedBox(height: 24), + Text('Please sign in', 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), + ) + : const Text('Login', key: ValueKey('text')), + ), + ), + ), + const SizedBox(height: 12), + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: _loading + ? Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Signing you in…', + style: theme.textTheme.bodySmall, + ), + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/finlog_app/app/lib/main.dart b/finlog_app/app/lib/main.dart index 8f08deb..9da0093 100644 --- a/finlog_app/app/lib/main.dart +++ b/finlog_app/app/lib/main.dart @@ -1,3 +1,6 @@ +import 'package:app/app/router.dart'; +import 'package:app/app/startup/domain/initialize_app.dart'; +import 'package:app/app/theme.dart'; import 'package:flutter/material.dart'; import 'package:fluttery/fluttery.dart'; import 'package:fluttery/logger.dart'; @@ -14,7 +17,25 @@ Future main() async { final logger = App.service(); logger.debug("[MAIN] Registered all default services"); - runApp(const MyApp()); + // Run initialization before building router + final init = InitializeAppUseCase(); + final startRoute = await init(); + + final themeController = ThemeController(); + await themeController.init(); + + runApp( + AnimatedBuilder( + animation: themeController, + builder: (context, _) => MaterialApp.router( + title: 'App', + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: themeController.themeMode, + routerConfig: buildAppRouter(startRoute), + ), + ), + ); } class MyApp extends StatelessWidget { @@ -76,11 +97,7 @@ class _MyHomePageState extends State { '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), - TextButton( - onPressed: () { - }, - child: Text("Print workers"), - ), + TextButton(onPressed: () {}, child: Text("Print workers")), ], ), ), diff --git a/finlog_app/app/pubspec.yaml b/finlog_app/app/pubspec.yaml index 4a153ec..2b162fe 100644 --- a/finlog_app/app/pubspec.yaml +++ b/finlog_app/app/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: # 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 dev_dependencies: flutter_test: diff --git a/finlog_app/fluttery/test/mocks/mocks.dart b/finlog_app/fluttery/test/mocks/mocks.dart index 82c20fa..4dca3ec 100644 --- a/finlog_app/fluttery/test/mocks/mocks.dart +++ b/finlog_app/fluttery/test/mocks/mocks.dart @@ -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;