import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../l10n/app_localizations.dart'; import '../../models/product.dart'; import '../widgets/product_tile.dart'; class ProductGrid extends StatelessWidget { final List products; final int rows; final bool hasSelectedTab; final ValueChanged onProductTap; const ProductGrid({ super.key, required this.products, required this.rows, required this.hasSelectedTab, required this.onProductTap, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context); if (products.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( shape: BoxShape.circle, color: scheme.onSurface.withValues(alpha: 0.05), ), child: Icon( Icons.inventory_2_outlined, size: 40, color: scheme.onSurface.withValues(alpha: 0.3), ), ), const SizedBox(height: 16), Text( l10n.noProductsYet, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( l10n.addFirstProduct, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 20), FilledButton.icon( onPressed: () => context.push('/admin'), icon: const Icon(Icons.admin_panel_settings_outlined), label: Text(l10n.admin), ), ], ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 18, 20, 4), child: Row( children: [ Text( l10n.products, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(width: 10), if (!hasSelectedTab) Flexible( child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: scheme.onSurface.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.info_outline_rounded, size: 14, color: scheme.onSurface.withValues(alpha: 0.5), ), const SizedBox(width: 4), Flexible( child: Text( l10n.selectTabToAddItems, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ), ), ], ), ), Expanded( child: LayoutBuilder( builder: (context, constraints) { const padding = EdgeInsets.fromLTRB(16, 12, 16, 16); const spacing = 12.0; const minTileHeight = 72.0; // Exactly [rows] rows fill the height; the column count keeps // the tiles as close to square as the width allows final rowCount = rows.clamp(2, 6).toInt(); final tileHeight = max( minTileHeight, (constraints.maxHeight - padding.vertical - (rowCount - 1) * spacing) / rowCount, ); final columns = max( 1, ((constraints.maxWidth - padding.horizontal + spacing) / (tileHeight + spacing)) .round(), ); return GridView.builder( key: ValueKey('product-grid-$rowCount'), padding: padding, itemCount: products.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: columns, crossAxisSpacing: spacing, mainAxisSpacing: spacing, mainAxisExtent: tileHeight, ), itemBuilder: (context, index) { final product = products[index]; return _FireTile( key: ValueKey(product.id), enabled: hasSelectedTab, onTap: () => onProductTap(product), builder: (onTap) => ProductTile( product: product, enabled: hasSelectedTab, onTap: onTap, ), ); }, ); }, ), ), ], ); } } /// Five quick taps on the same product set its tile on fire for a moment. class _FireTile extends StatefulWidget { final bool enabled; final VoidCallback onTap; final Widget Function(VoidCallback onTap) builder; const _FireTile({ super.key, required this.enabled, required this.onTap, required this.builder, }); @override State<_FireTile> createState() => _FireTileState(); } class _FireTileState extends State<_FireTile> with SingleTickerProviderStateMixin { static const _tapsNeeded = 5; static const _tapWindow = Duration(milliseconds: 700); static const _burnTime = Duration(milliseconds: 2500); late final AnimationController _flicker = AnimationController( vsync: this, duration: const Duration(milliseconds: 180), ); int _tapCount = 0; DateTime? _lastTap; Timer? _burnTimer; bool get _isOnFire => _burnTimer != null; @override void dispose() { _burnTimer?.cancel(); _flicker.dispose(); super.dispose(); } void _onTap() { widget.onTap(); final now = DateTime.now(); final last = _lastTap; _tapCount = last != null && now.difference(last) < _tapWindow ? _tapCount + 1 : 1; _lastTap = now; if (_tapCount < _tapsNeeded && !_isOnFire) return; // Each extra quick tap keeps it burning. _burnTimer?.cancel(); _burnTimer = Timer(_burnTime, () { if (!mounted) return; _flicker.stop(); setState(() => _burnTimer = null); }); if (!_flicker.isAnimating) _flicker.repeat(reverse: true); setState(() {}); } @override Widget build(BuildContext context) { return Stack( fit: StackFit.expand, clipBehavior: Clip.none, children: [ widget.builder(_onTap), IgnorePointer( child: AnimatedOpacity( opacity: _isOnFire ? 1 : 0, duration: const Duration(milliseconds: 300), child: AnimatedBuilder( animation: _flicker, builder: (context, _) { final heat = _flicker.value; return Stack( fit: StackFit.expand, clipBehavior: Clip.none, children: [ DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all( color: Color.lerp( Colors.deepOrange, Colors.amber, heat, )!, width: 2.5, ), boxShadow: [ BoxShadow( color: Colors.deepOrange.withValues( alpha: 0.45 + 0.3 * heat, ), blurRadius: 14 + 10 * heat, spreadRadius: 1 + 2 * heat, ), ], ), ), Positioned( left: 8, bottom: 6, child: Transform.scale( scale: 1 + 0.15 * heat, alignment: Alignment.bottomCenter, child: const Text('🔥', style: TextStyle(fontSize: 26)), ), ), ], ); }, ), ), ), ], ); } }