Build APK / build (push) Successful in 2m49s
- 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>
114 lines
4.0 KiB
Dart
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;
|
|
}
|
|
}
|