Kml parser utf-8 támogatás, induláskor minimum app verzió ellenőrzése
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
// SQLite → imported_layers tábla (metadata)
|
||||
// Supabase → Storage (megosztás, később)
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||
import 'package:terepi_seged/services/layer_sync_service.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../models/imported_layer.dart';
|
||||
@@ -201,6 +203,23 @@ class LayerImportService extends GetxService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Réteg megosztása a projekt csapatával (feltöltés a Storage-ba +
|
||||
/// metaadat a terepi_seged_layers táblába).
|
||||
Future<void> publishLayer(String id) async {
|
||||
if (!Get.isRegistered<LayerSyncService>()) return;
|
||||
final projectUuid = ProjectService.to.activeProject.value?.uuid;
|
||||
if (projectUuid == null) {
|
||||
throw Exception('Nincs aktív (megosztott) projekt.');
|
||||
}
|
||||
|
||||
final metas = await AppDatabase.instance.listImportedLayers();
|
||||
final meta = metas.firstWhereOrNull((m) => m.id == id);
|
||||
if (meta == null) throw Exception('A réteg nem található.');
|
||||
|
||||
await LayerSyncService.to
|
||||
.publishLayer(layer: meta, projectUuid: projectUuid);
|
||||
}
|
||||
|
||||
Future<void> removeLayer(String id) async {
|
||||
final metas = await AppDatabase.instance.listImportedLayers();
|
||||
final meta = metas.firstWhereOrNull((m) => m.id == id);
|
||||
@@ -259,7 +278,9 @@ class LayerImportService extends GetxService {
|
||||
return _parseGeoJson(String.fromCharCodes(bytes), name, id,
|
||||
isVisible: isVisible);
|
||||
case 'kml':
|
||||
return _kmlParser.parseKml(String.fromCharCodes(bytes), name,
|
||||
// return _kmlParser.parseKml(String.fromCharCodes(bytes), name,
|
||||
// id: id, isVisible: isVisible);
|
||||
return _kmlParser.parseKml(KmlParser.decodeText(bytes), name,
|
||||
id: id, isVisible: isVisible);
|
||||
case 'kmz':
|
||||
return _kmlParser.parseKmz(bytes, name, id: id, isVisible: isVisible);
|
||||
|
||||
@@ -4,6 +4,7 @@ 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/app_logger.dart';
|
||||
import 'package:terepi_seged/services/layer_import_service.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -29,6 +30,7 @@ class LayerSyncService extends GetxService {
|
||||
static const bucket = 'terepi_seged-layers';
|
||||
|
||||
final isSyncing = false.obs;
|
||||
final lastError = Rxn<String>();
|
||||
|
||||
SupabaseClient get _client => Supabase.instance.client;
|
||||
AppDatabase get _db => AppDatabase.instance;
|
||||
@@ -55,10 +57,14 @@ class LayerSyncService extends GetxService {
|
||||
// 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);
|
||||
try {
|
||||
await _pullProjectLayers(project.id!, project.uuid);
|
||||
} catch (e) {
|
||||
// EGY projekt rétegeinek hibája ne akassza meg a többi
|
||||
// projektet (vagy a háttér-szinkron többi lépését).
|
||||
lastError.value = 'Réteg-szinkron hiba (${project.name}): $e';
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
@@ -77,34 +83,54 @@ class LayerSyncService extends GetxService {
|
||||
|
||||
for (final r in rows) {
|
||||
final id = r['id'] as String;
|
||||
final remoteVersion = (r['version'] as int?) ?? 1;
|
||||
final localVersion = await _db.getImportedLayerVersion(id);
|
||||
try {
|
||||
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;
|
||||
// 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);
|
||||
// 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);
|
||||
// A helyi fájl kiterjesztése kövesse a TÉNYLEGES forrás-típust —
|
||||
// eddig mindig .geojson-nak mentette, ami a KML/KMZ rétegeket
|
||||
// a GeoJSON-értelmezőre terelte (nyers zip-bájtok jsonDecode-ra).
|
||||
final sourceType = (r['source_type'] as String?) ?? 'geojson';
|
||||
final ext = switch (sourceType) {
|
||||
'kml' => 'kml',
|
||||
'kmz' => 'kmz',
|
||||
_ => 'geojson',
|
||||
};
|
||||
|
||||
// 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,
|
||||
});
|
||||
final dir = await _layerDir();
|
||||
final localPath = p.join(dir.path, '$id.$ext');
|
||||
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': sourceType,
|
||||
'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,
|
||||
});
|
||||
} catch (e) {
|
||||
// EGY réteg hibája (jogosultság, hiányzó storage-fájl, hálózat)
|
||||
// ne akassza meg a TÖBBI réteg letöltését — csak ezt hagyjuk ki,
|
||||
// a következő ciklus újrapróbálja (a verziószám nem lép, tehát
|
||||
// nem "felejtődik el" a hiba, csak nem blokkol tovább).
|
||||
lastError.value = 'Réteg letöltési hiba ($id): $e';
|
||||
print(lastError.value);
|
||||
AppLogger.e('LAYER_SYNC', lastError.value!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
/// Az app fallback csomagneve, ha a konfigurációs táblában nincs
|
||||
/// (vagy még nem tölthető le) update_url.
|
||||
const _fallbackUpdateUrl =
|
||||
'https://play.google.com/store/apps/details?id=hu.app_dev.terepi_seged';
|
||||
|
||||
class VersionGateResult {
|
||||
final bool blocked;
|
||||
final String message;
|
||||
final String updateUrl;
|
||||
|
||||
const VersionGateResult({
|
||||
required this.blocked,
|
||||
this.message = '',
|
||||
this.updateUrl = '',
|
||||
});
|
||||
|
||||
static const allowed = VersionGateResult(blocked: false);
|
||||
}
|
||||
|
||||
/// App-indításkor lefutó minimum-build ellenőrzés.
|
||||
///
|
||||
/// FONTOS — az app offline-first: ha a lekérdezés BÁRMI okból nem
|
||||
/// teljesül (nincs net, időtúllépés, a tábla/kulcs még nem létezik,
|
||||
/// bármilyen váratlan hiba), az ellenőrzés MINDIG átenged (fail-open).
|
||||
/// Kizárólag akkor blokkol, ha a szervertől egyértelmű, jól formázott
|
||||
/// választ kapott, hogy a jelenlegi build a minimum alatt van.
|
||||
Future<VersionGateResult> checkVersionGate() async {
|
||||
try {
|
||||
final rows = await Supabase.instance.client
|
||||
.from('terepi_seged_app_config')
|
||||
.select('key, value')
|
||||
.inFilter('key', const [
|
||||
'min_build_number',
|
||||
'update_message',
|
||||
'update_url',
|
||||
]).timeout(const Duration(seconds: 4));
|
||||
|
||||
final config = {
|
||||
for (final r in rows) r['key'] as String: r['value'] as String,
|
||||
};
|
||||
|
||||
final minBuild = int.tryParse(config['min_build_number'] ?? '');
|
||||
if (minBuild == null) {
|
||||
// Nincs (érvényesen) beállítva minimum — nincs mit ellenőrizni.
|
||||
return VersionGateResult.allowed;
|
||||
}
|
||||
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final currentBuild = int.tryParse(info.buildNumber);
|
||||
if (currentBuild == null) {
|
||||
// A saját build-számunkat sem sikerült megállapítani — ez a mi
|
||||
// hibánk, nem a felhasználóé; nem blokkolunk emiatt.
|
||||
return VersionGateResult.allowed;
|
||||
}
|
||||
|
||||
if (currentBuild < minBuild) {
|
||||
return VersionGateResult(
|
||||
blocked: true,
|
||||
message: config['update_message'] ??
|
||||
'Az alkalmazás egy régebbi verzióját használod, ami már nem '
|
||||
'kompatibilis a szerverrel. Kérjük, telepítsd a legfrissebb '
|
||||
'verziót a folytatáshoz.',
|
||||
updateUrl: config['update_url'] ?? _fallbackUpdateUrl,
|
||||
);
|
||||
}
|
||||
return VersionGateResult.allowed;
|
||||
} catch (_) {
|
||||
// Hálózati hiba, timeout, hiányzó tábla stb. → MINDIG átenged.
|
||||
return VersionGateResult.allowed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user