// Kétszintű Supabase szinkronizáció: // 1. Élő pozíció — minden 3 mp-ben UPSERT → device_positions // 2. Track pontok — batch INSERT (10 pont vagy 8 mp) → terepi_track_points import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'app_database.dart'; import 'device_identity_service.dart'; import '../models/track.dart'; class TrackSyncService extends GetxService { static TrackSyncService get to => Get.find(); final _supabase = Supabase.instance.client; // ── Konfiguráció ────────────────────────────────────────────────── static const _batchSize = 10; static const _batchIntervalSec = 8; static const _positionIntervalSec = 3; // ── Belső állapot ───────────────────────────────────────────────── final _buffer = []; Timer? _batchTimer; Timer? _posTimer; Track? _track; LatLng? _lastPos; // utoljára kapott pozíció (broadcasthoz) bool _online = false; // ── Publikus állapot ────────────────────────────────────────────── final isSyncing = false.obs; final pendingCount = 0.obs; // ── Lifecycle ───────────────────────────────────────────────────── @override Future onInit() async { super.onInit(); _online = await _checkOnline(); _listenConnectivity(); } @override void onClose() { _stopTimers(); super.onClose(); } // ── Session vezérlés (TrackingController hívja) ─────────────────── void startSession(Track track) { _track = track; _buffer.clear(); pendingCount.value = 0; if (track.isLocalOnly) return; _batchTimer = Timer.periodic( const Duration(seconds: _batchIntervalSec), (_) => _flush(), ); _posTimer = Timer.periodic( const Duration(seconds: _positionIntervalSec), (_) => _broadcastPosition(), ); } Future stopSession(Track track) async { _stopTimers(); if (track.isLocalOnly) return; await _flush(); // utolsó batch // Track fejléc lezárása Supabase-ben if (track.supabaseId != null) { await _supabase.from('terepi_seged_tracks').update({ 'end_time': track.endTime?.toIso8601String(), 'status': 'finished', 'distance_m': track.distanceMeters, 'point_count': track.pointCount, }).eq('id', track.supabaseId!); } await _setInactive(); _track = null; _lastPos = null; } // ── Pont pufferelés ─────────────────────────────────────────────── /// TrackingController._onPosition() hívja minden pontnál void onNewPoint(TrackPoint point) { _lastPos = LatLng(point.latitude, point.longitude); if (_track == null || _track!.isLocalOnly) return; _buffer.add(point); pendingCount.value = _buffer.length; if (_buffer.length >= _batchSize) _flush(); } // ── Supabase track létrehozása ──────────────────────────────────── /// startRecording()-ban hívandó, visszaadja a Supabase UUID-t Future createRemoteTrack(Track track) async { if (!_online) return null; try { final res = await _supabase .from('terepi_seged_tracks') .insert({ 'device_id': DeviceIdentityService.to.deviceId, 'name': track.name, 'source': track.source, 'start_time': track.startTime.toIso8601String(), 'status': 'recording', }) .select('id') .single(); return res['id'] as String?; } catch (e) { return null; } } // ── Batch feltöltés ─────────────────────────────────────────────── Future _flush() async { if (_buffer.isEmpty || !_online) return; final supabaseId = _track?.supabaseId; if (supabaseId == null) return; final batch = List.from(_buffer); _buffer.clear(); pendingCount.value = 0; try { isSyncing.value = true; await _supabase.from('terepi_seged_track_points').insert( batch .map((p) => { 'track_id': supabaseId, 'latitude': p.latitude, 'longitude': p.longitude, 'altitude': p.altitude, 'accuracy': p.accuracy, 'speed': p.speed, 'heading': p.heading, 'timestamp': p.timestamp.toIso8601String(), }) .toList(), ); } catch (_) { // Hiba → visszateszi a bufferbe _buffer.insertAll(0, batch); pendingCount.value = _buffer.length; } finally { isSyncing.value = false; } } // ── Élő pozíció broadcast ───────────────────────────────────────── Future _broadcastPosition() async { final pos = _lastPos; if (pos == null || !_online) return; final device = DeviceIdentityService.to; try { await _supabase.from('terepi_seged_device_positions').upsert({ 'device_id': device.deviceId, 'user_name': device.deviceLabel.value, 'latitude': pos.latitude, 'longitude': pos.longitude, 'track_id': _track?.supabaseId, 'is_active': true, 'updated_at': DateTime.now().toUtc().toIso8601String(), }, onConflict: 'device_id'); } catch (_) {} } Future _setInactive() async { final deviceId = DeviceIdentityService.to.deviceId; try { await _supabase.from('terepi_seged_device_positions').update( {'is_active': false, 'track_id': null}).eq('device_id', deviceId); } catch (_) {} } // ── Offline → Online szinkron ───────────────────────────────────── Future syncTrack(Track track) async { if (!_online) return; String? supabaseId = track.supabaseId; if (supabaseId == null) { supabaseId = await createRemoteTrack(track); if (supabaseId == null) return; final updated = track.copyWith(supabaseId: supabaseId); await AppDatabase.instance.updateTrack(updated); } final points = await AppDatabase.instance.getPoints(track.id!); if (points.isEmpty) return; const chunk = 100; for (int i = 0; i < points.length; i += chunk) { final slice = points.sublist(i, (i + chunk).clamp(0, points.length)); await _supabase.from('terepi_seged_track_points').insert( slice .map((p) => { 'track_id': supabaseId, 'latitude': p.latitude, 'longitude': p.longitude, 'altitude': p.altitude, 'accuracy': p.accuracy, 'speed': p.speed, 'heading': p.heading, 'timestamp': p.timestamp.toIso8601String(), }) .toList(), ); } await AppDatabase.instance.updateTrack( track.copyWith(supabaseId: supabaseId), ); } // ── Kapcsolat figyelés ──────────────────────────────────────────── void _listenConnectivity() { Connectivity().onConnectivityChanged.listen((results) async { final wasOffline = !_online; _online = results.any((r) => r != ConnectivityResult.none); if (wasOffline && _online) await _flush(); }); } Future _checkOnline() async { final r = await Connectivity().checkConnectivity(); return r.any((r) => r != ConnectivityResult.none); } void _stopTimers() { _batchTimer?.cancel(); _posTimer?.cancel(); _batchTimer = null; _posTimer = null; } }