157 lines
6.1 KiB
Dart
157 lines
6.1 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flame/components.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import '../config.dart';
|
|
|
|
/// تصاویرِ کارت از backend میآیند (نه bundle داخل اپ) تا حجمِ اپ کم بماند، اما هر
|
|
/// تصویر فقط **یکبار برای همیشه** دانلود میشود: بایتها روی دیسکِ ماندگار ذخیره و
|
|
/// در جلسههای بعد از دیسک خوانده میشوند (بدونِ شبکه). یک کشِ درونحافظهایِ
|
|
/// همگام ([_ready]) هم نگه میداریم تا کارتها بدونِ پرش/انتظار رندر شوند.
|
|
///
|
|
/// مسیرِ سرور: `/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,
|
|
));
|
|
|
|
// اسپرایتِ آماده و decodeشده (کلید: "<skin>/<file>") — دسترسیِ همگام برای رندرِ فوری.
|
|
static final Map<String, Sprite> _ready = {};
|
|
// Futureهای در حالِ اجرا/تمامشده برای جلوگیری از بارگذاریِ تکراری.
|
|
static final Map<String, Future<Sprite?>> _inflight = {};
|
|
static Future<String>? _dir;
|
|
|
|
static String _url(String skin, String file) =>
|
|
'${AppConfig.baseUrl}/cards/$skin/$file';
|
|
|
|
// ===== دسترسیِ همگام (اگر از قبل در حافظه آماده باشد) =====
|
|
|
|
/// اسپرایتِ رویِ کارت اگر همین حالا در حافظه آماده باشد، وگرنه null.
|
|
static Sprite? cachedFront(String skin, String code) => _ready['$skin/$code.png'];
|
|
|
|
/// اسپرایتِ پشتِ کارت اگر در حافظه آماده باشد، وگرنه null.
|
|
static Sprite? cachedBack(String skin) => _ready['$skin/back.jpg'];
|
|
|
|
// ===== بارگذاریِ ناهمگام (دیسک → شبکه) =====
|
|
|
|
/// اسپرایتِ رویِ یک کارت (مثل "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');
|
|
|
|
/// کلِ دسته (۵۲ کارت + پشت) را گرم میکند: از دیسک (فوری) یا در اولین بار از شبکه.
|
|
/// fire-and-forget؛ در طولِ صفحهی «جستجوی حریف» اجرا میشود تا هنگامِ پخش آماده باشند.
|
|
static void preloadDeck(String skin) {
|
|
const ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
|
|
const suits = ['S', 'H', 'D', 'C'];
|
|
for (final s in suits) {
|
|
for (final r in ranks) {
|
|
_load(skin, '$r$s.png');
|
|
}
|
|
}
|
|
_load(skin, 'back.jpg');
|
|
}
|
|
|
|
static Future<Sprite?> _load(String skin, String file) {
|
|
final key = '$skin/$file';
|
|
final ready = _ready[key];
|
|
if (ready != null) return Future.value(ready);
|
|
return _inflight.putIfAbsent(key, () => _fetch(skin, file));
|
|
}
|
|
|
|
static Future<Sprite?> _fetch(String skin, String file) async {
|
|
var s = await _fromDiskOrNet(skin, file);
|
|
if (s == null && skin != _fallbackSkin) {
|
|
s = await _fromDiskOrNet(_fallbackSkin, file);
|
|
}
|
|
if (s != null) _ready['$skin/$file'] = s;
|
|
return s;
|
|
}
|
|
|
|
static Future<Sprite?> _fromDiskOrNet(String skin, String file) async {
|
|
// ۱) دیسکِ ماندگار (اگر قبلاً دانلود شده).
|
|
final disk = await _readDisk(skin, file);
|
|
if (disk != null) {
|
|
final sp = await _toSprite(disk);
|
|
if (sp != null) return sp;
|
|
}
|
|
// ۲) شبکه — سپس ذخیره روی دیسک برای دفعهی بعد.
|
|
try {
|
|
final res = await _dio.get<List<int>>(_url(skin, file));
|
|
final data = res.data;
|
|
if (data == null || data.isEmpty) return null;
|
|
final bytes = Uint8List.fromList(data);
|
|
unawaited(_writeDisk(skin, file, bytes));
|
|
return _toSprite(bytes);
|
|
} catch (e) {
|
|
debugPrint('CardImages: $skin/$file failed: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// تصویرِ فرش از URL کاملِ backend (کششده در حافظه با کلیدِ URL).
|
|
static Future<Sprite?> carpetByUrl(String url) {
|
|
if (url.isEmpty) return Future.value(null);
|
|
final key = 'carpet/$url';
|
|
final ready = _ready[key];
|
|
if (ready != null) return Future.value(ready);
|
|
return _inflight.putIfAbsent(key, () async {
|
|
try {
|
|
final res = await _dio.get<List<int>>(url);
|
|
final data = res.data;
|
|
if (data == null || data.isEmpty) return null;
|
|
final sp = await _toSprite(Uint8List.fromList(data));
|
|
if (sp != null) _ready[key] = sp;
|
|
return sp;
|
|
} catch (e) {
|
|
debugPrint('CardImages: carpet $url failed: $e');
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== دیسک =====
|
|
|
|
static Future<String> _cacheDir() =>
|
|
_dir ??= getApplicationSupportDirectory().then((d) => '${d.path}/hakem_cards');
|
|
|
|
static Future<Uint8List?> _readDisk(String skin, String file) async {
|
|
try {
|
|
final f = File('${await _cacheDir()}/$skin/$file');
|
|
if (await f.exists()) return f.readAsBytes();
|
|
} catch (_) {}
|
|
return null;
|
|
}
|
|
|
|
static Future<void> _writeDisk(String skin, String file, Uint8List bytes) async {
|
|
try {
|
|
final f = File('${await _cacheDir()}/$skin/$file');
|
|
await f.parent.create(recursive: true);
|
|
await f.writeAsBytes(bytes, flush: false);
|
|
} catch (_) {}
|
|
}
|
|
|
|
static Future<Sprite?> _toSprite(Uint8List bytes) async {
|
|
try {
|
|
final codec = await ui.instantiateImageCodec(bytes);
|
|
final frame = await codec.getNextFrame();
|
|
return Sprite(frame.image);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|