0873fa42bb
Test Suite / test (24.15.0) (push) Has been cancelled
- Introduced a new function `_shellBranchIndexForPath` to determine the index of the shell branch based on the path. - Replaced `ShellRoute` with `StatefulShellRoute.indexedStack` for better state management during navigation. - Updated `AppShell` to handle navigation path changes and integrate with the new routing structure. - Organized routes into `StatefulShellBranch` for better modularity and clarity. - Enhanced admin panel functionality with improved alias management and UI updates. - Added new methods in `ReceiptImportSessionNotifier` for managing selected items and edits more efficiently. - Improved UI components in receipt import and admin panels for better performance and user experience. - Added PageStorageKeys to various ListViews to maintain scroll positions across navigation. - Documented performance goals and profiling strategies in a new PERFORMANCE.md file.
279 lines
9.6 KiB
Dart
279 lines
9.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../ui/app_shell.dart';
|
|
import '../ui/async_state_views.dart';
|
|
import '../../features/auth/data/auth_providers.dart';
|
|
import '../../core/auth/jwt_decoder.dart';
|
|
import '../../features/auth/presentation/login_screen.dart';
|
|
import '../../features/profile/presentation/profile_screen.dart';
|
|
import '../../features/recipes/presentation/create_recipe_screen.dart';
|
|
import '../../features/recipes/presentation/ai_recipe_suggestions_screen.dart';
|
|
import '../../features/recipes/presentation/recipe_detail_screen.dart';
|
|
import '../../features/recipes/presentation/recipe_edit_screen.dart';
|
|
import '../../features/recipes/presentation/recipes_screen.dart';
|
|
import '../../features/inventory/presentation/inventory_screen.dart';
|
|
import '../../features/inventory/presentation/inventory_detail_screen.dart';
|
|
import '../../features/inventory/presentation/create_inventory_screen.dart';
|
|
import '../../features/inventory/presentation/inventory_edit_screen.dart';
|
|
import '../../features/inventory/presentation/consume_inventory_screen.dart';
|
|
import '../../features/inventory/presentation/consumption_history_screen.dart';
|
|
import '../../features/meal_plan/presentation/meal_plan_screen.dart';
|
|
import '../../features/pantry/presentation/pantry_screen.dart';
|
|
import '../../features/import/presentation/import_screen.dart';
|
|
import '../../features/admin/presentation/admin_screen.dart';
|
|
|
|
int? _shellBranchIndexForPath(String path) {
|
|
if (path.startsWith('/recipes')) return 0;
|
|
if (path.startsWith('/inventory')) return 1;
|
|
if (path.startsWith('/matsedel')) return 2;
|
|
if (path.startsWith('/baslager')) return 3;
|
|
if (path.startsWith('/import')) return 4;
|
|
if (path.startsWith('/profile')) return 5;
|
|
if (path.startsWith('/admin')) return 6;
|
|
return null;
|
|
}
|
|
|
|
final appRouterProvider = Provider<GoRouter>((ref) {
|
|
final authState = ref.watch(authStateProvider);
|
|
|
|
return GoRouter(
|
|
initialLocation: '/',
|
|
redirect: (context, state) {
|
|
final isLoading = authState.isLoading;
|
|
final token = authState.maybeWhen(data: (t) => t, orElse: () => null);
|
|
final isLoggedIn = token != null && token.isNotEmpty;
|
|
final location = state.matchedLocation;
|
|
final isSplash = location == '/';
|
|
final isLogin = location == '/login';
|
|
|
|
if (isLoading) {
|
|
return isSplash ? null : '/';
|
|
}
|
|
|
|
if (isSplash) {
|
|
return isLoggedIn ? '/recipes' : '/login';
|
|
}
|
|
|
|
if (!isLoggedIn && !isLogin) {
|
|
return '/login';
|
|
}
|
|
|
|
if (isLoggedIn && isLogin) {
|
|
return '/recipes';
|
|
}
|
|
|
|
return null;
|
|
},
|
|
routes: [
|
|
GoRoute(
|
|
path: '/',
|
|
builder: (context, state) => const Scaffold(
|
|
body: LoadingStateView(label: 'Startar...'),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/login',
|
|
builder: (context, state) => const LoginScreen(),
|
|
),
|
|
// Detail routes — outside ShellRoute to get full-screen with back button.
|
|
// /recipes/create must be listed before /recipes/:id to avoid conflict.
|
|
GoRoute(
|
|
path: '/recipes/ai-suggestions',
|
|
builder: (context, state) => const AiRecipeSuggestionsScreen(),
|
|
),
|
|
GoRoute(
|
|
path: '/recipes/create',
|
|
builder: (context, state) {
|
|
final extra = state.extra;
|
|
String? initialMarkdown;
|
|
String? initialImageUrl;
|
|
// Use 'is Map' without type params — Dart reifies generics, so
|
|
// Map<String,String?> does NOT match Map<String,dynamic> at runtime.
|
|
if (extra is Map) {
|
|
initialMarkdown = extra['markdown'] as String?;
|
|
initialImageUrl = extra['imageUrl'] as String?;
|
|
} else if (extra is String) {
|
|
// Backwards-compat: plain string means markdown only.
|
|
initialMarkdown = extra;
|
|
}
|
|
return CreateRecipeScreen(
|
|
initialMarkdown: initialMarkdown,
|
|
initialImageUrl: initialImageUrl,
|
|
);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: '/recipes/:id',
|
|
redirect: (context, state) {
|
|
final raw = state.pathParameters['id'] ?? '';
|
|
if (int.tryParse(raw) == null) return '/recipes';
|
|
return null;
|
|
},
|
|
builder: (context, state) {
|
|
final id = int.parse(state.pathParameters['id']!);
|
|
return RecipeDetailScreen(recipeId: id);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: '/recipes/:id/edit',
|
|
redirect: (context, state) {
|
|
final raw = state.pathParameters['id'] ?? '';
|
|
if (int.tryParse(raw) == null) return '/recipes';
|
|
return null;
|
|
},
|
|
builder: (context, state) {
|
|
final id = int.parse(state.pathParameters['id']!);
|
|
return RecipeEditScreen(recipeId: id);
|
|
},
|
|
),
|
|
// Inventory detail routes — outside ShellRoute for full-screen.
|
|
// /inventory/create must be listed before /inventory/:id.
|
|
GoRoute(
|
|
path: '/inventory/create',
|
|
builder: (context, state) {
|
|
final destination = state.uri.queryParameters['destination'];
|
|
return CreateInventoryScreen(initialDestination: destination);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: '/inventory/:id',
|
|
redirect: (context, state) {
|
|
final raw = state.pathParameters['id'] ?? '';
|
|
if (int.tryParse(raw) == null) return '/inventory';
|
|
return null;
|
|
},
|
|
builder: (context, state) {
|
|
final id = int.parse(state.pathParameters['id']!);
|
|
return InventoryDetailScreen(itemId: id);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: '/inventory/:id/edit',
|
|
redirect: (context, state) {
|
|
final raw = state.pathParameters['id'] ?? '';
|
|
if (int.tryParse(raw) == null) return '/inventory';
|
|
return null;
|
|
},
|
|
builder: (context, state) {
|
|
final id = int.parse(state.pathParameters['id']!);
|
|
return InventoryEditScreen(itemId: id);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: '/inventory/:id/consume',
|
|
redirect: (context, state) {
|
|
final raw = state.pathParameters['id'] ?? '';
|
|
if (int.tryParse(raw) == null) return '/inventory';
|
|
return null;
|
|
},
|
|
builder: (context, state) {
|
|
final id = int.parse(state.pathParameters['id']!);
|
|
return ConsumeInventoryScreen(itemId: id);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: '/inventory/:id/history',
|
|
redirect: (context, state) {
|
|
final raw = state.pathParameters['id'] ?? '';
|
|
if (int.tryParse(raw) == null) return '/inventory';
|
|
return null;
|
|
},
|
|
builder: (context, state) {
|
|
final id = int.parse(state.pathParameters['id']!);
|
|
return ConsumptionHistoryScreen(itemId: id);
|
|
},
|
|
),
|
|
// Shell routes — shared AppShell with navigation bar.
|
|
StatefulShellRoute.indexedStack(
|
|
builder: (context, state, navigationShell) {
|
|
return AppShell(
|
|
location: state.uri.path,
|
|
onNavigateToPath: (path) {
|
|
final index = _shellBranchIndexForPath(path);
|
|
if (index == null) {
|
|
context.go(path);
|
|
return;
|
|
}
|
|
|
|
if (index == navigationShell.currentIndex) {
|
|
if (state.uri.path != path) {
|
|
context.go(path);
|
|
}
|
|
return;
|
|
}
|
|
|
|
navigationShell.goBranch(index);
|
|
},
|
|
child: navigationShell,
|
|
);
|
|
},
|
|
branches: [
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: '/recipes',
|
|
builder: (context, state) => const RecipesScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: '/inventory',
|
|
builder: (context, state) => const InventoryScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: '/matsedel',
|
|
builder: (context, state) => const MealPlanScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: '/baslager',
|
|
builder: (context, state) => const PantryScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: '/import',
|
|
builder: (context, state) => const ImportScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: '/profile',
|
|
builder: (context, state) => const ProfileScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: '/admin',
|
|
redirect: (context, state) {
|
|
final token = ref.read(authStateProvider)
|
|
.maybeWhen(data: (t) => t, orElse: () => null);
|
|
return jwtIsAdmin(token) ? null : '/recipes';
|
|
},
|
|
builder: (context, state) => const AdminScreen(),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
});
|