feat: profile and subs

This commit is contained in:
2026-06-17 11:32:18 +03:30
parent 943ba13872
commit e9f294fa19
19 changed files with 1762 additions and 34 deletions
+91 -6
View File
@@ -6,12 +6,52 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/network/ws_client.dart';
import 'game_models.dart';
/// یک بازیکن در اتاق انتظارِ میز خصوصی.
class LobbyPlayer {
final String name;
final bool host;
const LobbyPlayer(this.name, this.host);
}
/// وضعیت اتاق انتظارِ میز خصوصی (دورهمی).
class TableLobby {
final String code;
final List<LobbyPlayer> players;
final bool isHost;
final int remaining;
final bool unlimited;
const TableLobby({
required this.code,
required this.players,
required this.isHost,
required this.remaining,
required this.unlimited,
});
factory TableLobby.fromJson(Map<String, dynamic> j) => TableLobby(
code: (j['code'] ?? '') as String,
players: ((j['players'] as List?) ?? [])
.map((e) => LobbyPlayer(
(e['name'] ?? '') as String, (e['host'] ?? false) as bool))
.toList(),
isHost: (j['host'] ?? false) as bool,
remaining: (j['remaining'] ?? 0) as int,
unlimited: (j['unlimited'] ?? false) as bool,
);
String get sig => '$code|$isHost|$remaining|$unlimited|'
'${players.map((p) => '${p.name}${p.host ? '*' : ''}').join(',')}';
}
class GameUiState extends Equatable {
final WsStatus connection;
final GameState? state;
final HandResult? handResult; // اوورلی نتیجه‌ی هَند (گذرا)
final GameOver? gameOver; // اوورلی پایان بازی
final String? notice; // پیام گذرا (خطا/خروج بازیکن)
final TableLobby? lobby; // اتاق انتظارِ میز خصوصی (پیش از شروع)
final int? countdown; // شمارش معکوس پیش از شروعِ بازیِ خصوصی
final bool tableClosed; // میز خصوصی منحل شد (میزبان خارج شد)
const GameUiState({
this.connection = WsStatus.connecting,
@@ -19,6 +59,9 @@ class GameUiState extends Equatable {
this.handResult,
this.gameOver,
this.notice,
this.lobby,
this.countdown,
this.tableClosed = false,
});
GameUiState copyWith({
@@ -27,6 +70,9 @@ class GameUiState extends Equatable {
HandResult? handResult,
GameOver? gameOver,
String? notice,
TableLobby? lobby,
int? countdown,
bool? tableClosed,
bool clearHandResult = false,
bool clearNotice = false,
}) =>
@@ -36,30 +82,59 @@ class GameUiState extends Equatable {
handResult: clearHandResult ? null : (handResult ?? this.handResult),
gameOver: gameOver ?? this.gameOver,
notice: clearNotice ? null : (notice ?? this.notice),
lobby: lobby ?? this.lobby,
countdown: countdown ?? this.countdown,
tableClosed: tableClosed ?? this.tableClosed,
);
@override
List<Object?> get props => [connection, state, handResult, gameOver, notice];
List<Object?> get props => [
connection,
state,
handResult,
gameOver,
notice,
lobby?.sig,
countdown,
tableClosed,
];
}
class GameCubit extends Cubit<GameUiState> {
final WsClient _ws;
final String tier;
/// اقدامِ ورود پس از اتصال (یک‌بار). پیش‌فرض: ورود به صفِ عمومی.
/// برای میز خصوصی: {'type':'create_table'} یا {'type':'join_table','code':...}.
final Map<String, dynamic> _joinAction;
bool _joined = false;
late final StreamSubscription _msgSub;
late final StreamSubscription _statusSub;
GameCubit(this._ws, this.tier) : super(const GameUiState()) {
GameCubit(this._ws, this.tier, {Map<String, dynamic>? joinAction})
: _joinAction = joinAction ?? {'type': 'join_queue', 'tier': tier},
super(const GameUiState()) {
_msgSub = _ws.messages.listen(_onMessage);
_statusSub = _ws.status.listen(_onStatus);
_ws.connect();
}
/// سازنده‌ی میز خصوصی: ساختِ میز جدید.
GameCubit.createPrivate(WsClient ws)
: this(ws, 'private', joinAction: {'type': 'create_table'});
/// سازنده‌ی میز خصوصی: پیوستن با کد.
GameCubit.joinPrivate(WsClient ws, String code)
: this(ws, 'private', joinAction: {'type': 'join_table', 'code': code});
void _onStatus(WsStatus s) {
emit(state.copyWith(connection: s));
// پس از برقراری اتصال، درخواست ورود به صف؛ در صورت reconnect سرور خودش
// بازیکن را به میز برمی‌گرداند (این پیام را نادیده می‌گیرد).
if (s == WsStatus.connected) {
_ws.send({'type': 'join_queue', 'tier': tier});
// اقدامِ ورود فقط یک‌بار در اولین اتصال؛ در reconnect سرور خودش بازیکن را
// به میز برمی‌گرداند (نباید دوباره create/join فرستاده شود).
if (s == WsStatus.connected && !_joined) {
_joined = true;
_ws.send(_joinAction);
}
}
@@ -74,6 +149,12 @@ class GameCubit extends Cubit<GameUiState> {
emit(state.copyWith(handResult: HandResult.fromJson(msg)));
case 'game_over':
emit(state.copyWith(gameOver: GameOver.fromJson(msg)));
case 'table_lobby':
emit(state.copyWith(lobby: TableLobby.fromJson(msg)));
case 'countdown':
emit(state.copyWith(countdown: (msg['seconds'] ?? 3) as int));
case 'table_closed':
emit(state.copyWith(tableClosed: true, notice: 'میز توسط میزبان بسته شد'));
case 'player_disconnected':
emit(state.copyWith(notice: 'یک بازیکن قطع شد'));
case 'player_reconnected':
@@ -91,6 +172,10 @@ class GameCubit extends Cubit<GameUiState> {
void leave() => _ws.send({'type': 'leave'});
void startTable() => _ws.send({'type': 'start_table'});
void leaveTable() => _ws.send({'type': 'leave_table'});
void clearNotice() => emit(state.copyWith(clearNotice: true));
@override