From 7c067d36f2449aa4778ecc90e896f8a460561fb3 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Thu, 9 Jul 2026 07:24:49 +0330 Subject: [PATCH] style: shop --- .../shop/presentation/screen/shop_screen.dart | 16 +- .../screen/shop_screen.parts.dart | 389 ++++++++++++------ 2 files changed, 277 insertions(+), 128 deletions(-) diff --git a/lib/feature/shop/presentation/screen/shop_screen.dart b/lib/feature/shop/presentation/screen/shop_screen.dart index e5498de..a63f006 100644 --- a/lib/feature/shop/presentation/screen/shop_screen.dart +++ b/lib/feature/shop/presentation/screen/shop_screen.dart @@ -121,10 +121,14 @@ class ShopScreen extends StatelessWidget { Widget _header(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.fromLTRB(10, 8, 10, 6), child: Row( children: [ const AppBackButton(), + const SizedBox(width: 6), + const Icon(Icons.storefront, color: AppColors.gold, size: 22), + const SizedBox(width: 6), + const GlowText('فروشگاه', size: 20), const Spacer(), BlocBuilder( builder: (context, s) { @@ -134,14 +138,16 @@ class ShopScreen extends StatelessWidget { : null; return Row( children: [ - StatChip( + _BalancePill( icon: Icons.confirmation_number, - value: '${w?.tickets ?? 0}', + iconColor: const Color(0xFFC77DFF), + value: _fmt(w?.tickets ?? 0), ), const SizedBox(width: 8), - StatChip( + _BalancePill( icon: Icons.monetization_on, - value: '${w?.coins ?? 0}', + iconColor: AppColors.gold, + value: _fmt(w?.coins ?? 0), ), ], ); diff --git a/lib/feature/shop/presentation/screen/shop_screen.parts.dart b/lib/feature/shop/presentation/screen/shop_screen.parts.dart index 0edb0c3..46b0ae0 100644 --- a/lib/feature/shop/presentation/screen/shop_screen.parts.dart +++ b/lib/feature/shop/presentation/screen/shop_screen.parts.dart @@ -3,32 +3,56 @@ part of 'shop_screen.dart'; class _ShopTabs extends StatelessWidget { const _ShopTabs(); + + static const _tabs = <(IconData, String)>[ + (Icons.monetization_on, 'سکه'), + (Icons.confirmation_number, 'بلیط'), + (Icons.style, 'کارت'), + (Icons.casino, 'فرش'), + (Icons.filter_center_focus, 'قاب'), + (Icons.bolt, 'تجهیزات'), + (Icons.workspace_premium, 'VIP'), + ]; + @override Widget build(BuildContext context) { - return const TabBar( + return TabBar( + isScrollable: true, + tabAlignment: TabAlignment.center, + indicatorSize: TabBarIndicatorSize.tab, indicator: BoxDecoration( - gradient: LinearGradient( + gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [AppColors.lapis, AppColors.suitDark], ), - borderRadius: BorderRadius.all(Radius.circular(12)), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.goldDark), + boxShadow: [ + BoxShadow( + color: AppColors.gold.withValues(alpha: 0.25), + blurRadius: 8, + ), + ], ), - indicatorSize: TabBarIndicatorSize.tab, labelColor: AppColors.gold, unselectedLabelColor: Colors.white60, - labelStyle: TextStyle(fontWeight: FontWeight.bold), + labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), dividerColor: Colors.transparent, - isScrollable: true, - tabAlignment: TabAlignment.center, + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), tabs: [ - Tab(text: 'سکه'), - Tab(text: 'بلیط'), - Tab(text: 'کارت'), - Tab(text: 'فرش'), - Tab(text: 'قاب'), - Tab(text: 'تجهیزات'), - Tab(text: 'VIP'), + for (final (icon, label) in _tabs) + Tab( + height: 40, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 16), + const SizedBox(width: 5), + Text(label), + ], + ), + ), ], ); } @@ -293,19 +317,8 @@ class _CarpetCard extends StatelessWidget { Widget build(BuildContext context) { final c = carpet; return Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [AppColors.lapisHi, AppColors.lapisLo], - ), - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: selected ? AppColors.gold : AppColors.goldDark, - width: selected ? 2 : 1.4, - ), - ), + padding: const EdgeInsets.all(10), + decoration: _shopCardDeco(highlight: selected), child: Column( children: [ Text( @@ -315,9 +328,10 @@ class _CarpetCard extends StatelessWidget { style: const TextStyle( color: AppColors.gold, fontWeight: FontWeight.bold, + fontSize: 14, ), ), - const SizedBox(height: 6), + const SizedBox(height: 8), Expanded(child: _preview(c)), const SizedBox(height: 6), SizedBox(width: double.infinity, child: _action(c)), @@ -491,27 +505,18 @@ class _FrameCard extends StatelessWidget { final f = frame; final grad = frameGradient(f.id) ?? const [Color(0xFFF3D27A), AppColors.goldDark]; return Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [AppColors.lapisHi, AppColors.lapisLo], - ), - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: selected ? AppColors.gold : AppColors.goldDark, - width: selected ? 2 : 1.4, - ), - ), + padding: const EdgeInsets.all(10), + decoration: _shopCardDeco(highlight: selected), child: Column( children: [ Text(f.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: AppColors.gold, fontWeight: FontWeight.bold)), - const SizedBox(height: 6), + color: AppColors.gold, + fontWeight: FontWeight.bold, + fontSize: 14)), + const SizedBox(height: 8), // پیش‌نمایش: آواتار درونِ قاب با گرادیانِ همان قاب. Expanded( child: Center( @@ -570,18 +575,37 @@ Widget _grid({required String note, required List children}) { return Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 6), - child: Text( - note, - textAlign: TextAlign.center, - style: const TextStyle(color: AppColors.gold, fontSize: 13), + padding: const EdgeInsets.fromLTRB(12, 10, 12, 2), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppColors.gold.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.goldFaint), + ), + child: Row( + children: [ + const Icon(Icons.info_outline, color: AppColors.gold, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + note, + style: const TextStyle( + color: AppColors.gold, + fontSize: 12.5, + height: 1.3, + ), + ), + ), + ], + ), ), ), Expanded( child: GridView.count( crossAxisCount: 2, padding: const EdgeInsets.all(12), - childAspectRatio: 0.74, + childAspectRatio: 0.72, mainAxisSpacing: 12, crossAxisSpacing: 12, children: children, @@ -591,6 +615,85 @@ Widget _grid({required String note, required List children}) { ); } +/// قابِ مشترکِ کارت‌های فروشگاه (سکه/کارت/فرش/…): گرادیانِ لاجوردی، حاشیه‌ی طلایی +/// و عمقِ سایه. [highlight] برای آیتمِ انتخاب‌شده هاله‌ی طلایی اضافه می‌کند. +BoxDecoration _shopCardDeco({bool highlight = false}) => BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF1C4A80), Color(0xFF0B2646)], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: highlight ? AppColors.gold : AppColors.goldDark, + width: highlight ? 2 : 1.3, + ), + boxShadow: [ + const BoxShadow( + color: Colors.black54, blurRadius: 10, offset: Offset(0, 5)), + if (highlight) + BoxShadow( + color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 14), + ], + ); + +/// عددِ موجودی با جداکننده‌ی هزارگان (مثلِ ۱۲٬۵۰۰) برای خواناییِ بهتر. +String _fmt(int n) { + final s = n.abs().toString(); + final b = StringBuffer(n < 0 ? '-' : ''); + for (var i = 0; i < s.length; i++) { + if (i > 0 && (s.length - i) % 3 == 0) b.write('٬'); + b.write(s[i]); + } + return b.toString(); +} + +/// حبابِ موجودی (سکه/بلیط) در سربرگِ فروشگاه — براق و برجسته. +class _BalancePill extends StatelessWidget { + final IconData icon; + final Color iconColor; + final String value; + const _BalancePill({ + required this.icon, + required this.iconColor, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF17345C), Color(0xFF0C2848)], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.goldDark), + boxShadow: const [ + BoxShadow(color: Colors.black45, blurRadius: 6, offset: Offset(0, 2)), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: iconColor, size: 18), + const SizedBox(width: 6), + Text( + value, + style: const TextStyle( + color: AppColors.gold, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + ); + } +} + // ===== ویجت‌های کارت آیتم ===== class _ItemCard extends StatelessWidget { @@ -614,19 +717,8 @@ class _ItemCard extends StatelessWidget { @override Widget build(BuildContext context) { final card = Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [AppColors.lapisHi, AppColors.lapisLo], - ), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.gold, width: 1.6), - boxShadow: const [ - BoxShadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 4)), - ], - ), + padding: const EdgeInsets.all(10), + decoration: _shopCardDeco(highlight: ribbon != null), child: Column( children: [ Text( @@ -636,39 +728,51 @@ class _ItemCard extends StatelessWidget { style: const TextStyle( color: AppColors.gold, fontWeight: FontWeight.bold, + fontSize: 14, ), ), - const SizedBox(height: 6), + const SizedBox(height: 8), Expanded(child: art ?? _GlowArt(color: glowColor, icon: icon)), - const SizedBox(height: 4), + const SizedBox(height: 6), Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Colors.white70, fontSize: 11), + style: const TextStyle(color: Colors.white70, fontSize: 11.5), ), - const SizedBox(height: 6), + const SizedBox(height: 8), SizedBox(width: double.infinity, child: action), ], ), ); if (ribbon == null) return card; + // ریبونِ گوشه: درصدِ هدیه سبز، «بهترین/ویژه» طلایی — جلبِ‌توجه. + final isBonus = ribbon!.contains('٪') || ribbon!.contains('%'); return Stack( clipBehavior: Clip.none, children: [ card, Positioned( - top: 8, - left: -22, + top: 10, + left: -24, child: Transform.rotate( angle: -0.7, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 3), - color: AppColors.green, + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isBonus + ? const [AppColors.success, AppColors.successDark] + : const [Color(0xFFFFD54F), AppColors.goldDark], + ), + boxShadow: const [ + BoxShadow(color: Colors.black54, blurRadius: 4), + ], + ), child: Text( ribbon!, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: isBonus ? Colors.white : AppColors.bg, fontSize: 11, fontWeight: FontWeight.bold, ), @@ -690,14 +794,30 @@ class _GlowArt extends StatelessWidget { return Container( width: double.infinity, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(12), gradient: RadialGradient( - colors: [color, Colors.black.withValues(alpha: 0.85)], - radius: 0.9, + center: const Alignment(-0.15, -0.35), + radius: 1.05, + colors: [ + Color.lerp(color, Colors.white, 0.15)!, + color, + Colors.black.withValues(alpha: 0.9), + ], + stops: const [0.0, 0.45, 1.0], + ), + border: Border.all(color: AppColors.goldFaint), + ), + child: Center( + child: Icon( + icon, + size: 46, + color: AppColors.gold, + shadows: [ + Shadow(color: color.withValues(alpha: 0.9), blurRadius: 18), + const Shadow(color: Colors.black87, blurRadius: 6), + ], ), - border: Border.all(color: Colors.black26), ), - child: Center(child: Icon(icon, size: 44, color: AppColors.gold)), ); } } @@ -770,7 +890,7 @@ class _CardSkinArt extends StatelessWidget { } } -class _PriceButton extends StatelessWidget { +class _PriceButton extends StatefulWidget { final String label; final VoidCallback? onTap; final bool green; @@ -784,59 +904,82 @@ class _PriceButton extends StatelessWidget { this.disabled = false, }); + @override + State<_PriceButton> createState() => _PriceButtonState(); +} + +class _PriceButtonState extends State<_PriceButton> { + bool _down = false; + @override Widget build(BuildContext context) { final busy = context.select((ShopBloc c) => c.state.busy); - final colors = - disabled - ? const [Color(0xFF555555), Color(0xFF333333)] - : green + final enabled = !(widget.disabled || busy) && widget.onTap != null; + final colors = widget.disabled + ? const [Color(0xFF555555), Color(0xFF333333)] + : widget.green ? const [AppColors.success, AppColors.successDark] : const [ - AppColors.accent, - Color(0xFF7A1019), - ]; // شنگرفِ ایرانی (هم‌رنگِ تم) - return Opacity( - opacity: disabled ? 0.85 : 1, - child: GestureDetector( - onTap: (disabled || busy) ? null : onTap, - child: Container( - height: 36, - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 6), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: colors, + AppColors.accent, + Color(0xFF7A1019), + ]; // شنگرفِ ایرانی (هم‌رنگِ تم) + return GestureDetector( + onTapDown: enabled ? (_) => setState(() => _down = true) : null, + onTapUp: enabled ? (_) => setState(() => _down = false) : null, + onTapCancel: enabled ? () => setState(() => _down = false) : null, + onTap: enabled ? widget.onTap : null, + child: AnimatedScale( + scale: _down ? 0.94 : 1, + duration: const Duration(milliseconds: 90), + child: Opacity( + opacity: widget.disabled ? 0.85 : 1, + child: Container( + height: 38, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: colors, + ), + borderRadius: BorderRadius.circular(11), + border: Border.all(color: AppColors.gold, width: 1.2), + boxShadow: enabled + ? [ + BoxShadow( + color: colors.last.withValues(alpha: 0.55), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ] + : null, ), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.gold, width: 1.2), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (coin) ...[ - const Icon( - Icons.monetization_on, - color: AppColors.gold, - size: 16, - ), - const SizedBox(width: 4), - ], - Flexible( - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.coin) ...[ + const Icon( + Icons.monetization_on, + color: AppColors.gold, + size: 16, + ), + const SizedBox(width: 4), + ], + Flexible( + child: Text( + widget.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), ), ), - ), - ], + ], + ), ), ), ),