import 'dart:typed_data'; 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, this.onTap, this.imageBytes, }); final GameServer server; final String iconUrl; final bool busy; final VoidCallback onToggle; final VoidCallback? onTap; final Uint8List? imageBytes; @override Widget build(BuildContext context) { final t = server.theme; final badge = server.statusBadge; final hasImg = imageBytes != null; return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Ink( decoration: hasImg ? BoxDecoration( image: DecorationImage(image: MemoryImage(imageBytes!), fit: BoxFit.cover), ) : BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [t.start, t.end], ), ), child: Container( decoration: hasImg ? const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0x66000000), Color(0xCC000000)], ), ) : null, padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (!hasImg) ...[ _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 && iconUrl.isNotEmpty) ? 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'), ); } }