refactor: all screens refactor
This commit is contained in:
@@ -8,6 +8,8 @@ import '../../../../core/theme/ranks.dart';
|
||||
import '../../domain/entities/game_entities.dart';
|
||||
import '../bloc/game_state.dart';
|
||||
|
||||
part 'table_hud.parts.dart';
|
||||
|
||||
/// اوورلیِ روی صحنهی Flame: آواتارِ بازیکنان با قابِ طلایی، نام، نشانِ رتبه،
|
||||
/// شمارندهی نوبت، تاجِ حاکم و تعدادِ کارت — بهعلاوهی پنلِ امتیاز/حکم (بالا-چپ)،
|
||||
/// نشانگرِ اتصال و دکمهی خروج (بالا-راست) و دکمهی گفتگو (پایین-راست).
|
||||
@@ -217,425 +219,3 @@ class TableHud extends StatelessWidget {
|
||||
/// رتبه (رنگ از سرور). باتها رتبه ندارند ⇒ رنگِ پیشفرض.
|
||||
List<Color> _seatFrame(GamePlayer p) =>
|
||||
frameGradient(p.frame) ?? rankGradient(p.bot ? '' : p.rankTier);
|
||||
|
||||
class _PlayerSeat extends StatelessWidget {
|
||||
final GamePlayer player;
|
||||
final double diameter;
|
||||
final bool isTurn;
|
||||
final bool isHakem;
|
||||
final bool nameAbove;
|
||||
final int cardCount;
|
||||
final int
|
||||
teamTricks; // دستهای بردهی تیمِ این بازیکن در این هَند (tricks_won)
|
||||
final String turnToken;
|
||||
final double turnSeconds;
|
||||
final bool showRing;
|
||||
final ChatBubble? bubble; // پیام/شکلکِ فعالِ این بازیکن (یا null)
|
||||
const _PlayerSeat({
|
||||
required this.player,
|
||||
required this.diameter,
|
||||
required this.isTurn,
|
||||
required this.isHakem,
|
||||
required this.nameAbove,
|
||||
required this.cardCount,
|
||||
required this.teamTricks,
|
||||
required this.turnToken,
|
||||
required this.turnSeconds,
|
||||
required this.showRing,
|
||||
required this.bubble,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ringW = diameter * 0.08;
|
||||
final name = Container(
|
||||
constraints: BoxConstraints(maxWidth: diameter * 1.7),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xCC14100B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isTurn ? AppColors.gold : const Color(0x55B8860B),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
player.bot ? '${player.name} (ربات)' : player.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: isTurn ? AppColors.gold : Colors.white,
|
||||
fontSize: diameter * 0.18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final avatar = SizedBox(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// قابِ آواتار: قابِ اختصاصیِ خریداریشده (اگر باشد)، وگرنه قابِ رتبه.
|
||||
Container(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: _seatFrame(player),
|
||||
),
|
||||
boxShadow: [
|
||||
const BoxShadow(color: Colors.black54, blurRadius: 6),
|
||||
if (isTurn)
|
||||
BoxShadow(
|
||||
color: _seatFrame(player)[0].withValues(alpha: 0.6),
|
||||
blurRadius: 12),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.all(ringW),
|
||||
child: ClipOval(
|
||||
child: Container(
|
||||
color: const Color(0xFF1A2740),
|
||||
child:
|
||||
player.bot
|
||||
? Icon(
|
||||
Icons.smart_toy,
|
||||
color: Colors.white70,
|
||||
size: diameter * 0.5,
|
||||
)
|
||||
: RandomAvatar(
|
||||
player.avatarSeed.isEmpty ? '?' : player.avatarSeed),
|
||||
),
|
||||
),
|
||||
),
|
||||
// حلقهی شمارشِ نوبت (روی قاب).
|
||||
if (isTurn && showRing)
|
||||
Positioned.fill(
|
||||
child: _TurnRing(
|
||||
token: turnToken,
|
||||
seconds: turnSeconds,
|
||||
stroke: ringW * 1.1,
|
||||
),
|
||||
),
|
||||
// تاجِ حاکم بالای آواتار.
|
||||
if (isHakem)
|
||||
Positioned(
|
||||
top: -diameter * 0.40,
|
||||
child: Text('👑', style: TextStyle(fontSize: diameter * 0.4)),
|
||||
),
|
||||
// دستهای بردهی تیمِ این بازیکن در این هَند (بالا-چپ، با نشانِ تکخال).
|
||||
if (teamTricks > 0)
|
||||
Positioned(
|
||||
left: -diameter * 0.06,
|
||||
top: -diameter * 0.06,
|
||||
child: _TricksBadge(count: teamTricks, diameter: diameter),
|
||||
),
|
||||
|
||||
// حبابِ تعدادِ کارت (پایین-راست).
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// نشانِ رتبهی فشرده (تصویر) کنارِ آواتار.
|
||||
final badge =
|
||||
(!player.bot && player.rankTier.isNotEmpty)
|
||||
? Image.asset(
|
||||
'assets/images/badge/${player.rankTier}.png',
|
||||
width: diameter * 0.34,
|
||||
height: diameter * 0.34,
|
||||
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
|
||||
final nameRow = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
badge,
|
||||
if (!player.bot && player.rankTier.isNotEmpty) const SizedBox(width: 4),
|
||||
Flexible(child: name),
|
||||
if (!player.bot && player.coins > 0) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${player.coins}',
|
||||
style: TextStyle(
|
||||
color: AppColors.gold,
|
||||
fontSize: diameter * 0.16,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
final content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children:
|
||||
nameAbove
|
||||
? [nameRow, SizedBox(height: diameter * 0.06), avatar]
|
||||
: [avatar, SizedBox(height: diameter * 0.06), nameRow],
|
||||
);
|
||||
|
||||
if (bubble == null) return content;
|
||||
// حبابِ پیام بالای آواتار (برای بازیکنِ بالا، زیرِ آواتار تا از صفحه بیرون نزند).
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
content,
|
||||
Positioned(
|
||||
top: nameAbove ? null : -diameter * 0.62,
|
||||
bottom: nameAbove ? -diameter * 0.62 : null,
|
||||
child: _ChatBubbleView(bubble: bubble!, diameter: diameter),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// نمایشِ حبابِ پیام/شکلک با یک «پاپ»ِ ورود. شکلک بزرگ و متن در یک پیلِ روشن.
|
||||
class _ChatBubbleView extends StatelessWidget {
|
||||
final ChatBubble bubble;
|
||||
final double diameter;
|
||||
const _ChatBubbleView({required this.bubble, required this.diameter});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEmoji = (bubble.emoji ?? '').isNotEmpty;
|
||||
final child = isEmoji
|
||||
? Text(bubble.emoji!, style: TextStyle(fontSize: diameter * 0.7))
|
||||
: Container(
|
||||
constraints: BoxConstraints(maxWidth: diameter * 2.4),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: diameter * 0.16, vertical: diameter * 0.08),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(diameter * 0.3),
|
||||
border: Border.all(color: AppColors.goldDark, width: 1.4),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 5)
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
bubble.text ?? '',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: AppColors.bgDark,
|
||||
fontSize: diameter * 0.2,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
return TweenAnimationBuilder<double>(
|
||||
key: ValueKey(bubble.id),
|
||||
tween: Tween(begin: 0.5, end: 1.0),
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutBack,
|
||||
builder: (_, v, c) => Transform.scale(scale: v, child: c),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// حلقهی شمارشِ معکوسِ نوبت: از پر به خالی طیِ `seconds`؛ نزدیکِ پایان قرمز
|
||||
/// میشود. با تغییرِ `token` (نوبتِ جدید) از نو شروع میشود.
|
||||
class _TurnRing extends StatefulWidget {
|
||||
final String token;
|
||||
final double seconds;
|
||||
final double stroke;
|
||||
const _TurnRing({
|
||||
required this.token,
|
||||
required this.seconds,
|
||||
required this.stroke,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_TurnRing> createState() => _TurnRingState();
|
||||
}
|
||||
|
||||
class _TurnRingState extends State<_TurnRing>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _c;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_c = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: (widget.seconds * 1000).round()),
|
||||
)..forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _TurnRing old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (old.token != widget.token) {
|
||||
_c.duration = Duration(milliseconds: (widget.seconds * 1000).round());
|
||||
_c
|
||||
..reset()
|
||||
..forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_c.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _c,
|
||||
builder: (_, __) {
|
||||
final left = (1 - _c.value).clamp(0.0, 1.0);
|
||||
return CustomPaint(
|
||||
painter: _RingPainter(left: left, stroke: widget.stroke),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RingPainter extends CustomPainter {
|
||||
final double left; // ۱..۰
|
||||
final double stroke;
|
||||
_RingPainter({required this.left, required this.stroke});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
final center = rect.center;
|
||||
final radius = (size.shortestSide - stroke) / 2;
|
||||
const start = -1.5708; // بالا
|
||||
final sweep = 6.28318 * left;
|
||||
final color =
|
||||
left < 0.3 ? AppColors.suitRed : AppColors.online;
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
start,
|
||||
sweep,
|
||||
false,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeWidth = stroke
|
||||
..color = color,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _RingPainter old) =>
|
||||
old.left != left || old.stroke != stroke;
|
||||
}
|
||||
|
||||
/// نشانِ دستهای بردهی تیم؛ هنگامِ نمایش و هر بار که عدد زیاد میشود یک «پاپ»
|
||||
/// (بزرگنماییِ کوتاه) میزند تا کاربر بفهمد دستهای برده اینجا نشان داده میشوند.
|
||||
class _TricksBadge extends StatefulWidget {
|
||||
final int count;
|
||||
final double diameter;
|
||||
const _TricksBadge({required this.count, required this.diameter});
|
||||
|
||||
@override
|
||||
State<_TricksBadge> createState() => _TricksBadgeState();
|
||||
}
|
||||
|
||||
class _TricksBadgeState extends State<_TricksBadge>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _c;
|
||||
late final Animation<double> _scale;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_c = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 420));
|
||||
_scale = TweenSequence<double>([
|
||||
TweenSequenceItem(tween: Tween(begin: 0.4, end: 1.35), weight: 50),
|
||||
TweenSequenceItem(tween: Tween(begin: 1.35, end: 1.0), weight: 50),
|
||||
]).animate(CurvedAnimation(parent: _c, curve: Curves.easeOut));
|
||||
_c.forward(); // پاپِ اولیه هنگامِ ظاهرشدن
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _TricksBadge old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (old.count != widget.count) {
|
||||
_c
|
||||
..reset()
|
||||
..forward(); // پاپ هر بار که دستِ برده اضافه میشود
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_c.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final d = widget.diameter;
|
||||
return ScaleTransition(
|
||||
scale: _scale,
|
||||
child: Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: d * 0.08, vertical: d * 0.03),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF7B0E14),
|
||||
borderRadius: BorderRadius.circular(d),
|
||||
border: Border.all(color: AppColors.gold, width: 1.4),
|
||||
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 4)],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.layers, color: Colors.white, size: d * 0.18),
|
||||
SizedBox(width: d * 0.03),
|
||||
Text('${widget.count}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: d * 0.2,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoundBtn extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
final double size;
|
||||
const _RoundBtn({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
this.size = 46,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.gold, width: 1.6),
|
||||
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)],
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: size * 0.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user