Files
neckfireandClaude Opus 4.8 30786f9706
Build APK / build (push) Successful in 2m49s
feat: ajout de serveurs depuis l'app + charte flamme sombre
- backend: GET /api/containers (liste lisible), POST/DELETE /api/servers
  (serveurs custom persistés en /data/servers.json, mergés aux intégrés),
  champ custom dans le statut.
- app: écran AddServer (form + sélecteur multi-conteneurs filtrable), FAB
  Ajouter, suppression depuis le détail (serveurs custom only).
- thème: palette flamme/chaud sombre (anthracite + orange→rouge), remplace le
  Material violet/teal par défaut (scaffold, appbar, tabs, boutons, FAB, champs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 13:33:22 +02:00

114 lines
4.0 KiB
Dart

import 'package:flutter/material.dart';
/// Un serveur de jeu tel que renvoyé par l'API `/api/servers`.
class GameServer {
final String id;
final String name;
final String game;
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;
final String? motd;
final bool hasIcon;
final bool hasImage;
final bool custom;
const GameServer({
required this.id,
required this.name,
required this.game,
this.subtitle,
this.connect,
this.onDemand = false,
this.env = 'prod',
this.web = false,
this.openUrl,
required this.status,
this.players,
this.maxPlayers,
this.motd,
this.hasIcon = false,
this.hasImage = false,
this.custom = false,
});
factory GameServer.fromJson(Map<String, dynamic> json) => GameServer(
id: json['id'] as String,
name: json['name'] as String,
game: json['game'] as String,
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?,
motd: json['motd'] as String?,
hasIcon: json['has_icon'] as bool? ?? false,
hasImage: json['has_image'] as bool? ?? false,
custom: json['custom'] as bool? ?? false,
);
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 {
final g = game.toLowerCase();
if (g.contains('minecraft')) {
final medieval = name.toLowerCase().contains('mmc') || name.toLowerCase().contains('medieval');
return medieval
? (start: const Color(0xFF7B5E3B), end: const Color(0xFF3E2C1A), icon: Icons.castle)
: (start: const Color(0xFF4CAF50), end: const Color(0xFF1B5E20), icon: Icons.grass);
}
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);
}
({String label, Color color}) get statusBadge => switch (status) {
'online' => (label: 'En ligne', color: const Color(0xFF34C759)),
'running' => (label: 'Démarré', color: const Color(0xFF30B0C7)),
'starting' => (label: 'Démarrage…', color: const Color(0xFFFF9F0A)),
'partial' => (label: 'Partiel', color: const Color(0xFFFF9F0A)),
_ => (label: 'Éteint', color: const Color(0xFF8E8E93)),
};
}
/// Conteneur Docker (pour lier un serveur depuis l'app).
class DockerContainer {
DockerContainer({required this.name, required this.image, required this.state});
final String name;
final String image;
final String state;
factory DockerContainer.fromJson(Map<String, dynamic> j) => DockerContainer(
name: j['name'] as String,
image: (j['image'] as String?) ?? '',
state: (j['state'] as String?) ?? '',
);
bool get running => state == 'running';
/// Image sans le registre ni le digest, pour la lisibilité.
/// ex: git.nfteam.ovh/neckfire/the-cycle:preprod → the-cycle:preprod
String get prettyImage {
var s = image.split('/').last;
if (s.length > 34) s = '${s.substring(0, 33)}…';
return s;
}
}