feat: refactor code

This commit is contained in:
2026-06-17 12:57:28 +03:30
parent e9f294fa19
commit 519752b478
106 changed files with 3459 additions and 2309 deletions
@@ -0,0 +1,96 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/network/ws_client.dart';
import '../../domain/entities/game_entities.dart';
import '../../domain/repository/game_repository.dart';
import 'game_event.dart';
import 'game_state.dart';
/// بلوکِ realtime بازی: به استریمِ پیام‌ها/وضعیتِ repository گوش می‌دهد و
/// اقدام‌های بازیکن را به سرور می‌فرستد. متدهای کمکی برای موتور Flame هم دارد.
class GameBloc extends Bloc<GameEvent, GameUiState> {
final GameRepository repository;
late final StreamSubscription _msgSub;
late final StreamSubscription _statusSub;
Map<String, dynamic> _joinAction = const {};
bool _joined = false;
GameBloc(this.repository) : super(const GameUiState()) {
_msgSub =
repository.messages.listen((m) => add(GameMessageReceived(m)));
_statusSub =
repository.status.listen((s) => add(GameStatusChanged(s)));
on<ConnectGameEvent>((event, emit) async {
_joinAction = event.joinAction;
_joined = false;
await repository.connect();
});
on<GameStatusChanged>((event, emit) {
emit(state.copyWith(connection: event.status));
if (event.status == WsStatus.connected && !_joined) {
_joined = true;
repository.send(_joinAction);
}
});
on<GameMessageReceived>((event, emit) => _onMessage(event.message, emit));
on<ChooseTrumpEvent>(
(event, emit) => repository.send({'type': 'choose_trump', 'suit': event.suit}));
on<PlayCardEvent>(
(event, emit) => repository.send({'type': 'play_card', 'card': event.card}));
on<LeaveGameEvent>((event, emit) => repository.send({'type': 'leave'}));
on<StartTableEvent>((event, emit) => repository.send({'type': 'start_table'}));
on<LeaveTableEvent>((event, emit) => repository.send({'type': 'leave_table'}));
on<ClearNoticeEvent>((event, emit) => emit(state.copyWith(clearNotice: true)));
}
void _onMessage(Map<String, dynamic> msg, Emitter<GameUiState> emit) {
switch (msg['type']) {
case 'state':
final gs = GameState.fromJson(msg);
final clear = gs.phase == 'choose_trump' || gs.phase == 'playing';
emit(state.copyWith(state: gs, clearHandResult: clear));
case 'hand_over':
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':
emit(state.copyWith(notice: 'بازیکن بازگشت'));
case 'player_left':
emit(state.copyWith(notice: 'یک بازیکن میز را ترک کرد'));
case 'error':
emit(state.copyWith(notice: (msg['message'] ?? 'خطا').toString()));
}
}
// --- متدهای کمکی برای موتور Flame و صفحه‌ها ---
void chooseTrump(String suit) => add(ChooseTrumpEvent(suit));
void playCard(String card) => add(PlayCardEvent(card));
void leave() => add(LeaveGameEvent());
void startTable() => add(StartTableEvent());
void leaveTable() => add(LeaveTableEvent());
void clearNotice() => add(ClearNoticeEvent());
@override
Future<void> close() {
_msgSub.cancel();
_statusSub.cancel();
repository.disconnect();
return super.close();
}
}
@@ -0,0 +1,39 @@
import '../../../../core/network/ws_client.dart';
abstract class GameEvent {}
/// شروع اتصال با اقدامِ ورود (join_queue / create_table / join_table).
class ConnectGameEvent extends GameEvent {
final Map<String, dynamic> joinAction;
ConnectGameEvent(this.joinAction);
}
/// پیام دریافتی از سرور (داخلی).
class GameMessageReceived extends GameEvent {
final Map<String, dynamic> message;
GameMessageReceived(this.message);
}
/// تغییر وضعیت اتصال (داخلی).
class GameStatusChanged extends GameEvent {
final WsStatus status;
GameStatusChanged(this.status);
}
class ChooseTrumpEvent extends GameEvent {
final String suit;
ChooseTrumpEvent(this.suit);
}
class PlayCardEvent extends GameEvent {
final String card;
PlayCardEvent(this.card);
}
class LeaveGameEvent extends GameEvent {}
class StartTableEvent extends GameEvent {}
class LeaveTableEvent extends GameEvent {}
class ClearNoticeEvent extends GameEvent {}
@@ -0,0 +1,98 @@
import 'package:equatable/equatable.dart';
import '../../../../core/network/ws_client.dart';
import '../../domain/entities/game_entities.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,
this.state,
this.handResult,
this.gameOver,
this.notice,
this.lobby,
this.countdown,
this.tableClosed = false,
});
GameUiState copyWith({
WsStatus? connection,
GameState? state,
HandResult? handResult,
GameOver? gameOver,
String? notice,
TableLobby? lobby,
int? countdown,
bool? tableClosed,
bool clearHandResult = false,
bool clearNotice = false,
}) =>
GameUiState(
connection: connection ?? this.connection,
state: state ?? this.state,
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,
lobby?.sig,
countdown,
tableClosed,
];
}
@@ -0,0 +1,41 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/resources/data_state.dart';
import '../../../../core/usecase/use_case.dart';
import '../../domain/entities/table_entities.dart';
import '../../domain/use_cases/get_tables_info_usecase.dart';
abstract class PrivateInfoEvent {}
class LoadTablesInfoEvent extends PrivateInfoEvent {}
abstract class PrivateInfoState {}
class PrivateInfoInitial extends PrivateInfoState {}
class PrivateInfoLoading extends PrivateInfoState {}
class PrivateInfoLoaded extends PrivateInfoState {
final TablesInfo info;
PrivateInfoLoaded(this.info);
}
class PrivateInfoError extends PrivateInfoState {
final String message;
PrivateInfoError(this.message);
}
class PrivateInfoBloc extends Bloc<PrivateInfoEvent, PrivateInfoState> {
final GetTablesInfoUseCase getTablesInfoUseCase;
PrivateInfoBloc(this.getTablesInfoUseCase) : super(PrivateInfoInitial()) {
on<LoadTablesInfoEvent>((event, emit) async {
emit(PrivateInfoLoading());
final res = await getTablesInfoUseCase(const NoParams());
if (res is DataSuccess) {
emit(PrivateInfoLoaded(res.data!));
} else {
emit(PrivateInfoError(res.error!));
}
});
}
}
@@ -0,0 +1,41 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/resources/data_state.dart';
import '../../../../core/usecase/use_case.dart';
import '../../domain/entities/table_entities.dart';
import '../../domain/use_cases/get_tiers_usecase.dart';
abstract class TierEvent {}
class LoadTiersEvent extends TierEvent {}
abstract class TierState {}
class TierInitial extends TierState {}
class TierLoading extends TierState {}
class TierLoaded extends TierState {
final List<TableTier> tiers;
TierLoaded(this.tiers);
}
class TierError extends TierState {
final String message;
TierError(this.message);
}
class TierBloc extends Bloc<TierEvent, TierState> {
final GetTiersUseCase getTiersUseCase;
TierBloc(this.getTiersUseCase) : super(TierInitial()) {
on<LoadTiersEvent>((event, emit) async {
emit(TierLoading());
final res = await getTiersUseCase(const NoParams());
if (res is DataSuccess) {
emit(TierLoaded(res.data!));
} else {
emit(TierError(res.error!));
}
});
}
}