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.
258 lines
8.0 KiB
Dart
258 lines
8.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../features/auth/data/auth_providers.dart';
|
|
import '../../features/recipes/data/recipes_grid_provider.dart';
|
|
|
|
const _profileHeaderDestination = _AppDestination(
|
|
path: '/profile',
|
|
title: 'Profil',
|
|
icon: Icons.person,
|
|
label: 'Profil',
|
|
);
|
|
|
|
const _adminHeaderDestination = _AppDestination(
|
|
path: '/admin',
|
|
title: 'Admin',
|
|
icon: Icons.admin_panel_settings_outlined,
|
|
label: 'Admin',
|
|
);
|
|
|
|
class AppShell extends ConsumerWidget {
|
|
final String location;
|
|
final ValueChanged<String> onNavigateToPath;
|
|
final Widget child;
|
|
|
|
const AppShell({
|
|
super.key,
|
|
required this.location,
|
|
required this.onNavigateToPath,
|
|
required this.child,
|
|
});
|
|
|
|
static const _baseDestinations = [
|
|
_AppDestination(
|
|
path: '/recipes',
|
|
title: 'Recept',
|
|
icon: Icons.restaurant_menu,
|
|
label: 'Recept',
|
|
),
|
|
_AppDestination(
|
|
path: '/inventory',
|
|
title: 'Inventarie',
|
|
icon: Icons.inventory_2_outlined,
|
|
label: 'Inventarie',
|
|
),
|
|
_AppDestination(
|
|
path: '/matsedel',
|
|
title: 'Matsedel',
|
|
icon: Icons.calendar_month_outlined,
|
|
label: 'Matsedel',
|
|
),
|
|
_AppDestination(
|
|
path: '/baslager',
|
|
title: 'Baslager',
|
|
icon: Icons.storefront_outlined,
|
|
label: 'Baslager',
|
|
),
|
|
_AppDestination(
|
|
path: '/import',
|
|
title: 'Importera',
|
|
icon: Icons.upload_file_outlined,
|
|
label: 'Importera',
|
|
),
|
|
];
|
|
|
|
List<_AppDestination> _destinations(bool isAdmin) => isAdmin
|
|
? [..._baseDestinations, _adminHeaderDestination]
|
|
: _baseDestinations;
|
|
|
|
int? _selectedIndex(List<_AppDestination> destinations) {
|
|
final index = destinations.indexWhere(
|
|
(destination) => location.startsWith(destination.path),
|
|
);
|
|
return index < 0 ? null : index;
|
|
}
|
|
|
|
_AppDestination _selectedHeaderDestination(
|
|
List<_AppDestination> destinations,
|
|
bool isAdmin,
|
|
) {
|
|
if (location.startsWith('/profile')) {
|
|
return _profileHeaderDestination;
|
|
}
|
|
if (location.startsWith('/admin') && isAdmin) {
|
|
return _adminHeaderDestination;
|
|
}
|
|
final selectedIndex = _selectedIndex(destinations);
|
|
if (selectedIndex != null) {
|
|
return destinations[selectedIndex];
|
|
}
|
|
return destinations.first;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final isAdmin = ref.watch(isAdminProvider);
|
|
final dests = _destinations(isAdmin);
|
|
final selectedIndex = _selectedIndex(dests);
|
|
final selectedDestination = _selectedHeaderDestination(dests, isAdmin);
|
|
final isWide = MediaQuery.of(context).size.width >= 900;
|
|
|
|
void navigateTo(int index) {
|
|
final target = dests[index].path;
|
|
if (target != location && context.mounted) {
|
|
onNavigateToPath(target);
|
|
}
|
|
}
|
|
|
|
final isRecipesRoute = location.startsWith('/recipes') &&
|
|
!location.startsWith('/recipes/');
|
|
final isImportRoute = location == '/import';
|
|
|
|
Future<void> logout() async {
|
|
await ref.read(authStateProvider.notifier).logout();
|
|
if (context.mounted) {
|
|
context.go('/login');
|
|
}
|
|
}
|
|
|
|
Widget shell = Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(selectedDestination.title),
|
|
bottom: isImportRoute
|
|
? const TabBar(
|
|
tabs: [
|
|
Tab(
|
|
icon: Icon(Icons.restaurant_menu_outlined),
|
|
text: 'Recept',
|
|
),
|
|
Tab(
|
|
icon: Icon(Icons.receipt_long_outlined),
|
|
text: 'Kvitto',
|
|
),
|
|
],
|
|
)
|
|
: null,
|
|
actions: [
|
|
if (isRecipesRoute)
|
|
Consumer(
|
|
builder: (context, ref, child) {
|
|
final view = ref.watch(recipesViewProvider).maybeWhen(
|
|
data: (v) => v,
|
|
orElse: () => (mode: RecipesViewMode.grid, columns: 2),
|
|
);
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
IconButton(
|
|
tooltip: view.mode == RecipesViewMode.grid
|
|
? 'Visa som lista'
|
|
: 'Visa som grid',
|
|
icon: Icon(view.mode == RecipesViewMode.grid
|
|
? Icons.view_list
|
|
: Icons.grid_view),
|
|
onPressed: () =>
|
|
ref.read(recipesViewProvider.notifier).toggleMode(),
|
|
),
|
|
if (view.mode == RecipesViewMode.grid)
|
|
PopupMenuButton<int>(
|
|
icon: const Icon(Icons.grid_view),
|
|
tooltip: 'Välj antal kolumner',
|
|
onSelected: (columns) =>
|
|
ref.read(recipesViewProvider.notifier).setColumns(columns),
|
|
itemBuilder: (context) => const [
|
|
PopupMenuItem(value: 2, child: Text('2 kolumner')),
|
|
PopupMenuItem(value: 4, child: Text('4 kolumner')),
|
|
PopupMenuItem(value: 6, child: Text('6 kolumner')),
|
|
PopupMenuItem(value: 8, child: Text('8 kolumner')),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
PopupMenuButton<String>(
|
|
tooltip: 'Profil och konto',
|
|
icon: const Icon(Icons.account_circle_outlined),
|
|
onSelected: (value) {
|
|
switch (value) {
|
|
case 'profile':
|
|
if (location != '/profile' && context.mounted) {
|
|
onNavigateToPath('/profile');
|
|
}
|
|
}
|
|
},
|
|
itemBuilder: (context) => const [
|
|
PopupMenuItem<String>(
|
|
value: 'profile',
|
|
child: ListTile(
|
|
leading: Icon(Icons.person_outline),
|
|
title: Text('Profil'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
body: isWide
|
|
? Row(
|
|
children: [
|
|
NavigationRail(
|
|
selectedIndex: selectedIndex ?? 0,
|
|
onDestinationSelected: navigateTo,
|
|
labelType: NavigationRailLabelType.all,
|
|
destinations: dests
|
|
.map(
|
|
(destination) => NavigationRailDestination(
|
|
icon: Icon(destination.icon),
|
|
label: Text(destination.label),
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
const VerticalDivider(width: 1),
|
|
Expanded(child: child),
|
|
],
|
|
)
|
|
: child,
|
|
bottomNavigationBar: isWide
|
|
? null
|
|
: NavigationBar(
|
|
selectedIndex: selectedIndex ?? 0,
|
|
onDestinationSelected: navigateTo,
|
|
destinations: dests
|
|
.map(
|
|
(destination) => NavigationDestination(
|
|
icon: Icon(destination.icon),
|
|
label: destination.label,
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
);
|
|
|
|
if (isImportRoute) {
|
|
shell = DefaultTabController(length: 2, child: shell);
|
|
}
|
|
|
|
return shell;
|
|
}
|
|
}
|
|
|
|
class _AppDestination {
|
|
final String path;
|
|
final String title;
|
|
final IconData icon;
|
|
final String label;
|
|
|
|
const _AppDestination({
|
|
required this.path,
|
|
required this.title,
|
|
required this.icon,
|
|
required this.label,
|
|
});
|
|
}
|