import 'package:flutter/material.dart'; import '../models.dart'; import '../services/api_service.dart'; /// Formulaire d'ajout d'un serveur : infos + sélection du/des conteneur(s) /// Docker liés (liste lisible : nom + image courte + état). class AddServerScreen extends StatefulWidget { const AddServerScreen({super.key, required this.api}); final ApiService api; @override State createState() => _AddServerScreenState(); } class _AddServerScreenState extends State { final _name = TextEditingController(); final _game = TextEditingController(); final _subtitle = TextEditingController(); final _connect = TextEditingController(); final _openUrl = TextEditingController(); final _minecraft = TextEditingController(); final _search = TextEditingController(); String _env = 'prod'; bool _web = false; bool _onDemand = false; final _selected = {}; List? _containers; String? _error; bool _saving = false; @override void initState() { super.initState(); _loadContainers(); _search.addListener(() => setState(() {})); } @override void dispose() { for (final c in [_name, _game, _subtitle, _connect, _openUrl, _minecraft, _search]) { c.dispose(); } super.dispose(); } Future _loadContainers() async { try { final list = await widget.api.containers(); if (mounted) setState(() => _containers = list); } catch (e) { if (mounted) setState(() => _error = '$e'); } } Future _save() async { if (_name.text.trim().isEmpty || _selected.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Nom et au moins un conteneur requis.')), ); return; } setState(() => _saving = true); try { await widget.api.createServer({ 'name': _name.text.trim(), 'game': _game.text.trim(), 'subtitle': _subtitle.text.trim(), 'env': _env, 'containers': _selected.toList(), 'connect': _web ? _openUrl.text.trim() : _connect.text.trim(), 'web': _web, 'open_url': _web ? _openUrl.text.trim() : null, 'minecraft': _minecraft.text.trim(), 'on_demand': _onDemand, }); if (mounted) Navigator.pop(context, true); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e'))); setState(() => _saving = false); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Nouveau serveur')), body: ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 100), children: [ TextField( controller: _name, textCapitalization: TextCapitalization.words, decoration: const InputDecoration(labelText: 'Nom *', hintText: 'ex: Valheim'), ), const SizedBox(height: 12), TextField( controller: _game, decoration: const InputDecoration(labelText: 'Jeu', hintText: 'ex: Valheim'), ), const SizedBox(height: 12), TextField( controller: _subtitle, decoration: const InputDecoration(labelText: 'Sous-titre', hintText: 'ex: serveur privé'), ), const SizedBox(height: 16), _envSelector(), const SizedBox(height: 16), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Jeu web (bouton « Ouvrir »)'), value: _web, onChanged: (v) => setState(() => _web = v), ), if (_web) TextField( controller: _openUrl, keyboardType: TextInputType.url, decoration: const InputDecoration(labelText: 'URL du jeu', hintText: 'https://…'), ) else ...[ TextField( controller: _connect, decoration: const InputDecoration(labelText: 'Adresse de connexion', hintText: 'ex: jeu.nfteam.ovh'), ), const SizedBox(height: 12), TextField( controller: _minecraft, decoration: const InputDecoration( labelText: 'Minecraft host:port (MOTD, optionnel)', hintText: 'ex: 192.168.1.136:25565', ), ), ], SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Serveur à la demande'), value: _onDemand, onChanged: (v) => setState(() => _onDemand = v), ), const Divider(height: 28), Text('Conteneur(s) lié(s) *', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), _containerPicker(), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: _saving ? null : _save, icon: _saving ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.check), label: const Text('Créer'), ), ); } Widget _envSelector() { return SegmentedButton( segments: const [ ButtonSegment(value: 'prod', label: Text('Production'), icon: Icon(Icons.public)), ButtonSegment(value: 'preprod', label: Text('Preprod'), icon: Icon(Icons.science_outlined)), ], selected: {_env}, onSelectionChanged: (s) => setState(() => _env = s.first), ); } Widget _containerPicker() { if (_error != null) { return Text('Impossible de charger les conteneurs.\n$_error'); } if (_containers == null) { return const Padding(padding: EdgeInsets.all(20), child: Center(child: CircularProgressIndicator())); } final q = _search.text.toLowerCase(); final list = _containers! .where((c) => q.isEmpty || c.name.toLowerCase().contains(q) || c.image.toLowerCase().contains(q)) .toList(); return Column( children: [ TextField( controller: _search, decoration: const InputDecoration( prefixIcon: Icon(Icons.search), hintText: 'Filtrer les conteneurs…', isDense: true, ), ), const SizedBox(height: 8), if (_selected.isNotEmpty) Align( alignment: Alignment.centerLeft, child: Text('${_selected.length} sélectionné(s)', style: Theme.of(context).textTheme.labelMedium), ), Card( child: Column( children: [ for (final c in list) CheckboxListTile( dense: true, value: _selected.contains(c.name), onChanged: (v) => setState(() { v == true ? _selected.add(c.name) : _selected.remove(c.name); }), title: Text(c.name, style: const TextStyle(fontWeight: FontWeight.w600)), subtitle: Text(c.prettyImage, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontFamily: 'monospace', fontSize: 11)), secondary: Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: c.running ? const Color(0xFF34C759) : const Color(0xFF8E8E93), ), ), ), if (list.isEmpty) const Padding(padding: EdgeInsets.all(16), child: Text('Aucun conteneur.')), ], ), ), ], ); } }