From ba4e931f5c83a9daeb0206c56484fd4c16654ff4 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Sat, 25 Apr 2026 07:09:15 +0200 Subject: [PATCH] feat: refactor RecipesScreen and RecipesViewNotifier to support dynamic view modes and column selection --- flutter/lib/core/ui/app_shell.dart | 49 ++++++++--- .../recipes/data/recipes_grid_provider.dart | 31 +++++-- .../recipes/presentation/recipes_screen.dart | 82 +++++++++++-------- 3 files changed, 110 insertions(+), 52 deletions(-) diff --git a/flutter/lib/core/ui/app_shell.dart b/flutter/lib/core/ui/app_shell.dart index c6e18fe0..9475b726 100644 --- a/flutter/lib/core/ui/app_shell.dart +++ b/flutter/lib/core/ui/app_shell.dart @@ -4,6 +4,9 @@ import 'package:go_router/go_router.dart'; import '../../features/auth/data/auth_providers.dart'; import '../../features/recipes/data/recipes_grid_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; const _adminDestination = _AppDestination( path: '/admin', @@ -102,18 +105,40 @@ class AppShell extends ConsumerWidget { 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')), - ], + if (isRecipesRoute) ...existing code... + Consumer( + builder: (context, ref, child) { + final view = ref.watch(recipesViewProvider).valueOrNull ?? + (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( + 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')), + ], + ), + ], + ); + }, ), IconButton( tooltip: 'Logga ut', diff --git a/flutter/lib/features/recipes/data/recipes_grid_provider.dart b/flutter/lib/features/recipes/data/recipes_grid_provider.dart index 2cef77e0..ebb9a5f4 100644 --- a/flutter/lib/features/recipes/data/recipes_grid_provider.dart +++ b/flutter/lib/features/recipes/data/recipes_grid_provider.dart @@ -1,21 +1,36 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -const _prefsKey = 'recipes_grid_columns'; +enum RecipesViewMode { grid, list } + +class RecipesViewNotifier extends AsyncNotifier<({RecipesViewMode mode, int columns})> { + static const _modeKey = 'recipes_view_mode'; + static const _columnsKey = 'recipes_grid_columns'; -class RecipesGridNotifier extends AsyncNotifier { @override - Future build() async { + Future<({RecipesViewMode mode, int columns})> build() async { final prefs = await SharedPreferences.getInstance(); - return prefs.getInt(_prefsKey) ?? 2; + final mode = RecipesViewMode.values[prefs.getInt(_modeKey) ?? 0]; + final columns = prefs.getInt(_columnsKey) ?? 2; + return (mode: mode, columns: columns); + } + + Future toggleMode() async { + final prefs = await SharedPreferences.getInstance(); + final current = state.valueOrNull ?? (mode: RecipesViewMode.grid, columns: 2); + final newMode = current.mode == RecipesViewMode.grid ? RecipesViewMode.list : RecipesViewMode.grid; + await prefs.setInt(_modeKey, newMode.index); + state = AsyncData((mode: newMode, columns: current.columns)); } Future setColumns(int columns) async { final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(_prefsKey, columns); - state = AsyncData(columns); + final current = state.valueOrNull ?? (mode: RecipesViewMode.grid, columns: 2); + await prefs.setInt(_columnsKey, columns); + state = AsyncData((mode: current.mode, columns: columns)); } } -final recipesGridProvider = - AsyncNotifierProvider(RecipesGridNotifier.new); +final recipesViewProvider = + AsyncNotifierProvider( + RecipesViewNotifier.new); diff --git a/flutter/lib/features/recipes/presentation/recipes_screen.dart b/flutter/lib/features/recipes/presentation/recipes_screen.dart index 8ecc5e6c..9ab0fdcc 100644 --- a/flutter/lib/features/recipes/presentation/recipes_screen.dart +++ b/flutter/lib/features/recipes/presentation/recipes_screen.dart @@ -13,10 +13,8 @@ class RecipesScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final recipesAsync = ref.watch(recipesProvider); - final columns = ref.watch(recipesGridProvider).maybeWhen( - data: (v) => v, - orElse: () => 2, - ); + final view = ref.watch(recipesViewProvider).valueOrNull ?? + (mode: RecipesViewMode.grid, columns: 2); return Stack( children: [ @@ -34,36 +32,56 @@ class RecipesScreen extends ConsumerWidget { ); } - 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( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.0), - color: Colors.grey[200], - image: recipe.imageUrl != null - ? DecorationImage( - image: NetworkImage(recipe.imageUrl!), - fit: BoxFit.cover, - ) + if (view.mode == RecipesViewMode.grid) { + return GridView.builder( + padding: const EdgeInsets.only(bottom: 88), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: view.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( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + color: Colors.grey[200], + image: recipe.imageUrl != null + ? DecorationImage( + image: NetworkImage(recipe.imageUrl!), + fit: BoxFit.cover, + ) + : null, + ), + child: recipe.imageUrl == null + ? const Center(child: Icon(Icons.restaurant, size: 32)) : null, ), - child: recipe.imageUrl == null - ? const Center(child: Icon(Icons.restaurant, size: 32)) - : null, - ), - ); - }, - ); + ); + }, + ); + } else { + return ListView.builder( + padding: const EdgeInsets.only(bottom: 88), + itemCount: recipes.length, + itemBuilder: (context, index) { + final recipe = recipes[index]; + return ListTile( + leading: recipe.imageUrl != null + ? CircleAvatar( + backgroundImage: NetworkImage(recipe.imageUrl!), + ) + : const CircleAvatar(child: Icon(Icons.restaurant)), + title: Text(recipe.name), + subtitle: Text(recipe.description ?? ''), + onTap: () => context.push('/recipes/${recipe.id}'), + ); + }, + ); + } }, ), Positioned(