diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart new file mode 100644 index 0000000..eac70a5 --- /dev/null +++ b/lib/core/router/router.dart @@ -0,0 +1,42 @@ +import 'package:go_router/go_router.dart'; + +import 'package:household_keeper/features/home/presentation/home_screen.dart'; +import 'package:household_keeper/features/rooms/presentation/rooms_screen.dart'; +import 'package:household_keeper/features/settings/presentation/settings_screen.dart'; +import 'package:household_keeper/shell/app_shell.dart'; + +final router = GoRouter( + initialLocation: '/', + routes: [ + StatefulShellRoute.indexedStack( + builder: (context, state, navigationShell) => + AppShell(navigationShell: navigationShell), + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/rooms', + builder: (context, state) => const RoomsScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/settings', + builder: (context, state) => const SettingsScreen(), + ), + ], + ), + ], + ), + ], +); diff --git a/lib/features/home/presentation/home_screen.dart b/lib/features/home/presentation/home_screen.dart new file mode 100644 index 0000000..f4e8688 --- /dev/null +++ b/lib/features/home/presentation/home_screen.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:household_keeper/l10n/app_localizations.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.checklist_rounded, + size: 80, + color: theme.colorScheme.onSurface.withValues(alpha: 0.4), + ), + const SizedBox(height: 24), + Text( + l10n.homeEmptyTitle, + style: theme.textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + l10n.homeEmptyMessage, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + FilledButton.tonal( + onPressed: () => context.go('/rooms'), + child: Text(l10n.homeEmptyAction), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/rooms/presentation/rooms_screen.dart b/lib/features/rooms/presentation/rooms_screen.dart new file mode 100644 index 0000000..ec33f46 --- /dev/null +++ b/lib/features/rooms/presentation/rooms_screen.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'package:household_keeper/l10n/app_localizations.dart'; + +class RoomsScreen extends StatelessWidget { + const RoomsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.door_front_door_rounded, + size: 80, + color: theme.colorScheme.onSurface.withValues(alpha: 0.4), + ), + const SizedBox(height: 24), + Text( + l10n.roomsEmptyTitle, + style: theme.textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + l10n.roomsEmptyMessage, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + FilledButton.tonal( + onPressed: () { + // Room creation will be implemented in Phase 2 + }, + child: Text(l10n.roomsEmptyAction), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart new file mode 100644 index 0000000..9d5db40 --- /dev/null +++ b/lib/features/settings/presentation/settings_screen.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:household_keeper/core/theme/theme_provider.dart'; +import 'package:household_keeper/l10n/app_localizations.dart'; + +class SettingsScreen extends ConsumerWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final currentThemeMode = ref.watch(themeProvider); + + return ListView( + children: [ + // Section 1: Appearance (Darstellung) + Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), + child: Text( + l10n.settingsSectionAppearance, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ), + ListTile( + title: Text(l10n.settingsThemeLabel), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8), + child: SegmentedButton( + segments: [ + ButtonSegment( + value: ThemeMode.system, + label: Text(l10n.themeSystem), + icon: const Icon(Icons.settings_suggest_outlined), + ), + ButtonSegment( + value: ThemeMode.light, + label: Text(l10n.themeLight), + icon: const Icon(Icons.light_mode_outlined), + ), + ButtonSegment( + value: ThemeMode.dark, + label: Text(l10n.themeDark), + icon: const Icon(Icons.dark_mode_outlined), + ), + ], + selected: {currentThemeMode}, + onSelectionChanged: (selection) { + ref + .read(themeProvider.notifier) + .setThemeMode(selection.first); + }, + ), + ), + ), + + const Divider(indent: 16, endIndent: 16, height: 32), + + // Section 2: About (Ueber) + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Text( + l10n.settingsSectionAbout, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ), + ListTile( + title: Text(l10n.aboutAppName), + subtitle: Text(l10n.aboutTagline), + ), + ListTile( + title: const Text('Version'), + subtitle: Text(l10n.aboutVersion('0.1.0')), + ), + ], + ); + } +} diff --git a/lib/shell/app_shell.dart b/lib/shell/app_shell.dart new file mode 100644 index 0000000..6866727 --- /dev/null +++ b/lib/shell/app_shell.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:household_keeper/l10n/app_localizations.dart'; + +class AppShell extends StatelessWidget { + const AppShell({ + required this.navigationShell, + super.key, + }); + + final StatefulNavigationShell navigationShell; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + + return Scaffold( + body: navigationShell, + bottomNavigationBar: NavigationBar( + selectedIndex: navigationShell.currentIndex, + onDestinationSelected: (index) { + navigationShell.goBranch( + index, + initialLocation: index == navigationShell.currentIndex, + ); + }, + destinations: [ + NavigationDestination( + icon: const Icon(Icons.checklist_outlined), + selectedIcon: const Icon(Icons.checklist), + label: l10n.tabHome, + ), + NavigationDestination( + icon: const Icon(Icons.door_front_door_outlined), + selectedIcon: const Icon(Icons.door_front_door), + label: l10n.tabRooms, + ), + NavigationDestination( + icon: const Icon(Icons.tune_outlined), + selectedIcon: const Icon(Icons.tune), + label: l10n.tabSettings, + ), + ], + ), + ); + } +} diff --git a/test/shell/app_shell_test.dart b/test/shell/app_shell_test.dart new file mode 100644 index 0000000..4dc2a0c --- /dev/null +++ b/test/shell/app_shell_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:household_keeper/core/router/router.dart'; +import 'package:household_keeper/l10n/app_localizations.dart'; + +void main() { + group('AppShell Navigation', () { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + testWidgets('renders 3 navigation destinations with correct German labels', + (tester) async { + await tester.pumpWidget( + ProviderScope( + child: MaterialApp.router( + routerConfig: router, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: const [Locale('de')], + locale: const Locale('de'), + ), + ), + ); + await tester.pumpAndSettle(); + + // Verify 3 NavigationDestination widgets are rendered + expect(find.byType(NavigationDestination), findsNWidgets(3)); + + // Verify correct German labels from ARB (with umlauts) + expect(find.text('\u00dcbersicht'), findsOneWidget); + expect(find.text('R\u00e4ume'), findsOneWidget); + expect(find.text('Einstellungen'), findsOneWidget); + }); + + testWidgets('tapping a destination changes the selected tab', + (tester) async { + await tester.pumpWidget( + ProviderScope( + child: MaterialApp.router( + routerConfig: router, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: const [Locale('de')], + locale: const Locale('de'), + ), + ), + ); + await tester.pumpAndSettle(); + + // Initially on Home tab (index 0) -- verify home empty state is shown + expect(find.text('Noch nichts zu tun!'), findsOneWidget); + + // Tap the Rooms tab (second destination) + await tester.tap(find.text('R\u00e4ume')); + await tester.pumpAndSettle(); + + // Verify we see Rooms content now + expect(find.text('Hier ist noch alles leer!'), findsOneWidget); + + // Tap the Settings tab (third destination) + await tester.tap(find.text('Einstellungen')); + await tester.pumpAndSettle(); + + // Verify we see Settings content now + expect(find.text('Darstellung'), findsOneWidget); + }); + }); +}