feat: refactor code
This commit is contained in:
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user