feat: add characters

This commit is contained in:
2026-07-10 00:56:58 +03:30
parent 7c067d36f2
commit 97d156c492
28 changed files with 486 additions and 94 deletions
+2
View File
@@ -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/presentation/bloc/leaderboard_bloc.dart';
import '../../feature/shop/data/data_source/remote/shop_api_provider.dart';
import '../../feature/shop/data/data_source/remote/avatar_api_provider.dart';
import '../../feature/shop/data/data_source/remote/carpet_api_provider.dart';
import '../../feature/shop/data/data_source/remote/frame_api_provider.dart';
import '../../feature/tournament/data/data_source/remote/tournament_api_provider.dart';
@@ -71,6 +72,7 @@ Future<void> setupLocator() async {
locator.registerSingleton<ShopApiProvider>(ShopApiProvider());
locator.registerSingleton<CarpetApiProvider>(CarpetApiProvider());
locator.registerSingleton<FrameApiProvider>(FrameApiProvider());
locator.registerSingleton<AvatarApiProvider>(AvatarApiProvider());
locator.registerSingleton<ProfileApiProvider>(ProfileApiProvider());
locator.registerSingleton<RankedApiProvider>(RankedApiProvider());
locator.registerSingleton<ChatApiProvider>(ChatApiProvider());
+6 -6
View File
@@ -57,17 +57,17 @@ class CardImages {
}
}
/// تصویرِ فرش (برای سطحِ میز) از backend: `/carpets/<id>.jpg`. کش‌شده.
static Future<Sprite?> carpet(String id) {
return _cache.putIfAbsent('carpet/$id', () async {
/// تصویرِ فرش (برای سطحِ میز) از URL کاملِ backend. کش‌شده با کلیدِ URL.
static Future<Sprite?> carpetByUrl(String url) {
if (url.isEmpty) return Future.value(null);
return _cache.putIfAbsent('carpet/$url', () async {
try {
final res = await _dio
.get<List<int>>('${AppConfig.baseUrl}/carpets/$id.jpg');
final res = await _dio.get<List<int>>(url);
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');
debugPrint('CardImages: carpet $url failed: $e');
return null;
}
});
+51
View File
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:random_avatar/random_avatar.dart';
/// آواتارِ یک بازیکن در کلِ بازی: اگر شخصیتِ آواتاری خریده و انتخاب کرده باشد
/// ([imageUrl] یک URL کاملِ svg/png از سرور) همان را نشان می‌دهد؛ وگرنه به آواتارِ
/// تصادفیِ پیش‌فرض بر اساسِ [seed] برمی‌گردد.
///
/// URLِ تصویر همیشه از backend می‌آید (URL-safe)؛ کلاینت آن را نمی‌سازد. یک منبعِ
/// واحد برای رندرِ آواتار در سرِ میز، پروفایل، لابی و فروشگاه (svg با flutter_svg،
/// png/jpg با Image.network).
class PlayerAvatar extends StatelessWidget {
final String seed; // seed آواتارِ تصادفی (fallback)
final String imageUrl; // URL کاملِ تصویرِ شخصیت (از سرور) — خالی ⇒ تصادفی
final double? size; // اگر null، فضای موجود را پر می‌کند
const PlayerAvatar({
super.key,
required this.seed,
this.imageUrl = '',
this.size,
});
bool get _hasCharacter => imageUrl.isNotEmpty;
@override
Widget build(BuildContext context) {
final Widget child;
if (_hasCharacter) {
child =
imageUrl.toLowerCase().endsWith('.svg')
? SvgPicture.network(
imageUrl,
fit: BoxFit.cover,
placeholderBuilder: (_) => _fallback(),
)
: Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _fallback(),
);
} else {
child = _fallback();
}
final clipped = ClipOval(child: SizedBox.expand(child: child));
if (size == null) return clipped;
return SizedBox(width: size, height: size, child: clipped);
}
Widget _fallback() => RandomAvatar(seed.isEmpty ? 'hakem-1' : seed);
}