feat: app Flutter GameManager (OIDC pocket-id, contrôle serveurs, MOTD live, auto-update) + CI APK signée
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2c5476b1db
commit
3286640b37
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.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: [
|
||||
Text(server.name,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w800),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
if (server.subtitle != null)
|
||||
Text(server.subtitle!,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 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.withValues(alpha: 0.7),
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
_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.withValues(alpha: 0.15),
|
||||
child: Icon(fallback, color: Colors.white, size: 28),
|
||||
);
|
||||
}
|
||||
|
||||
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.withValues(alpha: 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.withValues(alpha: 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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user