Build APK / build (push) Successful in 2m27s
- servers groupés par environnement (Production / Preprod) avec en-têtes - ANTS prod (ants-web) + preprod (ants-web-rd) intégrés : jeu web, bouton Ouvrir - The Cycle preprod (the-cycle-api-rd) ajouté - badge PREPROD sur les cartes concernées, thème ANTS (fourmi/ambre) - supprime le widget_test.dart par défaut (cassé, non compilé dans l'APK) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
5.4 KiB
Dart
189 lines
5.4 KiB
Dart
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<ServersScreen> createState() => _ServersScreenState();
|
|
}
|
|
|
|
class _ServersScreenState extends State<ServersScreen> {
|
|
List<GameServer>? _servers;
|
|
String? _error;
|
|
final _busy = <String>{};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_refresh();
|
|
_checkUpdate();
|
|
}
|
|
|
|
Future<void> _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<void> _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<void> _checkUpdate() async {
|
|
final info = await UpdateService(widget.api.baseUrl).check();
|
|
if (info == null || !mounted) return;
|
|
final go = await showDialog<bool>(
|
|
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<void> _openSettings() async {
|
|
await Navigator.push(context,
|
|
MaterialPageRoute(builder: (_) => SettingsScreen(api: widget.api)));
|
|
_refresh();
|
|
}
|
|
|
|
Future<void> _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 = <Widget>[];
|
|
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<String>(
|
|
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)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|