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 createState() => _AttachmentImageState(); } class _AttachmentImageState extends State { late Future _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 _load() async { final cache = locator(); // 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(); final response = await api.dio.get>( '/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( 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, ), ), ], ), ); } }