578 lines
18 KiB
Dart
578 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:wordpress/providers/api_controller.dart';
|
|
import 'package:wordpress/viewmodel/all_product_model.dart';
|
|
|
|
class AllProductScreen extends ConsumerStatefulWidget {
|
|
const AllProductScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<AllProductScreen> createState() => _AllProductScreenState();
|
|
}
|
|
|
|
class _AllProductScreenState extends ConsumerState<AllProductScreen>
|
|
with TickerProviderStateMixin {
|
|
final ScrollController _scrollController = ScrollController();
|
|
final int _itemsPerPage = 10;
|
|
int _currentPage = 1;
|
|
List<AllProductModel> _displayedProducts = [];
|
|
bool _isLoadingMore = false;
|
|
|
|
late AnimationController _fadeController;
|
|
late AnimationController _slideController;
|
|
late Animation<double> _fadeAnimation;
|
|
late Animation<Offset> _slideAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initializeAnimations();
|
|
_setupScrollListener();
|
|
}
|
|
|
|
void _initializeAnimations() {
|
|
_fadeController = AnimationController(
|
|
duration: const Duration(milliseconds: 800),
|
|
vsync: this,
|
|
);
|
|
_slideController = AnimationController(
|
|
duration: const Duration(milliseconds: 600),
|
|
vsync: this,
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
_slideAnimation =
|
|
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
|
|
CurvedAnimation(parent: _slideController, curve: Curves.easeOutBack),
|
|
);
|
|
}
|
|
|
|
void _setupScrollListener() {
|
|
_scrollController.addListener(() {
|
|
if (_scrollController.position.pixels >=
|
|
_scrollController.position.maxScrollExtent - 200) {
|
|
_loadMoreProducts();
|
|
}
|
|
});
|
|
}
|
|
|
|
void _loadMoreProducts() {
|
|
if (_isLoadingMore) return;
|
|
|
|
final asyncProducts = ref.read(allProductProvider);
|
|
asyncProducts.whenData((allProducts) {
|
|
if (_displayedProducts.length < allProducts.length) {
|
|
setState(() {
|
|
_isLoadingMore = true;
|
|
});
|
|
|
|
Future.delayed(const Duration(milliseconds: 500), () {
|
|
setState(() {
|
|
final startIndex = _currentPage * _itemsPerPage;
|
|
final endIndex = ((startIndex + _itemsPerPage) > allProducts.length)
|
|
? allProducts.length
|
|
: startIndex + _itemsPerPage;
|
|
|
|
_displayedProducts.addAll(
|
|
allProducts.sublist(startIndex, endIndex),
|
|
);
|
|
_currentPage++;
|
|
_isLoadingMore = false;
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
_fadeController.dispose();
|
|
_slideController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final asyncProducts = ref.watch(allProductProvider);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.grey[50],
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'All Products',
|
|
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
|
|
),
|
|
backgroundColor: Colors.blue[600],
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
),
|
|
body: asyncProducts.when(
|
|
loading: () => _buildLoadingWidget(),
|
|
error: (err, stack) => _buildErrorWidget(err),
|
|
data: (products) {
|
|
if (products.isEmpty) {
|
|
return _buildEmptyWidget();
|
|
}
|
|
|
|
// Initialize displayed products on first load
|
|
if (_displayedProducts.isEmpty) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
setState(() {
|
|
_displayedProducts = products.take(_itemsPerPage).toList();
|
|
});
|
|
_fadeController.forward();
|
|
_slideController.forward();
|
|
});
|
|
}
|
|
|
|
return _buildProductGrid(products);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadingWidget() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
TweenAnimationBuilder<double>(
|
|
duration: const Duration(seconds: 1),
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
builder: (context, value, child) {
|
|
return Transform.rotate(
|
|
angle: value * 2 * 3.14159,
|
|
child: Container(
|
|
width: 60,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(30),
|
|
gradient: const LinearGradient(
|
|
colors: [Colors.blue, Colors.purple],
|
|
),
|
|
),
|
|
child: const Icon(
|
|
Icons.shopping_bag,
|
|
color: Colors.white,
|
|
size: 30,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
"Loading amazing products...",
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.grey,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildErrorWidget(Object error) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 64, color: Colors.red[400]),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Oops! Something went wrong',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[800],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Error: $error',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
ref.invalidate(allProductProvider);
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Retry'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue[600],
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyWidget() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.shopping_bag_outlined, size: 64, color: Colors.grey[400]),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'No products found',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[800],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Check back later for new products',
|
|
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProductGrid(List<AllProductModel> allProducts) {
|
|
return FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: SlideTransition(
|
|
position: _slideAnimation,
|
|
child: CustomScrollView(
|
|
controller: _scrollController,
|
|
physics: const BouncingScrollPhysics(),
|
|
slivers: [
|
|
SliverPadding(
|
|
padding: const EdgeInsets.all(16),
|
|
sliver: SliverGrid(
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 2,
|
|
crossAxisSpacing: 16,
|
|
mainAxisSpacing: 16,
|
|
childAspectRatio: 0.75,
|
|
),
|
|
delegate: SliverChildBuilderDelegate((context, index) {
|
|
return AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 500),
|
|
transitionBuilder: (child, animation) {
|
|
return SlideTransition(
|
|
position: Tween<Offset>(
|
|
begin: const Offset(0, 0.3),
|
|
end: Offset.zero,
|
|
).animate(animation),
|
|
child: FadeTransition(opacity: animation, child: child),
|
|
);
|
|
},
|
|
child: ProductCard(
|
|
product: _displayedProducts[index],
|
|
key: ValueKey(_displayedProducts[index].id),
|
|
animationDelay: Duration(milliseconds: index * 100),
|
|
),
|
|
);
|
|
}, childCount: _displayedProducts.length),
|
|
),
|
|
),
|
|
if (_isLoadingMore ||
|
|
_displayedProducts.length < allProducts.length)
|
|
SliverToBoxAdapter(child: _buildLoadMoreWidget()),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadMoreWidget() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Center(
|
|
child: _isLoadingMore
|
|
? Column(
|
|
children: [
|
|
CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.blue[600]!,
|
|
),
|
|
strokeWidth: 2,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Loading more products...',
|
|
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
|
),
|
|
],
|
|
)
|
|
: ElevatedButton.icon(
|
|
onPressed: _loadMoreProducts,
|
|
icon: const Icon(Icons.expand_more),
|
|
label: const Text('Load More'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue[600],
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(25),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ProductCard extends StatefulWidget {
|
|
final AllProductModel product;
|
|
final Duration animationDelay;
|
|
|
|
const ProductCard({
|
|
super.key,
|
|
required this.product,
|
|
this.animationDelay = Duration.zero,
|
|
});
|
|
|
|
@override
|
|
State<ProductCard> createState() => _ProductCardState();
|
|
}
|
|
|
|
class _ProductCardState extends State<ProductCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _hoverController;
|
|
late Animation<double> _scaleAnimation;
|
|
late Animation<double> _elevationAnimation;
|
|
bool _isVisible = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_hoverController = AnimationController(
|
|
duration: const Duration(milliseconds: 200),
|
|
vsync: this,
|
|
);
|
|
|
|
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.05).animate(
|
|
CurvedAnimation(parent: _hoverController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
_elevationAnimation = Tween<double>(begin: 4.0, end: 12.0).animate(
|
|
CurvedAnimation(parent: _hoverController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
// Staggered animation entrance
|
|
Future.delayed(widget.animationDelay, () {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isVisible = true;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_hoverController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedOpacity(
|
|
opacity: _isVisible ? 1.0 : 0.0,
|
|
duration: const Duration(milliseconds: 600),
|
|
child: AnimatedBuilder(
|
|
animation: _hoverController,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: GestureDetector(
|
|
onTapDown: (_) => _hoverController.forward(),
|
|
onTapUp: (_) => _hoverController.reverse(),
|
|
onTapCancel: () => _hoverController.reverse(),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: _elevationAnimation.value,
|
|
offset: Offset(0, _elevationAnimation.value / 2),
|
|
spreadRadius: 1,
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [_buildProductImage(), _buildProductDetails()],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProductImage() {
|
|
return Expanded(
|
|
flex: 3,
|
|
child: Container(
|
|
width: double.infinity,
|
|
decoration: const BoxDecoration(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Hero(
|
|
tag: 'product_${widget.product.id}',
|
|
child: Image.network(
|
|
widget.product.images.isNotEmpty
|
|
? widget.product.images.first.src
|
|
: '',
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
fit: BoxFit.cover,
|
|
loadingBuilder: (context, child, loadingProgress) {
|
|
if (loadingProgress == null) return child;
|
|
return Container(
|
|
color: Colors.grey[100],
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
value: loadingProgress.expectedTotalBytes != null
|
|
? loadingProgress.cumulativeBytesLoaded /
|
|
loadingProgress.expectedTotalBytes!
|
|
: null,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.blue[300]!,
|
|
),
|
|
strokeWidth: 2,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorBuilder: (context, error, stackTrace) => Container(
|
|
color: Colors.grey[100],
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.image_not_supported_outlined,
|
|
color: Colors.grey[400],
|
|
size: 32,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Image not available',
|
|
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Gradient overlay
|
|
Positioned(
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: Container(
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.black.withOpacity(0.15),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProductDetails() {
|
|
return Expanded(
|
|
flex: 3,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
widget.product.name,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
height: 1.3,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
"₹${widget.product.price}",
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green[600],
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue[600],
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.blue.withOpacity(0.3),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.add_shopping_cart,
|
|
color: Colors.white,
|
|
size: 18,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|