recipe-manager/mobile/lib/core/providers.dart

291 lines
8.8 KiB
Dart

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<SecureStorageService>((ref) {
return SecureStorageService();
});
final apiClientProvider = Provider<ApiClient>((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<String, dynamic>? 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<String, dynamic> user) =>
AuthState(isAuthenticated: true, user: user);
factory AuthState.unauthenticated([String? error]) =>
AuthState(isAuthenticated: false, errorMessage: error);
}
class AuthNotifier extends StateNotifier<AuthState> {
final ApiClient _api;
final SecureStorageService _storage;
AuthNotifier(this._api, this._storage) : super(AuthState.initial()) {
checkInitialAuth();
}
Future<void> 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<bool> 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<String, dynamic>;
state = AuthState.authenticated(user);
return true;
} catch (e) {
state = AuthState.unauthenticated(e.toString());
return false;
}
}
Future<void> logout() async {
await _api.logout();
state = AuthState.unauthenticated();
}
}
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
final api = ref.watch(apiClientProvider);
final storage = ref.watch(secureStorageProvider);
return AuthNotifier(api, storage);
});
// Recipe List State Notifier
class RecipeListNotifier extends StateNotifier<AsyncValue<List<Recipe>>> {
final ApiClient _api;
RecipeListNotifier(this._api) : super(const AsyncValue.loading()) {
loadRecipes();
}
Future<void> 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<void> 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<void> 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<RecipeListNotifier, AsyncValue<List<Recipe>>>((ref) {
final api = ref.watch(apiClientProvider);
return RecipeListNotifier(api);
});
// Community / Public Recipes State Notifier
class PublicRecipesNotifier extends StateNotifier<AsyncValue<List<Recipe>>> {
final ApiClient _api;
PublicRecipesNotifier(this._api) : super(const AsyncValue.loading()) {
loadPublicRecipes();
}
Future<void> 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<Recipe> 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<PublicRecipesNotifier, AsyncValue<List<Recipe>>>((ref) {
final api = ref.watch(apiClientProvider);
return PublicRecipesNotifier(api);
});
// Shopping Lists State Notifier
class ShoppingListsNotifier extends StateNotifier<AsyncValue<List<ShoppingList>>> {
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<void> loadLists() async {
state = const AsyncValue.loading();
try {
final data = await _api.getShoppingLists();
final lists = data.map((json) => ShoppingList.fromJson(json as Map<String, dynamic>)).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<void> 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<void> 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<void> 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<void> addItems(String listId, List<String> 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<void> deleteItems(String listId, List<String> itemIds) async {
final updatedJson = await _api.deleteShoppingListItems(listId, itemIds);
final updatedList = ShoppingList.fromJson(updatedJson);
_replaceList(updatedList);
}
Future<void> 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<ShoppingListsNotifier, AsyncValue<List<ShoppingList>>>((ref) {
final api = ref.watch(apiClientProvider);
return ShoppingListsNotifier(api);
});