feat: app Flutter GameManager (OIDC pocket-id, contrôle serveurs, MOTD live, auto-update) + CI APK signée
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2c5476b1db
commit
3286640b37
@@ -0,0 +1,155 @@
|
||||
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) _applyUpdate(info);
|
||||
}
|
||||
|
||||
void _applyUpdate(UpdateInfo info) {
|
||||
UpdateService(widget.api.baseUrl).apply(info).listen((_) {}, onError: (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('MAJ échouée : $e')));
|
||||
}
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Téléchargement de la mise à jour…')),
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
itemCount: _servers!.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 14),
|
||||
itemBuilder: (_, i) {
|
||||
final s = _servers![i];
|
||||
return FutureBuilder<String>(
|
||||
future: widget.api.iconUrl(s.id),
|
||||
builder: (_, snap) => ServerCard(
|
||||
server: s,
|
||||
iconUrl: snap.data ?? '',
|
||||
busy: _busy.contains(s.id),
|
||||
onToggle: () => _toggle(s),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user