import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'api/api_client.dart'; import 'models/recipe.dart'; import 'models/shopping_list.dart'; import 'storage/secure_storage_service.dart'; final secureStorageProvider = Provider((ref) { return SecureStorageService(); }); final apiClientProvider = Provider((ref) { final storage = ref.watch(secureStorageProvider); return ApiClient(storage); }); // Authentication State Notifier class AuthState { final bool isAuthenticated; final bool isLoading; final String? errorMessage; final Map? user; const AuthState({ required this.isAuthenticated, this.isLoading = false, this.errorMessage, this.user, }); factory AuthState.initial() => const AuthState(isAuthenticated: false); factory AuthState.loading() => const AuthState(isAuthenticated: false, isLoading: true); factory AuthState.authenticated(Map user) => AuthState(isAuthenticated: true, user: user); factory AuthState.unauthenticated([String? error]) => AuthState(isAuthenticated: false, errorMessage: error); } class AuthNotifier extends StateNotifier { final ApiClient _api; final SecureStorageService _storage; AuthNotifier(this._api, this._storage) : super(AuthState.initial()) { checkInitialAuth(); } Future checkInitialAuth() async { final isValid = await _storage.hasValidSession(); if (isValid) { final user = await _storage.getUser(); state = AuthState.authenticated(user ?? {'username': 'Chef'}); } else { state = AuthState.unauthenticated(); } } Future login({ required String serverUrl, required String username, required String password, required String deviceName, }) async { state = AuthState.loading(); try { final res = await _api.login( serverUrl: serverUrl, username: username, password: password, deviceName: deviceName, ); final user = res['user'] as Map; state = AuthState.authenticated(user); return true; } catch (e) { state = AuthState.unauthenticated(e.toString()); return false; } } Future logout() async { await _api.logout(); state = AuthState.unauthenticated(); } } final authProvider = StateNotifierProvider((ref) { final api = ref.watch(apiClientProvider); final storage = ref.watch(secureStorageProvider); return AuthNotifier(api, storage); }); // Recipe List State Notifier class RecipeListNotifier extends StateNotifier>> { final ApiClient _api; RecipeListNotifier(this._api) : super(const AsyncValue.loading()) { loadRecipes(); } Future loadRecipes() async { state = const AsyncValue.loading(); try { final recipes = await _api.fetchRecipes(); state = AsyncValue.data(recipes); } catch (e, st) { state = AsyncValue.error(e, st); } } Future addOrUpdate(Recipe recipe) async { try { final saved = await _api.saveRecipe(recipe); final current = state.value ?? []; final idx = current.indexWhere((r) => r.id == saved.id); if (idx >= 0) { current[idx] = saved; state = AsyncValue.data([...current]); } else { state = AsyncValue.data([saved, ...current]); } } catch (e) { rethrow; } } Future remove(String id) async { await _api.deleteRecipe(id); final current = state.value ?? []; state = AsyncValue.data(current.where((r) => r.id != id).toList()); } } final recipeListProvider = StateNotifierProvider>>((ref) { final api = ref.watch(apiClientProvider); return RecipeListNotifier(api); }); // Community / Public Recipes State Notifier class PublicRecipesNotifier extends StateNotifier>> { final ApiClient _api; PublicRecipesNotifier(this._api) : super(const AsyncValue.loading()) { loadPublicRecipes(); } Future loadPublicRecipes() async { state = const AsyncValue.loading(); try { final recipes = await _api.getPublicRecipes(); state = AsyncValue.data(recipes); } catch (e, st) { state = AsyncValue.error(e, st); } } Future cloneRecipe(String id, WidgetRef? ref) async { final cloned = await _api.cloneRecipe(id); if (ref != null) { ref.read(recipeListProvider.notifier).loadRecipes(); } return cloned; } } final publicRecipesProvider = StateNotifierProvider>>((ref) { final api = ref.watch(apiClientProvider); return PublicRecipesNotifier(api); }); // Shopping Lists State Notifier class ShoppingListsNotifier extends StateNotifier>> { final ApiClient _api; String? _activeListId; ShoppingListsNotifier(this._api) : super(const AsyncValue.loading()) { loadLists(); } String? get activeListId => _activeListId; void setActiveListId(String id) { _activeListId = id; // Trigger notification if data present if (state.hasValue) { state = AsyncValue.data([...state.value!]); } } Future loadLists() async { state = const AsyncValue.loading(); try { final data = await _api.getShoppingLists(); final lists = data.map((json) => ShoppingList.fromJson(json as Map)).toList(); if (lists.isNotEmpty && (_activeListId == null || !lists.any((l) => l.id == _activeListId))) { _activeListId = lists.first.id; } state = AsyncValue.data(lists); } catch (e, st) { state = AsyncValue.error(e, st); } } Future createList(String name) async { final res = await _api.createShoppingList(name); final created = ShoppingList.fromJson(res); _activeListId = created.id; final current = state.value ?? []; state = AsyncValue.data([...current, created]); } Future deleteList(String id) async { await _api.deleteShoppingList(id); final current = (state.value ?? []).where((l) => l.id != id).toList(); if (_activeListId == id) { _activeListId = current.isNotEmpty ? current.first.id : null; } state = AsyncValue.data(current); } Future toggleItemCheck(String listId, String itemId, bool checked) async { // Optimistic local update final current = state.value ?? []; final listIdx = current.indexWhere((l) => l.id == listId); if (listIdx >= 0) { final list = current[listIdx]; final itemIdx = list.items.indexWhere((i) => i.id == itemId); if (itemIdx >= 0) { final updatedItems = [...list.items]; updatedItems[itemIdx] = updatedItems[itemIdx].copyWith(checked: checked); current[listIdx] = ShoppingList( id: list.id, name: list.name, userId: list.userId, createdBy: list.createdBy, shareToken: list.shareToken, createdAt: list.createdAt, updatedAt: DateTime.now().toIso8601String(), visitorCount: list.visitorCount, items: updatedItems, ); state = AsyncValue.data([...current]); } } try { final updatedJson = await _api.checkShoppingListItems(listId, [itemId], checked); final updatedList = ShoppingList.fromJson(updatedJson); _replaceList(updatedList); } catch (e) { // Reload on failure to revert optimistic update loadLists(); rethrow; } } Future addItems(String listId, List items, {String? recipeId, String? recipeTitle}) async { final updatedJson = await _api.addShoppingListItems(listId, items, recipeId: recipeId, recipeTitle: recipeTitle); final updatedList = ShoppingList.fromJson(updatedJson); _replaceList(updatedList); } Future deleteItems(String listId, List itemIds) async { final updatedJson = await _api.deleteShoppingListItems(listId, itemIds); final updatedList = ShoppingList.fromJson(updatedJson); _replaceList(updatedList); } Future clearList(String listId, {bool onlyChecked = true}) async { final updatedJson = await _api.clearShoppingList(listId, onlyChecked: onlyChecked); final updatedList = ShoppingList.fromJson(updatedJson); _replaceList(updatedList); } void _replaceList(ShoppingList updated) { final current = state.value ?? []; final idx = current.indexWhere((l) => l.id == updated.id); if (idx >= 0) { current[idx] = updated; state = AsyncValue.data([...current]); } else { state = AsyncValue.data([...current, updated]); } } } final shoppingListsProvider = StateNotifierProvider>>((ref) { final api = ref.watch(apiClientProvider); return ShoppingListsNotifier(api); });