258 lines
8.4 KiB
Dart
258 lines
8.4 KiB
Dart
|
|
// 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 = <TrackPoint>[];
|
||
|
|
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<void> 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<void> 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<String?> 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<void> _flush() async {
|
||
|
|
if (_buffer.isEmpty || !_online) return;
|
||
|
|
|
||
|
|
final supabaseId = _track?.supabaseId;
|
||
|
|
if (supabaseId == null) return;
|
||
|
|
|
||
|
|
final batch = List<TrackPoint>.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<void> _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<void> _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<void> 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<bool> _checkOnline() async {
|
||
|
|
final r = await Connectivity().checkConnectivity();
|
||
|
|
return r.any((r) => r != ConnectivityResult.none);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _stopTimers() {
|
||
|
|
_batchTimer?.cancel();
|
||
|
|
_posTimer?.cancel();
|
||
|
|
_batchTimer = null;
|
||
|
|
_posTimer = null;
|
||
|
|
}
|
||
|
|
}
|