fix product page
This commit is contained in:
@@ -11,6 +11,7 @@ library;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ library;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ part of 'cart_provider.dart';
|
|||||||
///
|
///
|
||||||
/// Manages cart state with API integration:
|
/// Manages cart state with API integration:
|
||||||
/// - Adding/removing items (syncs with API)
|
/// - Adding/removing items (syncs with API)
|
||||||
/// - Updating quantities (syncs with API with 5s debounce)
|
/// - Updating quantities (syncs with API with 3s debounce)
|
||||||
/// - Loading cart from API via initialize()
|
/// - Loading cart from API via initialize()
|
||||||
/// - Local-only operations: selection, warehouse, calculations
|
/// - Local-only operations: selection, warehouse, calculations
|
||||||
/// - keepAlive: true to maintain cart state across navigation
|
/// - keepAlive: true to maintain cart state across navigation
|
||||||
@@ -24,7 +24,7 @@ const cartProvider = CartProvider._();
|
|||||||
///
|
///
|
||||||
/// Manages cart state with API integration:
|
/// Manages cart state with API integration:
|
||||||
/// - Adding/removing items (syncs with API)
|
/// - Adding/removing items (syncs with API)
|
||||||
/// - Updating quantities (syncs with API with 5s debounce)
|
/// - Updating quantities (syncs with API with 3s debounce)
|
||||||
/// - Loading cart from API via initialize()
|
/// - Loading cart from API via initialize()
|
||||||
/// - Local-only operations: selection, warehouse, calculations
|
/// - Local-only operations: selection, warehouse, calculations
|
||||||
/// - keepAlive: true to maintain cart state across navigation
|
/// - keepAlive: true to maintain cart state across navigation
|
||||||
@@ -33,7 +33,7 @@ final class CartProvider extends $NotifierProvider<Cart, CartState> {
|
|||||||
///
|
///
|
||||||
/// Manages cart state with API integration:
|
/// Manages cart state with API integration:
|
||||||
/// - Adding/removing items (syncs with API)
|
/// - Adding/removing items (syncs with API)
|
||||||
/// - Updating quantities (syncs with API with 5s debounce)
|
/// - Updating quantities (syncs with API with 3s debounce)
|
||||||
/// - Loading cart from API via initialize()
|
/// - Loading cart from API via initialize()
|
||||||
/// - Local-only operations: selection, warehouse, calculations
|
/// - Local-only operations: selection, warehouse, calculations
|
||||||
/// - keepAlive: true to maintain cart state across navigation
|
/// - keepAlive: true to maintain cart state across navigation
|
||||||
@@ -64,13 +64,13 @@ final class CartProvider extends $NotifierProvider<Cart, CartState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$cartHash() => r'3bb1372a0e87268e35c7c8d424d2d8315b4d09b2';
|
String _$cartHash() => r'706de28734e7059b2e9484f3b1d94226a0e90bb9';
|
||||||
|
|
||||||
/// Cart Notifier
|
/// Cart Notifier
|
||||||
///
|
///
|
||||||
/// Manages cart state with API integration:
|
/// Manages cart state with API integration:
|
||||||
/// - Adding/removing items (syncs with API)
|
/// - Adding/removing items (syncs with API)
|
||||||
/// - Updating quantities (syncs with API with 5s debounce)
|
/// - Updating quantities (syncs with API with 3s debounce)
|
||||||
/// - Loading cart from API via initialize()
|
/// - Loading cart from API via initialize()
|
||||||
/// - Local-only operations: selection, warehouse, calculations
|
/// - Local-only operations: selection, warehouse, calculations
|
||||||
/// - keepAlive: true to maintain cart state across navigation
|
/// - keepAlive: true to maintain cart state across navigation
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ library;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart' hide Notification;
|
import 'package:flutter/material.dart' hide Notification;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
|||||||
@@ -184,13 +184,20 @@ class ProductsRemoteDataSource {
|
|||||||
|
|
||||||
/// Get products by category
|
/// Get products by category
|
||||||
///
|
///
|
||||||
/// Filters products by category.
|
/// Filters products by category with pagination support.
|
||||||
/// For now, we fetch all products and filter locally.
|
/// For now, we fetch products with pagination and filter locally.
|
||||||
/// In the future, the API might support category filtering.
|
/// In the future, the API might support category filtering.
|
||||||
Future<List<ProductModel>> getProductsByCategory(String categoryId) async {
|
Future<List<ProductModel>> getProductsByCategory(
|
||||||
// For now, fetch all products and filter locally
|
String categoryId, {
|
||||||
|
int limitStart = 0,
|
||||||
|
int limitPageLength = 12,
|
||||||
|
}) async {
|
||||||
|
// Fetch products with pagination and filter locally
|
||||||
// TODO: Implement server-side category filtering if API supports it
|
// TODO: Implement server-side category filtering if API supports it
|
||||||
final allProducts = await getAllProducts();
|
final allProducts = await getAllProducts(
|
||||||
|
limitStart: limitStart,
|
||||||
|
limitPageLength: limitPageLength,
|
||||||
|
);
|
||||||
|
|
||||||
if (categoryId == 'all') {
|
if (categoryId == 'all') {
|
||||||
return allProducts;
|
return allProducts;
|
||||||
|
|||||||
@@ -25,10 +25,16 @@ class ProductsRepositoryImpl implements ProductsRepository {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Product>> getAllProducts() async {
|
Future<List<Product>> getAllProducts({
|
||||||
|
int limitStart = 0,
|
||||||
|
int limitPageLength = 12,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
// Fetch from Frappe API
|
// Fetch from Frappe API with pagination
|
||||||
final productModels = await remoteDataSource.getAllProducts();
|
final productModels = await remoteDataSource.getAllProducts(
|
||||||
|
limitStart: limitStart,
|
||||||
|
limitPageLength: limitPageLength,
|
||||||
|
);
|
||||||
return productModels.map((model) => model.toEntity()).toList();
|
return productModels.map((model) => model.toEntity()).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[ProductsRepository] Error getting products: $e');
|
print('[ProductsRepository] Error getting products: $e');
|
||||||
@@ -49,11 +55,17 @@ class ProductsRepositoryImpl implements ProductsRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Product>> getProductsByCategory(String categoryId) async {
|
Future<List<Product>> getProductsByCategory(
|
||||||
|
String categoryId, {
|
||||||
|
int limitStart = 0,
|
||||||
|
int limitPageLength = 12,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
// Filter by category via remote API
|
// Filter by category via remote API with pagination
|
||||||
final productModels = await remoteDataSource.getProductsByCategory(
|
final productModels = await remoteDataSource.getProductsByCategory(
|
||||||
categoryId,
|
categoryId,
|
||||||
|
limitStart: limitStart,
|
||||||
|
limitPageLength: limitPageLength,
|
||||||
);
|
);
|
||||||
return productModels.map((model) => model.toEntity()).toList();
|
return productModels.map((model) => model.toEntity()).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -14,9 +14,14 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
|||||||
abstract class ProductsRepository {
|
abstract class ProductsRepository {
|
||||||
/// Get all products
|
/// Get all products
|
||||||
///
|
///
|
||||||
/// Returns a list of all available products.
|
/// Returns a list of all available products with pagination support.
|
||||||
|
/// [limitStart] - Starting index for pagination (default: 0)
|
||||||
|
/// [limitPageLength] - Number of items per page (default: 12)
|
||||||
/// Throws an exception if the operation fails.
|
/// Throws an exception if the operation fails.
|
||||||
Future<List<Product>> getAllProducts();
|
Future<List<Product>> getAllProducts({
|
||||||
|
int limitStart = 0,
|
||||||
|
int limitPageLength = 12,
|
||||||
|
});
|
||||||
|
|
||||||
/// Search products by query
|
/// Search products by query
|
||||||
///
|
///
|
||||||
@@ -27,8 +32,14 @@ abstract class ProductsRepository {
|
|||||||
/// Get products by category
|
/// Get products by category
|
||||||
///
|
///
|
||||||
/// [categoryId] - Category ID to filter by
|
/// [categoryId] - Category ID to filter by
|
||||||
|
/// [limitStart] - Starting index for pagination (default: 0)
|
||||||
|
/// [limitPageLength] - Number of items per page (default: 12)
|
||||||
/// Returns list of products in the specified category.
|
/// Returns list of products in the specified category.
|
||||||
Future<List<Product>> getProductsByCategory(String categoryId);
|
Future<List<Product>> getProductsByCategory(
|
||||||
|
String categoryId, {
|
||||||
|
int limitStart = 0,
|
||||||
|
int limitPageLength = 12,
|
||||||
|
});
|
||||||
|
|
||||||
/// Get product by ID
|
/// Get product by ID
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -17,12 +17,25 @@ class GetProducts {
|
|||||||
/// Execute the use case
|
/// Execute the use case
|
||||||
///
|
///
|
||||||
/// [categoryId] - Optional category ID to filter products
|
/// [categoryId] - Optional category ID to filter products
|
||||||
|
/// [limitStart] - Starting index for pagination (default: 0)
|
||||||
|
/// [limitPageLength] - Number of items per page (default: 12)
|
||||||
/// Returns list of products (all or filtered by category)
|
/// Returns list of products (all or filtered by category)
|
||||||
Future<List<Product>> call({String? categoryId}) async {
|
Future<List<Product>> call({
|
||||||
|
String? categoryId,
|
||||||
|
int limitStart = 0,
|
||||||
|
int limitPageLength = 12,
|
||||||
|
}) async {
|
||||||
if (categoryId == null || categoryId == 'all') {
|
if (categoryId == null || categoryId == 'all') {
|
||||||
return await repository.getAllProducts();
|
return await repository.getAllProducts(
|
||||||
|
limitStart: limitStart,
|
||||||
|
limitPageLength: limitPageLength,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return await repository.getProductsByCategory(categoryId);
|
return await repository.getProductsByCategory(
|
||||||
|
categoryId,
|
||||||
|
limitStart: limitStart,
|
||||||
|
limitPageLength: limitPageLength,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import 'package:worker/core/constants/ui_constants.dart';
|
|||||||
import 'package:worker/core/router/app_router.dart';
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
|
||||||
import 'package:worker/features/products/presentation/providers/categories_provider.dart';
|
|
||||||
import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart';
|
import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart';
|
||||||
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
||||||
import 'package:worker/features/products/presentation/widgets/category_filter_chips.dart';
|
import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart';
|
||||||
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
|
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
|
||||||
import 'package:worker/features/products/presentation/widgets/product_grid.dart';
|
import 'package:worker/features/products/presentation/widgets/product_grid.dart';
|
||||||
import 'package:worker/features/products/presentation/widgets/product_search_bar.dart';
|
import 'package:worker/features/products/presentation/widgets/product_search_bar.dart';
|
||||||
@@ -34,7 +33,6 @@ class ProductsPage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final categoriesAsync = ref.watch(categoriesProvider);
|
|
||||||
final productsAsync = ref.watch(productsProvider);
|
final productsAsync = ref.watch(productsProvider);
|
||||||
final cartItemCount = ref.watch(cartItemCountProvider);
|
final cartItemCount = ref.watch(cartItemCountProvider);
|
||||||
|
|
||||||
@@ -75,46 +73,35 @@ class ProductsPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Search Bar with Filter Button
|
// Search Bar with Filter Button
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Search Bar (Expanded)
|
// Search Bar (Expanded)
|
||||||
const Expanded(child: ProductSearchBar()),
|
const Expanded(child: ProductSearchBar()),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
// Filter Button
|
// Filter Button
|
||||||
SizedBox(
|
Container(
|
||||||
height: InputFieldSpecs.height,
|
height: InputFieldSpecs.height,
|
||||||
child: OutlinedButton.icon(
|
width: InputFieldSpecs.height,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Open filter drawer from right
|
// Open filter drawer from right
|
||||||
Scaffold.of(scaffoldContext).openEndDrawer();
|
Scaffold.of(scaffoldContext).openEndDrawer();
|
||||||
},
|
},
|
||||||
icon: const FaIcon(FontAwesomeIcons.sliders, size: 18),
|
icon: const FaIcon(FontAwesomeIcons.sliders, size: 18),
|
||||||
label: const Text('Lọc', style: TextStyle(fontSize: 12)),
|
color: AppColors.grey900,
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: AppColors.grey900,
|
|
||||||
side: const BorderSide(color: AppColors.white, width: 0),
|
|
||||||
backgroundColor: AppColors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Category Filter Chips
|
// Brand Filter Chips
|
||||||
categoriesAsync.when(
|
const BrandFilterChips(),
|
||||||
data: (categories) => CategoryFilterChips(categories: categories),
|
|
||||||
loading: () =>
|
|
||||||
const SizedBox(height: 48.0, child: Center(child: CircularProgressIndicator(strokeWidth: 2.0))),
|
|
||||||
error: (error, stack) => const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.sm),
|
|
||||||
|
|
||||||
// Products Grid
|
// Products Grid
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -124,8 +111,16 @@ class ProductsPage extends ConsumerWidget {
|
|||||||
return _buildEmptyState(context, l10n);
|
return _buildEmptyState(context, l10n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final productsNotifier = ref.read(productsProvider.notifier);
|
||||||
|
final hasMore = productsNotifier.hasMore;
|
||||||
|
|
||||||
return ProductGrid(
|
return ProductGrid(
|
||||||
products: products,
|
products: products,
|
||||||
|
hasMore: hasMore,
|
||||||
|
isLoadingMore: false,
|
||||||
|
onLoadMore: () async {
|
||||||
|
await productsNotifier.loadMore();
|
||||||
|
},
|
||||||
onProductTap: (product) {
|
onProductTap: (product) {
|
||||||
// Navigate to product detail page
|
// Navigate to product detail page
|
||||||
context.push('/products/${product.productId}');
|
context.push('/products/${product.productId}');
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import 'package:worker/features/products/domain/repositories/products_repository
|
|||||||
import 'package:worker/features/products/domain/usecases/get_products.dart';
|
import 'package:worker/features/products/domain/usecases/get_products.dart';
|
||||||
import 'package:worker/features/products/domain/usecases/search_products.dart';
|
import 'package:worker/features/products/domain/usecases/search_products.dart';
|
||||||
import 'package:worker/features/products/domain/usecases/get_product_detail.dart';
|
import 'package:worker/features/products/domain/usecases/get_product_detail.dart';
|
||||||
import 'package:worker/features/products/presentation/providers/selected_category_provider.dart';
|
import 'package:worker/features/products/presentation/providers/selected_brand_provider.dart';
|
||||||
import 'package:worker/features/products/presentation/providers/search_query_provider.dart';
|
import 'package:worker/features/products/presentation/providers/search_query_provider.dart';
|
||||||
|
|
||||||
part 'products_provider.g.dart';
|
part 'products_provider.g.dart';
|
||||||
@@ -50,7 +50,7 @@ Future<ProductsRepository> productsRepository(Ref ref) async {
|
|||||||
///
|
///
|
||||||
/// Fetches and filters products based on selected category and search query.
|
/// Fetches and filters products based on selected category and search query.
|
||||||
/// Automatically updates when category or search query changes.
|
/// Automatically updates when category or search query changes.
|
||||||
/// Data is fetched from Frappe ERPNext API.
|
/// Data is fetched from Frappe ERPNext API with pagination support.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -64,38 +64,108 @@ Future<ProductsRepository> productsRepository(Ref ref) async {
|
|||||||
/// ```
|
/// ```
|
||||||
@riverpod
|
@riverpod
|
||||||
class Products extends _$Products {
|
class Products extends _$Products {
|
||||||
|
static const int pageSize = 12;
|
||||||
|
int _currentPage = 0;
|
||||||
|
bool _hasMore = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Product>> build() async {
|
Future<List<Product>> build() async {
|
||||||
|
// Reset pagination when dependencies change
|
||||||
|
_currentPage = 0;
|
||||||
|
_hasMore = true;
|
||||||
|
|
||||||
// Watch dependencies
|
// Watch dependencies
|
||||||
final selectedCategory = ref.watch(selectedCategoryProvider);
|
final selectedBrand = ref.watch(selectedBrandProvider);
|
||||||
final searchQuery = ref.watch(searchQueryProvider);
|
final searchQuery = ref.watch(searchQueryProvider);
|
||||||
|
|
||||||
// Get repository with injected data sources
|
// Get repository with injected data sources
|
||||||
final repository = await ref.watch(productsRepositoryProvider.future);
|
final repository = await ref.watch(productsRepositoryProvider.future);
|
||||||
|
|
||||||
// Apply filters
|
// Fetch first page of products
|
||||||
List<Product> products;
|
List<Product> products;
|
||||||
|
|
||||||
if (searchQuery.isNotEmpty) {
|
if (searchQuery.isNotEmpty) {
|
||||||
// Search takes precedence over category filter
|
// Search takes precedence over brand filter
|
||||||
final searchUseCase = SearchProducts(repository);
|
final searchUseCase = SearchProducts(repository);
|
||||||
products = await searchUseCase(searchQuery);
|
products = await searchUseCase(searchQuery);
|
||||||
|
|
||||||
// If a category is selected, filter search results by category
|
// If a brand is selected, filter search results by brand
|
||||||
if (selectedCategory != 'all') {
|
if (selectedBrand != 'all') {
|
||||||
products = products
|
products = products
|
||||||
.where((product) => product.categoryId == selectedCategory)
|
.where((product) => product.brand == selectedBrand)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For search, we fetch all results at once, so no more pages
|
||||||
|
_hasMore = false;
|
||||||
} else {
|
} else {
|
||||||
// No search query, use category filter
|
// No search query, fetch all products with pagination
|
||||||
final getProductsUseCase = GetProducts(repository);
|
final getProductsUseCase = GetProducts(repository);
|
||||||
products = await getProductsUseCase(categoryId: selectedCategory);
|
products = await getProductsUseCase(
|
||||||
|
limitStart: 0,
|
||||||
|
limitPageLength: pageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by brand if not 'all'
|
||||||
|
if (selectedBrand != 'all') {
|
||||||
|
products = products
|
||||||
|
.where((product) => product.brand == selectedBrand)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we got less than pageSize, there are no more products
|
||||||
|
_hasMore = products.length >= pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage = 1;
|
||||||
return products;
|
return products;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load more products (next page)
|
||||||
|
Future<void> loadMore() async {
|
||||||
|
if (!_hasMore) return;
|
||||||
|
|
||||||
|
// Watch dependencies to get current filters
|
||||||
|
final selectedBrand = ref.read(selectedBrandProvider);
|
||||||
|
final searchQuery = ref.read(searchQueryProvider);
|
||||||
|
|
||||||
|
// Don't paginate search results (already fetched all)
|
||||||
|
if (searchQuery.isNotEmpty) return;
|
||||||
|
|
||||||
|
// Get repository
|
||||||
|
final repository = await ref.read(productsRepositoryProvider.future);
|
||||||
|
|
||||||
|
// Calculate pagination parameters
|
||||||
|
final limitStart = _currentPage * pageSize;
|
||||||
|
|
||||||
|
// Fetch next page from API
|
||||||
|
final getProductsUseCase = GetProducts(repository);
|
||||||
|
var newProducts = await getProductsUseCase(
|
||||||
|
limitStart: limitStart,
|
||||||
|
limitPageLength: pageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by brand if not 'all'
|
||||||
|
if (selectedBrand != 'all') {
|
||||||
|
newProducts = newProducts
|
||||||
|
.where((product) => product.brand == selectedBrand)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got less than pageSize, there are no more products
|
||||||
|
_hasMore = newProducts.length >= pageSize;
|
||||||
|
|
||||||
|
// Increment page counter
|
||||||
|
_currentPage++;
|
||||||
|
|
||||||
|
// Append new products to existing list
|
||||||
|
final currentProducts = state.value ?? [];
|
||||||
|
state = AsyncValue.data([...currentProducts, ...newProducts]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if there are more products to load
|
||||||
|
bool get hasMore => _hasMore;
|
||||||
|
|
||||||
/// Refresh products data
|
/// Refresh products data
|
||||||
///
|
///
|
||||||
/// Forces a refresh from the datasource.
|
/// Forces a refresh from the datasource.
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ String _$productsRepositoryHash() =>
|
|||||||
///
|
///
|
||||||
/// Fetches and filters products based on selected category and search query.
|
/// Fetches and filters products based on selected category and search query.
|
||||||
/// Automatically updates when category or search query changes.
|
/// Automatically updates when category or search query changes.
|
||||||
/// Data is fetched from Frappe ERPNext API.
|
/// Data is fetched from Frappe ERPNext API with pagination support.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -179,7 +179,7 @@ const productsProvider = ProductsProvider._();
|
|||||||
///
|
///
|
||||||
/// Fetches and filters products based on selected category and search query.
|
/// Fetches and filters products based on selected category and search query.
|
||||||
/// Automatically updates when category or search query changes.
|
/// Automatically updates when category or search query changes.
|
||||||
/// Data is fetched from Frappe ERPNext API.
|
/// Data is fetched from Frappe ERPNext API with pagination support.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -197,7 +197,7 @@ final class ProductsProvider
|
|||||||
///
|
///
|
||||||
/// Fetches and filters products based on selected category and search query.
|
/// Fetches and filters products based on selected category and search query.
|
||||||
/// Automatically updates when category or search query changes.
|
/// Automatically updates when category or search query changes.
|
||||||
/// Data is fetched from Frappe ERPNext API.
|
/// Data is fetched from Frappe ERPNext API with pagination support.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -228,13 +228,13 @@ final class ProductsProvider
|
|||||||
Products create() => Products();
|
Products create() => Products();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$productsHash() => r'b892402a88484d301cdabd1fde5822ddd29538bf';
|
String _$productsHash() => r'5fe0fdb46c3a6845327221ff26ba5f3624fcf3bf';
|
||||||
|
|
||||||
/// Products Provider
|
/// Products Provider
|
||||||
///
|
///
|
||||||
/// Fetches and filters products based on selected category and search query.
|
/// Fetches and filters products based on selected category and search query.
|
||||||
/// Automatically updates when category or search query changes.
|
/// Automatically updates when category or search query changes.
|
||||||
/// Data is fetched from Frappe ERPNext API.
|
/// Data is fetched from Frappe ERPNext API with pagination support.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/// Provider: Selected Brand Provider
|
||||||
|
///
|
||||||
|
/// Manages the currently selected brand filter state.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'selected_brand_provider.g.dart';
|
||||||
|
|
||||||
|
/// Selected Brand Provider
|
||||||
|
///
|
||||||
|
/// Stores the currently selected brand ID for filtering products.
|
||||||
|
/// Default: 'all' (no brand filter)
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// // Watch selected brand
|
||||||
|
/// final selectedBrand = ref.watch(selectedBrandProvider);
|
||||||
|
///
|
||||||
|
/// // Update selected brand
|
||||||
|
/// ref.read(selectedBrandProvider.notifier).updateBrand('VASTA');
|
||||||
|
/// ```
|
||||||
|
@riverpod
|
||||||
|
class SelectedBrand extends _$SelectedBrand {
|
||||||
|
@override
|
||||||
|
String build() {
|
||||||
|
return 'all'; // Default: show all brands
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the selected brand
|
||||||
|
void updateBrand(String brandId) {
|
||||||
|
state = brandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset to default (all brands)
|
||||||
|
void reset() {
|
||||||
|
state = 'all';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'selected_brand_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
/// Selected Brand Provider
|
||||||
|
///
|
||||||
|
/// Stores the currently selected brand ID for filtering products.
|
||||||
|
/// Default: 'all' (no brand filter)
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// // Watch selected brand
|
||||||
|
/// final selectedBrand = ref.watch(selectedBrandProvider);
|
||||||
|
///
|
||||||
|
/// // Update selected brand
|
||||||
|
/// ref.read(selectedBrandProvider.notifier).updateBrand('VASTA');
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@ProviderFor(SelectedBrand)
|
||||||
|
const selectedBrandProvider = SelectedBrandProvider._();
|
||||||
|
|
||||||
|
/// Selected Brand Provider
|
||||||
|
///
|
||||||
|
/// Stores the currently selected brand ID for filtering products.
|
||||||
|
/// Default: 'all' (no brand filter)
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// // Watch selected brand
|
||||||
|
/// final selectedBrand = ref.watch(selectedBrandProvider);
|
||||||
|
///
|
||||||
|
/// // Update selected brand
|
||||||
|
/// ref.read(selectedBrandProvider.notifier).updateBrand('VASTA');
|
||||||
|
/// ```
|
||||||
|
final class SelectedBrandProvider
|
||||||
|
extends $NotifierProvider<SelectedBrand, String> {
|
||||||
|
/// Selected Brand Provider
|
||||||
|
///
|
||||||
|
/// Stores the currently selected brand ID for filtering products.
|
||||||
|
/// Default: 'all' (no brand filter)
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// // Watch selected brand
|
||||||
|
/// final selectedBrand = ref.watch(selectedBrandProvider);
|
||||||
|
///
|
||||||
|
/// // Update selected brand
|
||||||
|
/// ref.read(selectedBrandProvider.notifier).updateBrand('VASTA');
|
||||||
|
/// ```
|
||||||
|
const SelectedBrandProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'selectedBrandProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$selectedBrandHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
SelectedBrand create() => SelectedBrand();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(String value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<String>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$selectedBrandHash() => r'1295bffdcea67b78f7d55ce42f476603e042b19b';
|
||||||
|
|
||||||
|
/// Selected Brand Provider
|
||||||
|
///
|
||||||
|
/// Stores the currently selected brand ID for filtering products.
|
||||||
|
/// Default: 'all' (no brand filter)
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// // Watch selected brand
|
||||||
|
/// final selectedBrand = ref.watch(selectedBrandProvider);
|
||||||
|
///
|
||||||
|
/// // Update selected brand
|
||||||
|
/// ref.read(selectedBrandProvider.notifier).updateBrand('VASTA');
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
abstract class _$SelectedBrand extends $Notifier<String> {
|
||||||
|
String build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<String, String>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<String, String>,
|
||||||
|
String,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/// Widget: Brand Filter Chips
|
||||||
|
///
|
||||||
|
/// Horizontal scrolling filter chips for product brands.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart';
|
||||||
|
import 'package:worker/features/products/presentation/providers/selected_brand_provider.dart';
|
||||||
|
|
||||||
|
/// Brand Filter Chips Widget
|
||||||
|
///
|
||||||
|
/// Displays brands as horizontally scrolling chips.
|
||||||
|
/// Updates selected brand when tapped.
|
||||||
|
class BrandFilterChips extends ConsumerWidget {
|
||||||
|
const BrandFilterChips({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final selectedBrand = ref.watch(selectedBrandProvider);
|
||||||
|
final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||||
|
|
||||||
|
return filterOptionsAsync.when(
|
||||||
|
data: (options) {
|
||||||
|
// Add "All" option at the beginning
|
||||||
|
final allBrands = [
|
||||||
|
const FilterOption(value: 'all', label: 'Tất cả'),
|
||||||
|
...options.brands,
|
||||||
|
];
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 48.0,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
|
itemCount: allBrands.length,
|
||||||
|
separatorBuilder: (context, index) =>
|
||||||
|
const SizedBox(width: AppSpacing.sm),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final brand = allBrands[index];
|
||||||
|
final isSelected = selectedBrand == brand.value;
|
||||||
|
|
||||||
|
return FilterChip(
|
||||||
|
label: Text(
|
||||||
|
brand.label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
color: isSelected ? AppColors.white : AppColors.grey900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selected: isSelected,
|
||||||
|
onSelected: (selected) {
|
||||||
|
if (selected) {
|
||||||
|
ref
|
||||||
|
.read(selectedBrandProvider.notifier)
|
||||||
|
.updateBrand(brand.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: AppColors.white,
|
||||||
|
selectedColor: AppColors.primaryBlue,
|
||||||
|
checkmarkColor: AppColors.white,
|
||||||
|
side: BorderSide(
|
||||||
|
color: isSelected ? AppColors.primaryBlue : AppColors.grey100,
|
||||||
|
width: isSelected ? 2.0 : 1.0,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.md,
|
||||||
|
vertical: AppSpacing.sm,
|
||||||
|
),
|
||||||
|
elevation: isSelected ? AppElevation.low : 0,
|
||||||
|
showCheckmark: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const SizedBox(
|
||||||
|
height: 48.0,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, stack) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Widget: Product Grid
|
/// Widget: Product Grid
|
||||||
///
|
///
|
||||||
/// Grid view displaying product cards.
|
/// Grid view displaying product cards with pagination support.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -10,22 +10,59 @@ import 'package:worker/features/products/presentation/widgets/product_card.dart'
|
|||||||
|
|
||||||
/// Product Grid Widget
|
/// Product Grid Widget
|
||||||
///
|
///
|
||||||
/// Displays products in a 2-column grid layout.
|
/// Displays products in a 2-column grid layout with scroll-to-load-more.
|
||||||
class ProductGrid extends StatelessWidget {
|
class ProductGrid extends StatefulWidget {
|
||||||
final List<Product> products;
|
final List<Product> products;
|
||||||
final void Function(Product)? onProductTap;
|
final void Function(Product)? onProductTap;
|
||||||
final void Function(Product)? onAddToCart;
|
final void Function(Product)? onAddToCart;
|
||||||
|
final VoidCallback? onLoadMore;
|
||||||
|
final bool hasMore;
|
||||||
|
final bool isLoadingMore;
|
||||||
|
|
||||||
const ProductGrid({
|
const ProductGrid({
|
||||||
super.key,
|
super.key,
|
||||||
required this.products,
|
required this.products,
|
||||||
this.onProductTap,
|
this.onProductTap,
|
||||||
this.onAddToCart,
|
this.onAddToCart,
|
||||||
|
this.onLoadMore,
|
||||||
|
this.hasMore = false,
|
||||||
|
this.isLoadingMore = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProductGrid> createState() => _ProductGridState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductGridState extends State<ProductGrid> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController.addListener(_onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.removeListener(_onScroll);
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScroll() {
|
||||||
|
if (_scrollController.position.pixels >=
|
||||||
|
_scrollController.position.maxScrollExtent - 200) {
|
||||||
|
// Load more when 200px from bottom
|
||||||
|
if (widget.hasMore && !widget.isLoadingMore && widget.onLoadMore != null) {
|
||||||
|
widget.onLoadMore!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.all(AppSpacing.xs),
|
padding: const EdgeInsets.all(AppSpacing.xs),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: GridSpecs.productGridColumns,
|
crossAxisCount: GridSpecs.productGridColumns,
|
||||||
@@ -33,14 +70,24 @@ class ProductGrid extends StatelessWidget {
|
|||||||
mainAxisSpacing: AppSpacing.xs,
|
mainAxisSpacing: AppSpacing.xs,
|
||||||
childAspectRatio: 0.62, // Width / Height ratio (adjusted for 2 buttons)
|
childAspectRatio: 0.62, // Width / Height ratio (adjusted for 2 buttons)
|
||||||
),
|
),
|
||||||
itemCount: products.length,
|
itemCount: widget.products.length + (widget.hasMore ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final product = products[index];
|
// Show loading indicator at the end
|
||||||
|
if (index == widget.products.length) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(AppSpacing.md),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final product = widget.products[index];
|
||||||
|
|
||||||
return ProductCard(
|
return ProductCard(
|
||||||
product: product,
|
product: product,
|
||||||
onTap: onProductTap != null ? () => onProductTap!(product) : null,
|
onTap: widget.onProductTap != null ? () => widget.onProductTap!(product) : null,
|
||||||
onAddToCart: onAddToCart != null ? () => onAddToCart!(product) : null,
|
onAddToCart: widget.onAddToCart != null ? () => widget.onAddToCart!(product) : null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -912,10 +912,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1437,26 +1437,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
|
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.26.2"
|
version: "1.26.3"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.7"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
|
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.11"
|
version: "0.6.12"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user