import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import '../../model/account_data.dart'; import '../../model/endpoint_data.dart'; import '../../share_intent/remote_file_ref.dart'; import '../app_progress_indicator.dart'; /// Nextcloud's `/index.php/core/preview` endpoint — returns a rasterized /// thumbnail for any file the server has a preview provider for (images, /// PDFs with the right backend, Office in some setups). Falls back to an /// HTTP 404 when no preview is available, which lets [CachedNetworkImage] /// trigger its `errorWidget`. Prefers `fileId` because the path variant /// is unreliable on some server configurations. String _ncPreviewUrl(RemoteFileRef remote, {int width = 1024}) { final host = EndpointData().nextcloud().full(); final id = remote.fileId; final selector = id != null ? 'fileId=$id' : 'file=${Uri.encodeQueryComponent(remote.path)}'; return 'https://$host/index.php/core/preview?$selector&x=$width&y=-1&a=1'; } /// Header block for the "Vorschau nicht verfügbar" screen. /// /// Two visual modes — kept layout-equivalent so the screen looks identical /// whether the server already said "no preview" or the probe failed late: /// * **No preview available** (server said no, no remoteFile, or probe /// errored): compact "file icon + 'Vorschau nicht verfügbar' text". /// * **Preview rendering / loaded**: mid-sized thumbnail without text. class UnknownPreviewBlock extends StatefulWidget { final RemoteFileRef? remoteFile; const UnknownPreviewBlock({super.key, required this.remoteFile}); @override State createState() => _UnknownPreviewBlockState(); } class _UnknownPreviewBlockState extends State { static const double _previewSize = 180; bool _failed = false; Widget _compact(ThemeData theme) => Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.insert_drive_file_outlined, size: 60), const SizedBox(height: 16), Text( 'Vorschau nicht verfügbar', style: theme.textTheme.titleMedium, textAlign: TextAlign.center, ), ], ); @override Widget build(BuildContext context) { final theme = Theme.of(context); final remote = widget.remoteFile; final canProbe = remote != null && remote.hasPreview != false && remote.fileId != null && !_failed; if (!canProbe) return _compact(theme); return SizedBox( width: _previewSize, height: _previewSize, child: CachedNetworkImage( httpHeaders: AccountData().authHeaders(), imageUrl: _ncPreviewUrl(remote, width: 360), fadeInDuration: Duration.zero, fadeOutDuration: Duration.zero, // Late probe failure: re-render into the compact layout so the // screen doesn't keep a 180×180 box around a tiny icon. Deferred // to the next frame because setState during build is illegal. errorListener: (_) { if (!mounted) return; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => _failed = true); }); }, placeholder: (_, _) => const Center(child: AppProgressIndicator.large()), // Briefly empty while the post-frame setState swaps layouts. errorWidget: (_, _, _) => const SizedBox.shrink(), imageBuilder: (_, imageProvider) => ClipRRect( borderRadius: BorderRadius.circular(8), child: Image( image: imageProvider, fit: BoxFit.contain, width: _previewSize, height: _previewSize, ), ), ), ); } }