100 lines
3.7 KiB
Dart
100 lines
3.7 KiB
Dart
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<UnknownPreviewBlock> createState() => _UnknownPreviewBlockState();
|
||
}
|
||
|
||
class _UnknownPreviewBlockState extends State<UnknownPreviewBlock> {
|
||
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,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|