Files
MobilApp/lib/services/layer_sync_service.dart
T

168 lines
6.7 KiB
Dart

import 'dart:io';
import 'package:get/get.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:terepi_seged/services/layer_import_service.dart';
import 'package:uuid/uuid.dart';
import 'app_database.dart';
import '../models/imported_layer_meta.dart';
/// Megosztott referencia-rétegek (pl. gázhálózat nyomvonala) kezelése.
///
/// Modell: a réteg egy FÁJL (GeoJSON) a Supabase Storage-ban + egy
/// metaadat-sor a ts_layers táblában, verziószámmal.
///
/// * PULL: minden tagsági projekt rétegeinél a verziószámot hasonlítjuk a
/// lokálishoz — csak akkor töltünk le, ha nőtt. A letöltött fájl a
/// meglévő imported_layers mechanizmusba kerül, így a térkép ugyanúgy
/// jeleníti meg, mint a kézzel importált rétegeket.
/// * OFFLINE: a lokálisan cache-elt fájl net nélkül is ott van — a réteg
/// mindig a legutóbb letöltött állapotában látszik.
/// * PUBLISH: egy lokálisan importált réteg megosztása = fájl feltöltése
/// a Storage-ba + ts_layers sor (verzió +1 újrapublikáláskor).
class LayerSyncService extends GetxService {
static LayerSyncService get to => Get.find();
static const bucket = 'terepi_seged-layers';
final isSyncing = false.obs;
SupabaseClient get _client => Supabase.instance.client;
AppDatabase get _db => AppDatabase.instance;
/// A rétegfájlok lokális cache-könyvtára.
Future<Directory> _layerDir() async {
final base = await getExternalStorageDirectory() ??
await getApplicationDocumentsDirectory();
final dir = Directory(p.join(base.path, 'layers'));
if (!await dir.exists()) await dir.create(recursive: true);
return dir;
}
// ═════════════════════════════════════════════════════════════════
// PULL — a TsSyncService hívja minden szinkron végén
// ═════════════════════════════════════════════════════════════════
Future<void> pullAll() async {
if (_client.auth.currentUser == null) return;
if (isSyncing.value) return;
isSyncing.value = true;
try {
// Csak a szinkronizált (nem lokális) projektek rétegei érdekelnek.
final projects = await _db.listProjects();
for (final project in projects.where((pr) => !pr.isLocalOnly)) {
await _pullProjectLayers(project.id!, project.uuid);
}
// Ha közben egy csapattárs frissített/megosztott egy réteget, ez
// jelenjen meg a térképen azonnal, újraindítás nélkül.
if (Get.isRegistered<LayerImportService>()) {
await LayerImportService.to.reload();
}
} finally {
isSyncing.value = false;
}
}
Future<void> _pullProjectLayers(
int localProjectId, String projectUuid) async {
final rows = await _client
.from('terepi_seged_layers')
.select()
.eq('project_id', projectUuid)
.filter('deleted_at', 'is', null);
for (final r in rows) {
final id = r['id'] as String;
final remoteVersion = (r['version'] as int?) ?? 1;
final localVersion = await _db.getImportedLayerVersion(id);
// Már megvan a legfrissebb verzió → nincs teendő.
if (localVersion >= remoteVersion) continue;
// Letöltés a Storage-ból, lokális cache-be.
final storagePath = r['storage_path'] as String;
final bytes = await _client.storage.from(bucket).download(storagePath);
final dir = await _layerDir();
final localPath = p.join(dir.path, '$id.geojson');
await File(localPath).writeAsBytes(bytes, flush: true);
// Metaadat upsert a meglévő imported_layers táblába — a térkép
// rétegkezelője innentől ugyanúgy látja, mint a kézi importot.
await _db.upsertImportedLayerRaw({
'id': id,
'name': r['name'],
'source_type': r['source_type'] ?? 'geojson',
'local_path': localPath,
'storage_path': storagePath,
'is_visible': 1,
'project_id': localProjectId,
'imported_at': r['created_at'] ?? DateTime.now().toIso8601String(),
'synced_at': DateTime.now().toIso8601String(),
'version': remoteVersion,
});
}
}
// ═════════════════════════════════════════════════════════════════
// PUBLISH — lokális réteg megosztása a projekt tagjaival
// ═════════════════════════════════════════════════════════════════
/// Feltölti a réteg fájlját és létrehozza/frissíti a ts_layers sorát.
/// Újrapublikáláskor a verziószám nő → a többi eszköz a következő
/// szinkronnál automatikusan frissíti a példányát.
Future<void> publishLayer({
required ImportedLayerMeta layer,
required String projectUuid,
}) async {
final id = layer.id.isNotEmpty ? layer.id : const Uuid().v4();
final fileName = p.basename(layer.localPath);
final storagePath = '$projectUuid/$id/$fileName';
// 1. Fájl a Storage-ba (upsert: újrapublikálásnál felülírja).
await _client.storage.from(bucket).upload(
storagePath,
File(layer.localPath),
fileOptions: const FileOptions(upsert: true),
);
// 2. Verzió léptetése, ha már létezik a sor.
final existing = await _client
.from('terepi_seged_layers')
.select('version')
.eq('id', id)
.maybeSingle();
final newVersion = ((existing?['version'] as int?) ?? 0) + 1;
await _client.from('terepi_seged_layers').upsert({
'id': id,
'project_id': projectUuid,
'name': layer.name,
'source_type': layer.sourceType.name,
'storage_path': storagePath,
'version': newVersion,
});
// 3. Lokális meta frissítése, hogy a saját eszköz ne töltse le újra.
await _db.upsertImportedLayerRaw({
...layer.toMap(),
'id': id,
'storage_path': storagePath,
'synced_at': DateTime.now().toIso8601String(),
'version': newVersion,
});
}
/// Réteg visszavonása a megosztásból (soft delete a szerveren;
/// a lokális cache-példányok megmaradnak az eszközökön).
Future<void> unpublishLayer(String layerId) async {
await _client.from('terepi_seged_layers').update({
'deleted_at': DateTime.now().toUtc().toIso8601String(),
}).eq('id', layerId);
}
}