Implement Login Page, Add App Initialization, Router, and Theme Management

This commit is contained in:
2025-09-23 22:45:47 +02:00
parent c867133c6b
commit 3f1b295b65
7 changed files with 263 additions and 10 deletions

View File

@@ -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<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,
);
},
);
}
/// 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<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,
);
}
/// 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()),
],
);
}

View File

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

View File

@@ -0,0 +1,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<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.
Future<void> 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;
}

View File

@@ -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<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 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(),
),
],
),
),
),
);
}
}

View File

@@ -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:flutter/material.dart';
import 'package:fluttery/fluttery.dart'; import 'package:fluttery/fluttery.dart';
import 'package:fluttery/logger.dart'; import 'package:fluttery/logger.dart';
@@ -14,7 +17,25 @@ Future<void> main() async {
final logger = App.service<Logger>(); final logger = App.service<Logger>();
logger.debug("[MAIN] Registered all default services"); logger.debug("[MAIN] Registered all default services");
runApp(const MyApp()); // Run initialization before building router
final init = InitializeAppUseCase();
final startRoute = await init();
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 { class MyApp extends StatelessWidget {
@@ -76,11 +97,7 @@ class _MyHomePageState extends State<MyHomePage> {
'$_counter', '$_counter',
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
), ),
TextButton( TextButton(onPressed: () {}, child: Text("Print workers")),
onPressed: () {
},
child: Text("Print workers"),
),
], ],
), ),
), ),

View File

@@ -37,6 +37,8 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
go_router: ^16.2.2
animations: ^2.0.11
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

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