aves_mio0.31/lib/remote/remote_http_api.dart
2026-07-18 13:39:22 +02:00

179 lines
5.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'auth_client.dart';
import 'remote_models.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;
/// Recupera i photo items per una lista di ids.
Future<List<RemotePhotoItem>> getPhotosByIds(List<String> ids) async {
if (ids.isEmpty) return <RemotePhotoItem>[];
const int chunkSize = 100;
final List<RemotePhotoItem> out = [];
for (var i = 0; i < ids.length; i += chunkSize) {
final end = (i + chunkSize < ids.length) ? i + chunkSize : ids.length;
final chunk = ids.sublist(i, end);
// prova batch endpoint
try {
final idsParam = chunk.map(Uri.encodeComponent).join(',');
final uri = _base.resolve('photos?ids=$idsParam');
final r = await _getWithAuth(uri);
final list = _decodeList(r, label: 'getPhotosByIds');
out.addAll(list.map(RemotePhotoItem.fromJson));
continue;
} catch (e) {
debugPrint('[remote][api] batch getPhotosByIds failed for chunk starting at $i: $e');
// fallback: prova richieste singole per questo chunk
}
// fallback per chunk: richieste singole
for (final id in chunk) {
try {
final map = await getPhotoById(id);
if (map != null) out.add(RemotePhotoItem.fromJson(map));
} catch (e) {
debugPrint('[remote][api] getPhotosByIds fallback error for id=$id: $e');
}
}
}
return out;
}
// -------------------------
// 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<void> sendSyncDone({
required String sessionId,
required String deviceId,
required int lastSyncMs,
}) async {
final uri = _base.resolve('photos/sync_done');
final headers = await auth.authHeaders();
headers['Content-Type'] = 'application/json';
final body = jsonEncode({
'session_id': sessionId,
'device_id': deviceId,
'last_sync': lastSyncMs,
});
final res = await http
.post(uri, headers: headers, body: body)
.timeout(timeout);
if (res.statusCode < 200 || res.statusCode >= 300) {
final snippet = utf8.decode(res.bodyBytes.take(300).toList());
throw Exception('sync_done: HTTP ${res.statusCode} $snippet');
}
}
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;
}
}