part of '../neon_files.dart'; abstract interface class FilesBlocEvents { void uploadFile(final List path, final String localPath); void uploadBytes(final List path, final Uint8List bytes); void syncFile(final List path); void openFile(final List path, final String etag, final String? mimeType); void shareFileNative(final List path, final String etag); void delete(final List path); void rename(final List path, final String name); void move(final List path, final List destination); void copy(final List path, final List destination); void addFavorite(final List path); void removeFavorite(final List path); } abstract interface class FilesBlocStates { BehaviorSubject> 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> tasks = BehaviorSubject>.seeded([]); @override void addFavorite(final List path) { wrapAction( () async => account.client.webdav.proppatch( Uri(pathSegments: path), set: WebDavProp(ocfavorite: 1), ), ); } @override void copy(final List path, final List destination) { wrapAction(() async => account.client.webdav.copy(Uri(pathSegments: path), Uri(pathSegments: destination))); } @override void delete(final List path) { wrapAction(() async => account.client.webdav.delete(Uri(pathSegments: path))); } @override void move(final List path, final List destination) { wrapAction(() async => account.client.webdav.move(Uri(pathSegments: path), Uri(pathSegments: destination))); } @override void openFile(final List 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 path, final String etag) { wrapAction( () async { final file = await _cacheFile(path, etag); await Share.shareXFiles([XFile(file.path)]); }, disableTimeout: true, ); } @override Future refresh() async { await browser.refresh(); } @override void removeFavorite(final List path) { wrapAction( () async => account.client.webdav.proppatch( Uri(pathSegments: path), set: WebDavProp(ocfavorite: 0), ), ); } @override void rename(final List 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 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 path, final String localPath) { wrapAction( () async { final task = FilesUploadFileTask( 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, ); } @override void uploadBytes(final List path, final Uint8List bytes) { wrapAction( () async { final task = FilesUploadBytesTask( path: path, bytes: bytes, ); tasks.add(tasks.value..add(task)); await _uploadQueue.add(() => task.execute(account.client)); tasks.add(tasks.value..removeWhere((final t) => t == task)); }, disableTimeout: true, ); } Future _cacheFile(final List 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 _downloadFile( final List path, final File file, ) async { final task = FilesDownloadFileTask( 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? 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) => FilesLocalizations.of(context).errorUnableToOpenFile, ); }