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:
+103
-35
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user