This commit is contained in:
Phuoc Nguyen
2025-10-24 16:20:48 +07:00
parent eaaa9921f5
commit b27c5d7742
17 changed files with 3245 additions and 5 deletions

View File

@@ -0,0 +1,273 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/favorites/data/datasources/favorites_local_datasource.dart';
import 'package:worker/features/favorites/data/models/favorite_model.dart';
import 'package:worker/features/products/data/datasources/products_local_datasource.dart';
import 'package:worker/features/products/data/repositories/products_repository_impl.dart';
import 'package:worker/features/products/domain/entities/product.dart';
import 'package:worker/features/products/domain/usecases/get_products.dart';
part 'favorites_provider.g.dart';
// ============================================================================
// DATASOURCE PROVIDER
// ============================================================================
/// Provides instance of FavoritesLocalDataSource
@riverpod
FavoritesLocalDataSource favoritesLocalDataSource(Ref ref) {
return FavoritesLocalDataSource();
}
// ============================================================================
// CURRENT USER ID PROVIDER
// ============================================================================
/// Provides the current logged-in user's ID
///
/// TODO: Replace with actual auth provider integration
/// For now, using hardcoded userId for development
@riverpod
String currentUserId(Ref ref) {
// TODO: Integrate with actual auth provider when available
// Example: return ref.watch(authProvider).user?.id ?? 'user_001';
return 'user_001';
}
// ============================================================================
// MAIN FAVORITES PROVIDER
// ============================================================================
/// Manages the favorites state for the current user
///
/// Uses a Set<String> to store product IDs for efficient lookup.
/// Data is persisted to Hive for offline access.
@riverpod
class Favorites extends _$Favorites {
late FavoritesLocalDataSource _dataSource;
late String _userId;
@override
Future<Set<String>> build() async {
_dataSource = ref.read(favoritesLocalDataSourceProvider);
_userId = ref.read(currentUserIdProvider);
// Load favorites from Hive
return await _loadFavorites();
}
// ==========================================================================
// PRIVATE METHODS
// ==========================================================================
/// Load favorites from Hive database
Future<Set<String>> _loadFavorites() async {
try {
final favorites = await _dataSource.getAllFavorites(_userId);
final productIds = favorites.map((fav) => fav.productId).toSet();
debugPrint('Loaded ${productIds.length} favorites for user: $_userId');
return productIds;
} catch (e) {
debugPrint('Error loading favorites: $e');
return {};
}
}
/// Generate a unique favorite ID
String _generateFavoriteId(String productId) {
// Using format: userId_productId_timestamp
final timestamp = DateTime.now().millisecondsSinceEpoch;
return '${_userId}_${productId}_$timestamp';
}
// ==========================================================================
// PUBLIC METHODS
// ==========================================================================
/// Add a product to favorites
///
/// Creates a new favorite entry and persists it to Hive.
/// If the product is already favorited, this operation is a no-op.
Future<void> addFavorite(String productId) async {
try {
// Check if already favorited
final currentState = state.value ?? <String>{};
if (currentState.contains(productId)) {
debugPrint('Product $productId is already favorited');
return;
}
// Create favorite model
final favorite = FavoriteModel(
favoriteId: _generateFavoriteId(productId),
productId: productId,
userId: _userId,
createdAt: DateTime.now(),
);
// Persist to Hive
await _dataSource.addFavorite(favorite);
// Update state
final newState = <String>{...currentState, productId};
state = AsyncValue.data(newState);
debugPrint('Added favorite: $productId');
} catch (e, stackTrace) {
debugPrint('Error adding favorite: $e');
state = AsyncValue.error(e, stackTrace);
}
}
/// Remove a product from favorites
///
/// Removes the favorite entry from Hive.
/// If the product is not favorited, this operation is a no-op.
Future<void> removeFavorite(String productId) async {
try {
// Check if favorited
final currentState = state.value ?? <String>{};
if (!currentState.contains(productId)) {
debugPrint('Product $productId is not favorited');
return;
}
// Remove from Hive
await _dataSource.removeFavorite(productId, _userId);
// Update state
final newState = <String>{...currentState};
newState.remove(productId);
state = AsyncValue.data(newState);
debugPrint('Removed favorite: $productId');
} catch (e, stackTrace) {
debugPrint('Error removing favorite: $e');
state = AsyncValue.error(e, stackTrace);
}
}
/// Toggle favorite status for a product
///
/// If the product is favorited, it will be removed.
/// If the product is not favorited, it will be added.
Future<void> toggleFavorite(String productId) async {
final currentState = state.value ?? <String>{};
if (currentState.contains(productId)) {
await removeFavorite(productId);
} else {
await addFavorite(productId);
}
}
/// Refresh favorites from database
///
/// Useful for syncing state after external changes or on app resume.
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await _loadFavorites();
});
}
/// Clear all favorites for the current user
///
/// Removes all favorite entries from Hive.
Future<void> clearAll() async {
try {
await _dataSource.clearFavorites(_userId);
state = const AsyncValue.data({});
debugPrint('Cleared all favorites for user: $_userId');
} catch (e, stackTrace) {
debugPrint('Error clearing favorites: $e');
state = AsyncValue.error(e, stackTrace);
}
}
}
// ============================================================================
// HELPER PROVIDERS
// ============================================================================
/// Check if a specific product is favorited
///
/// Returns true if the product is in the user's favorites, false otherwise.
/// Safe to use in build methods - will return false during loading/error states.
@riverpod
bool isFavorite(Ref ref, String productId) {
final favoritesAsync = ref.watch(favoritesProvider);
return favoritesAsync.when(
data: (favorites) => favorites.contains(productId),
loading: () => false,
error: (_, __) => false,
);
}
/// Get the total count of favorites
///
/// Returns the number of products in the user's favorites.
/// Safe to use in build methods - will return 0 during loading/error states.
@riverpod
int favoriteCount(Ref ref) {
final favoritesAsync = ref.watch(favoritesProvider);
return favoritesAsync.when(
data: (favorites) => favorites.length,
loading: () => 0,
error: (_, __) => 0,
);
}
/// Get all favorite product IDs as a list
///
/// Useful for filtering product lists or bulk operations.
/// Returns an empty list during loading/error states.
@riverpod
List<String> favoriteProductIds(Ref ref) {
final favoritesAsync = ref.watch(favoritesProvider);
return favoritesAsync.when(
data: (favorites) => favorites.toList(),
loading: () => <String>[],
error: (_, __) => <String>[],
);
}
// ============================================================================
// FAVORITE PRODUCTS PROVIDER
// ============================================================================
/// Get actual Product entities for favorited product IDs
///
/// Combines favorites state with products data to return full Product objects.
/// This is useful for displaying favorite products with complete information.
@riverpod
Future<List<Product>> favoriteProducts(Ref ref) async {
final favoriteIds = ref.watch(favoriteProductIdsProvider);
if (favoriteIds.isEmpty) {
return [];
}
// Import products provider to get all products
const productsRepository = ProductsRepositoryImpl(
localDataSource: ProductsLocalDataSourceImpl(),
);
const getProductsUseCase = GetProducts(productsRepository);
final allProducts = await getProductsUseCase();
// Filter to only include favorited products
return allProducts.where((product) => favoriteIds.contains(product.productId)).toList();
}
// ============================================================================
// DEBUG UTILITIES
// ============================================================================
/// Debug print helper
void debugPrint(String message) {
print('[FavoritesProvider] $message');
}

View File

@@ -0,0 +1,443 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'favorites_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provides instance of FavoritesLocalDataSource
@ProviderFor(favoritesLocalDataSource)
const favoritesLocalDataSourceProvider = FavoritesLocalDataSourceProvider._();
/// Provides instance of FavoritesLocalDataSource
final class FavoritesLocalDataSourceProvider
extends
$FunctionalProvider<
FavoritesLocalDataSource,
FavoritesLocalDataSource,
FavoritesLocalDataSource
>
with $Provider<FavoritesLocalDataSource> {
/// Provides instance of FavoritesLocalDataSource
const FavoritesLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'favoritesLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$favoritesLocalDataSourceHash();
@$internal
@override
$ProviderElement<FavoritesLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
FavoritesLocalDataSource create(Ref ref) {
return favoritesLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(FavoritesLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<FavoritesLocalDataSource>(value),
);
}
}
String _$favoritesLocalDataSourceHash() =>
r'2f6ff99042b7cc1087d8cfdad517f448952c25be';
/// Provides the current logged-in user's ID
///
/// TODO: Replace with actual auth provider integration
/// For now, using hardcoded userId for development
@ProviderFor(currentUserId)
const currentUserIdProvider = CurrentUserIdProvider._();
/// Provides the current logged-in user's ID
///
/// TODO: Replace with actual auth provider integration
/// For now, using hardcoded userId for development
final class CurrentUserIdProvider
extends $FunctionalProvider<String, String, String>
with $Provider<String> {
/// Provides the current logged-in user's ID
///
/// TODO: Replace with actual auth provider integration
/// For now, using hardcoded userId for development
const CurrentUserIdProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentUserIdProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentUserIdHash();
@$internal
@override
$ProviderElement<String> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
String create(Ref ref) {
return currentUserId(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<String>(value),
);
}
}
String _$currentUserIdHash() => r'7f968e463454a4ad87bce0442f62ecc24a6f756e';
/// Manages the favorites state for the current user
///
/// Uses a Set<String> to store product IDs for efficient lookup.
/// Data is persisted to Hive for offline access.
@ProviderFor(Favorites)
const favoritesProvider = FavoritesProvider._();
/// Manages the favorites state for the current user
///
/// Uses a Set<String> to store product IDs for efficient lookup.
/// Data is persisted to Hive for offline access.
final class FavoritesProvider
extends $AsyncNotifierProvider<Favorites, Set<String>> {
/// Manages the favorites state for the current user
///
/// Uses a Set<String> to store product IDs for efficient lookup.
/// Data is persisted to Hive for offline access.
const FavoritesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'favoritesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$favoritesHash();
@$internal
@override
Favorites create() => Favorites();
}
String _$favoritesHash() => r'fccd46f5cd1bbf2b58a13ea90c6d1644ece767b0';
/// Manages the favorites state for the current user
///
/// Uses a Set<String> to store product IDs for efficient lookup.
/// Data is persisted to Hive for offline access.
abstract class _$Favorites extends $AsyncNotifier<Set<String>> {
FutureOr<Set<String>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<Set<String>>, Set<String>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<Set<String>>, Set<String>>,
AsyncValue<Set<String>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Check if a specific product is favorited
///
/// Returns true if the product is in the user's favorites, false otherwise.
/// Safe to use in build methods - will return false during loading/error states.
@ProviderFor(isFavorite)
const isFavoriteProvider = IsFavoriteFamily._();
/// Check if a specific product is favorited
///
/// Returns true if the product is in the user's favorites, false otherwise.
/// Safe to use in build methods - will return false during loading/error states.
final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
with $Provider<bool> {
/// Check if a specific product is favorited
///
/// Returns true if the product is in the user's favorites, false otherwise.
/// Safe to use in build methods - will return false during loading/error states.
const IsFavoriteProvider._({
required IsFavoriteFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'isFavoriteProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$isFavoriteHash();
@override
String toString() {
return r'isFavoriteProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
bool create(Ref ref) {
final argument = this.argument as String;
return isFavorite(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
@override
bool operator ==(Object other) {
return other is IsFavoriteProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$isFavoriteHash() => r'8d69e5efe981a3717eebdd7ee192fd75afe722d5';
/// Check if a specific product is favorited
///
/// Returns true if the product is in the user's favorites, false otherwise.
/// Safe to use in build methods - will return false during loading/error states.
final class IsFavoriteFamily extends $Family
with $FunctionalFamilyOverride<bool, String> {
const IsFavoriteFamily._()
: super(
retry: null,
name: r'isFavoriteProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Check if a specific product is favorited
///
/// Returns true if the product is in the user's favorites, false otherwise.
/// Safe to use in build methods - will return false during loading/error states.
IsFavoriteProvider call(String productId) =>
IsFavoriteProvider._(argument: productId, from: this);
@override
String toString() => r'isFavoriteProvider';
}
/// Get the total count of favorites
///
/// Returns the number of products in the user's favorites.
/// Safe to use in build methods - will return 0 during loading/error states.
@ProviderFor(favoriteCount)
const favoriteCountProvider = FavoriteCountProvider._();
/// Get the total count of favorites
///
/// Returns the number of products in the user's favorites.
/// Safe to use in build methods - will return 0 during loading/error states.
final class FavoriteCountProvider extends $FunctionalProvider<int, int, int>
with $Provider<int> {
/// Get the total count of favorites
///
/// Returns the number of products in the user's favorites.
/// Safe to use in build methods - will return 0 during loading/error states.
const FavoriteCountProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'favoriteCountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$favoriteCountHash();
@$internal
@override
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
int create(Ref ref) {
return favoriteCount(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$favoriteCountHash() => r'1f147fe5ef28b1477034bd567cfc05ab3e8e90db';
/// Get all favorite product IDs as a list
///
/// Useful for filtering product lists or bulk operations.
/// Returns an empty list during loading/error states.
@ProviderFor(favoriteProductIds)
const favoriteProductIdsProvider = FavoriteProductIdsProvider._();
/// Get all favorite product IDs as a list
///
/// Useful for filtering product lists or bulk operations.
/// Returns an empty list during loading/error states.
final class FavoriteProductIdsProvider
extends $FunctionalProvider<List<String>, List<String>, List<String>>
with $Provider<List<String>> {
/// Get all favorite product IDs as a list
///
/// Useful for filtering product lists or bulk operations.
/// Returns an empty list during loading/error states.
const FavoriteProductIdsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'favoriteProductIdsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$favoriteProductIdsHash();
@$internal
@override
$ProviderElement<List<String>> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
List<String> create(Ref ref) {
return favoriteProductIds(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<String> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<String>>(value),
);
}
}
String _$favoriteProductIdsHash() =>
r'a6814af9a1775b908b4101e64ce3056e1534b561';
/// Get actual Product entities for favorited product IDs
///
/// Combines favorites state with products data to return full Product objects.
/// This is useful for displaying favorite products with complete information.
@ProviderFor(favoriteProducts)
const favoriteProductsProvider = FavoriteProductsProvider._();
/// Get actual Product entities for favorited product IDs
///
/// Combines favorites state with products data to return full Product objects.
/// This is useful for displaying favorite products with complete information.
final class FavoriteProductsProvider
extends
$FunctionalProvider<
AsyncValue<List<Product>>,
List<Product>,
FutureOr<List<Product>>
>
with $FutureModifier<List<Product>>, $FutureProvider<List<Product>> {
/// Get actual Product entities for favorited product IDs
///
/// Combines favorites state with products data to return full Product objects.
/// This is useful for displaying favorite products with complete information.
const FavoriteProductsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'favoriteProductsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$favoriteProductsHash();
@$internal
@override
$FutureProviderElement<List<Product>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<Product>> create(Ref ref) {
return favoriteProducts(ref);
}
}
String _$favoriteProductsHash() => r'cb3af4f84591c94e9eed3322b167fd8050a40aa1';