124 lines
4.1 KiB
Dart
124 lines
4.1 KiB
Dart
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<List<MeasuredRecord>> watchRecords(String projectId) {
|
|
late StreamController<List<MeasuredRecord>> controller;
|
|
RealtimeChannel? channel;
|
|
Timer? debounce;
|
|
var disposed = false;
|
|
var fetching = false;
|
|
var again = false;
|
|
|
|
Future<void> 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<List<MeasuredRecord>>(
|
|
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<List<MeasuredRecord>> _fetchAll(String projectId) async {
|
|
final out = <MeasuredRecord>[];
|
|
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<Uint8List?> 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;
|
|
}
|
|
}
|