Build APK / build (push) Successful in 2m27s
- servers groupés par environnement (Production / Preprod) avec en-têtes - ANTS prod (ants-web) + preprod (ants-web-rd) intégrés : jeu web, bouton Ouvrir - The Cycle preprod (the-cycle-api-rd) ajouté - badge PREPROD sur les cartes concernées, thème ANTS (fourmi/ambre) - supprime le widget_test.dart par défaut (cassé, non compilé dans l'APK) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
222 lines
7.1 KiB
Dart
222 lines
7.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
import '../models.dart';
|
|
|
|
class ServerCard extends StatelessWidget {
|
|
const ServerCard({
|
|
super.key,
|
|
required this.server,
|
|
required this.iconUrl,
|
|
required this.busy,
|
|
required this.onToggle,
|
|
});
|
|
|
|
final GameServer server;
|
|
final String iconUrl;
|
|
final bool busy;
|
|
final VoidCallback onToggle;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final t = server.theme;
|
|
final badge = server.statusBadge;
|
|
return Card(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [t.start, t.end],
|
|
),
|
|
),
|
|
padding: const EdgeInsets.all(18),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
_Icon(server: server, iconUrl: iconUrl, fallback: t.icon),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Flexible(
|
|
child: Text(server.name,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 19,
|
|
fontWeight: FontWeight.w800),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis),
|
|
),
|
|
if (server.isPreprod) ...[
|
|
const SizedBox(width: 8),
|
|
const _Tag(label: 'PREPROD'),
|
|
],
|
|
],
|
|
),
|
|
if (server.subtitle != null)
|
|
Text(server.subtitle!,
|
|
style: TextStyle(
|
|
color: Colors.white.withOpacity(0.75), fontSize: 13)),
|
|
],
|
|
),
|
|
),
|
|
_StatusPill(label: badge.label, color: badge.color, pulse: server.isBusy),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
if (server.status == 'online') ...[
|
|
const Icon(Icons.people, color: Colors.white70, size: 18),
|
|
const SizedBox(width: 5),
|
|
Text('${server.players ?? 0}/${server.maxPlayers ?? 0}',
|
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
|
|
const SizedBox(width: 14),
|
|
],
|
|
if (server.connect != null)
|
|
Expanded(
|
|
child: Text(server.connect!,
|
|
style: TextStyle(
|
|
color: Colors.white.withOpacity(0.7),
|
|
fontFamily: 'monospace',
|
|
fontSize: 12),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis),
|
|
)
|
|
else
|
|
const Spacer(),
|
|
if (server.web && server.openUrl != null) ...[
|
|
_OpenButton(url: server.openUrl!),
|
|
const SizedBox(width: 8),
|
|
],
|
|
_ToggleButton(up: server.isUp, busy: busy, onToggle: onToggle),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Icon extends StatelessWidget {
|
|
const _Icon({required this.server, required this.iconUrl, required this.fallback});
|
|
final GameServer server;
|
|
final String iconUrl;
|
|
final IconData fallback;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: SizedBox(
|
|
width: 52,
|
|
height: 52,
|
|
child: (server.hasIcon)
|
|
? Image.network(iconUrl,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, __, ___) => _fallbackBox())
|
|
: _fallbackBox(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _fallbackBox() => Container(
|
|
color: Colors.white.withOpacity(0.15),
|
|
child: Icon(fallback, color: Colors.white, size: 28),
|
|
);
|
|
}
|
|
|
|
class _Tag extends StatelessWidget {
|
|
const _Tag({required this.label});
|
|
final String label;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withOpacity(0.28),
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: Colors.white.withOpacity(0.5), width: 1),
|
|
),
|
|
child: Text(label,
|
|
style: const TextStyle(
|
|
color: Colors.white, fontSize: 10, fontWeight: FontWeight.w800, letterSpacing: 0.6)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OpenButton extends StatelessWidget {
|
|
const _OpenButton({required this.url});
|
|
final String url;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return OutlinedButton.icon(
|
|
onPressed: () => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.white,
|
|
side: BorderSide(color: Colors.white.withOpacity(0.7)),
|
|
),
|
|
icon: const Icon(Icons.open_in_new, size: 18),
|
|
label: const Text('Ouvrir'),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _StatusPill extends StatelessWidget {
|
|
const _StatusPill({required this.label, required this.color, required this.pulse});
|
|
final String label;
|
|
final Color color;
|
|
final bool pulse;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.22),
|
|
borderRadius: BorderRadius.circular(999),
|
|
border: Border.all(color: color, width: 1),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(width: 7, height: 7, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
|
const SizedBox(width: 6),
|
|
Text(label, style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w700)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ToggleButton extends StatelessWidget {
|
|
const _ToggleButton({required this.up, required this.busy, required this.onToggle});
|
|
final bool up;
|
|
final bool busy;
|
|
final VoidCallback onToggle;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FilledButton.icon(
|
|
onPressed: busy ? null : onToggle,
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: up ? Colors.white.withOpacity(0.9) : Colors.white,
|
|
foregroundColor: up ? const Color(0xFFB3261E) : const Color(0xFF1B5E20),
|
|
),
|
|
icon: busy
|
|
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
|
|
: Icon(up ? Icons.stop : Icons.play_arrow),
|
|
label: Text(up ? 'Éteindre' : 'Démarrer'),
|
|
);
|
|
}
|
|
}
|