import 'dart:typed_data'; 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 'server_detail_screen.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 = {}; final _imgCache = {}; Future _image(GameServer s) async { if (!s.hasImage) return null; if (_imgCache.containsKey(s.id)) return _imgCache[s.id]; final b = await widget.api.imageBytes(s.id).catchError((_) => null); _imgCache[s.id] = b; return b; } @override void initState() { super.initState(); _refresh(); _checkUpdate(); } Future _refresh() async { try { final servers = await widget.api.servers(); if (mounted) setState(() { _servers = servers; _error = null; _imgCache.clear(); // recharge les images (elles ont pu changer) }); } 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 || !mounted) return; final progress = ValueNotifier(0); showDialog( context: context, barrierDismissible: false, builder: (_) => AlertDialog( title: const Text('Téléchargement…'), content: ValueListenableBuilder( valueListenable: progress, builder: (_, p, __) => Column( mainAxisSize: MainAxisSize.min, children: [ LinearProgressIndicator(value: p), const SizedBox(height: 10), Text(p == null ? '' : '${(p * 100).round()} %'), ], ), ), ), ); final ok = await UpdateService(widget.api.baseUrl) .apply(info, onProgress: (p) => progress.value = p); if (mounted) Navigator.of(context, rootNavigator: true).pop(); // ferme la progression if (mounted && !ok) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Mise à jour impossible.')), ); } } Future _openDetail(GameServer s) async { await Navigator.push(context, MaterialPageRoute(builder: (_) => ServerDetailScreen(server: s, api: widget.api))); _refresh(); } 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 DefaultTabController( length: 2, child: 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)), ], bottom: const TabBar( tabs: [ Tab(icon: Icon(Icons.public), text: 'Production'), Tab(icon: Icon(Icons.science_outlined), text: 'Preprod'), ], ), ), body: _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(); return TabBarView( children: [_envList(prod), _envList(preprod)], ); } Widget _envList(List list) { return RefreshIndicator( onRefresh: _refresh, child: list.isEmpty ? ListView(children: const [ SizedBox(height: 120), Center(child: Text('Aucun serveur dans cet environnement.')), ]) : ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), children: list.map(_card).toList(), ), ); } Widget _card(GameServer s) => Padding( padding: const EdgeInsets.only(bottom: 14), child: FutureBuilder( future: _image(s), builder: (_, snap) => ServerCard( server: s, iconUrl: '', imageBytes: snap.data, busy: _busy.contains(s.id), onToggle: () => _toggle(s), onTap: () => _openDetail(s), ), ), ); }