feat: get cards from backend

This commit is contained in:
2026-06-24 08:35:31 +03:30
parent d837aca96e
commit 108a972761
83 changed files with 593 additions and 38 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ class AppConfig {
/// با تغییر شبکه/IP مک، مقدار dart-define یا همین پیش‌فرض را عوض کنید.
static const String baseUrl = String.fromEnvironment(
'BASE_URL',
defaultValue: 'http://192.168.1.105:8080',
defaultValue: 'http://192.168.1.104:8080',
);
static String get apiUrl => '$baseUrl/api';
+10
View File
@@ -24,6 +24,11 @@ import '../../feature/profile/domain/repository/profile_repository.dart';
import '../../feature/profile/domain/use_cases/get_profile_usecase.dart';
import '../../feature/profile/domain/use_cases/save_profile_usecase.dart';
import '../../feature/profile/presentation/bloc/profile_bloc.dart';
import '../../feature/ranked/data/data_source/remote/ranked_api_provider.dart';
import '../../feature/ranked/data/repository/ranked_repository_impl.dart';
import '../../feature/ranked/domain/repository/ranked_repository.dart';
import '../../feature/ranked/domain/use_cases/get_leaderboard_usecase.dart';
import '../../feature/ranked/presentation/bloc/leaderboard_bloc.dart';
import '../../feature/shop/data/data_source/remote/shop_api_provider.dart';
import '../../feature/shop/data/repository/shop_repository_impl.dart';
import '../../feature/shop/domain/repository/shop_repository.dart';
@@ -56,6 +61,7 @@ Future<void> setupLocator() async {
locator.registerSingleton<WalletApiProvider>(WalletApiProvider());
locator.registerSingleton<ShopApiProvider>(ShopApiProvider());
locator.registerSingleton<ProfileApiProvider>(ProfileApiProvider());
locator.registerSingleton<RankedApiProvider>(RankedApiProvider());
locator.registerSingleton<GameApiProvider>(GameApiProvider());
locator.registerSingleton<GameWsProvider>(GameWsProvider(locator()));
@@ -67,6 +73,7 @@ Future<void> setupLocator() async {
locator.registerSingleton<ShopRepository>(ShopRepositoryImpl(locator()));
locator.registerSingleton<ProfileRepository>(
ProfileRepositoryImpl(locator()));
locator.registerSingleton<RankedRepository>(RankedRepositoryImpl(locator()));
locator.registerSingleton<GameRepository>(
GameRepositoryImpl(locator(), locator()));
@@ -85,6 +92,8 @@ Future<void> setupLocator() async {
locator.registerSingleton<AdRewardUseCase>(AdRewardUseCase(locator()));
locator.registerSingleton<GetProfileUseCase>(GetProfileUseCase(locator()));
locator.registerSingleton<SaveProfileUseCase>(SaveProfileUseCase(locator()));
locator.registerSingleton<GetLeaderboardUseCase>(
GetLeaderboardUseCase(locator()));
locator.registerSingleton<GetTiersUseCase>(GetTiersUseCase(locator()));
locator.registerSingleton<GetTablesInfoUseCase>(
GetTablesInfoUseCase(locator()));
@@ -97,6 +106,7 @@ Future<void> setupLocator() async {
ShopBloc(locator(), locator(), locator(), locator(), locator()));
locator.registerFactory<ProfileBloc>(
() => ProfileBloc(locator(), locator()));
locator.registerFactory<LeaderboardBloc>(() => LeaderboardBloc(locator()));
locator.registerFactory<GameBloc>(() => GameBloc(locator()));
locator.registerFactory<TierBloc>(() => TierBloc(locator()));
locator.registerFactory<PrivateInfoBloc>(() => PrivateInfoBloc(locator()));
+65
View File
@@ -0,0 +1,65 @@
import 'dart:ui' as ui;
import 'package:dio/dio.dart';
import 'package:flame/components.dart';
import 'package:flutter/foundation.dart';
import '../config.dart';
/// بارگذاریِ تصاویرِ کارت از backend (نه bundle داخل اپ) تا حجم اپ کم بماند.
/// هر اسکین یک پوشه روی سرور دارد: `/cards/<skin>/<code>.png` و `/cards/<skin>/back.jpg`.
/// تصاویرِ دانلودشده در حافظه کش می‌شوند تا هر کارت فقط یک‌بار از شبکه بیاید.
class CardImages {
CardImages._();
static const _fallbackSkin = 'simple';
static final Dio _dio = Dio(BaseOptions(
responseType: ResponseType.bytes,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 12),
validateStatus: (s) => s != null && s >= 200 && s < 300,
));
static final Map<String, Future<Sprite?>> _cache = {};
static String _url(String skin, String file) =>
'${AppConfig.baseUrl}/cards/$skin/$file';
/// اسپرایتِ رویِ یک کارت (مثل "AS"). در صورت خطا/۴۰۴ به اسکینِ پیش‌فرض و
/// سپس به `null` (کشیدنِ برداری در CardComponent) برمی‌گردد.
static Future<Sprite?> front(String skin, String code) =>
_load(skin, '$code.png');
/// اسپرایتِ پشتِ کارت برای اسکین داده‌شده.
static Future<Sprite?> back(String skin) => _load(skin, 'back.jpg');
static Future<Sprite?> _load(String skin, String file) {
final key = '$skin/$file';
return _cache.putIfAbsent(key, () => _fetch(skin, file));
}
static Future<Sprite?> _fetch(String skin, String file) async {
final sprite = await _fetchFrom(skin, file);
if (sprite != null) return sprite;
if (skin != _fallbackSkin) return _fetchFrom(_fallbackSkin, file);
return null;
}
static Future<Sprite?> _fetchFrom(String skin, String file) async {
try {
final res = await _dio.get<List<int>>(_url(skin, file));
final data = res.data;
if (data == null || data.isEmpty) return null;
final image = await _decode(Uint8List.fromList(data));
return Sprite(image);
} catch (e) {
debugPrint('CardImages: $skin/$file failed: $e');
return null;
}
}
static Future<ui.Image> _decode(Uint8List bytes) async {
final codec = await ui.instantiateImageCodec(bytes);
final frame = await codec.getNextFrame();
return frame.image;
}
}
+50
View File
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
/// نشانِ رتبه (برنز/نقره/طلا/الماس/پادشاه) با رنگ و برچسبِ فارسی.
class RankBadge extends StatelessWidget {
final String tier; // bronze..king
final int? points; // در صورت نمایش امتیاز
final double size;
const RankBadge({super.key, required this.tier, this.points, this.size = 16});
static const _tiers = {
'bronze': ('برنز', [Color(0xFFCD7F32), Color(0xFF8C5A2B)]),
'silver': ('نقره', [Color(0xFFBFC6CC), Color(0xFF8A949B)]),
'gold': ('طلا', [Color(0xFFFFD54F), Color(0xFFB8860B)]),
'diamond': ('الماس', [Color(0xFF5BE0E6), Color(0xFF1E8A99)]),
'king': ('پادشاه', [Color(0xFFB388FF), Color(0xFF6A1B9A)]),
};
@override
Widget build(BuildContext context) {
final t = _tiers[tier] ?? _tiers['bronze']!;
final label = t.$1;
final colors = t.$2;
return Container(
padding: EdgeInsets.symmetric(horizontal: size * 0.6, vertical: size * 0.25),
decoration: BoxDecoration(
gradient: LinearGradient(colors: colors),
borderRadius: BorderRadius.circular(size),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 4)],
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Image.asset(
'assets/images/badge/$tier.png',
width: size * 1.5,
height: size * 1.5,
errorBuilder: (_, __, ___) =>
Icon(Icons.military_tech, color: Colors.white, size: size * 1.1),
),
SizedBox(width: size * 0.3),
Text(
points == null ? label : '$label · $points',
style: TextStyle(
color: Colors.white,
fontSize: size * 0.85,
fontWeight: FontWeight.bold,
),
),
]),
);
}
}