543 lines
17 KiB
Dart
543 lines
17 KiB
Dart
// ویجتهای خصوصیِ صفحهی پروفایل (کارتِ سرآمد، نوارِ برجسته، کاشیها، شیتِ ویرایش).
|
|
part of 'profile_screen.dart';
|
|
|
|
/// کارتِ سرآمدِ پروفایل (فشرده و افقی): آواتار در کنارِ نام/رتبه، و نوارِ سطح/XP در پایین.
|
|
class _HeroCard extends StatelessWidget {
|
|
final ProfileEntity profile;
|
|
final VoidCallback onEdit;
|
|
const _HeroCard({required this.profile, required this.onEdit});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final d = profile;
|
|
final grad = rankGradient(d.rankTier);
|
|
final pct = d.xpNext == 0 ? 0.0 : (d.xpInto / d.xpNext).clamp(0.0, 1.0);
|
|
return GamePanel(
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
_avatar(d, grad),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Flexible(child: GlowText(d.name, size: 20)),
|
|
if (d.vip) ...[
|
|
const SizedBox(width: 8),
|
|
const _VipBadge(),
|
|
],
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: RankBadge(tier: d.rankTier, points: d.rankPoints),
|
|
),
|
|
if (d.mobile.isNotEmpty) ...[
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
d.mobile,
|
|
style: const TextStyle(
|
|
color: Colors.white38, fontSize: 12),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'سطح ${d.level}',
|
|
style: const TextStyle(
|
|
color: AppColors.gold,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
'${d.xpInto} / ${d.xpNext} XP',
|
|
style: const TextStyle(color: Colors.white38, fontSize: 11),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 5),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(6),
|
|
child: LinearProgressIndicator(
|
|
value: pct.toDouble(),
|
|
minHeight: 8,
|
|
backgroundColor: Colors.white10,
|
|
valueColor: const AlwaysStoppedAnimation(AppColors.gold),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _avatar(ProfileEntity d, List<Color> grad) {
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(3),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: grad,
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: grad.first.withValues(alpha: 0.5),
|
|
blurRadius: 12,
|
|
spreadRadius: 1,
|
|
),
|
|
],
|
|
),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(2.5),
|
|
decoration: const BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: AppColors.bgDark,
|
|
),
|
|
child: PlayerAvatar(
|
|
seed: d.avatar,
|
|
imageUrl: d.avatarImg,
|
|
size: 64,
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: -2,
|
|
right: -2,
|
|
child: GestureDetector(
|
|
onTap: onEdit,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFFFFD54F), AppColors.goldDark],
|
|
),
|
|
border: Border.all(color: AppColors.bgDark, width: 2),
|
|
),
|
|
child: const Icon(Icons.edit, color: AppColors.bg, size: 14),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// نوارِ سهگانهی برجسته: سطح، جام، امتیازِ رتبه.
|
|
class _HighlightStrip extends StatelessWidget {
|
|
final ProfileEntity profile;
|
|
const _HighlightStrip({required this.profile});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: _Highlight(
|
|
icon: Icons.star,
|
|
value: '${profile.level}',
|
|
label: 'سطح',
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: _Highlight(
|
|
icon: Icons.emoji_events,
|
|
value: '${profile.trophies}',
|
|
label: 'جام',
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: _Highlight(
|
|
icon: Icons.military_tech,
|
|
value: '${profile.rankPoints}',
|
|
label: 'امتیاز',
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Highlight extends StatelessWidget {
|
|
final IconData icon;
|
|
final String value;
|
|
final String label;
|
|
const _Highlight({
|
|
required this.icon,
|
|
required this.value,
|
|
required this.label,
|
|
});
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.panel.withValues(alpha: 0.55),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: AppColors.goldFaint),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: AppColors.gold, size: 22),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(color: Colors.white54, fontSize: 11),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// عنوانِ بخش: برچسبِ کوچک و رسمی + خطِ جداکنندهی کمرنگ (بدونِ درخشش).
|
|
class _SectionHeader extends StatelessWidget {
|
|
final IconData icon;
|
|
final String title;
|
|
const _SectionHeader({required this.icon, required this.title});
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: [
|
|
Icon(icon, color: AppColors.goldDark, size: 15),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
color: AppColors.gold,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Container(height: 1, color: AppColors.goldFaint),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// سلولِ آماری (آیکون + مقدار + برچسب) بدونِ قاب — داخلِ کارتِ واحدِ آمار قرار میگیرد.
|
|
/// accent ⇒ سلولِ برجستهی «نرخ برد» با تهِرنگِ طلایی.
|
|
class _StatTile extends StatelessWidget {
|
|
final String label;
|
|
final String value;
|
|
final IconData icon;
|
|
final bool accent;
|
|
const _StatTile(this.label, this.value, this.icon, {this.accent = false});
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: accent
|
|
? AppColors.gold.withValues(alpha: 0.13)
|
|
: Colors.white.withValues(alpha: 0.035),
|
|
borderRadius: BorderRadius.circular(11),
|
|
border: accent
|
|
? Border.all(color: AppColors.gold.withValues(alpha: 0.55))
|
|
: null,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: accent ? AppColors.gold : AppColors.goldDark,
|
|
size: 19,
|
|
),
|
|
const SizedBox(width: 9),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
color: accent ? AppColors.gold : Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(color: Colors.white54, fontSize: 11),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// ردیفِ تنظیمات (آیکون + برچسب + فلش) با ظاهرِ هماهنگ با سوئیچِ گرافیک.
|
|
class _SettingsTile extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
final VoidCallback onTap;
|
|
const _SettingsTile({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.onTap,
|
|
});
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.panel.withValues(alpha: 0.5),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: AppColors.goldFaint),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, color: AppColors.gold, size: 20),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
label,
|
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
|
),
|
|
),
|
|
const Icon(
|
|
Icons.chevron_left,
|
|
color: Colors.white38,
|
|
size: 20,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _VipBadge extends StatelessWidget {
|
|
const _VipBadge();
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFFFFD54F), AppColors.goldDark],
|
|
),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Text(
|
|
'VIP',
|
|
style: TextStyle(
|
|
color: AppColors.bg,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// شیتِ ویرایش نام و آواتار (با تأیید، مقدار جدید را برمیگرداند).
|
|
class _EditProfileSheet extends StatefulWidget {
|
|
final String name;
|
|
final String avatar;
|
|
const _EditProfileSheet({required this.name, required this.avatar});
|
|
|
|
@override
|
|
State<_EditProfileSheet> createState() => _EditProfileSheetState();
|
|
}
|
|
|
|
class _EditProfileSheetState extends State<_EditProfileSheet> {
|
|
late final TextEditingController _name;
|
|
late final List<String> _seeds;
|
|
late String _selected;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_name = TextEditingController(text: widget.name);
|
|
_seeds = List.generate(12, (i) => 'hakem-${i + 1}');
|
|
if (!_seeds.contains(widget.avatar)) _seeds.insert(0, widget.avatar);
|
|
_selected = widget.avatar;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_name.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
bool get _valid => _name.text.trim().length >= 2;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
top: false,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
),
|
|
child: Container(
|
|
decoration: const BoxDecoration(
|
|
color: AppColors.bgDark,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
border: Border(top: BorderSide(color: AppColors.gold, width: 2)),
|
|
),
|
|
padding: const EdgeInsets.all(18),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const GlowText('ویرایش پروفایل', size: 20),
|
|
const SizedBox(height: 14),
|
|
Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: AppColors.gold, width: 2),
|
|
),
|
|
child: RandomAvatar(_selected, height: 72, width: 72),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _name,
|
|
textAlign: TextAlign.center,
|
|
maxLength: 20,
|
|
inputFormatters: [LengthLimitingTextInputFormatter(20)],
|
|
decoration: const InputDecoration(
|
|
hintText: 'نام نمایشی (۲ تا ۲۰ حرف)',
|
|
counterText: '',
|
|
),
|
|
onChanged: (_) => setState(() {}),
|
|
),
|
|
const SizedBox(height: 10),
|
|
const Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Text(
|
|
'انتخاب آواتار',
|
|
style: TextStyle(color: AppColors.gold),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
GridView.count(
|
|
crossAxisCount: 4,
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
mainAxisSpacing: 10,
|
|
crossAxisSpacing: 10,
|
|
children: [
|
|
for (final s in _seeds)
|
|
GestureDetector(
|
|
onTap: () => setState(() => _selected = s),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: AppColors.panel,
|
|
border: Border.all(
|
|
color:
|
|
_selected == s
|
|
? AppColors.gold
|
|
: Colors.transparent,
|
|
width: 2.5,
|
|
),
|
|
),
|
|
child: RandomAvatar(s),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
GameButton(
|
|
label: 'تأیید',
|
|
width: double.infinity,
|
|
colors: const [AppColors.success, AppColors.successDark],
|
|
onTap:
|
|
_valid
|
|
? () => Navigator.pop(context, {
|
|
'name': _name.text.trim(),
|
|
'avatar': _selected,
|
|
})
|
|
: null,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// سوئیچِ «گرافیک کممصرف»: افکتهای اتمسفری را برای گوشیهای ضعیف خاموش میکند.
|
|
class _GraphicsToggle extends StatelessWidget {
|
|
const _GraphicsToggle();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ValueListenableBuilder<bool>(
|
|
valueListenable: AppSettings.I.lowGraphics,
|
|
builder: (context, low, _) => Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.panel.withValues(alpha: 0.5),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: AppColors.goldFaint),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.bolt, color: AppColors.gold, size: 20),
|
|
const SizedBox(width: 10),
|
|
const Expanded(
|
|
child: Text(
|
|
'گرافیک کممصرف',
|
|
style: TextStyle(color: Colors.white, fontSize: 14),
|
|
),
|
|
),
|
|
Switch(
|
|
value: low,
|
|
activeThumbColor: AppColors.gold,
|
|
onChanged: (v) => AppSettings.I.setLowGraphics(v),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|