179 lines
5.4 KiB
Dart
179 lines
5.4 KiB
Dart
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;
|
||
}
|
||
}
|