Final commit.
This commit is contained in:
184
lib/widget/attachment_image.dart
Normal file
184
lib/widget/attachment_image.dart
Normal file
@ -0,0 +1,184 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user