110 lines
3.5 KiB
Dart
110 lines
3.5 KiB
Dart
import 'dart:convert';
|
||
import 'package:http/http.dart' as http;
|
||
|
||
import 'auth_client.dart';
|
||
|
||
class RemoteHttpApi {
|
||
RemoteHttpApi({
|
||
required this.baseUrl,
|
||
required this.auth,
|
||
this.timeout = const Duration(seconds: 20),
|
||
}) : _base = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/');
|
||
|
||
final String baseUrl;
|
||
final Uri _base;
|
||
final RemoteAuth auth;
|
||
final Duration timeout;
|
||
|
||
// -------------------------
|
||
// Helpers
|
||
// -------------------------
|
||
|
||
Future<http.Response> _getWithAuth(Uri uri) async {
|
||
var headers = await auth.authHeaders();
|
||
|
||
http.Response res;
|
||
try {
|
||
res = await http.get(uri, headers: headers).timeout(timeout);
|
||
} catch (e) {
|
||
throw Exception('GET fallita: errore di rete verso $uri: $e');
|
||
}
|
||
|
||
// Follow redirect 307/308 se presenti (raro ma possibile)
|
||
if ({307, 308}.contains(res.statusCode) && res.headers['location'] != null) {
|
||
final redirectUri = uri.resolve(res.headers['location']!);
|
||
try {
|
||
res = await http.get(redirectUri, headers: headers).timeout(timeout);
|
||
} catch (e) {
|
||
throw Exception('GET fallita: errore di rete verso $redirectUri: $e');
|
||
}
|
||
}
|
||
|
||
// Se token scaduto -> refresh e retry una sola volta
|
||
if (res.statusCode == 401) {
|
||
headers = await auth.refreshAndHeaders();
|
||
try {
|
||
res = await http.get(uri, headers: headers).timeout(timeout);
|
||
} catch (e) {
|
||
throw Exception('GET fallita dopo refresh token verso $uri: $e');
|
||
}
|
||
}
|
||
|
||
// Gestione errori HTTP
|
||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||
final snippet = utf8.decode(res.bodyBytes.take(300).toList());
|
||
throw Exception('HTTP ${res.statusCode} ${res.reasonPhrase} su $uri – $snippet');
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
List<Map<String, dynamic>> _decodeList(http.Response r, {required String label}) {
|
||
final decoded = jsonDecode(utf8.decode(r.bodyBytes));
|
||
if (decoded is! List) {
|
||
throw Exception('$label: risposta non è una lista JSON');
|
||
}
|
||
return decoded.cast<Map<String, dynamic>>();
|
||
}
|
||
|
||
// -------------------------
|
||
// API
|
||
// -------------------------
|
||
|
||
Future<List<Map<String, dynamic>>> getAllPhotos() async {
|
||
final uri = _base.resolve('photos');
|
||
final r = await _getWithAuth(uri);
|
||
return _decodeList(r, label: 'getAllPhotos');
|
||
}
|
||
|
||
Future<List<Map<String, dynamic>>> getChanges(String sinceIso) async {
|
||
final uri = _base.resolve('photos/changes?since=${Uri.encodeComponent(sinceIso)}');
|
||
final r = await _getWithAuth(uri);
|
||
return _decodeList(r, label: 'getChanges');
|
||
}
|
||
|
||
Future<List<Map<String, dynamic>>> getDeletedHard(String sinceIso) async {
|
||
final uri = _base.resolve('photos/deleted_hard?since=${Uri.encodeComponent(sinceIso)}');
|
||
final r = await _getWithAuth(uri);
|
||
|
||
final decoded = jsonDecode(utf8.decode(r.bodyBytes));
|
||
if (decoded is! Map<String, dynamic>) {
|
||
throw Exception('getDeletedHard: risposta non è un oggetto JSON');
|
||
}
|
||
|
||
final deleted = (decoded['deleted'] ?? []) as List;
|
||
return deleted.cast<Map<String, dynamic>>();
|
||
}
|
||
|
||
Future<Map<String, dynamic>?> getPhotoById(String id) async {
|
||
final uri = _base.resolve('photos/$id');
|
||
final r = await _getWithAuth(uri);
|
||
|
||
final decoded = jsonDecode(utf8.decode(r.bodyBytes));
|
||
if (decoded is List && decoded.isNotEmpty) {
|
||
final first = decoded.first;
|
||
if (first is Map<String, dynamic>) return first;
|
||
throw Exception('getPhotoById: primo elemento non è un oggetto JSON');
|
||
}
|
||
return null;
|
||
}
|
||
}
|