diff --git a/flutter/README.md b/flutter/README.md index 4e7073f9..26a76f1d 100644 --- a/flutter/README.md +++ b/flutter/README.md @@ -1,3 +1,12 @@ +# Senaste ändringar (2026-04-24) + +**Arkitektur- och UX-förbättringar:** +- Grid-vy för recept: Kolumnval (2/4/6/8) via ikon i AppShell, med Riverpod-provider och SharedPreferences. +- RecipesScreen är nu body-only, ingen egen Scaffold/AppBar. +- AppShell visar grid-ikon endast på /recipes. +- Buggfix: Produktväljaren i pantry/inventarie (ProductPickerField) — bottenark implementeras. +- Kodkvalitet: Inga absoluta Windows-sökvägar. +- Dokumentation och next_steps uppdaterade. # Flutter Frontend - User Guide This README describes how to use the Flutter frontend for Recipe App from a user and operator perspective. diff --git a/flutter/lib/core/ui/app_shell.dart b/flutter/lib/core/ui/app_shell.dart index 2b035c32..c6e18fe0 100644 --- a/flutter/lib/core/ui/app_shell.dart +++ b/flutter/lib/core/ui/app_shell.dart @@ -3,6 +3,7 @@ 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 _adminDestination = _AppDestination( path: '/admin', @@ -94,10 +95,26 @@ class AppShell extends ConsumerWidget { } } + final isRecipesRoute = location.startsWith('/recipes') && + !location.startsWith('/recipes/'); + return Scaffold( appBar: AppBar( title: Text(selectedDestination.title), actions: [ + if (isRecipesRoute) + PopupMenuButton( + icon: const Icon(Icons.grid_view), + tooltip: 'Välj antal kolumner', + onSelected: (columns) => + ref.read(recipesGridProvider.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')), + ], + ), IconButton( tooltip: 'Logga ut', icon: const Icon(Icons.logout), diff --git a/flutter/lib/features/recipes/data/recipes_grid_provider.dart b/flutter/lib/features/recipes/data/recipes_grid_provider.dart new file mode 100644 index 00000000..2cef77e0 --- /dev/null +++ b/flutter/lib/features/recipes/data/recipes_grid_provider.dart @@ -0,0 +1,21 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const _prefsKey = 'recipes_grid_columns'; + +class RecipesGridNotifier extends AsyncNotifier { + @override + Future build() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getInt(_prefsKey) ?? 2; + } + + Future setColumns(int columns) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_prefsKey, columns); + state = AsyncData(columns); + } +} + +final recipesGridProvider = + AsyncNotifierProvider(RecipesGridNotifier.new); diff --git a/flutter/lib/features/recipes/presentation/recipes_screen.dart b/flutter/lib/features/recipes/presentation/recipes_screen.dart index 0ed1cf63..8ecc5e6c 100644 --- a/flutter/lib/features/recipes/presentation/recipes_screen.dart +++ b/flutter/lib/features/recipes/presentation/recipes_screen.dart @@ -1,104 +1,55 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import '../../../core/api/api_error_mapper.dart'; import '../../../core/ui/async_state_views.dart'; import '../data/recipe_providers.dart'; +import '../data/recipes_grid_provider.dart'; -class RecipesScreen extends ConsumerStatefulWidget { +class RecipesScreen extends ConsumerWidget { const RecipesScreen({super.key}); @override - ConsumerState createState() => _RecipesScreenState(); -} - -class _RecipesScreenState extends ConsumerState { - int _selectedColumns = 2; - - @override - void initState() { - super.initState(); - _loadSelectedColumns(); - } - - Future _loadSelectedColumns() async { - final prefs = await SharedPreferences.getInstance(); - setState(() { - _selectedColumns = prefs.getInt('selectedColumns') ?? 2; - }); - } - - Future _saveSelectedColumns(int columns) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt('selectedColumns', columns); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final recipesAsync = ref.watch(recipesProvider); + final columns = ref.watch(recipesGridProvider).maybeWhen( + data: (v) => v, + orElse: () => 2, + ); - return Scaffold( - appBar: AppBar( - title: const Text('Recept'), - actions: [ - PopupMenuButton( - icon: const Icon(Icons.grid_view), - tooltip: 'Välj antal kolumner', - onSelected: (int columns) { - setState(() { - _selectedColumns = columns; - }); - _saveSelectedColumns(columns); - }, - itemBuilder: (BuildContext context) => >[ - const PopupMenuItem( - value: 2, - child: Text('2 kolumner'), - ), - const PopupMenuItem( - value: 4, - child: Text('4 kolumner'), - ), - const PopupMenuItem( - value: 6, - child: Text('6 kolumner'), - ), - const PopupMenuItem( - value: 8, - child: Text('8 kolumner'), - ), - ], + return Stack( + children: [ + recipesAsync.when( + loading: () => const LoadingStateView(label: 'Laddar recept...'), + error: (error, _) => ErrorStateView( + message: mapErrorToUserMessage(error, context), + onRetry: () => ref.invalidate(recipesProvider), ), - ], - ), - body: recipesAsync.when( - loading: () => const LoadingStateView(label: 'Laddar recept...'), - error: (error, _) => ErrorStateView( - message: mapErrorToUserMessage(error, context), - onRetry: () => ref.invalidate(recipesProvider), - ), - data: (recipes) { - if (recipes.isEmpty) { - return const EmptyStateView( - title: 'Inga recept hittades', - description: 'Lägg till ett recept för att komma igång.', - ); - } + data: (recipes) { + if (recipes.isEmpty) { + return const EmptyStateView( + title: 'Inga recept hittades', + description: 'Lägg till ett recept för att komma igång.', + ); + } - return GridView.count( - crossAxisCount: _selectedColumns, - children: List.generate( - recipes.length, - (index) { + return GridView.builder( + padding: const EdgeInsets.only(bottom: 88), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + itemCount: recipes.length, + itemBuilder: (context, index) { final recipe = recipes[index]; return GestureDetector( onTap: () => context.push('/recipes/${recipe.id}'), child: Container( - margin: const EdgeInsets.all(4.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.0), + color: Colors.grey[200], image: recipe.imageUrl != null ? DecorationImage( image: NetworkImage(recipe.imageUrl!), @@ -107,20 +58,24 @@ class _RecipesScreenState extends ConsumerState { : null, ), child: recipe.imageUrl == null - ? const Center(child: Icon(Icons.restaurant)) + ? const Center(child: Icon(Icons.restaurant, size: 32)) : null, ), ); }, - ), - ); - }, - ), - floatingActionButton: FloatingActionButton( - tooltip: 'Nytt recept', - onPressed: () => context.push('/recipes/create'), - child: const Icon(Icons.add), - ), + ); + }, + ), + Positioned( + right: 16, + bottom: 16, + child: FloatingActionButton( + tooltip: 'Nytt recept', + onPressed: () => context.push('/recipes/create'), + child: const Icon(Icons.add), + ), + ), + ], ); } } diff --git a/flutter/next_steps_flutter.md b/flutter/next_steps_flutter.md index fb12fd9a..b1720b31 100644 --- a/flutter/next_steps_flutter.md +++ b/flutter/next_steps_flutter.md @@ -1,3 +1,12 @@ +# Senaste ändringar (2026-04-24) + +**Arkitektur- och UX-förbättringar:** +- Grid-vy för recept: Kolumnval (2/4/6/8) via ikon i AppShell, med Riverpod-provider och SharedPreferences. +- RecipesScreen är nu body-only, ingen egen Scaffold/AppBar. +- AppShell visar grid-ikon endast på /recipes. +- Buggfix: Produktväljaren i pantry/inventarie (ProductPickerField) — bottenark implementeras. +- Kodkvalitet: Inga absoluta Windows-sökvägar. +- Dokumentation och next_steps uppdaterade. # Next Steps: Flutter-migrering Relaterade dokument: diff --git a/flutter/teknisk_beskrivning_flutter.md b/flutter/teknisk_beskrivning_flutter.md index 42fde235..a8ac4582 100644 --- a/flutter/teknisk_beskrivning_flutter.md +++ b/flutter/teknisk_beskrivning_flutter.md @@ -1,3 +1,12 @@ +# Senaste ändringar (2026-04-24) + +**Arkitektur- och UX-förbättringar:** +- Grid-vy för recept: Kolumnval (2/4/6/8) via ikon i AppShell, med Riverpod-provider och SharedPreferences. +- RecipesScreen är nu body-only, ingen egen Scaffold/AppBar. +- AppShell visar grid-ikon endast på /recipes. +- Buggfix: Produktväljaren i pantry/inventarie (ProductPickerField) — bottenark implementeras. +- Kodkvalitet: Inga absoluta Windows-sökvägar. +- Dokumentation och next_steps uppdaterade. # Teknisk Beskrivning - Flutter Frontend Viktigt att komma ihåg vid implementering av nya funktioner och kodning är att inte använda windows sökvägar. Att inte använda c:/dev/recpie-app.... Detta eftersom bygg- och testmiljön är på en remote ubuntu-server. Utveckling sker lokalt och test samt drift sker på remote server. Säkerställ att inga absoluta Windows-sökvägar används i koden, för att stödja bygg och drift på Linux/Ubuntu