feat: add carpet
This commit is contained in:
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"type": "dart",
|
"type": "dart",
|
||||||
"program": "lib/main.dart",
|
"program": "lib/main.dart",
|
||||||
"args": [
|
"args": [
|
||||||
"--dart-define=BASE_URL=http://192.168.8.210:8080",
|
"--dart-define=BASE_URL=http://192.168.1.105:8080",
|
||||||
"--target-platform=android-arm64"
|
"--target-platform=android-arm64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class AppConfig {
|
|||||||
/// با تغییر شبکه/IP مک، مقدار dart-define یا همین پیشفرض را عوض کنید.
|
/// با تغییر شبکه/IP مک، مقدار dart-define یا همین پیشفرض را عوض کنید.
|
||||||
static const String baseUrl = String.fromEnvironment(
|
static const String baseUrl = String.fromEnvironment(
|
||||||
'BASE_URL',
|
'BASE_URL',
|
||||||
defaultValue: 'http://192.168.8.210:8080',
|
defaultValue: 'http://192.168.1.105:8080',
|
||||||
);
|
);
|
||||||
|
|
||||||
static String get apiUrl => '$baseUrl/api';
|
static String get apiUrl => '$baseUrl/api';
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import '../../feature/ranked/domain/repository/ranked_repository.dart';
|
|||||||
import '../../feature/ranked/domain/use_cases/get_leaderboard_usecase.dart';
|
import '../../feature/ranked/domain/use_cases/get_leaderboard_usecase.dart';
|
||||||
import '../../feature/ranked/presentation/bloc/leaderboard_bloc.dart';
|
import '../../feature/ranked/presentation/bloc/leaderboard_bloc.dart';
|
||||||
import '../../feature/shop/data/data_source/remote/shop_api_provider.dart';
|
import '../../feature/shop/data/data_source/remote/shop_api_provider.dart';
|
||||||
|
import '../../feature/shop/data/data_source/remote/carpet_api_provider.dart';
|
||||||
import '../../feature/shop/data/repository/shop_repository_impl.dart';
|
import '../../feature/shop/data/repository/shop_repository_impl.dart';
|
||||||
import '../../feature/shop/domain/repository/shop_repository.dart';
|
import '../../feature/shop/domain/repository/shop_repository.dart';
|
||||||
import '../../feature/shop/domain/use_cases/ad_reward_usecase.dart';
|
import '../../feature/shop/domain/use_cases/ad_reward_usecase.dart';
|
||||||
@@ -61,6 +62,7 @@ Future<void> setupLocator() async {
|
|||||||
locator.registerSingleton<AuthApiProvider>(AuthApiProvider());
|
locator.registerSingleton<AuthApiProvider>(AuthApiProvider());
|
||||||
locator.registerSingleton<WalletApiProvider>(WalletApiProvider());
|
locator.registerSingleton<WalletApiProvider>(WalletApiProvider());
|
||||||
locator.registerSingleton<ShopApiProvider>(ShopApiProvider());
|
locator.registerSingleton<ShopApiProvider>(ShopApiProvider());
|
||||||
|
locator.registerSingleton<CarpetApiProvider>(CarpetApiProvider());
|
||||||
locator.registerSingleton<ProfileApiProvider>(ProfileApiProvider());
|
locator.registerSingleton<ProfileApiProvider>(ProfileApiProvider());
|
||||||
locator.registerSingleton<RankedApiProvider>(RankedApiProvider());
|
locator.registerSingleton<RankedApiProvider>(RankedApiProvider());
|
||||||
locator.registerSingleton<ChatApiProvider>(ChatApiProvider());
|
locator.registerSingleton<ChatApiProvider>(ChatApiProvider());
|
||||||
|
|||||||
@@ -57,6 +57,22 @@ class CardImages {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// تصویرِ فرش (برای سطحِ میز) از backend: `/carpets/<id>.jpg`. کششده.
|
||||||
|
static Future<Sprite?> carpet(String id) {
|
||||||
|
return _cache.putIfAbsent('carpet/$id', () async {
|
||||||
|
try {
|
||||||
|
final res = await _dio
|
||||||
|
.get<List<int>>('${AppConfig.baseUrl}/carpets/$id.jpg');
|
||||||
|
final data = res.data;
|
||||||
|
if (data == null || data.isEmpty) return null;
|
||||||
|
return Sprite(await _decode(Uint8List.fromList(data)));
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('CardImages: carpet/$id failed: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static Future<ui.Image> _decode(Uint8List bytes) async {
|
static Future<ui.Image> _decode(Uint8List bytes) async {
|
||||||
final codec = await ui.instantiateImageCodec(bytes);
|
final codec = await ui.instantiateImageCodec(bytes);
|
||||||
final frame = await codec.getNextFrame();
|
final frame = await codec.getNextFrame();
|
||||||
|
|||||||
@@ -53,11 +53,17 @@ class AppTheme {
|
|||||||
final base = ThemeData.dark(useMaterial3: true);
|
final base = ThemeData.dark(useMaterial3: true);
|
||||||
return base.copyWith(
|
return base.copyWith(
|
||||||
scaffoldBackgroundColor: AppColors.bg,
|
scaffoldBackgroundColor: AppColors.bg,
|
||||||
primaryColor: AppColors.accent,
|
primaryColor: AppColors.lapis,
|
||||||
|
canvasColor: AppColors.bg,
|
||||||
|
cardColor: AppColors.panel,
|
||||||
|
dividerColor: AppColors.goldFaint,
|
||||||
colorScheme: base.colorScheme.copyWith(
|
colorScheme: base.colorScheme.copyWith(
|
||||||
primary: AppColors.gold,
|
primary: AppColors.gold,
|
||||||
secondary: AppColors.accent,
|
onPrimary: AppColors.lapisInk,
|
||||||
|
secondary: AppColors.lapis,
|
||||||
|
tertiary: AppColors.accent,
|
||||||
surface: AppColors.panel,
|
surface: AppColors.panel,
|
||||||
|
onSurface: AppColors.text,
|
||||||
),
|
),
|
||||||
textTheme: base.textTheme.apply(
|
textTheme: base.textTheme.apply(
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
@@ -69,6 +75,36 @@ class AppTheme {
|
|||||||
foregroundColor: AppColors.gold,
|
foregroundColor: AppColors.gold,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
|
// دیالوگ/منو/اسنکبار با همان قابِ لاجورد + طلا.
|
||||||
|
dialogTheme: DialogThemeData(
|
||||||
|
backgroundColor: AppColors.panel,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
side: const BorderSide(color: AppColors.gold, width: 1.5),
|
||||||
|
),
|
||||||
|
titleTextStyle: const TextStyle(
|
||||||
|
fontFamily: fontFamily,
|
||||||
|
color: AppColors.gold,
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
|
contentTextStyle:
|
||||||
|
const TextStyle(fontFamily: fontFamily, color: AppColors.text),
|
||||||
|
),
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
backgroundColor: AppColors.lapisMid,
|
||||||
|
contentTextStyle: TextStyle(fontFamily: fontFamily, color: AppColors.text),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
tabBarTheme: const TabBarThemeData(
|
||||||
|
labelColor: AppColors.gold,
|
||||||
|
unselectedLabelColor: Colors.white60,
|
||||||
|
indicatorColor: AppColors.gold,
|
||||||
|
),
|
||||||
|
bottomSheetTheme: const BottomSheetThemeData(
|
||||||
|
backgroundColor: AppColors.panel,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
),
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.bgDark.withValues(alpha: 0.6),
|
fillColor: AppColors.bgDark.withValues(alpha: 0.6),
|
||||||
|
|||||||
@@ -1,8 +1,41 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../service/app_sounds.dart';
|
import '../service/app_sounds.dart';
|
||||||
import '../theme/app_theme.dart';
|
import '../theme/app_theme.dart';
|
||||||
|
|
||||||
|
/// دکمهی بازگشتِ سراسری: قابِ طلایی هماهنگ با تمِ بازی. اگر [onTap] داده نشود،
|
||||||
|
/// بهصورت پیشفرض یک صفحه به عقب میرود (context.pop()).
|
||||||
|
class AppBackButton extends StatelessWidget {
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final double size;
|
||||||
|
const AppBackButton({super.key, this.onTap, this.size = 46});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
AppSounds.button();
|
||||||
|
if (onTap != null) {
|
||||||
|
onTap!();
|
||||||
|
} else {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.panel,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: AppColors.gold, width: 1.5),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.arrow_back, color: AppColors.gold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// مجموعهی ویجتهای گرافیکیِ مشترک برای ظاهرِ بازیِ (Unity-style):
|
/// مجموعهی ویجتهای گرافیکیِ مشترک برای ظاهرِ بازیِ (Unity-style):
|
||||||
/// پسزمینهی گرادیانی، پنل براق، دکمهی جواهرگون، و عنوانِ درخشان.
|
/// پسزمینهی گرادیانی، پنل براق، دکمهی جواهرگون، و عنوانِ درخشان.
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ class _ChatPanelState extends State<ChatPanel> {
|
|||||||
_future = _api.getChatPacks();
|
_future = _api.getChatPacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _reload() => setState(() => _future = _api.getChatPacks());
|
void _reload() => setState(() {
|
||||||
|
_future = _api.getChatPacks();
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> _buy(ChatPack p) async {
|
Future<void> _buy(ChatPack p) async {
|
||||||
setState(() => _busyPack = p.id);
|
setState(() => _busyPack = p.id);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class GameScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _GameScreenState extends State<GameScreen> {
|
class _GameScreenState extends State<GameScreen> {
|
||||||
late final HokmGame _game;
|
late final HokmGame _game;
|
||||||
|
String _carpet = 'classic'; // فرشِ انتخابی (برای پسزمینهی پشتِ صحنه)
|
||||||
Timer? _introTimer;
|
Timer? _introTimer;
|
||||||
bool _introHidden = false;
|
bool _introHidden = false;
|
||||||
int _countdown = 0; // شمارشِ معکوسِ شروعِ بازی پس از یافتنِ حریفان (۳…۲…۱)
|
int _countdown = 0; // شمارشِ معکوسِ شروعِ بازی پس از یافتنِ حریفان (۳…۲…۱)
|
||||||
@@ -40,9 +41,11 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
final ws = context.read<WalletBloc>().state.walletStatus;
|
final ws = context.read<WalletBloc>().state.walletStatus;
|
||||||
final skin = ws is WalletLoaded ? ws.wallet.selectedCard : 'simple';
|
final skin = ws is WalletLoaded ? ws.wallet.selectedCard : 'simple';
|
||||||
|
_carpet = ws is WalletLoaded ? ws.wallet.selectedCarpet : 'classic';
|
||||||
_game = HokmGame(
|
_game = HokmGame(
|
||||||
context.read<GameBloc>(),
|
context.read<GameBloc>(),
|
||||||
skin: skin.isEmpty ? 'simple' : skin,
|
skin: skin.isEmpty ? 'simple' : skin,
|
||||||
|
carpet: _carpet.isEmpty ? 'classic' : _carpet,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +117,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
}
|
}
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
// فرش بهعنوانِ سطحِ میز داخلِ صحنهی Flame (Felt) کشیده میشود.
|
||||||
GameWidget(game: _game),
|
GameWidget(game: _game),
|
||||||
if (!_showSearch(state) && state.state != null)
|
if (!_showSearch(state) && state.state != null)
|
||||||
TableHud(
|
TableHud(
|
||||||
|
|||||||
@@ -65,25 +65,11 @@ class _PrivateEntryScreenState extends State<PrivateEntryScreen> {
|
|||||||
final canCreate = unlimited || remaining > 0;
|
final canCreate = unlimited || remaining > 0;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
const Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: EdgeInsets.all(10),
|
||||||
child: GestureDetector(
|
child: AppBackButton(),
|
||||||
onTap: () => context.pop(),
|
|
||||||
child: Container(
|
|
||||||
width: 46,
|
|
||||||
height: 46,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.panel,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border:
|
|
||||||
Border.all(color: AppColors.gold, width: 1.5),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.arrow_back,
|
|
||||||
color: AppColors.gold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -20,18 +20,22 @@ class PrivateTableScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocConsumer<GameBloc, GameUiState>(
|
return BlocConsumer<GameBloc, GameUiState>(
|
||||||
listenWhen: (a, b) =>
|
listenWhen:
|
||||||
(a.notice != b.notice && b.notice != null) ||
|
(a, b) =>
|
||||||
(!a.tableClosed && b.tableClosed),
|
(a.notice != b.notice && b.notice != null) ||
|
||||||
|
(!a.tableClosed && b.tableClosed),
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.tableClosed) {
|
if (state.tableClosed) {
|
||||||
if (context.canPop()) context.pop();
|
if (context.canPop()) context.pop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.notice != null) {
|
if (state.notice != null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
content: Text(state.notice!),
|
content: Text(state.notice!),
|
||||||
duration: const Duration(seconds: 2)));
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
context.read<GameBloc>().clearNotice();
|
context.read<GameBloc>().clearNotice();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -81,11 +85,14 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
children: [
|
children: [
|
||||||
_topBar(context, lobby),
|
_topBar(context, lobby),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: connecting
|
child:
|
||||||
? const Center(
|
connecting
|
||||||
child: CircularProgressIndicator(
|
? const Center(
|
||||||
color: AppColors.gold))
|
child: CircularProgressIndicator(
|
||||||
: _content(context, lobby),
|
color: AppColors.gold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _content(context, lobby),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -102,18 +109,18 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
// نوار بالا: بازگشت (چپ) و اشتراکگذاریِ کد (راست).
|
// نوار بالا: بازگشت (چپ) و اشتراکگذاریِ کد (راست).
|
||||||
Widget _topBar(BuildContext context, TableLobby? lobby) {
|
Widget _topBar(BuildContext context, TableLobby? lobby) {
|
||||||
Widget btn(IconData icon, VoidCallback onTap) => GestureDetector(
|
Widget btn(IconData icon, VoidCallback onTap) => GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 46,
|
width: 46,
|
||||||
height: 46,
|
height: 46,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.panel,
|
color: AppColors.panel,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: AppColors.gold, width: 1.5),
|
border: Border.all(color: AppColors.gold, width: 1.5),
|
||||||
),
|
),
|
||||||
child: Icon(icon, color: AppColors.gold),
|
child: Icon(icon, color: AppColors.gold),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: Directionality(
|
child: Directionality(
|
||||||
@@ -121,13 +128,16 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
btn(Icons.arrow_back, _leave),
|
|
||||||
if (lobby != null)
|
if (lobby != null)
|
||||||
btn(Icons.share, () {
|
btn(Icons.share, () {
|
||||||
Clipboard.setData(ClipboardData(text: lobby.code));
|
Clipboard.setData(ClipboardData(text: lobby.code));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text('کد میز کپی شد؛ برای دوستانت بفرست')));
|
const SnackBar(
|
||||||
|
content: Text('کد میز کپی شد؛ برای دوستانت بفرست'),
|
||||||
|
),
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
btn(Icons.arrow_forward, _leave),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -160,7 +170,9 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 28, vertical: 36),
|
horizontal: 28,
|
||||||
|
vertical: 36,
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const RadialGradient(
|
gradient: const RadialGradient(
|
||||||
@@ -168,29 +180,42 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
radius: 0.9,
|
radius: 0.9,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(160),
|
borderRadius: BorderRadius.circular(160),
|
||||||
border: Border.all(color: const Color(0xFF8A5A12), width: 6),
|
border: Border.all(
|
||||||
),
|
color: const Color(0xFF8A5A12),
|
||||||
child: const Center(
|
width: 6,
|
||||||
child: GlowText('سلطان حکم', size: 18),
|
),
|
||||||
),
|
),
|
||||||
|
child: const Center(child: GlowText('سلطان حکم', size: 18)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Align(alignment: Alignment.topCenter, child: _seat(top, false)),
|
Align(alignment: Alignment.topCenter, child: _seat(top, false)),
|
||||||
Align(alignment: Alignment.centerLeft, child: _seat(left, false)),
|
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight, child: _seat(right, false)),
|
alignment: Alignment.centerLeft,
|
||||||
Align(alignment: Alignment.bottomCenter, child: _seat(me, true)),
|
child: _seat(left, false),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: _seat(right, false),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _seat(me, true),
|
||||||
|
),
|
||||||
// چرخشِ جایگاهها (فقط میزبان) — تغییرِ تیمبندی.
|
// چرخشِ جایگاهها (فقط میزبان) — تغییرِ تیمبندی.
|
||||||
if (lobby.isHost) ...[
|
if (lobby.isHost) ...[
|
||||||
Align(
|
Align(
|
||||||
alignment: const Alignment(-1, -0.35),
|
alignment: const Alignment(-1, -0.35),
|
||||||
child: _rotateBtn(Icons.rotate_left,
|
child: _rotateBtn(
|
||||||
() => context.read<GameBloc>().rotateTable(-1)),
|
Icons.rotate_left,
|
||||||
|
() => context.read<GameBloc>().rotateTable(-1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: const Alignment(1, -0.35),
|
alignment: const Alignment(1, -0.35),
|
||||||
child: _rotateBtn(Icons.rotate_right,
|
child: _rotateBtn(
|
||||||
() => context.read<GameBloc>().rotateTable(1)),
|
Icons.rotate_right,
|
||||||
|
() => context.read<GameBloc>().rotateTable(1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -215,12 +240,16 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
onTap: () => context.read<GameBloc>().startTable(_hands),
|
onTap: () => context.read<GameBloc>().startTable(_hands),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const Text('در انتظار شروع توسط میزبان…',
|
const Text(
|
||||||
style: TextStyle(color: AppColors.gold, fontSize: 15)),
|
'در انتظار شروع توسط میزبان…',
|
||||||
|
style: TextStyle(color: AppColors.gold, fontSize: 15),
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
if (lobby.isHost)
|
if (lobby.isHost)
|
||||||
const Text('جایهای خالی با ربات پر میشوند',
|
const Text(
|
||||||
style: TextStyle(color: Colors.white54, fontSize: 12)),
|
'جایهای خالی با ربات پر میشوند',
|
||||||
|
style: TextStyle(color: Colors.white54, fontSize: 12),
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -244,15 +273,20 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
color: AppColors.bgDark,
|
color: AppColors.bgDark,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isYou ? AppColors.gold : AppColors.goldDark,
|
color: isYou ? AppColors.gold : AppColors.goldDark,
|
||||||
width: 2),
|
width: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: empty
|
child:
|
||||||
? const Icon(Icons.person_add_alt_1, color: Colors.white24)
|
empty
|
||||||
: ClipRRect(
|
? const Icon(
|
||||||
borderRadius: BorderRadius.circular(9),
|
Icons.person_add_alt_1,
|
||||||
child: RandomAvatar(p.name),
|
color: Colors.white24,
|
||||||
),
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(9),
|
||||||
|
child: RandomAvatar(p.name),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (!empty)
|
if (!empty)
|
||||||
const Positioned(
|
const Positioned(
|
||||||
@@ -274,8 +308,10 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (p?.host == true)
|
if (p?.host == true)
|
||||||
const Text('میزبان',
|
const Text(
|
||||||
style: TextStyle(color: AppColors.gold, fontSize: 10)),
|
'میزبان',
|
||||||
|
style: TextStyle(color: AppColors.gold, fontSize: 10),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -314,14 +350,17 @@ class _LobbyViewState extends State<_LobbyView> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
gradient: selected
|
gradient:
|
||||||
? const LinearGradient(
|
selected
|
||||||
colors: [AppColors.gold, AppColors.goldDark])
|
? const LinearGradient(
|
||||||
: null,
|
colors: [AppColors.gold, AppColors.goldDark],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
color: selected ? null : AppColors.panel,
|
color: selected ? null : AppColors.panel,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: selected ? AppColors.gold : AppColors.goldDark,
|
color: selected ? AppColors.gold : AppColors.goldDark,
|
||||||
width: 2),
|
width: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
fa[h]!,
|
fa[h]!,
|
||||||
@@ -377,8 +416,9 @@ class _CountdownOverlayState extends State<_CountdownOverlay> {
|
|||||||
tween: Tween(begin: 0.4, end: 1.2),
|
tween: Tween(begin: 0.4, end: 1.2),
|
||||||
duration: const Duration(milliseconds: 700),
|
duration: const Duration(milliseconds: 700),
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
builder: (context, scale, child) =>
|
builder:
|
||||||
Transform.scale(scale: scale, child: child),
|
(context, scale, child) =>
|
||||||
|
Transform.scale(scale: scale, child: child),
|
||||||
child: GlowText(_n > 0 ? '$_n' : 'شروع!', size: 96),
|
child: GlowText(_n > 0 ? '$_n' : 'شروع!', size: 96),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class TierListScreen extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
_BackButton(onTap: () => context.pop()),
|
const AppBackButton(),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const GlowText('انتخاب میز', size: 26),
|
const GlowText('انتخاب میز', size: 26),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@@ -64,25 +64,6 @@ class TierListScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BackButton extends StatelessWidget {
|
|
||||||
final VoidCallback onTap;
|
|
||||||
const _BackButton({required this.onTap});
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => GestureDetector(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.panel,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: AppColors.gold, width: 1.5),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.arrow_back, color: AppColors.gold),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TierCard extends StatelessWidget {
|
class _TierCard extends StatelessWidget {
|
||||||
final TableTier tier;
|
final TableTier tier;
|
||||||
final int index;
|
final int index;
|
||||||
|
|||||||
@@ -69,14 +69,18 @@ class HokmGame extends FlameGame {
|
|||||||
double _hbAccum = 0; // انباشتِ زمان برای تکرارِ منظمِ ضربان تا پایانِ نوبت
|
double _hbAccum = 0; // انباشتِ زمان برای تکرارِ منظمِ ضربان تا پایانِ نوبت
|
||||||
bool _wasMyTurn = false;
|
bool _wasMyTurn = false;
|
||||||
|
|
||||||
HokmGame(this.cubit, {this.skin = 'simple'});
|
/// فرشِ انتخابی؛ 'classic' یعنی سطحِ میزِ ترسیمی. فرشِ تصویری بهعنوانِ سطحِ
|
||||||
|
/// میز (بریده در شکلِ میز) روی زمینِ لاجوردی کشیده میشود.
|
||||||
|
final String carpet;
|
||||||
|
|
||||||
|
HokmGame(this.cubit, {this.skin = 'simple', this.carpet = 'classic'});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color backgroundColor() => AppColors.lapisNight;
|
Color backgroundColor() => AppColors.lapisNight;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
add(Felt());
|
add(Felt(carpet: carpet));
|
||||||
_sub = cubit.stream.listen((ui) {
|
_sub = cubit.stream.listen((ui) {
|
||||||
if (ui.state == null) return;
|
if (ui.state == null) return;
|
||||||
final s = ui.state!;
|
final s = ui.state!;
|
||||||
|
|||||||
@@ -3,22 +3,39 @@ import 'dart:math' as math;
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../../core/network/card_images.dart';
|
||||||
import '../../../../../core/theme/app_theme.dart';
|
import '../../../../../core/theme/app_theme.dart';
|
||||||
|
|
||||||
// رنگهای مشترکِ میز (منبعِ واحد در AppColors).
|
// رنگهای مشترکِ میز (منبعِ واحد در AppColors).
|
||||||
const _gold = AppColors.gold;
|
const _gold = AppColors.gold;
|
||||||
const _goldDark = AppColors.goldDark;
|
const _goldDark = AppColors.goldDark;
|
||||||
|
|
||||||
/// میز: زمینِ سبزِ نمدی در کلِ صفحه + یک «میزِ» مرکزیِ سرمهای با قابِ طلاییِ
|
/// میز: زمینِ لاجوردیِ نمدی در کلِ صفحه + یک «میزِ» مرکزی با قابِ طلاییِ دولایه.
|
||||||
/// دولایه (مطابقِ تصویرِ مرجع). کارتهای زمین داخلِ ناحیهی سرمهای انداخته میشوند
|
/// اگر فرشی انتخاب شده باشد، سطحِ میز تصویرِ فرش است (بریدهشده در شکلِ میز)؛
|
||||||
/// و پشتکارتِ حریفان روی نمدِ سبزِ دورِ آن مینشیند.
|
/// وگرنه سطحِ سرمهایِ ترسیمی. کارتها داخلِ همین ناحیه انداخته میشوند.
|
||||||
class Felt extends PositionComponent with HasGameReference {
|
class Felt extends PositionComponent with HasGameReference {
|
||||||
|
/// شناسهی فرشِ انتخابی؛ 'classic' یعنی سطحِ ترسیمی (بدون تصویر).
|
||||||
|
final String carpet;
|
||||||
|
Felt({this.carpet = 'classic'});
|
||||||
|
|
||||||
|
Sprite? _carpetSprite;
|
||||||
|
|
||||||
|
bool get _hasCarpet => carpet != 'classic';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
if (_hasCarpet) {
|
||||||
|
_carpetSprite = await CardImages.carpet(carpet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
final w = game.size.x, h = game.size.y;
|
final w = game.size.x, h = game.size.y;
|
||||||
final center = Offset(w / 2, h / 2);
|
final center = Offset(w / 2, h / 2);
|
||||||
|
|
||||||
// ۱) زمینِ لاجوردیِ نمدی (کلِ صفحه) با وینیتِ شعاعی برای عمق.
|
// ۱) زمینِ لاجوردیِ نمدی (کلِ صفحه) با وینیتِ شعاعی برای عمق. همیشه کشیده
|
||||||
|
// میشود تا زیرِ میز/فرش یکدست باشد و لبههای فرش با پسزمینه جور شود.
|
||||||
canvas.drawRect(
|
canvas.drawRect(
|
||||||
Rect.fromLTWH(0, 0, w, h),
|
Rect.fromLTWH(0, 0, w, h),
|
||||||
Paint()
|
Paint()
|
||||||
@@ -30,7 +47,7 @@ class Felt extends PositionComponent with HasGameReference {
|
|||||||
).createShader(Rect.fromLTWH(0, 0, w, h)),
|
).createShader(Rect.fromLTWH(0, 0, w, h)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ۲) میزِ مرکزیِ سرمهای با قابِ طلایی.
|
// ۲) میزِ مرکزی با قابِ طلایی.
|
||||||
final rect = Rect.fromCenter(
|
final rect = Rect.fromCenter(
|
||||||
center: center, width: w * 0.72, height: h * 0.46);
|
center: center, width: w * 0.72, height: h * 0.46);
|
||||||
final radius = Radius.circular(h * 0.05);
|
final radius = Radius.circular(h * 0.05);
|
||||||
@@ -55,17 +72,32 @@ class Felt extends PositionComponent with HasGameReference {
|
|||||||
).createShader(rect),
|
).createShader(rect),
|
||||||
);
|
);
|
||||||
|
|
||||||
// سطحِ سرمهایِ میز با گرادیانِ شعاعی (مرکز روشنتر).
|
// سطحِ میز: تصویرِ فرش (بریده در شکلِ میز) یا سطحِ سرمهایِ ترسیمی.
|
||||||
canvas.drawRRect(
|
final sprite = _carpetSprite;
|
||||||
table,
|
if (_hasCarpet && sprite != null) {
|
||||||
Paint()
|
canvas.save();
|
||||||
..shader = RadialGradient(
|
canvas.clipRRect(table);
|
||||||
center: Alignment.center,
|
// فرش را بهصورتِ cover داخلِ مستطیلِ میز میکشیم.
|
||||||
radius: 0.9,
|
sprite.render(
|
||||||
colors: const [AppColors.lapisLo, AppColors.lapisInk],
|
canvas,
|
||||||
stops: const [0.4, 1.0],
|
position: Vector2(rect.left, rect.top),
|
||||||
).createShader(rect),
|
size: Vector2(rect.width, rect.height),
|
||||||
);
|
);
|
||||||
|
// تیرهترِ ملایم برای خوانایی کارتها روی فرش.
|
||||||
|
canvas.drawRRect(table, Paint()..color = const Color(0x1A000000));
|
||||||
|
canvas.restore();
|
||||||
|
} else {
|
||||||
|
canvas.drawRRect(
|
||||||
|
table,
|
||||||
|
Paint()
|
||||||
|
..shader = RadialGradient(
|
||||||
|
center: Alignment.center,
|
||||||
|
radius: 0.9,
|
||||||
|
colors: const [AppColors.lapisLo, AppColors.lapisInk],
|
||||||
|
stops: const [0.4, 1.0],
|
||||||
|
).createShader(rect),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// خطِ طلاییِ نازکِ داخلی (قابِ دولایه).
|
// خطِ طلاییِ نازکِ داخلی (قابِ دولایه).
|
||||||
canvas.drawRRect(
|
canvas.drawRRect(
|
||||||
|
|||||||
@@ -92,10 +92,7 @@ class ProfileScreen extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
const AppBackButton(),
|
||||||
onPressed: () => context.pop(),
|
|
||||||
icon: const Icon(Icons.arrow_back, color: AppColors.gold),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const GlowText('پروفایل', size: 24),
|
const GlowText('پروفایل', size: 24),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:random_avatar/random_avatar.dart';
|
import 'package:random_avatar/random_avatar.dart';
|
||||||
|
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
@@ -21,10 +20,7 @@ class LeaderboardScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
IconButton(
|
const AppBackButton(),
|
||||||
onPressed: () => context.pop(),
|
|
||||||
icon: const Icon(Icons.arrow_back, color: AppColors.gold),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const GlowText('رتبهبندی فصل', size: 22),
|
const GlowText('رتبهبندی فصل', size: 22),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import '../../../../../core/locator/locator.dart';
|
||||||
|
import '../../../../../core/network/api_provider_imp.dart';
|
||||||
|
import '../../../domain/entities/carpet.dart';
|
||||||
|
|
||||||
|
/// دادهٔ فرشها از backend (دریافت، خرید، انتخاب).
|
||||||
|
class CarpetApiProvider {
|
||||||
|
ApiProviderImp get _api => locator<ApiProviderImp>();
|
||||||
|
|
||||||
|
Future<CarpetCatalog> getCarpets() async {
|
||||||
|
final res = await _api.get('/carpets');
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception('carpets: ${res.statusCode}');
|
||||||
|
}
|
||||||
|
final list = (res.data['carpets'] as List?) ?? const [];
|
||||||
|
return CarpetCatalog(
|
||||||
|
carpets: list
|
||||||
|
.map((e) => Carpet.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList(),
|
||||||
|
selected: (res.data['selected'] ?? 'classic') as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// خرید؛ null یعنی موفق، وگرنه پیامِ خطای فارسی.
|
||||||
|
Future<String?> buyCarpet(String id) =>
|
||||||
|
_post('/shop/buy-carpet', id);
|
||||||
|
|
||||||
|
/// انتخاب؛ null یعنی موفق، وگرنه پیامِ خطای فارسی.
|
||||||
|
Future<String?> selectCarpet(String id) =>
|
||||||
|
_post('/shop/select-carpet', id);
|
||||||
|
|
||||||
|
Future<String?> _post(String path, String id) async {
|
||||||
|
final res = await _api.post(path, body: {'carpet_id': id});
|
||||||
|
if (res.statusCode == 200) return null;
|
||||||
|
switch (res.statusCode) {
|
||||||
|
case 402:
|
||||||
|
return 'سکهٔ کافی ندارید';
|
||||||
|
case 403:
|
||||||
|
return 'این فرش را ندارید';
|
||||||
|
case 409:
|
||||||
|
return 'این فرش را دارید';
|
||||||
|
default:
|
||||||
|
return 'عملیات ناموفق بود';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import '../../../../core/config.dart';
|
||||||
|
|
||||||
|
/// یک فرشِ ایرانی که بهجای میزِ بازی استفاده میشود. تصویر از backend میآید.
|
||||||
|
class Carpet {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final int priceCoins;
|
||||||
|
final bool vip; // برای VIP رایگان
|
||||||
|
final bool owned;
|
||||||
|
|
||||||
|
const Carpet({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.priceCoins,
|
||||||
|
required this.vip,
|
||||||
|
required this.owned,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 'classic' یعنی میزِ ترسیمیِ پیشفرض (بدون تصویر).
|
||||||
|
bool get isClassic => id == 'classic';
|
||||||
|
|
||||||
|
String get imageUrl => '${AppConfig.baseUrl}/carpets/$id.jpg';
|
||||||
|
|
||||||
|
factory Carpet.fromJson(Map<String, dynamic> j) => Carpet(
|
||||||
|
id: (j['id'] ?? '') as String,
|
||||||
|
title: (j['title'] ?? '') as String,
|
||||||
|
priceCoins: (j['price_coins'] ?? 0) as int,
|
||||||
|
vip: (j['vip'] ?? false) as bool,
|
||||||
|
owned: (j['owned'] ?? false) as bool,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// نتیجهی دریافتِ فرشها: فهرست + شناسهی فرشِ انتخابی.
|
||||||
|
class CarpetCatalog {
|
||||||
|
final List<Carpet> carpets;
|
||||||
|
final String selected;
|
||||||
|
const CarpetCatalog({required this.carpets, required this.selected});
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../../../core/config.dart';
|
import '../../../../core/config.dart';
|
||||||
|
import '../../../../core/locator/locator.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../core/widgets/game_ui.dart';
|
import '../../../../core/widgets/game_ui.dart';
|
||||||
import '../../../wallet/presentation/bloc/wallet_bloc.dart';
|
import '../../../wallet/presentation/bloc/wallet_bloc.dart';
|
||||||
import '../../../wallet/presentation/bloc/wallet_event.dart';
|
import '../../../wallet/presentation/bloc/wallet_event.dart';
|
||||||
import '../../../wallet/presentation/bloc/wallet_state.dart';
|
import '../../../wallet/presentation/bloc/wallet_state.dart';
|
||||||
import '../../../wallet/presentation/bloc/wallet_status.dart';
|
import '../../../wallet/presentation/bloc/wallet_status.dart';
|
||||||
|
import '../../data/data_source/remote/carpet_api_provider.dart';
|
||||||
|
import '../../domain/entities/carpet.dart';
|
||||||
import '../../domain/entities/shop_entities.dart';
|
import '../../domain/entities/shop_entities.dart';
|
||||||
import '../bloc/shop_bloc.dart';
|
import '../bloc/shop_bloc.dart';
|
||||||
import '../bloc/shop_event.dart';
|
import '../bloc/shop_event.dart';
|
||||||
@@ -22,7 +24,7 @@ class ShopScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 5,
|
length: 6,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
body: GameBackground(
|
body: GameBackground(
|
||||||
@@ -85,6 +87,7 @@ class ShopScreen extends StatelessWidget {
|
|||||||
_CoinsTab(packages: d.coinPackages),
|
_CoinsTab(packages: d.coinPackages),
|
||||||
_TicketsTab(packages: d.ticketPackages),
|
_TicketsTab(packages: d.ticketPackages),
|
||||||
_CardsTab(data: d),
|
_CardsTab(data: d),
|
||||||
|
const _CarpetsTab(),
|
||||||
_BoostersTab(boosters: d.boosters),
|
_BoostersTab(boosters: d.boosters),
|
||||||
_VipTab(packages: d.vipPackages),
|
_VipTab(packages: d.vipPackages),
|
||||||
],
|
],
|
||||||
@@ -114,19 +117,7 @@ class ShopScreen extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
const AppBackButton(),
|
||||||
onTap: () => context.pop(),
|
|
||||||
child: Container(
|
|
||||||
width: 46,
|
|
||||||
height: 46,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.panel,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: AppColors.gold, width: 1.5),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.arrow_back, color: AppColors.gold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
BlocBuilder<WalletBloc, WalletBlocState>(
|
BlocBuilder<WalletBloc, WalletBlocState>(
|
||||||
builder: (context, s) {
|
builder: (context, s) {
|
||||||
@@ -164,7 +155,7 @@ class _ShopTabs extends StatelessWidget {
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [Color(0xFFC62828), Color(0xFF7B0E14)],
|
colors: [AppColors.panel, AppColors.lapis],
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
|
||||||
),
|
),
|
||||||
@@ -179,6 +170,7 @@ class _ShopTabs extends StatelessWidget {
|
|||||||
Tab(text: 'سکه'),
|
Tab(text: 'سکه'),
|
||||||
Tab(text: 'بلیط'),
|
Tab(text: 'بلیط'),
|
||||||
Tab(text: 'کارت'),
|
Tab(text: 'کارت'),
|
||||||
|
Tab(text: 'فرش'),
|
||||||
Tab(text: 'تجهیزات'),
|
Tab(text: 'تجهیزات'),
|
||||||
Tab(text: 'VIP'),
|
Tab(text: 'VIP'),
|
||||||
],
|
],
|
||||||
@@ -349,6 +341,179 @@ class _CardsTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// تبِ فرشها: پیشنمایشِ تصویرِ هر فرش از backend با خرید (سکه/VIP) و انتخاب.
|
||||||
|
/// دادهها مستقل از ShopBloc از /api/carpets گرفته میشوند.
|
||||||
|
class _CarpetsTab extends StatefulWidget {
|
||||||
|
const _CarpetsTab();
|
||||||
|
@override
|
||||||
|
State<_CarpetsTab> createState() => _CarpetsTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CarpetsTabState extends State<_CarpetsTab> {
|
||||||
|
final _api = locator<CarpetApiProvider>();
|
||||||
|
late Future<CarpetCatalog> _future;
|
||||||
|
String? _busy;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_future = _api.getCarpets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reload() => setState(() {
|
||||||
|
_future = _api.getCarpets();
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> _act(Carpet c, {required bool buy}) async {
|
||||||
|
setState(() => _busy = c.id);
|
||||||
|
final err = buy ? await _api.buyCarpet(c.id) : await _api.selectCarpet(c.id);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _busy = null);
|
||||||
|
if (err == null) {
|
||||||
|
context.read<WalletBloc>().add(LoadWalletEvent());
|
||||||
|
_reload();
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text(err)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<CarpetCatalog>(
|
||||||
|
future: _future,
|
||||||
|
builder: (context, snap) {
|
||||||
|
if (snap.connectionState != ConnectionState.done) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(color: AppColors.gold));
|
||||||
|
}
|
||||||
|
if (!snap.hasData) {
|
||||||
|
return Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: _reload, child: const Text('تلاش مجدد')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final cat = snap.data!;
|
||||||
|
return _grid(
|
||||||
|
note: 'فرشِ زیرِ میز را عوض کن؛ بعضی فرشها ویژهی VIP هستند.',
|
||||||
|
children: [
|
||||||
|
for (final c in cat.carpets)
|
||||||
|
_CarpetCard(
|
||||||
|
carpet: c,
|
||||||
|
selected: cat.selected == c.id,
|
||||||
|
busy: _busy == c.id,
|
||||||
|
onSelect: () => _act(c, buy: false),
|
||||||
|
onBuy: () => _act(c, buy: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CarpetCard extends StatelessWidget {
|
||||||
|
final Carpet carpet;
|
||||||
|
final bool selected;
|
||||||
|
final bool busy;
|
||||||
|
final VoidCallback onSelect;
|
||||||
|
final VoidCallback onBuy;
|
||||||
|
const _CarpetCard({
|
||||||
|
required this.carpet,
|
||||||
|
required this.selected,
|
||||||
|
required this.busy,
|
||||||
|
required this.onSelect,
|
||||||
|
required this.onBuy,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
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: [Color(0xFF6E1322), Color(0xFF3A0A12)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? AppColors.gold : AppColors.goldDark,
|
||||||
|
width: selected ? 2 : 1.4),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(c.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.gold, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Expanded(child: _preview(c)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SizedBox(width: double.infinity, child: _action(c)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _preview(Carpet c) {
|
||||||
|
final frame = ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: c.isClassic
|
||||||
|
? Container(
|
||||||
|
color: const Color(0xFF0E3614),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.casino, color: AppColors.gold, size: 36)),
|
||||||
|
)
|
||||||
|
: Image.network(
|
||||||
|
c.imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) => Container(
|
||||||
|
color: const Color(0xFF2A0710),
|
||||||
|
child: const Icon(Icons.image_not_supported,
|
||||||
|
color: AppColors.goldDark)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Stack(fit: StackFit.expand, children: [
|
||||||
|
frame,
|
||||||
|
if (c.vip && !c.owned)
|
||||||
|
const Positioned(
|
||||||
|
top: 4,
|
||||||
|
left: 4,
|
||||||
|
child: Chip(
|
||||||
|
label: Text('VIP',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
|
||||||
|
backgroundColor: AppColors.goldDark,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _action(Carpet c) {
|
||||||
|
if (busy) {
|
||||||
|
return const _PriceButton(label: '...', disabled: true);
|
||||||
|
}
|
||||||
|
if (selected) {
|
||||||
|
return const _PriceButton(label: 'انتخاب شده', disabled: true);
|
||||||
|
}
|
||||||
|
if (c.owned) {
|
||||||
|
return _PriceButton(label: 'انتخاب', green: true, onTap: onSelect);
|
||||||
|
}
|
||||||
|
// قفل: VIP رایگان برای VIP (که owned میشود)، وگرنه خرید با سکه.
|
||||||
|
return _PriceButton(
|
||||||
|
label: '${c.priceCoins} سکه',
|
||||||
|
green: true,
|
||||||
|
coin: true,
|
||||||
|
onTap: onBuy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _grid({required String note, required List<Widget> children}) {
|
Widget _grid({required String note, required List<Widget> children}) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -506,13 +671,16 @@ class _CardSkinArt extends StatelessWidget {
|
|||||||
child: Image.network(
|
child: Image.network(
|
||||||
_url(file),
|
_url(file),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
errorBuilder:
|
||||||
color: const Color(0xFF0E3C73),
|
(_, __, ___) => Container(
|
||||||
child: const Icon(Icons.style, color: AppColors.gold),
|
color: const Color(0xFF0E3C73),
|
||||||
),
|
child: const Icon(Icons.style, color: AppColors.gold),
|
||||||
loadingBuilder: (ctx, child, progress) => progress == null
|
),
|
||||||
? child
|
loadingBuilder:
|
||||||
: Container(color: const Color(0xFF0A2547)),
|
(ctx, child, progress) =>
|
||||||
|
progress == null
|
||||||
|
? child
|
||||||
|
: Container(color: const Color(0xFF0A2547)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../core/widgets/game_ui.dart';
|
import '../../../../core/widgets/game_ui.dart';
|
||||||
@@ -60,10 +59,7 @@ class VipScreen extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
IconButton(
|
const AppBackButton(),
|
||||||
onPressed: () => context.pop(),
|
|
||||||
icon: const Icon(Icons.arrow_back, color: AppColors.gold),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const GlowText('اشتراک VIP', size: 24),
|
const GlowText('اشتراک VIP', size: 24),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class WalletModel extends WalletEntity {
|
|||||||
required super.xpForNext,
|
required super.xpForNext,
|
||||||
required super.vip,
|
required super.vip,
|
||||||
required super.selectedCard,
|
required super.selectedCard,
|
||||||
|
required super.selectedCarpet,
|
||||||
required super.name,
|
required super.name,
|
||||||
required super.avatar,
|
required super.avatar,
|
||||||
});
|
});
|
||||||
@@ -32,6 +33,7 @@ class WalletModel extends WalletEntity {
|
|||||||
xpForNext: (wallet['xp_for_next'] ?? 1) as int,
|
xpForNext: (wallet['xp_for_next'] ?? 1) as int,
|
||||||
vip: (wallet['vip'] ?? false) as bool,
|
vip: (wallet['vip'] ?? false) as bool,
|
||||||
selectedCard: (wallet['selected_card'] ?? 'simple') as String,
|
selectedCard: (wallet['selected_card'] ?? 'simple') as String,
|
||||||
|
selectedCarpet: (wallet['selected_carpet'] ?? 'classic') as String,
|
||||||
name: (name == null || name.isEmpty) ? 'بازیکن' : name,
|
name: (name == null || name.isEmpty) ? 'بازیکن' : name,
|
||||||
avatar: (avatar == null || avatar.isEmpty) ? 'hakem-1' : avatar,
|
avatar: (avatar == null || avatar.isEmpty) ? 'hakem-1' : avatar,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class WalletEntity {
|
|||||||
final int xpForNext;
|
final int xpForNext;
|
||||||
final bool vip;
|
final bool vip;
|
||||||
final String selectedCard;
|
final String selectedCard;
|
||||||
|
final String selectedCarpet;
|
||||||
final String name;
|
final String name;
|
||||||
final String avatar;
|
final String avatar;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class WalletEntity {
|
|||||||
required this.xpForNext,
|
required this.xpForNext,
|
||||||
required this.vip,
|
required this.vip,
|
||||||
required this.selectedCard,
|
required this.selectedCard,
|
||||||
|
required this.selectedCarpet,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.avatar,
|
required this.avatar,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user