Build APK / build (push) Successful in 2m49s
- backend: GET /api/containers (liste lisible), POST/DELETE /api/servers (serveurs custom persistés en /data/servers.json, mergés aux intégrés), champ custom dans le statut. - app: écran AddServer (form + sélecteur multi-conteneurs filtrable), FAB Ajouter, suppression depuis le détail (serveurs custom only). - thème: palette flamme/chaud sombre (anthracite + orange→rouge), remplace le Material violet/teal par défaut (scaffold, appbar, tabs, boutons, FAB, champs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
149 lines
5.7 KiB
Dart
149 lines
5.7 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../config.dart';
|
|
import '../models.dart';
|
|
import 'auth_service.dart';
|
|
|
|
/// Client de l'API GameManager (Bearer JWT pocket-id).
|
|
class ApiService {
|
|
ApiService(this._auth);
|
|
final AuthService _auth;
|
|
|
|
Future<String> get baseUrl async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
return prefs.getString('api_base') ?? Config.defaultApiBase;
|
|
}
|
|
|
|
Future<void> setBaseUrl(String url) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('api_base', url.trim().replaceAll(RegExp(r'/$'), ''));
|
|
}
|
|
|
|
Future<Map<String, String>> _headers() async {
|
|
final token = await _auth.validToken();
|
|
return {'Authorization': 'Bearer ${token ?? ''}', 'Accept': 'application/json'};
|
|
}
|
|
|
|
Future<List<GameServer>> servers() async {
|
|
final base = await baseUrl;
|
|
final res = await http.get(Uri.parse('$base/api/servers'), headers: await _headers());
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode != 200) throw ApiException('Erreur ${res.statusCode}');
|
|
final list = jsonDecode(res.body) as List;
|
|
return list.map((e) => GameServer.fromJson(e as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
Future<GameServer> server(String id) async {
|
|
final base = await baseUrl;
|
|
final res = await http.get(Uri.parse('$base/api/servers/$id'), headers: await _headers());
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode != 200) throw ApiException('Erreur ${res.statusCode}');
|
|
return GameServer.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<List<DockerContainer>> containers() async {
|
|
final base = await baseUrl;
|
|
final res = await http.get(Uri.parse('$base/api/containers'), headers: await _headers());
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode != 200) throw ApiException('Erreur ${res.statusCode}');
|
|
return (jsonDecode(res.body) as List)
|
|
.map((e) => DockerContainer.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
Future<GameServer> createServer(Map<String, dynamic> payload) async {
|
|
final base = await baseUrl;
|
|
final res = await http.post(
|
|
Uri.parse('$base/api/servers'),
|
|
headers: {...await _headers(), 'Content-Type': 'application/json'},
|
|
body: jsonEncode(payload),
|
|
);
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode >= 400) {
|
|
throw ApiException(_msg(res.body) ?? 'Création refusée (${res.statusCode})');
|
|
}
|
|
return GameServer.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<void> deleteServer(String id) async {
|
|
final base = await baseUrl;
|
|
final res = await http.delete(Uri.parse('$base/api/servers/$id'), headers: await _headers());
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode >= 400) {
|
|
throw ApiException(_msg(res.body) ?? 'Suppression refusée (${res.statusCode})');
|
|
}
|
|
}
|
|
|
|
String? _msg(String body) {
|
|
try {
|
|
return (jsonDecode(body) as Map<String, dynamic>)['detail'] as String?;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> start(String id) => _action(id, 'start');
|
|
Future<void> stop(String id) => _action(id, 'stop');
|
|
|
|
Future<void> _action(String id, String action) async {
|
|
final base = await baseUrl;
|
|
final res = await http.post(
|
|
Uri.parse('$base/api/servers/$id/$action'),
|
|
headers: await _headers(),
|
|
);
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode >= 400) throw ApiException('Action refusée (${res.statusCode})');
|
|
}
|
|
|
|
Future<String> iconUrl(String id) async => '${await baseUrl}/api/servers/$id/icon';
|
|
|
|
/// Stats d'utilisation du conteneur (CPU/RAM/réseau). `{running:false}` si éteint.
|
|
Future<Map<String, dynamic>> stats(String id) async {
|
|
final base = await baseUrl;
|
|
final res = await http.get(Uri.parse('$base/api/servers/$id/stats'), headers: await _headers());
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode != 200) throw ApiException('Erreur ${res.statusCode}');
|
|
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
}
|
|
|
|
/// Image importée du serveur (bytes), null si aucune.
|
|
Future<Uint8List?> imageBytes(String id) async {
|
|
final base = await baseUrl;
|
|
final res = await http.get(Uri.parse('$base/api/servers/$id/image'), headers: await _headers());
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode != 200) return null;
|
|
return res.bodyBytes;
|
|
}
|
|
|
|
Future<void> uploadImage(String id, String filePath) async {
|
|
final base = await baseUrl;
|
|
final token = await _auth.validToken();
|
|
final req = http.MultipartRequest('POST', Uri.parse('$base/api/servers/$id/image'))
|
|
..headers['Authorization'] = 'Bearer ${token ?? ''}'
|
|
..files.add(await http.MultipartFile.fromPath('file', filePath));
|
|
final res = await http.Response.fromStream(await req.send());
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode >= 400) throw ApiException('Envoi refusé (${res.statusCode})');
|
|
}
|
|
|
|
Future<void> deleteImage(String id) async {
|
|
final base = await baseUrl;
|
|
final res = await http.delete(Uri.parse('$base/api/servers/$id/image'), headers: await _headers());
|
|
if (res.statusCode == 401) throw UnauthorizedException();
|
|
if (res.statusCode >= 400) throw ApiException('Suppression refusée (${res.statusCode})');
|
|
}
|
|
}
|
|
|
|
class UnauthorizedException implements Exception {}
|
|
|
|
class ApiException implements Exception {
|
|
ApiException(this.message);
|
|
final String message;
|
|
@override
|
|
String toString() => message;
|
|
}
|