import 'dart:async'; import 'dart:typed_data'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/measured_record.dart'; /// Mérési felvételek (lövések) élő lekérdezése a PC-szoftver által /// feltöltött táblából. SZÁNDÉKOSAN nincs helyi tükör/gyorsítótár — ez /// folyamatosan érkező, élő adat, mindig a jelenlegi állapotot mutatja. class MeasuredDataService extends GetxService { static MeasuredDataService get to => Get.find(); SupabaseClient get _client => Supabase.instance.client; /// TODO: erősítsd meg a valós bucket-nevet, ha eltér ettől. static const _bucket = 'terepi_seged-measure-images'; static const _pageSize = 1000; /// A legfrissebb ennyi felvételt töltjük be (a PostgREST 1000-es /// max-rows miatt lapozva). static const _maxRecords = 3000; /// SZÁNDÉKOSAN nem `.stream()`: az rendezetlen, 1000 sorra vágott /// lekérdezést futtat, ezért sorok tűnnek el. Helyette szerveroldalon /// szűrt, lapozott lekérdezés + Realtime csatorna, ami csak jelzés /// (debounce-olt újratöltés). Stream> watchRecords(String projectId) { late StreamController> controller; RealtimeChannel? channel; Timer? debounce; var disposed = false; var fetching = false; var again = false; Future refetch() async { if (disposed) return; if (fetching) { again = true; return; } fetching = true; try { do { again = false; final list = await _fetchAll(projectId); if (!disposed) controller.add(list); } while (again && !disposed); } catch (e, s) { if (!disposed) controller.addError(e, s); } finally { fetching = false; } } controller = StreamController>( onListen: () { refetch(); channel = _client .channel('measure_records_$projectId') .onPostgresChanges( event: PostgresChangeEvent.all, schema: 'public', table: 'terepi_seged_measure_records', filter: PostgresChangeFilter( type: PostgresChangeFilterType.eq, column: 'project_id', value: projectId, ), callback: (_) { debounce?.cancel(); debounce = Timer(const Duration(milliseconds: 800), refetch); }, ) .subscribe(); }, onCancel: () async { disposed = true; debounce?.cancel(); if (channel != null) await _client.removeChannel(channel!); }, ); return controller.stream; } Future> _fetchAll(String projectId) async { final out = []; var from = 0; while (out.length < _maxRecords) { final rows = await _client .from('terepi_seged_measure_records') .select() .eq('project_id', projectId) .order('measured_at', ascending: false) .order('id', ascending: false) .range(from, from + _pageSize - 1); for (final r in rows) { out.add(MeasuredRecord.fromMap(r)); } if (rows.length < _pageSize) break; from += _pageSize; } return out; } /// A felvételhez tartozó kép betöltése — elsőbbséget adva az image_url-nek /// (ha a PC-szoftver már kész, letölthető URL-t is küld), egyébként a /// Storage-ból, image_path alapján. Future loadImage(MeasuredRecord record) async { if (record.imageUrl != null && record.imageUrl!.isNotEmpty) { final resp = await http.get(Uri.parse(record.imageUrl!)); if (resp.statusCode == 200) return resp.bodyBytes; throw Exception('Kép letöltése sikertelen (${resp.statusCode})'); } if (record.imagePath != null && record.imagePath!.isNotEmpty) { return _client.storage.from(_bucket).download(record.imagePath!); } return null; } }