Implement Login Page, Add App Initialization, Router, and Theme Management
This commit is contained in:
79
finlog_app/app/lib/app/router.dart
Normal file
79
finlog_app/app/lib/app/router.dart
Normal 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()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
12
finlog_app/app/lib/app/startup/domain/initialize_app.dart
Normal file
12
finlog_app/app/lib/app/startup/domain/initialize_app.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
finlog_app/app/lib/app/theme.dart
Normal file
58
finlog_app/app/lib/app/theme.dart
Normal 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;
|
||||||
|
}
|
||||||
82
finlog_app/app/lib/features/login/pages/login_page.dart
Normal file
82
finlog_app/app/lib/features/login/pages/login_page.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user