A framework for building convergent cross-platform Nextcloud clients using Flutter.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

228 lines
6.0 KiB

part of '../neon_files.dart';
abstract interface class FilesBlocEvents {
void uploadFile(final List<String> path, final String localPath);
void syncFile(final List<String> path);
void openFile(final List<String> path, final String etag, final String? mimeType);
void shareFileNative(final List<String> path, final String etag);
void delete(final List<String> path);
void rename(final List<String> path, final String name);
void move(final List<String> path, final List<String> destination);
void copy(final List<String> path, final List<String> destination);
void addFavorite(final List<String> path);
void removeFavorite(final List<String> path);
}
abstract interface class FilesBlocStates {
BehaviorSubject<List<FilesTask>> get tasks;
}
class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocStates {
FilesBloc(
this.options,
this.account,
) {
options.uploadQueueParallelism.addListener(_uploadParallelismListener);
options.downloadQueueParallelism.addListener(_downloadParallelismListener);
}
final FilesAppSpecificOptions options;
final Account account;
late final browser = getNewFilesBrowserBloc();
final _uploadQueue = Queue();
final _downloadQueue = Queue();
@override
void dispose() {
_uploadQueue.dispose();
_downloadQueue.dispose();
unawaited(tasks.close());
options.uploadQueueParallelism.removeListener(_uploadParallelismListener);
options.downloadQueueParallelism.removeListener(_downloadParallelismListener);
super.dispose();
}
@override
BehaviorSubject<List<FilesTask>> tasks = BehaviorSubject<List<FilesTask>>.seeded([]);
@override
void addFavorite(final List<String> path) {
wrapAction(
() async => account.client.webdav.proppatch(
Uri(pathSegments: path),
set: WebDavProp(ocfavorite: 1),
),
);
}
@override
void copy(final List<String> path, final List<String> destination) {
wrapAction(() async => account.client.webdav.copy(Uri(pathSegments: path), Uri(pathSegments: destination)));
}
@override
void delete(final List<String> path) {
wrapAction(() async => account.client.webdav.delete(Uri(pathSegments: path)));
}
@override
void move(final List<String> path, final List<String> destination) {
wrapAction(() async => account.client.webdav.move(Uri(pathSegments: path), Uri(pathSegments: destination)));
}
@override
void openFile(final List<String> path, final String etag, final String? mimeType) {
wrapAction(
() async {
final file = await _cacheFile(path, etag);
final result = await OpenFile.open(file.path, type: mimeType);
if (result.type != ResultType.done) {
throw const UnableToOpenFileException();
}
},
disableTimeout: true,
);
}
@override
void shareFileNative(final List<String> path, final String etag) {
wrapAction(
() async {
final file = await _cacheFile(path, etag);
await Share.shareXFiles([XFile(file.path)]);
},
disableTimeout: true,
);
}
@override
Future<void> refresh() async {
await browser.refresh();
}
@override
void removeFavorite(final List<String> path) {
wrapAction(
() async => account.client.webdav.proppatch(
Uri(pathSegments: path),
set: WebDavProp(ocfavorite: 0),
),
);
}
@override
void rename(final List<String> path, final String name) {
wrapAction(
() async => account.client.webdav.move(
Uri(pathSegments: path),
Uri(pathSegments: List.from(path)..last = name),
),
);
}
@override
void syncFile(final List<String> path) {
wrapAction(
() async {
final file = File(
p.join(
await NeonPlatform.instance.userAccessibleAppDataPath,
account.humanReadableID,
'files',
path.join(Platform.pathSeparator),
),
);
if (!file.parent.existsSync()) {
file.parent.createSync(recursive: true);
}
await _downloadFile(path, file);
},
disableTimeout: true,
);
}
@override
void uploadFile(final List<String> path, final String localPath) {
wrapAction(
() async {
final task = FilesUploadTask(
path: path,
file: File(localPath),
);
tasks.add(tasks.value..add(task));
await _uploadQueue.add(() => task.execute(account.client));
tasks.add(tasks.value..remove(task));
},
disableTimeout: true,
);
}
Future<File> _cacheFile(final List<String> path, final String etag) async {
final cacheDir = await getApplicationCacheDirectory();
final file = File(p.join(cacheDir.path, 'files', etag.replaceAll('"', ''), path.last));
if (!file.existsSync()) {
debugPrint('Downloading ${Uri(pathSegments: path)} since it does not exist');
if (!file.parent.existsSync()) {
await file.parent.create(recursive: true);
}
await _downloadFile(path, file);
}
return file;
}
Future<void> _downloadFile(
final List<String> path,
final File file,
) async {
final task = FilesDownloadTask(
path: path,
file: file,
);
tasks.add(tasks.value..add(task));
await _downloadQueue.add(() => task.execute(account.client));
tasks.add(tasks.value..remove(task));
}
FilesBrowserBloc getNewFilesBrowserBloc({
final List<String>? initialPath,
}) =>
FilesBrowserBloc(
options,
account,
initialPath: initialPath,
);
void _downloadParallelismListener() {
_downloadQueue.parallel = options.downloadQueueParallelism.value;
}
void _uploadParallelismListener() {
_uploadQueue.parallel = options.uploadQueueParallelism.value;
}
}
@immutable
class UnableToOpenFileException extends NeonException {
const UnableToOpenFileException();
@override
NeonExceptionDetails get details => NeonExceptionDetails(
getText: (final context) => AppLocalizations.of(context).errorUnableToOpenFile,
);
}