185 lines
5.4 KiB
Dart
185 lines
5.4 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:holzleitner_api/holzleitner_api.dart' show HolzleitnerApi;
|
|
|
|
import 'package:hl_lieferservice/data/cache/attachment_cache.dart';
|
|
import 'package:hl_lieferservice/main.dart' show locator;
|
|
|
|
/// Lädt und zeigt ein Attachment-Vorschaubild über
|
|
/// `GET /attachments/{id}` an. Nutzt die geteilte, authentifizierte
|
|
/// Dio-Instanz (`HolzleitnerApi.dio`) — der Auth-Interceptor hängt den
|
|
/// Bearer-Token an, ein generierter Client-Methodenaufruf ist für die
|
|
/// Binär-Antwort nicht zuverlässig.
|
|
///
|
|
/// Reines Lese-Widget (kein Bloc): Bild-Anzeige ist kein App-State.
|
|
class AttachmentImage extends StatefulWidget {
|
|
const AttachmentImage({
|
|
super.key,
|
|
required this.attachmentId,
|
|
this.width = 1024,
|
|
this.height = 1024,
|
|
this.quality = 80,
|
|
this.fit = BoxFit.cover,
|
|
this.deleted = false,
|
|
});
|
|
|
|
/// Unsere Attachment-UUID (steht in `DeliveryNote.imageAttachment`).
|
|
final String attachmentId;
|
|
|
|
/// Wenn `true`: die lokale Bilddatei wurde nach dem Report-Upload gelöscht
|
|
/// (das Bild steckt im Lieferbericht in DOCUframe). Statt eines Downloads
|
|
/// wird ein Hinweis gezeigt.
|
|
final bool deleted;
|
|
|
|
/// Angefragte Vorschau-Abmessungen (Backend rendert per DOCUframe).
|
|
final int width;
|
|
final int height;
|
|
final int quality;
|
|
final BoxFit fit;
|
|
|
|
@override
|
|
State<AttachmentImage> createState() => _AttachmentImageState();
|
|
}
|
|
|
|
class _AttachmentImageState extends State<AttachmentImage> {
|
|
late Future<Uint8List> _future;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Gelöschte Anhänge nicht laden — es gibt keine lokale Datei mehr.
|
|
_future = widget.deleted ? Future.value(Uint8List(0)) : _load();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(AttachmentImage oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.attachmentId != widget.attachmentId ||
|
|
oldWidget.width != widget.width ||
|
|
oldWidget.height != widget.height ||
|
|
oldWidget.quality != widget.quality) {
|
|
_future = _load();
|
|
}
|
|
}
|
|
|
|
static const _ext = 'jpeg';
|
|
|
|
Future<Uint8List> _load() async {
|
|
final cache = locator<AttachmentCache>();
|
|
|
|
// 1. Disk-Cache zuerst — Attachments sind unveränderlich, ein Treffer
|
|
// ist also immer gültig (auch offline).
|
|
final cached = await cache.read(
|
|
attachmentId: widget.attachmentId,
|
|
w: widget.width,
|
|
h: widget.height,
|
|
q: widget.quality,
|
|
ext: _ext,
|
|
);
|
|
if (cached != null) return cached;
|
|
|
|
// 2. Miss → über die authentifizierte Dio-Instanz aus DOCUframe holen.
|
|
final api = locator<HolzleitnerApi>();
|
|
final response = await api.dio.get<List<int>>(
|
|
'/attachments/${widget.attachmentId}',
|
|
queryParameters: {
|
|
'w': widget.width,
|
|
'h': widget.height,
|
|
'q': widget.quality,
|
|
'ext': _ext,
|
|
},
|
|
options: Options(responseType: ResponseType.bytes),
|
|
);
|
|
final bytes = Uint8List.fromList(response.data ?? const []);
|
|
|
|
// 3. Erfolgreichen Download persistieren (best-effort, blockiert die
|
|
// Anzeige nicht — write schluckt Fehler).
|
|
if (bytes.isNotEmpty) {
|
|
await cache.write(
|
|
attachmentId: widget.attachmentId,
|
|
w: widget.width,
|
|
h: widget.height,
|
|
q: widget.quality,
|
|
ext: _ext,
|
|
bytes: bytes,
|
|
);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
if (widget.deleted) {
|
|
return _DeletedHint(fit: widget.fit);
|
|
}
|
|
return FutureBuilder<Uint8List>(
|
|
future: _future,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState != ConnectionState.done) {
|
|
return const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: SizedBox(
|
|
width: 22,
|
|
height: 22,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
if (snapshot.hasError ||
|
|
snapshot.data == null ||
|
|
snapshot.data!.isEmpty) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Icon(
|
|
Icons.broken_image_outlined,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return Image.memory(snapshot.data!, fit: widget.fit);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Hinweis für gelöschte Bild-Anhänge: das Bild liegt im Lieferbericht.
|
|
class _DeletedHint extends StatelessWidget {
|
|
const _DeletedHint({required this.fit});
|
|
|
|
final BoxFit fit;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Container(
|
|
color: theme.colorScheme.surfaceContainerHighest,
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.picture_as_pdf_outlined,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Bild im Lieferbericht enthalten',
|
|
textAlign: TextAlign.center,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|