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,13 @@
import 'package:dio/dio.dart';
import '../../../../../core/locator/locator.dart';
import '../../../../../core/network/api_provider_imp.dart';
/// تماس‌های خامِ HTTP مربوط به کیف‌پول و پاداش روزانه.
class WalletApiProvider {
ApiProviderImp get _api => locator<ApiProviderImp>();
Future<Response> getWallet() => _api.get('/wallet');
Future<Response> getMe() => _api.get('/me');
Future<Response> claimDaily() => _api.post('/rewards/daily');
}
@@ -0,0 +1,39 @@
import '../../domain/entities/wallet_entity.dart';
/// مدلِ کیف‌پول؛ از پاسخِ /wallet و /me ساخته می‌شود.
class WalletModel extends WalletEntity {
const WalletModel({
required super.coins,
required super.tickets,
required super.xp,
required super.trophies,
required super.level,
required super.xpIntoLevel,
required super.xpForNext,
required super.vip,
required super.selectedCard,
required super.name,
required super.avatar,
});
factory WalletModel.fromJson(
Map<String, dynamic> wallet,
Map<String, dynamic> user,
) {
final name = (user['first_name'] as String?)?.trim();
final avatar = (user['avatar'] as String?)?.trim();
return WalletModel(
coins: (wallet['coins'] ?? 0) as int,
tickets: (wallet['tickets'] ?? 0) as int,
xp: (wallet['xp'] ?? 0) as int,
trophies: (wallet['trophies'] ?? 0) as int,
level: (wallet['level'] ?? 1) as int,
xpIntoLevel: (wallet['xp_into_level'] ?? 0) as int,
xpForNext: (wallet['xp_for_next'] ?? 1) as int,
vip: (wallet['vip'] ?? false) as bool,
selectedCard: (wallet['selected_card'] ?? 'simple') as String,
name: (name == null || name.isEmpty) ? 'بازیکن' : name,
avatar: (avatar == null || avatar.isEmpty) ? 'hakem-1' : avatar,
);
}
}
@@ -0,0 +1,38 @@
import 'package:dio/dio.dart';
import '../../../../core/error/custom_error.dart';
import '../../../../core/resources/data_state.dart';
import '../../domain/entities/wallet_entity.dart';
import '../../domain/repository/wallet_repository.dart';
import '../data_source/remote/wallet_api_provider.dart';
import '../model/wallet_model.dart';
class WalletRepositoryImpl extends WalletRepository {
final WalletApiProvider api;
WalletRepositoryImpl(this.api);
@override
Future<DataState<WalletEntity>> getWallet() async {
final results = await Future.wait([api.getWallet(), api.getMe()]);
final Response wallet = results[0];
final Response me = results[1];
if (wallet.statusCode == 200 && me.statusCode == 200) {
return DataSuccess(WalletModel.fromJson(
Map<String, dynamic>.from(wallet.data as Map),
Map<String, dynamic>.from((me.data['user'] ?? {}) as Map),
));
}
return DataError(errorConvertor(wallet.statusCode, null));
}
@override
Future<DataState<int>> claimDaily() async {
final Response res = await api.claimDaily();
if (res.statusCode == 200) {
return DataSuccess((res.data['amount'] ?? 0) as int);
}
final d = res.data;
final msg = (d is Map && d['message'] != null) ? d['message'].toString() : null;
return DataError(errorConvertor(res.statusCode, msg));
}
}
@@ -0,0 +1,28 @@
/// موجودیتِ کیف‌پول و وضعیت اقتصادیِ کاربر (به‌همراه نام و آواتار برای نوار لابی).
class WalletEntity {
final int coins;
final int tickets;
final int xp;
final int trophies;
final int level;
final int xpIntoLevel;
final int xpForNext;
final bool vip;
final String selectedCard;
final String name;
final String avatar;
const WalletEntity({
required this.coins,
required this.tickets,
required this.xp,
required this.trophies,
required this.level,
required this.xpIntoLevel,
required this.xpForNext,
required this.vip,
required this.selectedCard,
required this.name,
required this.avatar,
});
}
@@ -0,0 +1,9 @@
import '../../../../core/resources/data_state.dart';
import '../entities/wallet_entity.dart';
abstract class WalletRepository {
Future<DataState<WalletEntity>> getWallet();
/// دریافت سکه روزانه؛ مقدار دریافتی را برمی‌گرداند.
Future<DataState<int>> claimDaily();
}
@@ -0,0 +1,11 @@
import '../../../../core/resources/data_state.dart';
import '../../../../core/usecase/use_case.dart';
import '../repository/wallet_repository.dart';
class ClaimDailyUseCase implements UseCase<DataState<int>, NoParams> {
final WalletRepository repository;
ClaimDailyUseCase(this.repository);
@override
Future<DataState<int>> call(NoParams params) => repository.claimDaily();
}
@@ -0,0 +1,13 @@
import '../../../../core/resources/data_state.dart';
import '../../../../core/usecase/use_case.dart';
import '../entities/wallet_entity.dart';
import '../repository/wallet_repository.dart';
class GetWalletUseCase implements UseCase<DataState<WalletEntity>, NoParams> {
final WalletRepository repository;
GetWalletUseCase(this.repository);
@override
Future<DataState<WalletEntity>> call(NoParams params) =>
repository.getWallet();
}
@@ -0,0 +1,15 @@
abstract class DailyStatus {}
class DailyInitial extends DailyStatus {}
class DailyLoading extends DailyStatus {}
class DailySuccess extends DailyStatus {
final int amount;
DailySuccess(this.amount);
}
class DailyError extends DailyStatus {
final String message;
DailyError(this.message);
}
@@ -0,0 +1,43 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/resources/data_state.dart';
import '../../../../core/usecase/use_case.dart';
import '../../domain/use_cases/claim_daily_usecase.dart';
import '../../domain/use_cases/get_wallet_usecase.dart';
import 'daily_status.dart';
import 'wallet_event.dart';
import 'wallet_state.dart';
import 'wallet_status.dart';
class WalletBloc extends Bloc<WalletEvent, WalletBlocState> {
final GetWalletUseCase getWalletUseCase;
final ClaimDailyUseCase claimDailyUseCase;
WalletBloc(this.getWalletUseCase, this.claimDailyUseCase)
: super(WalletBlocState.initial()) {
on<LoadWalletEvent>((event, emit) async {
emit(state.copyWith(walletStatus: WalletLoading()));
final res = await getWalletUseCase(const NoParams());
if (res is DataSuccess) {
emit(state.copyWith(walletStatus: WalletLoaded(res.data!)));
} else {
emit(state.copyWith(walletStatus: WalletError(res.error!)));
}
});
on<ClaimDailyEvent>((event, emit) async {
emit(state.copyWith(dailyStatus: DailyLoading()));
final res = await claimDailyUseCase(const NoParams());
if (res is DataSuccess) {
emit(state.copyWith(dailyStatus: DailySuccess(res.data!)));
// پس از دریافت، کیف‌پول به‌روزرسانی شود.
final w = await getWalletUseCase(const NoParams());
if (w is DataSuccess) {
emit(state.copyWith(walletStatus: WalletLoaded(w.data!)));
}
} else {
emit(state.copyWith(dailyStatus: DailyError(res.error!)));
}
});
}
}
@@ -0,0 +1,5 @@
abstract class WalletEvent {}
class LoadWalletEvent extends WalletEvent {}
class ClaimDailyEvent extends WalletEvent {}
@@ -0,0 +1,23 @@
import 'daily_status.dart';
import 'wallet_status.dart';
class WalletBlocState {
final WalletStatus walletStatus;
final DailyStatus dailyStatus;
WalletBlocState({required this.walletStatus, required this.dailyStatus});
factory WalletBlocState.initial() => WalletBlocState(
walletStatus: WalletInitial(),
dailyStatus: DailyInitial(),
);
WalletBlocState copyWith({
WalletStatus? walletStatus,
DailyStatus? dailyStatus,
}) =>
WalletBlocState(
walletStatus: walletStatus ?? this.walletStatus,
dailyStatus: dailyStatus ?? this.dailyStatus,
);
}
@@ -0,0 +1,17 @@
import '../../domain/entities/wallet_entity.dart';
abstract class WalletStatus {}
class WalletInitial extends WalletStatus {}
class WalletLoading extends WalletStatus {}
class WalletLoaded extends WalletStatus {
final WalletEntity wallet;
WalletLoaded(this.wallet);
}
class WalletError extends WalletStatus {
final String message;
WalletError(this.message);
}
@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:random_avatar/random_avatar.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/game_ui.dart';
import '../../../auth/presentation/bloc/auth_bloc.dart';
import '../../../auth/presentation/bloc/auth_event.dart';
import '../../domain/entities/wallet_entity.dart';
import '../bloc/daily_status.dart';
import '../bloc/wallet_bloc.dart';
import '../bloc/wallet_event.dart';
import '../bloc/wallet_state.dart';
import '../bloc/wallet_status.dart';
/// لابی اصلی: کیف‌پول، دکمه بازی/دورهمی/فروشگاه/سکه روزانه.
class LobbyScreen extends StatefulWidget {
const LobbyScreen({super.key});
@override
State<LobbyScreen> createState() => _LobbyScreenState();
}
class _LobbyScreenState extends State<LobbyScreen> {
@override
void initState() {
super.initState();
context.read<WalletBloc>().add(LoadWalletEvent());
}
void _reload() => context.read<WalletBloc>().add(LoadWalletEvent());
@override
Widget build(BuildContext context) {
return Scaffold(
body: GameBackground(
child: BlocConsumer<WalletBloc, WalletBlocState>(
listenWhen: (a, b) => a.dailyStatus != b.dailyStatus,
listener: (context, state) {
final d = state.dailyStatus;
if (d is DailySuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('سکه روزانه دریافت شد: +${d.amount}')));
} else if (d is DailyError) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(d.message)));
}
},
builder: (context, state) {
final st = state.walletStatus;
final wallet = st is WalletLoaded ? st.wallet : null;
return Column(
children: [
_TopBar(wallet: wallet, onCoinTap: () => _openShop(context)),
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const GlowText('سلطان حکم', size: 44),
const SizedBox(height: 44),
GameButton(
label: 'بازی',
icon: Icons.style,
width: double.infinity,
colors: const [Color(0xFFC2185B), Color(0xFF6A0D38)],
onTap: () async {
await context.push('/game/tiers');
if (context.mounted) _reload();
},
),
const SizedBox(height: 16),
GameButton(
label: 'دورهمی',
icon: Icons.group_add,
width: double.infinity,
colors: const [Color(0xFF1565C0), Color(0xFF0A2E57)],
onTap: () async {
await context.push('/private');
if (context.mounted) _reload();
},
),
const SizedBox(height: 16),
GameButton(
label: 'فروشگاه',
icon: Icons.storefront,
width: double.infinity,
colors: const [Color(0xFF7B1FA2), Color(0xFF3E0A57)],
onTap: () => _openShop(context),
),
const SizedBox(height: 16),
GameButton(
label: 'سکه روزانه',
icon: Icons.monetization_on,
width: double.infinity,
colors: const [Color(0xFF3FA34D), Color(0xFF1B5E20)],
onTap: () =>
context.read<WalletBloc>().add(ClaimDailyEvent()),
),
],
),
),
),
),
TextButton.icon(
onPressed: () {
context.read<AuthBloc>().add(LogoutEvent());
context.go('/login');
},
icon: const Icon(Icons.logout, color: Colors.white54),
label: const Text('خروج',
style: TextStyle(color: Colors.white54)),
),
const SizedBox(height: 8),
],
);
},
),
),
);
}
Future<void> _openShop(BuildContext context) async {
await context.push('/shop');
if (context.mounted) _reload();
}
}
class _TopBar extends StatelessWidget {
final WalletEntity? wallet;
final VoidCallback onCoinTap;
const _TopBar({required this.wallet, required this.onCoinTap});
@override
Widget build(BuildContext context) {
final w = wallet;
return Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF3A0A12), Color(0xFF1A0106)],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.goldDark),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 8)],
),
child: Row(
children: [
GestureDetector(
onTap: () async {
await context.push('/profile');
if (context.mounted) {
context.read<WalletBloc>().add(LoadWalletEvent());
}
},
child: Container(
width: 48,
height: 48,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.panel,
border: Border.all(color: AppColors.gold, width: 2),
),
child: (w == null || w.avatar.isEmpty)
? const Icon(Icons.person, color: AppColors.gold)
: ClipOval(child: RandomAvatar(w.avatar)),
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [
if (w != null && w.name.isNotEmpty) ...[
Text(w.name,
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
const SizedBox(width: 6),
],
if (w?.vip == true) const _VipTag(),
]),
const SizedBox(height: 2),
Row(children: [
const Icon(Icons.star, color: AppColors.gold, size: 16),
const SizedBox(width: 4),
Text('سطح ${w?.level ?? '-'}',
style: const TextStyle(
color: AppColors.gold, fontWeight: FontWeight.bold)),
]),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: SizedBox(
width: 96,
height: 7,
child: LinearProgressIndicator(
value: (w == null || w.xpForNext == 0)
? 0
: w.xpIntoLevel / w.xpForNext,
backgroundColor: Colors.white10,
valueColor: const AlwaysStoppedAnimation(AppColors.gold),
),
),
),
],
),
const Spacer(),
GestureDetector(
onTap: onCoinTap,
child: StatChip(
icon: Icons.confirmation_number, value: '${w?.tickets ?? 0}'),
),
const SizedBox(width: 8),
GestureDetector(
onTap: onCoinTap,
child: StatChip(
icon: Icons.monetization_on, value: '${w?.coins ?? 0}'),
),
],
),
);
}
}
class _VipTag extends StatelessWidget {
const _VipTag();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFD54F), Color(0xFFB8860B)]),
borderRadius: BorderRadius.circular(6),
),
child: const Text('VIP',
style: TextStyle(
color: Color(0xFF3A0A12),
fontWeight: FontWeight.bold,
fontSize: 10)),
);
}
}