Build APK / build (push) Successful in 2m28s
- patch_android.py: gère 'signingConfig = signingConfigs.debug' (Flutter 3.24 utilise le '='), sans quoi le build restait signé en clé debug éphémère → signature différente à chaque run → 'conflit' à l'install de MAJ. Échoue désormais le build si la ligne n'est pas patchée (garde-fou). - update_service: télécharge l'APK in-app + lance l'installeur (open_filex), fallback navigateur ; barre de progression dans l'écran serveurs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
198 lines
5.8 KiB
Dart
198 lines
5.8 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 || !mounted) return;
|
|
|
|
final progress = ValueNotifier<double?>(0);
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (_) => AlertDialog(
|
|
title: const Text('Téléchargement…'),
|
|
content: ValueListenableBuilder<double?>(
|
|
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<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 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<GameServer> 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<String>(
|
|
future: widget.api.iconUrl(s.id),
|
|
builder: (_, snap) => ServerCard(
|
|
server: s,
|
|
iconUrl: snap.data ?? '',
|
|
busy: _busy.contains(s.id),
|
|
onToggle: () => _toggle(s),
|
|
),
|
|
),
|
|
);
|
|
}
|