implemented Nextcloud file previews for unknown file types using fileId and has-preview flags, updated file models, and refined manual refresh logic.

This commit is contained in:
2026-05-13 19:44:26 +02:00
parent 843686358f
commit 092f9b622b
6 changed files with 174 additions and 41 deletions
+103 -35
View File
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
@@ -14,6 +15,8 @@ import 'package:share_plus/share_plus.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import 'package:video_player/video_player.dart';
import '../model/account_data.dart';
import '../model/endpoint_data.dart';
import '../routing/app_routes.dart';
import '../share_intent/remote_file_ref.dart';
import '../state/app/modules/settings/bloc/settings_cubit.dart';
@@ -22,6 +25,21 @@ import 'centered_leading.dart';
import 'info_dialog.dart';
import 'share_position_origin.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';
}
class FileViewer extends StatefulWidget {
final String path;
final bool openExternal;
@@ -482,52 +500,58 @@ class _FileViewerState extends State<FileViewer> {
),
);
Widget _buildUnknownView() {
Widget _buildUnknownView() => Scaffold(
appBar: _appbar(showActionsMenu: false),
body: _buildUnknownPlaceholder(),
);
Widget _buildUnknownPlaceholder() {
final theme = Theme.of(context);
final descriptors = _availableActions();
return Scaffold(
appBar: _appbar(showActionsMenu: false),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 24),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
const Icon(Icons.insert_drive_file_outlined, size: 60),
const SizedBox(height: 16),
final remote = widget.remoteFile;
final hasPreview = remote?.hasPreview == true;
return ListView(
padding: const EdgeInsets.symmetric(vertical: 24),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
_UnknownPreviewHeader(remoteFile: remote),
const SizedBox(height: 16),
if (!hasPreview) ...[
Text(
'Vorschau nicht verfügbar',
style: theme.textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text(
widget.path.split('/').last,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Wähle eine Aktion, um mit der Datei weiterzuarbeiten.',
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
],
),
Text(
widget.path.split('/').last,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Wähle eine Aktion, um mit der Datei weiterzuarbeiten.',
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 24),
...descriptors.map(
(d) => ListTile(
leading: CenteredLeading(Icon(d.icon)),
title: Text(d.label),
onTap: () => _handleAction(d.action),
),
),
const SizedBox(height: 24),
...descriptors.map(
(d) => ListTile(
leading: CenteredLeading(Icon(d.icon)),
title: Text(d.label),
onTap: () => _handleAction(d.action),
),
],
),
),
],
);
}
@@ -583,6 +607,50 @@ class _TextPayload {
const _TextPayload({required this.content, required this.truncated});
}
/// Header for the "Vorschau nicht verfügbar" screen: tries to fetch the
/// Nextcloud thumbnail and shows it mid-sized if available, otherwise
/// falls back to the generic file icon.
class _UnknownPreviewHeader extends StatelessWidget {
final RemoteFileRef? remoteFile;
const _UnknownPreviewHeader({required this.remoteFile});
static const double _previewSize = 180;
Widget _fallbackIcon() =>
const Icon(Icons.insert_drive_file_outlined, size: 60);
@override
Widget build(BuildContext context) {
final remote = remoteFile;
// Skip the probe outright when the server already told us there is no
// preview — saves an HTTP request that would 404 anyway.
if (remote == null || remote.hasPreview == false) return _fallbackIcon();
return SizedBox(
width: _previewSize,
height: _previewSize,
child: CachedNetworkImage(
httpHeaders: AccountData().authHeaders(),
imageUrl: _ncPreviewUrl(remote, width: 360),
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
errorListener: (_) {},
placeholder: (_, _) =>
const Center(child: AppProgressIndicator.large()),
errorWidget: (_, _, _) => Center(child: _fallbackIcon()),
imageBuilder: (_, imageProvider) => ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image(
image: imageProvider,
fit: BoxFit.contain,
width: _previewSize,
height: _previewSize,
),
),
),
);
}
}
class _MediaPlayer extends StatefulWidget {
final String path;
final bool isAudio;