implemented a comprehensive Nextcloud file sharing system with support for user, group, and public link shares with gating based on server-side permissions; added sharing management interfaces including a share sheet; updated the file list with visual badges for incoming shares and improved OCS API response handling.

This commit is contained in:
2026-06-02 21:42:08 +02:00
parent b6d06dd3b4
commit baa26a6e79
33 changed files with 2453 additions and 29 deletions
@@ -24,6 +24,11 @@ class CacheableFile {
/// when a preview is going to load anyway.
bool? hasPreview;
/// True when this entry is an incoming share — i.e. shared with the current
/// user by someone else (`nc:mount-type == 'shared'`). Used to badge the
/// file/folder in the list. Nullable so older cached entries decode fine.
bool? isSharedWithMe;
CacheableFile({
required this.path,
required this.isDirectory,
@@ -35,6 +40,7 @@ class CacheableFile {
this.modifiedAt,
this.fileId,
this.hasPreview,
this.isSharedWithMe,
});
CacheableFile.fromDavFile(WebDavFile file) {
@@ -48,6 +54,11 @@ class CacheableFile {
modifiedAt = file.lastModified;
fileId = int.tryParse(file.fileId ?? '');
hasPreview = file.hasPreview;
// Incoming share: the item is mounted into the user's files by someone
// else. Outgoing shares ([isSharedByMe]) can't be derived from WebDAV with
// the pinned package, so they are filled in by ListFiles via one OCS call
// per folder.
isSharedWithMe = file.props.ncmounttype == 'shared';
}
factory CacheableFile.fromJson(Map<String, dynamic> json) =>
@@ -22,6 +22,7 @@ CacheableFile _$CacheableFileFromJson(Map<String, dynamic> json) =>
: DateTime.parse(json['modifiedAt'] as String),
fileId: (json['fileId'] as num?)?.toInt(),
hasPreview: json['hasPreview'] as bool?,
isSharedWithMe: json['isSharedWithMe'] as bool?,
);
Map<String, dynamic> _$CacheableFileToJson(CacheableFile instance) =>
@@ -36,4 +37,5 @@ Map<String, dynamic> _$CacheableFileToJson(CacheableFile instance) =>
'modifiedAt': instance.modifiedAt?.toIso8601String(),
'fileId': instance.fileId,
'hasPreview': instance.hasPreview,
'isSharedWithMe': instance.isSharedWithMe,
};
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:nextcloud/nextcloud.dart';
import '../../webdav_api.dart';
@@ -37,19 +39,46 @@ class ListFiles extends WebdavApi<ListFilesParams> {
ocsize: true,
nccreationtime: true,
nchaspreview: true,
// 'shared' here means an incoming share (mounted into the user's files
// by someone else); used to badge those entries in the list.
ncmounttype: true,
);
var files = await _fetch(webdav, prop, timeout);
// A freshly-entered incoming share sometimes answers its first PROPFIND
// without the OC/NC props (no fileid / has-preview / mount-type) while the
// share mount warms up server-side — which drops thumbnails AND share
// badges together. Retry a couple of times so the folder self-heals
// instead of needing manual re-entry.
for (var attempt = 0; attempt < 2 && _looksIncomplete(files); attempt++) {
await Future<void>.delayed(const Duration(milliseconds: 700));
files = await _fetch(webdav, prop, timeout);
}
return ListFilesResponse(files);
}
Future<Set<CacheableFile>> _fetch(
WebDavClient webdav,
WebDavPropWithoutValues prop,
Duration timeout,
) async {
final davFiles =
(await webdav
.propfind(PathUri.parse(params.path), prop: prop)
.timeout(timeout))
.toWebDavFiles();
final files = davFiles.map(CacheableFile.fromDavFile).toSet();
// somehow the current working folder is also listed, it is filtered here.
files.removeWhere(
(element) => element.path == '/${params.path}/' || element.path == '/',
);
return ListFilesResponse(files);
return files;
}
/// True when the server returned entries but none carry a `fileId` — a sign
/// the OC/NC properties were omitted (cold share mount), so thumbnails and
/// share badges would be missing for the whole folder.
bool _looksIncomplete(Set<CacheableFile> files) =>
files.isNotEmpty && files.every((file) => file.fileId == null);
}
@@ -40,9 +40,7 @@ class ListFilesCache extends SimpleCache<ListFilesResponse> {
/// [invalidate].
static int _cacheTimeFor(String path) {
final stripped = path.replaceAll('/', '').trim();
return stripped.isEmpty
? RequestCache.cacheDay
: RequestCache.cacheNothing;
return stripped.isEmpty ? RequestCache.cacheDay : RequestCache.cacheNothing;
}
/// Triggers a root-listing fetch in the background if no cached payload