import 'package:flutter/material.dart'; import '../models.dart'; import '../services/api_service.dart'; import '../services/auth_service.dart'; import '../services/update_service.dart'; import '../widgets/server_card.dart'; import 'settings_screen.dart'; class ServersScreen extends StatefulWidget { const ServersScreen({ super.key, required this.auth, required this.api, required this.onLoggedOut, }); final AuthService auth; final ApiService api; final VoidCallback onLoggedOut; @override State createState() => _ServersScreenState(); } class _ServersScreenState extends State { List? _servers; String? _error; final _busy = {}; @override void initState() { super.initState(); _refresh(); _checkUpdate(); } Future _refresh() async { try { final servers = await widget.api.servers(); if (mounted) setState(() { _servers = servers; _error = null; }); } on UnauthorizedException { await widget.auth.logout(); widget.onLoggedOut(); } catch (e) { if (mounted) setState(() => _error = '$e'); } } Future _toggle(GameServer s) async { setState(() => _busy.add(s.id)); try { s.isUp ? await widget.api.stop(s.id) : await widget.api.start(s.id); await Future.delayed(const Duration(seconds: 2)); await _refresh(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e'))); } } finally { if (mounted) setState(() => _busy.remove(s.id)); } } Future _checkUpdate() async { final info = await UpdateService(widget.api.baseUrl).check(); if (info == null || !mounted) return; final go = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text('Mise à jour ${info.version}'), content: Text(info.notes.isEmpty ? 'Une nouvelle version est disponible.' : info.notes), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Plus tard')), FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Installer')), ], ), ); if (go == true) { final ok = await UpdateService(widget.api.baseUrl).apply(info); if (mounted && !ok) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Impossible d\'ouvrir le téléchargement.')), ); } } } Future _openSettings() async { await Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen(api: widget.api))); _refresh(); } Future _logout() async { await widget.auth.logout(); widget.onLoggedOut(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Serveurs'), actions: [ IconButton(onPressed: _openSettings, icon: const Icon(Icons.settings_outlined)), IconButton(onPressed: _logout, icon: const Icon(Icons.logout)), ], ), body: RefreshIndicator( onRefresh: _refresh, child: _buildBody(), ), ); } Widget _buildBody() { if (_error != null && _servers == null) { return ListView(children: [ const SizedBox(height: 120), Icon(Icons.cloud_off, size: 60, color: Theme.of(context).colorScheme.outline), const SizedBox(height: 12), Center(child: Text('Connexion à l\'API impossible.\n$_error', textAlign: TextAlign.center)), const SizedBox(height: 12), Center(child: FilledButton(onPressed: _refresh, child: const Text('Réessayer'))), ]); } if (_servers == null) { return const Center(child: CircularProgressIndicator()); } 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, iconUrl: snap.data ?? '', 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)), ], ), ); } }