From 25890d7bbce814f2d2ab7bb997b83bae9b3ca48a Mon Sep 17 00:00:00 2001 From: neckfire Date: Mon, 13 Jul 2026 11:28:22 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20section=20preprod=20(env=20prod/preprod?= =?UTF-8?q?)=20+=20int=C3=A9gration=20ANTS=20(prod+preprod,=20jeu=20web)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- lib/models.dart | 13 +++++++ lib/screens/servers_screen.dart | 55 ++++++++++++++++++++++----- lib/widgets/server_card.dart | 67 +++++++++++++++++++++++++++++---- 3 files changed, 119 insertions(+), 16 deletions(-) diff --git a/lib/models.dart b/lib/models.dart index b24f551..c9ae91c 100644 --- a/lib/models.dart +++ b/lib/models.dart @@ -8,6 +8,9 @@ class GameServer { final String? subtitle; final String? connect; final bool onDemand; + final String env; // prod | preprod + final bool web; // jeu web → bouton « Ouvrir » + final String? openUrl; final String status; // stopped | starting | running | online | partial final int? players; final int? maxPlayers; @@ -21,6 +24,9 @@ class GameServer { this.subtitle, this.connect, this.onDemand = false, + this.env = 'prod', + this.web = false, + this.openUrl, required this.status, this.players, this.maxPlayers, @@ -35,6 +41,9 @@ class GameServer { subtitle: json['subtitle'] as String?, connect: json['connect'] as String?, onDemand: json['on_demand'] as bool? ?? false, + env: json['env'] as String? ?? 'prod', + web: json['web'] as bool? ?? false, + openUrl: json['open_url'] as String?, status: json['status'] as String? ?? 'stopped', players: json['players'] as int?, maxPlayers: json['max_players'] as int?, @@ -44,6 +53,7 @@ class GameServer { bool get isUp => status == 'online' || status == 'running'; bool get isBusy => status == 'starting' || status == 'partial'; + bool get isPreprod => env == 'preprod'; /// Couleur d'accent thématique par jeu (visuel sans dépendance externe). ({Color start, Color end, IconData icon}) get theme { @@ -57,6 +67,9 @@ class GameServer { if (g.contains('cycle')) { return (start: const Color(0xFF00BCD4), end: const Color(0xFF1A237E), icon: Icons.rocket_launch); } + if (g.contains('swarm') || g.contains('ants')) { + return (start: const Color(0xFFF9A825), end: const Color(0xFF6D4C00), icon: Icons.bug_report); + } return (start: const Color(0xFF7E57C2), end: const Color(0xFF311B92), icon: Icons.sports_esports); } diff --git a/lib/screens/servers_screen.dart b/lib/screens/servers_screen.dart index f7ca6b1..13ce531 100644 --- a/lib/screens/servers_screen.dart +++ b/lib/screens/servers_screen.dart @@ -130,13 +130,29 @@ class _ServersScreenState extends State { if (_servers == null) { return const Center(child: CircularProgressIndicator()); } - return ListView.separated( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), - itemCount: _servers!.length, - separatorBuilder: (_, __) => const SizedBox(height: 14), - itemBuilder: (_, i) { - final s = _servers![i]; - return FutureBuilder( + + final prod = _servers!.where((s) => !s.isPreprod).toList(); + final preprod = _servers!.where((s) => s.isPreprod).toList(); + + final children = []; + if (prod.isNotEmpty) { + children.add(const _SectionHeader(label: 'Production', icon: Icons.public)); + children.addAll(prod.map(_card)); + } + if (preprod.isNotEmpty) { + children.add(const _SectionHeader(label: 'Preprod', icon: Icons.science_outlined)); + children.addAll(preprod.map(_card)); + } + + return ListView( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 24), + children: children, + ); + } + + Widget _card(GameServer s) => Padding( + padding: const EdgeInsets.only(bottom: 14), + child: FutureBuilder( future: widget.api.iconUrl(s.id), builder: (_, snap) => ServerCard( server: s, @@ -144,8 +160,29 @@ class _ServersScreenState extends State { busy: _busy.contains(s.id), onToggle: () => _toggle(s), ), - ); - }, + ), + ); +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader({required this.label, required this.icon}); + final String label; + final IconData icon; + + @override + Widget build(BuildContext context) { + final c = Theme.of(context).colorScheme.onSurfaceVariant; + return Padding( + padding: const EdgeInsets.fromLTRB(4, 14, 4, 10), + child: Row( + children: [ + Icon(icon, size: 18, color: c), + const SizedBox(width: 8), + Text(label.toUpperCase(), + style: TextStyle( + color: c, fontSize: 13, fontWeight: FontWeight.w800, letterSpacing: 1.1)), + ], + ), ); } } diff --git a/lib/widgets/server_card.dart b/lib/widgets/server_card.dart index 505d34d..01ab9c1 100644 --- a/lib/widgets/server_card.dart +++ b/lib/widgets/server_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../models.dart'; @@ -41,13 +42,23 @@ class ServerCard extends StatelessWidget { 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), + 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( @@ -80,6 +91,10 @@ class ServerCard extends StatelessWidget { ) 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), ], ), @@ -118,6 +133,44 @@ class _Icon extends StatelessWidget { ); } +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;