PWA lud Mangel-Fotos bislang komplett unkomprimiert hoch (rohes File aus capture-Input) - Canvas-basierte Verkleinerung/Neukodierung (max. 1600px, JPEG q0.7) vor dem Enqueue ergänzt. Flutter-Client nutzte nur imageQuality (reine JPEG-Kompression ohne Auflösungs-Downscale) - maxWidth/maxHeight ergänzt. Beides zahlt sich bei schlechter Feld-Verbindung aus. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN
182 lines
6.3 KiB
Dart
182 lines
6.3 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:image_picker/image_picker.dart';
|
||
import 'package:path/path.dart' as p;
|
||
import 'package:path_provider/path_provider.dart';
|
||
|
||
import '../api/api_client.dart';
|
||
import '../offline/mangel_queue.dart';
|
||
|
||
/// MOBILE-004-Nachzug: Android-Pendant zu frontend/src/pages/MangelListePage.tsx
|
||
/// (Melde-Teil) - läuft wie die Kontroll-Erfassung immer über die lokale
|
||
/// Queue (MangelQueue), nie direkt blockierend gegen die API.
|
||
class MangelMeldenScreen extends StatefulWidget {
|
||
const MangelMeldenScreen({super.key, required this.apiClient, required this.objektId});
|
||
final ApiClient apiClient;
|
||
final int objektId;
|
||
|
||
@override
|
||
State<MangelMeldenScreen> createState() => _MangelMeldenScreenState();
|
||
}
|
||
|
||
class _MangelMeldenScreenState extends State<MangelMeldenScreen> {
|
||
final _beschreibungController = TextEditingController();
|
||
String _prioritaet = 'normal';
|
||
File? _foto;
|
||
bool _wirdGespeichert = false;
|
||
String? _fehler;
|
||
bool _online = true;
|
||
|
||
final _connectivity = Connectivity();
|
||
StreamSubscription<List<ConnectivityResult>>? _connectivitySub;
|
||
Timer? _periodischerSyncTimer;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_connectivity.checkConnectivity().then((ergebnis) {
|
||
if (mounted) setState(() => _online = !ergebnis.contains(ConnectivityResult.none));
|
||
});
|
||
_connectivitySub = _connectivity.onConnectivityChanged.listen((ergebnis) {
|
||
final online = !ergebnis.contains(ConnectivityResult.none);
|
||
if (online && !_online) {
|
||
unawaited(MangelQueue.instance.synchronisiere(widget.apiClient));
|
||
}
|
||
if (mounted) setState(() => _online = online);
|
||
});
|
||
_periodischerSyncTimer = Timer.periodic(const Duration(seconds: 15), (_) {
|
||
unawaited(MangelQueue.instance.synchronisiere(widget.apiClient));
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_connectivitySub?.cancel();
|
||
_periodischerSyncTimer?.cancel();
|
||
_beschreibungController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _fotoAufnehmen() async {
|
||
// Fund (Foto-Kompression-Check): `imageQuality` allein komprimiert nur die
|
||
// JPEG-Qualität, verkleinert aber NICHT die Auflösung - ein 12MP-Kamerafoto
|
||
// bleibt trotzdem mehrere MB groß. `maxWidth`/`maxHeight` skaliert zusätzlich
|
||
// herunter, was bei schlechter Feld-Verbindung den eigentlichen Unterschied
|
||
// macht (1600px reicht für einen Mangel-Nachweis völlig aus).
|
||
final bild = await ImagePicker().pickImage(
|
||
source: ImageSource.camera,
|
||
imageQuality: 70,
|
||
maxWidth: 1600,
|
||
maxHeight: 1600,
|
||
);
|
||
if (bild == null || !mounted) return;
|
||
setState(() => _foto = File(bild.path));
|
||
}
|
||
|
||
Future<void> _melden() async {
|
||
final beschreibung = _beschreibungController.text.trim();
|
||
if (beschreibung.isEmpty) {
|
||
setState(() => _fehler = 'Beschreibung darf nicht leer sein.');
|
||
return;
|
||
}
|
||
setState(() {
|
||
_wirdGespeichert = true;
|
||
_fehler = null;
|
||
});
|
||
|
||
// Foto in App-eigenes Verzeichnis kopieren - die von image_picker gelieferte
|
||
// Datei liegt in einem temporären Cache, der vom System jederzeit geräumt
|
||
// werden kann, während der Mangel noch in der Queue auf Sync wartet.
|
||
String? fotoPfad;
|
||
final foto = _foto;
|
||
if (foto != null) {
|
||
final verzeichnis = await getApplicationDocumentsDirectory();
|
||
final ziel = p.join(
|
||
verzeichnis.path,
|
||
'mangel_foto_${DateTime.now().microsecondsSinceEpoch}${p.extension(foto.path)}',
|
||
);
|
||
await foto.copy(ziel);
|
||
fotoPfad = ziel;
|
||
}
|
||
|
||
await MangelQueue.instance.enqueue(
|
||
objektId: widget.objektId,
|
||
beschreibung: beschreibung,
|
||
prioritaet: _prioritaet,
|
||
fotoPfad: fotoPfad,
|
||
);
|
||
unawaited(MangelQueue.instance.synchronisiere(widget.apiClient));
|
||
|
||
if (mounted) Navigator.of(context).pop();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('Mangel melden')),
|
||
body: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (!_online)
|
||
Container(
|
||
width: double.infinity,
|
||
color: Colors.amber.shade100,
|
||
padding: const EdgeInsets.all(8),
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
child: const Text(
|
||
'Kein Netz – Meldung wird lokal gespeichert und automatisch übertragen, '
|
||
'sobald wieder Verbindung besteht.',
|
||
),
|
||
),
|
||
TextField(
|
||
controller: _beschreibungController,
|
||
decoration: const InputDecoration(labelText: 'Beschreibung'),
|
||
maxLines: 4,
|
||
),
|
||
const SizedBox(height: 12),
|
||
DropdownButtonFormField<String>(
|
||
initialValue: _prioritaet,
|
||
decoration: const InputDecoration(labelText: 'Priorität'),
|
||
items: const [
|
||
DropdownMenuItem(value: 'niedrig', child: Text('Niedrig')),
|
||
DropdownMenuItem(value: 'normal', child: Text('Normal')),
|
||
DropdownMenuItem(value: 'hoch', child: Text('Hoch')),
|
||
],
|
||
onChanged: (wert) => setState(() => _prioritaet = wert ?? 'normal'),
|
||
),
|
||
const SizedBox(height: 12),
|
||
OutlinedButton.icon(
|
||
onPressed: _fotoAufnehmen,
|
||
icon: const Icon(Icons.camera_alt),
|
||
label: Text(_foto == null ? 'Foto aufnehmen' : 'Foto ersetzen'),
|
||
),
|
||
if (_foto != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 8),
|
||
child: Image.file(_foto!, height: 120),
|
||
),
|
||
if (_fehler != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 12),
|
||
child: Text(_fehler!, style: const TextStyle(color: Colors.red)),
|
||
),
|
||
const SizedBox(height: 20),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: FilledButton(
|
||
onPressed: _wirdGespeichert ? null : _melden,
|
||
child: const Text('Melden'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|