Felvételek képének megjelenítése a Mérés menüpontbam
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s

This commit is contained in:
2026-09-09 02:05:10 +02:00
parent c335b76d8b
commit e801090e90
7 changed files with 364 additions and 134 deletions
@@ -1,56 +1,113 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/models/seismic_records.dart';
import 'package:terepi_seged/models/measured_record.dart';
import 'package:terepi_seged/pages/measured_data/presentation/views/seismogram_viewer_dialog.dart';
import 'package:terepi_seged/services/measured_data_service.dart';
import 'package:terepi_seged/services/project_service.dart';
class MeasuredDataController extends GetxController {
final CollectionReference _seismicRecords =
FirebaseFirestore.instance.collection('seismicRecords');
static MeasuredDataController get to => Get.find();
// late Query<Map<String, dynamic>> records = _seismicRecords.orderBy('field');
late Stream<QuerySnapshot> seismicRecordSnapshot;
final records = <MeasuredRecord>[].obs;
final isLoading = true.obs;
final searchText = ''.obs;
final lineFilter = Rxn<double>();
@override
void onReady() async {
super.onReady();
}
StreamSubscription<List<MeasuredRecord>>? _sub;
@override
void onInit() {
super.onInit();
_subscribe();
ever(ProjectService.to.activeProject, (_) => _subscribe());
}
seismicRecordSnapshot = _seismicRecords
// .orderBy('sourcePoint', descending: true)
.where('gpsDateTime', isGreaterThan: DateTime(2024, 2, 25, 0, 0, 0))
.orderBy('gpsDateTime', descending: true)
.snapshots();
void _subscribe() {
_sub?.cancel();
lineFilter.value = null;
final projectId = ProjectService.to.activeProject.value?.uuid;
if (projectId == null) {
records.clear();
isLoading.value = false;
return;
}
isLoading.value = true;
_sub = MeasuredDataService.to.watchRecords(projectId).listen(
(data) {
records.value = data;
isLoading.value = false;
},
onError: (e) {
isLoading.value = false;
Get.snackbar('Hiba', 'Nem sikerült betölteni a mérési adatokat: $e',
snackPosition: SnackPosition.BOTTOM);
},
);
}
// ── Keresés/szűrés — a már memóriában lévő, élő listán, nem újabb
// lekérdezéssel (a teljes projekt-adat úgyis a stream-ben van). ──
List<MeasuredRecord> get filteredRecords {
var list = records.toList();
if (lineFilter.value != null) {
list = list.where((r) => r.sourceLineNumber == lineFilter.value).toList();
}
final q = searchText.value.trim();
if (q.isNotEmpty) {
list = list.where((r) {
return (r.ffid?.toString().contains(q) ?? false) ||
(r.sourcePointNumber?.toString().contains(q) ?? false) ||
(r.sourceLineNumber?.toString().contains(q) ?? false);
}).toList();
}
return list;
}
List<double> get availableLines {
final set =
records.map((r) => r.sourceLineNumber).whereType<double>().toSet();
final list = set.toList()..sort();
return list;
}
void onSearchChanged(String v) => searchText.value = v;
void setLineFilter(double? v) => lineFilter.value = v;
// ── Kép megtekintése ─────────────────────────────────────────────
Future<void> showSeismicImage(MeasuredRecord record) async {
if (!record.hasImage) {
Get.snackbar('Nincs kép', 'Ehhez a felvételhez nincs rögzített kép.',
snackPosition: SnackPosition.BOTTOM);
return;
}
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
try {
final Uint8List? bytes = await MeasuredDataService.to.loadImage(record);
Get.back(); // töltő-dialógus bezárása
if (bytes == null) {
Get.snackbar('Hiba', 'A kép nem tölthető be.',
snackPosition: SnackPosition.BOTTOM);
return;
}
Get.to(() => SeismogramViewerDialog(record: record, image: bytes),
transition: Transition.downToUp);
} catch (e) {
Get.back();
Get.snackbar('Hiba', 'Kép letöltése sikertelen: $e',
snackPosition: SnackPosition.BOTTOM);
}
}
@override
void onClose() async {
void onClose() {
_sub?.cancel();
super.onClose();
// seismicRecordSnapshot.
}
void showSeismicImage(int ffid) async {
late Uint8List image;
Reference reference =
FirebaseStorage.instance.ref().child('bp-4/${ffid}.jpg');
await reference
.getData()
.then((value) => {image = value!})
.catchError((error) => {});
Get.to(
() => SeismogramViewerDialog(
imageUrl: '',
image: image,
),
transition: Transition.downToUp);
}
}
@@ -1,83 +1,123 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/models/measured_record.dart';
import 'package:terepi_seged/pages/measured_data/presentation/controllers/measured_data_controller.dart';
import 'package:terepi_seged/pages/measured_data/presentation/views/seismogram_viewer_dialog.dart';
import 'package:terepi_seged/services/project_service.dart';
class MeasuredDataView extends GetView<MeasuredDataController> {
const MeasuredDataView({Key? key}) : super(key: key);
const MeasuredDataView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
extendBody: true,
appBar: AppBar(
title: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Mérési adatok'),
Text(
"Budapest",
style: TextStyle(fontSize: 12.0),
)
],
appBar: AppBar(
title: Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Mérési adatok'),
Text(
ProjectService.to.activeProject.value?.name ?? '',
style: const TextStyle(fontSize: 12.0),
),
],
)),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
flex: 2,
child: TextField(
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: 'FFID / forráspont / vonal…',
border: OutlineInputBorder(),
isDense: true,
),
onChanged: controller.onSearchChanged,
),
),
const SizedBox(width: 8),
Expanded(
child: Obx(() => DropdownButtonFormField<double?>(
value: controller.lineFilter.value,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Vonal', isDense: true),
items: [
const DropdownMenuItem(
value: null, child: Text('Mind')),
...controller.availableLines.map((l) =>
DropdownMenuItem(
value: l, child: Text(_fmtNum(l)))),
],
onChanged: controller.setLineFilter,
)),
),
],
),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 20.0),
child: GestureDetector(
onTap: () => Get.to(() => {}, transition: Transition.downToUp),
child: const Icon(
Icons.more_vert,
size: 26.0,
),
),
)
],
),
body: Column(children: [
Text("Mérési adatok"),
StreamBuilder<QuerySnapshot>(
stream: controller.seismicRecordSnapshot,
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const Text('Hiba ...');
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text('Adatbetöltés ...');
final list = controller.filteredRecords;
if (list.isEmpty) {
return const Center(child: Text('Nincs mérési adat.'));
}
return Expanded(
child: ListView(
// shrinkWrap: true,
children: snapshot.data!.docs
.map((DocumentSnapshot document) {
Map<String, dynamic> data =
document.data()! as Map<String, dynamic>;
return ListTile(
title: Text(data['sourcePoint'].toString()),
subtitle: Text(
'${data['sourceLine'].toString()} - ${(data['gpsDateTime'] as Timestamp).toDate()}'),
trailing: Wrap(spacing: -16, children: [
IconButton(
onPressed: () {
controller.showSeismicImage(data['ffid']);
},
icon: const Icon(Icons.image_outlined)),
IconButton(
onPressed: () {},
icon:
const Icon(Icons.arrow_forward_ios_sharp))
]));
})
.toList()
.cast(),
),
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, i) => _RecordTile(record: list[i]),
);
},
)
]));
}),
),
],
),
);
}
}
String _fmtNum(double v) =>
v == v.roundToDouble() ? v.toInt().toString() : v.toString();
String _fmtDate(DateTime dt) {
final local = dt.toLocal();
String two(int n) => n.toString().padLeft(2, '0');
return '${local.year}.${two(local.month)}.${two(local.day)} '
'${two(local.hour)}:${two(local.minute)}:${two(local.second)}';
}
class _RecordTile extends StatelessWidget {
final MeasuredRecord record;
const _RecordTile({required this.record});
@override
Widget build(BuildContext context) {
final hasImage = record.hasImage;
return ListTile(
title: Text(
'FFID ${record.ffid ?? "—"} · VP '
'${record.sourcePointNumber != null ? _fmtNum(record.sourcePointNumber!) : "—"}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
'Vonal ${record.sourceLineNumber != null ? _fmtNum(record.sourceLineNumber!) : "—"}'
'${record.geophoneRange != null ? " · Geofon ${record.geophoneRange}" : ""}'
'\n${_fmtDate(record.measuredAt)}',
),
isThreeLine: true,
trailing: Icon(
hasImage ? Icons.image_outlined : Icons.image_not_supported_outlined,
color: hasImage ? null : Colors.grey.shade400,
),
onTap: hasImage
? () => MeasuredDataController.to.showSeismicImage(record)
: null,
);
}
}
@@ -2,17 +2,19 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:photo_view/photo_view.dart';
import 'package:terepi_seged/pages/measured_data/presentation/controllers/measured_data_controller.dart';
import 'package:widget_zoom/widget_zoom.dart';
import '../../../../models/measured_record.dart';
class SeismogramViewerDialog extends StatelessWidget {
final controller = Get.find<MeasuredDataController>();
final String imageUrl;
final MeasuredRecord record;
final Uint8List image;
// final double sourcePoint;
SeismogramViewerDialog(
{required this.imageUrl, required this.image, super.key});
const SeismogramViewerDialog({
required this.record,
required this.image,
super.key,
});
@override
Widget build(BuildContext context) {
@@ -24,43 +26,47 @@ class SeismogramViewerDialog extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(Icons.close)),
IconButton(onPressed: Get.back, icon: const Icon(Icons.close)),
TextButton(
style: ButtonStyle(
overlayColor:
MaterialStateProperty.all(Colors.transparent)),
onPressed: () {
Get.back();
},
child: const Text(
'Bezár',
style: TextStyle(color: Colors.blue, fontSize: 14.0),
))
style: ButtonStyle(
overlayColor:
MaterialStateProperty.all(Colors.transparent)),
onPressed: Get.back,
child: const Text(
'Bezár',
style: TextStyle(color: Colors.blue, fontSize: 14.0),
),
),
],
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 15.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: Text(
'Felvétel megtekintése ()',
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
// Az eredeti verzióban ez a cím mindig üresen jelent meg
// ("Felvétel megtekintése ()") — itt tényleges adattal.
'Felvétel megtekintése — FFID ${record.ffid ?? "—"}',
style:
const TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
if (record.geophoneRange != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: Text('Geofon-tartomány: ${record.geophoneRange}',
style: TextStyle(fontSize: 13, color: Colors.grey.shade600)),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Center(
child: WidgetZoom(
zoomWidget: Image(image: MemoryImage(image)),
heroAnimationTag: 'tag',
heroAnimationTag: 'seismogram_${record.id}',
maxScaleFullscreen: 8,
maxScaleEmbeddedView: 8,
),
),
)
),
],
),
);