From b4defb9eda87c5358a85e520e1ce5ba147476216 Mon Sep 17 00:00:00 2001 From: Pupsi28 <larslukasneuhaus@gmx.de> Date: Fri, 5 Apr 2024 18:16:12 +0200 Subject: [PATCH 1/6] added upload with multiple files --- lib/view/pages/files/files.dart | 36 +- lib/view/pages/files/filesUploadDialog.dart | 318 ++++++++++++++++++ .../pages/talk/components/chatTextfield.dart | 53 +-- lib/widget/filePick.dart | 7 +- 4 files changed, 369 insertions(+), 45 deletions(-) create mode 100644 lib/view/pages/files/filesUploadDialog.dart diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index b377cbf..f0c9add 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -1,9 +1,11 @@ +import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:loader_overlay/loader_overlay.dart'; import 'package:nextcloud/nextcloud.dart'; +import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:provider/provider.dart'; import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart'; @@ -15,8 +17,8 @@ import '../../../storage/base/settingsProvider.dart'; import '../../../widget/loadingSpinner.dart'; import '../../../widget/placeholderView.dart'; import '../../../widget/filePick.dart'; -import 'fileUploadDialog.dart'; import 'fileElement.dart'; +import 'filesUploadDialog.dart'; class Files extends StatefulWidget { final List<String> path; @@ -90,7 +92,8 @@ class _FilesState extends State<Files> { ListFilesCache( path: widget.path.isEmpty ? '/' : widget.path.join('/'), onUpdate: (ListFilesResponse d) { - if(!context.mounted) return; // prevent setState when widget is possibly already disposed + log('_query'); + if(!context.mounted) return; // prevent setState when widget is possibly already disposed d.files.removeWhere((element) => element.name.isEmpty || element.name == widget.path.lastOrNull()); setState(() { data = d; @@ -99,6 +102,18 @@ class _FilesState extends State<Files> { ); } + void mediaUpload(List<String>? paths) async { + if(paths == null) return; + + pushScreen( + context, + withNavBar: false, + screen: FilesUploadDialog(filePaths: paths, remotePath: widget.path.join('/'), onUploadFinished: (uploadedFilePaths) => _query), + ); + + return; + } + @override Widget build(BuildContext context) { List<CacheableFile> files = data?.sortBy( @@ -149,7 +164,7 @@ class _FilesState extends State<Files> { children: [ Icon(SortOptions.getOption(key).icon, color: Theme.of(context).colorScheme.onSurface), const SizedBox(width: 15), - Text(SortOptions.getOption(key).displayName) + Text(SortOptions.getOption(key).displayName), ], ) )).toList(); @@ -204,7 +219,6 @@ class _FilesState extends State<Files> { leading: const Icon(Icons.upload_file), title: const Text('Aus Dateien hochladen'), onTap: () { - context.loaderOverlay.show(); FilePick.documentPick().then(mediaUpload); Navigator.of(context).pop(); }, @@ -215,9 +229,8 @@ class _FilesState extends State<Files> { leading: const Icon(Icons.add_a_photo_outlined), title: const Text('Aus Gallerie hochladen'), onTap: () { - context.loaderOverlay.show(); FilePick.galleryPick().then((value) { - mediaUpload(value?.path); + if(value != null) mediaUpload([value.path]); }); Navigator.of(context).pop(); }, @@ -247,15 +260,4 @@ class _FilesState extends State<Files> { ) ); } - - void mediaUpload(String? path) async { - context.loaderOverlay.hide(); - - if(path == null) { - return; - } - - var fileName = path.split(Platform.pathSeparator).last; - showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, onUploadFinished: _query), barrierDismissible: false); - } } diff --git a/lib/view/pages/files/filesUploadDialog.dart b/lib/view/pages/files/filesUploadDialog.dart new file mode 100644 index 0000000..9f9ce02 --- /dev/null +++ b/lib/view/pages/files/filesUploadDialog.dart @@ -0,0 +1,318 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:loader_overlay/loader_overlay.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../api/marianumcloud/webdav/webdavApi.dart'; +import '../../../widget/focusBehaviour.dart'; + +class FilesUploadDialog extends StatefulWidget { + final List<String> filePaths; + final String remotePath; + final void Function(List<String> uploadedFilePaths) onUploadFinished; + final bool uniqueNames; + + const FilesUploadDialog({super.key, required this.filePaths, required this.remotePath, required this.onUploadFinished, this.uniqueNames = false}); + + @override + State<FilesUploadDialog> createState() => _FilesUploadDialogState(); +} + +class UploadableFile { + TextEditingController fileNameController = TextEditingController(); + String filePath; + String fileName; + double? _uploadProgress; + bool isConflicting = false; + + UploadableFile(this.filePath, this.fileName); +} + + +class _FilesUploadDialogState extends State<FilesUploadDialog> { + final List<UploadableFile> _uploadableFiles = []; + bool _isUploading = false; + double _progressValue = 0.0; + String _infoText = ''; + + @override + void initState() { + super.initState(); + + _uploadableFiles.addAll(widget.filePaths.map((filePath) { + String fileName = filePath.split(Platform.pathSeparator).last; + return UploadableFile(filePath, fileName); + })); + } + + void showErrorMessage(int errorCode){ + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Ein Fehler ist aufgetreten'), + contentPadding: const EdgeInsets.all(10), + content: Text('Error code: $errorCode'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Schließen', textAlign: TextAlign.center), + ), + ], + ); + } + ); + } + + void uploadSelectedFiles({bool override = false}) async { + setState(() { + _isUploading = true; + _infoText = 'Vorbereiten'; + }); + + for (var element in _uploadableFiles) { + setState(() { + element.isConflicting = false; + }); + } + + WebDavClient webdavClient = await WebdavApi.webdav; + + if (!override) { + List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses; + List<UploadableFile> conflictingFiles = []; + + for (var file in _uploadableFiles) { + String fileName = file.fileName; + if (result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName'))) { + // konflikt + conflictingFiles.add(file); + } + } + + if(conflictingFiles.isNotEmpty) { + bool replaceFiles = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + contentPadding: const EdgeInsets.all(10), + title: const Text('Konflikt', textAlign: TextAlign.center), + content: conflictingFiles.length == 1 ? + Text( + 'Eine Datei mit dem Namen "${conflictingFiles.map((e) => e.fileName).first}" existiert bereits.\n' + '(Datei ${_uploadableFiles.indexOf(conflictingFiles.first)+1})', + textAlign: TextAlign.left, + ) : + Text( + '${conflictingFiles.length} Dateien mit den Namen ${conflictingFiles.map((e) => e.fileName).toList()} existieren bereits.\n' + '(Dateien ${conflictingFiles.map((e) => _uploadableFiles.indexOf(e)+1).toList()})', + textAlign: TextAlign.left, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: const Text('Bearbeiten', textAlign: TextAlign.center), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: const Text('Ersetzen', textAlign: TextAlign.center), + ), + ], + ); + } + ); + + if(!replaceFiles) { + for (var element in conflictingFiles) { + element.isConflicting = true; + } + setState(() { + _isUploading = false; + _progressValue = 0.0; + _infoText = ''; + }); + return; + } + } + } + + List<String> uploadetFilePaths = []; + for (var file in _uploadableFiles) { + String fileName = file.fileName; + String filePath = file.filePath; + + if(widget.uniqueNames) fileName = '${fileName.split('.').first}-${const Uuid().v4()}.${fileName.split('.').last}'; + + String fullRemotePath = '${widget.remotePath}/$fileName'; + + setState(() { + _infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}'; + }); + + HttpClientResponse uploadTask = await webdavClient.putFile( + File(filePath), + FileStat.statSync(filePath), + PathUri.parse(fullRemotePath), + onProgress: (progress) { + setState(() { + file._uploadProgress = progress; + _progressValue = ((progress + _uploadableFiles.indexOf(file)) / _uploadableFiles.length).toDouble(); + }); + }, + ); + + if(uploadTask.statusCode < 200 || uploadTask.statusCode > 299) { + // error code + setState(() { + _isUploading = false; + _progressValue = 0.0; + _infoText = ''; + }); + Navigator.of(context).pop(); + showErrorMessage(uploadTask.statusCode); + } else { + uploadetFilePaths.add(fullRemotePath); + } + } + + setState(() { + _isUploading = false; + _progressValue = 0.0; + _infoText = ''; + }); + Navigator.of(context).pop(); + widget.onUploadFinished(uploadetFilePaths); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Dateien hochladen'), + automaticallyImplyLeading: false, + ), + body: LoaderOverlay( + overlayWholeScreen: true, + child: Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: _uploadableFiles.length, + itemBuilder: (context, index) { + final currentFile = _uploadableFiles[index]; + currentFile.fileNameController.text = currentFile.fileName; + return ListTile( + title: TextField( + readOnly: _isUploading, + controller: currentFile.fileNameController, + decoration: InputDecoration( + border: const UnderlineInputBorder(), + label: Text('Datei ${index+1}'), + errorText: currentFile.isConflicting ? 'existiert bereits' : null, + errorStyle: const TextStyle(color: Colors.red), + ), + onChanged: (input) { + currentFile.fileName = input; + }, + onTapOutside: (PointerDownEvent event) { + FocusBehaviour.textFieldTapOutside(context); + if(currentFile.isConflicting){ + setState(() { + currentFile.isConflicting = false; + }); + } + }, + onEditingComplete: () { + if(currentFile.isConflicting){ + setState(() { + currentFile.isConflicting = false; + }); + } + }, + ), + subtitle: _isUploading && (currentFile._uploadProgress ?? 0) < 1 ? LinearProgressIndicator( + value: currentFile._uploadProgress, + borderRadius: const BorderRadius.all(Radius.circular(2)), + ) : null, + leading: Container( + width: 24, + height: 24, + padding: EdgeInsets.zero, + child: IconButton( + tooltip: 'Datei entfernen', + padding: EdgeInsets.zero, + onPressed: () { + if(!_isUploading) { + if(_uploadableFiles.length-1 <= 0) Navigator.of(context).pop(); + setState(() { + _uploadableFiles.removeAt(index); + }); + } + }, + icon: const Icon(Icons.close_outlined), + ), + ), + trailing: Container( + width: 24, + height: 24, + padding: EdgeInsets.zero, + child: IconButton( + tooltip: 'Namen löschen', + padding: EdgeInsets.zero, + onPressed: () { + if(!_isUploading) { + setState(() { + currentFile.fileName = ''; + }); + } + }, + icon: const Icon(Icons.delete_outlined), + ), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 15, right: 15, bottom: 15, top: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Visibility( + visible: !_isUploading, + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Abbrechen'), + ), + ), + const Expanded(child: SizedBox.shrink()), + Visibility( + visible: _isUploading, + replacement: TextButton( + onPressed: () => uploadSelectedFiles(override: widget.uniqueNames), + child: const Text('Hochladen'), + ), + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator(value: _progressValue), + Center(child: Text(_infoText)), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/view/pages/talk/components/chatTextfield.dart b/lib/view/pages/talk/components/chatTextfield.dart index 5513d8a..09c81d7 100644 --- a/lib/view/pages/talk/components/chatTextfield.dart +++ b/lib/view/pages/talk/components/chatTextfield.dart @@ -1,10 +1,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:loader_overlay/loader_overlay.dart'; import 'package:nextcloud/nextcloud.dart'; +import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:provider/provider.dart'; -import 'package:uuid/uuid.dart'; import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart'; import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart'; @@ -15,7 +14,7 @@ import '../../../../model/chatList/chatProps.dart'; import '../../../../storage/base/settingsProvider.dart'; import '../../../../widget/filePick.dart'; import '../../../../widget/focusBehaviour.dart'; -import '../../files/fileUploadDialog.dart'; +import '../../files/filesUploadDialog.dart'; class ChatTextfield extends StatefulWidget { final String sendToToken; @@ -34,34 +33,40 @@ class _ChatTextfieldState extends State<ChatTextfield> { Provider.of<ChatProps>(context, listen: false).run(); } - void mediaUpload(String? path) async { - context.loaderOverlay.hide(); - - if(path == null) { - return; + void share(String shareFolder, List<String> filePaths) { + for (var element in filePaths) { + String fileName = element.split(Platform.pathSeparator).last; + FileSharingApi().share(FileSharingApiParams( + shareType: 10, + shareWith: widget.sendToToken, + path: '$shareFolder/$fileName', + )).then((value) => _query()); } + } + + void mediaUpload(List<String>? paths) async { + if (paths == null) return; - String filename = "${path.split("/").last.split(".").first}-${const Uuid().v4()}.${path.split(".").last}"; String shareFolder = 'MarianumMobile'; WebdavApi.webdav.then((webdav) { webdav.mkcol(PathUri.parse('/$shareFolder')); }); - showDialog(context: context, builder: (context) => FileUploadDialog( - doShowFinish: false, - fileName: filename, - localPath: path, - remotePath: [shareFolder], - onUploadFinished: () { - FileSharingApi().share(FileSharingApiParams( - shareType: 10, - shareWith: widget.sendToToken, - path: '$shareFolder/$filename', - )).then((value) => _query()); - }, - ), barrierDismissible: false); + pushScreen( + context, + withNavBar: false, + screen: FilesUploadDialog( + filePaths: paths, + remotePath: shareFolder, + onUploadFinished: (uploadedFilePaths) { + share(shareFolder, uploadedFilePaths); + }, + uniqueNames: true, + ), + ); } + void setDraft(String text) { if(text.isNotEmpty) { settings.val(write: true).talkSettings.drafts[widget.sendToToken] = text; @@ -98,7 +103,6 @@ class _ChatTextfieldState extends State<ChatTextfield> { leading: const Icon(Icons.file_open), title: const Text('Aus Dateien auswählen'), onTap: () { - context.loaderOverlay.show(); FilePick.documentPick().then(mediaUpload); Navigator.of(context).pop(); }, @@ -109,9 +113,8 @@ class _ChatTextfieldState extends State<ChatTextfield> { leading: const Icon(Icons.image), title: const Text('Aus Gallerie auswählen'), onTap: () { - context.loaderOverlay.show(); FilePick.galleryPick().then((value) { - mediaUpload(value?.path); + if(value != null) mediaUpload([value.path]); }); Navigator.of(context).pop(); }, diff --git a/lib/widget/filePick.dart b/lib/widget/filePick.dart index b37fef4..0d7c0e5 100644 --- a/lib/widget/filePick.dart +++ b/lib/widget/filePick.dart @@ -13,8 +13,9 @@ class FilePick { return null; } - static Future<String?> documentPick() async { - FilePickerResult? result = await FilePicker.platform.pickFiles(); - return result?.files.single.path; + static Future<List<String>?> documentPick() async { + FilePickerResult? result = await FilePicker.platform.pickFiles(allowMultiple: true); + List<String?>? paths = result?.files.nonNulls.map((e) => e.path).toList(); + return paths?.nonNulls.toList(); } } \ No newline at end of file -- 2.30.2 From 277b3366f9d0ea308fbde23e8519c8fbbfdac514 Mon Sep 17 00:00:00 2001 From: Pupsi28 <larslukasneuhaus@gmx.de> Date: Fri, 5 Apr 2024 18:19:49 +0200 Subject: [PATCH 2/6] added upload with multiple files --- lib/view/pages/files/fileUploadDialog.dart | 233 --------------------- 1 file changed, 233 deletions(-) delete mode 100644 lib/view/pages/files/fileUploadDialog.dart diff --git a/lib/view/pages/files/fileUploadDialog.dart b/lib/view/pages/files/fileUploadDialog.dart deleted file mode 100644 index 6322ee5..0000000 --- a/lib/view/pages/files/fileUploadDialog.dart +++ /dev/null @@ -1,233 +0,0 @@ -import 'dart:developer'; -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:flutter/material.dart'; -import 'package:nextcloud/nextcloud.dart'; - -import '../../../api/marianumcloud/webdav/webdavApi.dart'; - -class FileUploadDialog extends StatefulWidget { - final String localPath; - final List<String> remotePath; - final String fileName; - final void Function() onUploadFinished; - - final bool doShowFinish; - - const FileUploadDialog({super.key, required this.localPath, required this.remotePath, required this.fileName, required this.onUploadFinished, this.doShowFinish = true}); - - @override - State<FileUploadDialog> createState() => _FileUploadDialogState(); -} - -class _FileUploadDialogState extends State<FileUploadDialog> { - FileUploadState state = FileUploadState.naming; - CancelableOperation? cancelableOperation; - late String targetFileName; - late String remoteFolderName; - late String fullRemotePath = "${widget.remotePath.join("/")}/$targetFileName"; - String? lastError; - - TextEditingController fileNameController = TextEditingController(); - - - void upload({bool override = false}) async { - setState(() { - state = FileUploadState.upload; - }); - - WebDavClient webdavClient = await WebdavApi.webdav; - - if(!override) { - setState(() { - state = FileUploadState.checkConflict; - }); - List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath.join('/')))).responses; - if(result.any((element) => element.href!.endsWith('/$targetFileName'))) { - setState(() { - state = FileUploadState.conflict; - }); - return; - } else { - setState(() { - state = FileUploadState.upload; - }); - } - } - - Future<HttpClientResponse> uploadTask = webdavClient.putFile(File(widget.localPath), FileStat.statSync(widget.localPath), PathUri.parse(fullRemotePath)); // TODO use onProgress from putFile - uploadTask.then(Future<HttpClientResponse?>.value).catchError((e) { - setState(() { - state = FileUploadState.error; - }); - return null; - }); - - - cancelableOperation = CancelableOperation<HttpClientResponse>.fromFuture( - uploadTask, - onCancel: () => log('Upload cancelled'), - ); - - cancelableOperation!.then((value) { - setState(() { - state = FileUploadState.done; - }); - }); - } - - void cancel() { - cancelableOperation?.cancel(); - setState(() { - state = FileUploadState.naming; - }); - } - - @override - void initState() { - super.initState(); - targetFileName = widget.fileName; - remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : '/'; - fileNameController.text = widget.fileName; - } - - @override - Widget build(BuildContext context) { - if(state == FileUploadState.naming) { - return AlertDialog( - title: const Text('Datei hochladen'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: fileNameController, - onChanged: (input) { - targetFileName = input; - }, - autocorrect: false, - decoration: const InputDecoration( - labelText: 'Dateiname', - ), - ), - ], - ), - actions: [ - TextButton(onPressed: () { - Navigator.of(context).pop(); - }, child: const Text('Abbrechen')), - TextButton(onPressed: () async { - upload(); - }, child: const Text('Hochladen')), - ], - - ); - } - - if(state == FileUploadState.conflict) { - return AlertDialog( - icon: const Icon(Icons.error_outline), - title: const Text('Datei konflikt'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Es gibt bereits eine Datei mit dem Namen $targetFileName in dem ausgewählten Ordner '$remoteFolderName'", textAlign: TextAlign.center), - ], - ), - actions: [ - TextButton(onPressed: () { - setState(() { - state = FileUploadState.naming; - }); - }, child: const Text('Datei umbenennen')), - TextButton(onPressed: () { - upload(override: true); - }, child: const Text('Datei überschreiben')), - ], - - ); - } - - if(state == FileUploadState.upload || state == FileUploadState.checkConflict) { - return AlertDialog( - icon: const Icon(Icons.upload), - title: const Text('Hochladen'), - content: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Visibility( - visible: state == FileUploadState.upload, - replacement: const Text('Prüfe auf dateikonflikte...'), - child: const Text('Upload läuft!\nDies kann je nach Dateigröße einige Zeit dauern...', textAlign: TextAlign.center), - ), - const SizedBox(height: 30), - const CircularProgressIndicator() - ], - ), - actions: const [ - // TODO implement working upload cancelling - // TextButton(onPressed: () { - // cancel(); - // }, child: const Text("Abbrechen")), - ], - - ); - } - - if(state == FileUploadState.done) { - widget.onUploadFinished(); - if(!widget.doShowFinish) { - Navigator.of(context).pop(); - return const SizedBox.shrink(); - } - return AlertDialog( - icon: const Icon(Icons.done), - title: const Text('Upload fertig'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Die Datei wurde erfolgreich nach '$remoteFolderName' hochgeladen!", textAlign: TextAlign.center), - ], - ), - actions: [ - TextButton(onPressed: () { - Navigator.of(context).pop(); - }, child: const Text('Fertig')), - ], - - ); - } - - if(state == FileUploadState.error) { - return AlertDialog( - icon: const Icon(Icons.error_outline), - title: const Text('Fehler'), - content: const Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Es ist ein Fehler aufgetreten!', textAlign: TextAlign.center), - ], - ), - actions: [ - TextButton(onPressed: () { - Navigator.of(context).pop(); - }, child: const Text('Schlißen')), - ], - - ); - } - - throw UnimplementedError('Invalid state'); - - } -} - -enum FileUploadState { - naming, - checkConflict, - conflict, - upload, - done, - error -} \ No newline at end of file -- 2.30.2 From e901f139d60902f0134d9d7eb58eb39e4c21adbf Mon Sep 17 00:00:00 2001 From: Pupsi28 <larslukasneuhaus@gmx.de> Date: Sat, 6 Apr 2024 13:34:52 +0200 Subject: [PATCH 3/6] solved some pr comments --- lib/view/pages/files/files.dart | 3 - lib/view/pages/files/filesUploadDialog.dart | 70 +++++++++++++-------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index f0c9add..3402bba 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -1,5 +1,4 @@ -import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -92,8 +91,6 @@ class _FilesState extends State<Files> { ListFilesCache( path: widget.path.isEmpty ? '/' : widget.path.join('/'), onUpdate: (ListFilesResponse d) { - log('_query'); - if(!context.mounted) return; // prevent setState when widget is possibly already disposed d.files.removeWhere((element) => element.name.isEmpty || element.name == widget.path.lastOrNull()); setState(() { data = d; diff --git a/lib/view/pages/files/filesUploadDialog.dart b/lib/view/pages/files/filesUploadDialog.dart index 9f9ce02..8e19d8a 100644 --- a/lib/view/pages/files/filesUploadDialog.dart +++ b/lib/view/pages/files/filesUploadDialog.dart @@ -6,6 +6,7 @@ import 'package:nextcloud/nextcloud.dart'; import 'package:uuid/uuid.dart'; import '../../../api/marianumcloud/webdav/webdavApi.dart'; +import '../../../widget/confirmDialog.dart'; import '../../../widget/focusBehaviour.dart'; class FilesUploadDialog extends StatefulWidget { @@ -32,29 +33,34 @@ class UploadableFile { class _FilesUploadDialogState extends State<FilesUploadDialog> { - final List<UploadableFile> _uploadableFiles = []; + late List<UploadableFile> _uploadableFiles; bool _isUploading = false; - double _progressValue = 0.0; + double _overallProgressValue = 0.0; String _infoText = ''; @override void initState() { super.initState(); - _uploadableFiles.addAll(widget.filePaths.map((filePath) { + _uploadableFiles = widget.filePaths.map((filePath) { String fileName = filePath.split(Platform.pathSeparator).last; return UploadableFile(filePath, fileName); - })); + }).toList(); + + /*_uploadableFiles.addAll(widget.filePaths.map((filePath) { + String fileName = filePath.split(Platform.pathSeparator).last; + return UploadableFile(filePath, fileName); + }));*/ } - void showErrorMessage(int errorCode){ + void showHttpErrorCode(int httpErrorCode){ showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Ein Fehler ist aufgetreten'), contentPadding: const EdgeInsets.all(10), - content: Text('Error code: $errorCode'), + content: Text('Error code: $httpErrorCode'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), @@ -66,31 +72,27 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { ); } - void uploadSelectedFiles({bool override = false}) async { + void uploadFiles({bool override = false}) async { setState(() { _isUploading = true; _infoText = 'Vorbereiten'; + for (var file in _uploadableFiles) { + file.isConflicting = false; + } }); - for (var element in _uploadableFiles) { - setState(() { - element.isConflicting = false; - }); - } - WebDavClient webdavClient = await WebdavApi.webdav; if (!override) { List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses; - List<UploadableFile> conflictingFiles = []; - - for (var file in _uploadableFiles) { + List<UploadableFile> conflictingFiles = _uploadableFiles.where((file) { String fileName = file.fileName; if (result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName'))) { // konflikt - conflictingFiles.add(file); + return true; } - } + return false; + }).toList(); if(conflictingFiles.isNotEmpty) { bool replaceFiles = await showDialog( @@ -120,9 +122,23 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { ), TextButton( onPressed: () { - Navigator.pop(context, true); + showDialog( + context: context, + builder: (context) { + return ConfirmDialog( + title: 'Bist du sicher?', + content: '', + onConfirm: () { + Navigator.pop(context, true); + }, + confirmButton: 'Ja', + cancelButton: 'Nein', + ); + } + ); + }, - child: const Text('Ersetzen', textAlign: TextAlign.center), + child: const Text('Überschreiben', textAlign: TextAlign.center), ), ], ); @@ -135,7 +151,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { } setState(() { _isUploading = false; - _progressValue = 0.0; + _overallProgressValue = 0.0; _infoText = ''; }); return; @@ -163,7 +179,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { onProgress: (progress) { setState(() { file._uploadProgress = progress; - _progressValue = ((progress + _uploadableFiles.indexOf(file)) / _uploadableFiles.length).toDouble(); + _overallProgressValue = ((progress + _uploadableFiles.indexOf(file)) / _uploadableFiles.length).toDouble(); }); }, ); @@ -172,11 +188,11 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { // error code setState(() { _isUploading = false; - _progressValue = 0.0; + _overallProgressValue = 0.0; _infoText = ''; }); Navigator.of(context).pop(); - showErrorMessage(uploadTask.statusCode); + showHttpErrorCode(uploadTask.statusCode); } else { uploadetFilePaths.add(fullRemotePath); } @@ -184,7 +200,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { setState(() { _isUploading = false; - _progressValue = 0.0; + _overallProgressValue = 0.0; _infoText = ''; }); Navigator.of(context).pop(); @@ -296,13 +312,13 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { Visibility( visible: _isUploading, replacement: TextButton( - onPressed: () => uploadSelectedFiles(override: widget.uniqueNames), + onPressed: () => uploadFiles(override: widget.uniqueNames), child: const Text('Hochladen'), ), child: Stack( alignment: Alignment.center, children: [ - CircularProgressIndicator(value: _progressValue), + CircularProgressIndicator(value: _overallProgressValue), Center(child: Text(_infoText)), ], ), -- 2.30.2 From 8131ccae1e77dfaf11455004880cf6bc353f8dff Mon Sep 17 00:00:00 2001 From: Pupsi28 <larslukasneuhaus@gmx.de> Date: Sun, 7 Apr 2024 15:49:43 +0200 Subject: [PATCH 4/6] solved some pr comments --- lib/view/pages/files/files.dart | 2 +- lib/view/pages/files/filesUploadDialog.dart | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index 3402bba..00b6719 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -105,7 +105,7 @@ class _FilesState extends State<Files> { pushScreen( context, withNavBar: false, - screen: FilesUploadDialog(filePaths: paths, remotePath: widget.path.join('/'), onUploadFinished: (uploadedFilePaths) => _query), + screen: FilesUploadDialog(filePaths: paths, remotePath: widget.path.join('/'), onUploadFinished: (uploadedFilePaths) => _query()), ); return; diff --git a/lib/view/pages/files/filesUploadDialog.dart b/lib/view/pages/files/filesUploadDialog.dart index 8e19d8a..c90b5b0 100644 --- a/lib/view/pages/files/filesUploadDialog.dart +++ b/lib/view/pages/files/filesUploadDialog.dart @@ -108,11 +108,14 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { '(Datei ${_uploadableFiles.indexOf(conflictingFiles.first)+1})', textAlign: TextAlign.left, ) : - Text( - '${conflictingFiles.length} Dateien mit den Namen ${conflictingFiles.map((e) => e.fileName).toList()} existieren bereits.\n' - '(Dateien ${conflictingFiles.map((e) => _uploadableFiles.indexOf(e)+1).toList()})', - textAlign: TextAlign.left, + SingleChildScrollView( + child: Text( + '${conflictingFiles.length} Dateien mit den Namen: ${conflictingFiles.map((e) => '\n${e.fileName}').join(', ')}\n existieren bereits.\n' + '(Dateien ${conflictingFiles.map((e) => _uploadableFiles.indexOf(e)+1).join(', ')})', + textAlign: TextAlign.left, + ), ), + actions: [ TextButton( onPressed: () { @@ -126,8 +129,8 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { context: context, builder: (context) { return ConfirmDialog( - title: 'Bist du sicher?', - content: '', + title: 'Bestätigen', + content: 'Bist du sicher, dass du ${conflictingFiles.map((e) => e.fileName).toList()} überschreiben möchtest?', onConfirm: () { Navigator.pop(context, true); }, -- 2.30.2 From d8c72a5d2865249e15c9613d28e7300ebab27f07 Mon Sep 17 00:00:00 2001 From: Pupsi28 <larslukasneuhaus@gmx.de> Date: Sun, 7 Apr 2024 16:48:38 +0200 Subject: [PATCH 5/6] solved most pr comments and a bug --- lib/view/pages/files/files.dart | 6 +- lib/view/pages/files/filesUploadDialog.dart | 108 ++++++++---------- .../pages/talk/components/chatTextfield.dart | 6 +- 3 files changed, 55 insertions(+), 65 deletions(-) diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index 2b1a6f9..a39bdbe 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -65,9 +65,7 @@ class SortOptions { ) }; - static BetterSortOption getOption(SortOption option) { - return options[option]!; - } + static BetterSortOption getOption(SortOption option) => options[option]!; } class _FilesState extends State<Files> { @@ -99,7 +97,7 @@ class _FilesState extends State<Files> { ); } - void mediaUpload(List<String>? paths) async { + Future<void> mediaUpload(List<String>? paths) async { if(paths == null) return; pushScreen( diff --git a/lib/view/pages/files/filesUploadDialog.dart b/lib/view/pages/files/filesUploadDialog.dart index c90b5b0..794d9a7 100644 --- a/lib/view/pages/files/filesUploadDialog.dart +++ b/lib/view/pages/files/filesUploadDialog.dart @@ -43,21 +43,15 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { super.initState(); _uploadableFiles = widget.filePaths.map((filePath) { - String fileName = filePath.split(Platform.pathSeparator).last; + var fileName = filePath.split(Platform.pathSeparator).last; return UploadableFile(filePath, fileName); }).toList(); - - /*_uploadableFiles.addAll(widget.filePaths.map((filePath) { - String fileName = filePath.split(Platform.pathSeparator).last; - return UploadableFile(filePath, fileName); - }));*/ } void showHttpErrorCode(int httpErrorCode){ showDialog( context: context, - builder: (BuildContext context) { - return AlertDialog( + builder: (BuildContext context) => AlertDialog( title: const Text('Ein Fehler ist aufgetreten'), contentPadding: const EdgeInsets.all(10), content: Text('Error code: $httpErrorCode'), @@ -67,12 +61,11 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { child: const Text('Schließen', textAlign: TextAlign.center), ), ], - ); - } + ) ); } - void uploadFiles({bool override = false}) async { + Future<void> uploadFiles({bool override = false}) async { setState(() { _isUploading = true; _infoText = 'Vorbereiten'; @@ -81,41 +74,33 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { } }); - WebDavClient webdavClient = await WebdavApi.webdav; + var webdavClient = await WebdavApi.webdav; if (!override) { - List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses; - List<UploadableFile> conflictingFiles = _uploadableFiles.where((file) { - String fileName = file.fileName; - if (result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName'))) { - // konflikt - return true; - } - return false; + var result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses; + var conflictingFiles = _uploadableFiles.where((file) { + var fileName = file.fileName; + return result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName')); }).toList(); if(conflictingFiles.isNotEmpty) { bool replaceFiles = await showDialog( context: context, barrierDismissible: false, - builder: (context) { - return AlertDialog( + builder: (context) => AlertDialog( contentPadding: const EdgeInsets.all(10), title: const Text('Konflikt', textAlign: TextAlign.center), content: conflictingFiles.length == 1 ? Text( - 'Eine Datei mit dem Namen "${conflictingFiles.map((e) => e.fileName).first}" existiert bereits.\n' - '(Datei ${_uploadableFiles.indexOf(conflictingFiles.first)+1})', + 'Eine Datei mit dem Namen "${conflictingFiles.map((e) => e.fileName).first}" existiert bereits.', textAlign: TextAlign.left, ) : SingleChildScrollView( child: Text( - '${conflictingFiles.length} Dateien mit den Namen: ${conflictingFiles.map((e) => '\n${e.fileName}').join(', ')}\n existieren bereits.\n' - '(Dateien ${conflictingFiles.map((e) => _uploadableFiles.indexOf(e)+1).join(', ')})', + '${conflictingFiles.length} Dateien mit folgenden Namen existieren bereits: \n${conflictingFiles.map((e) => '\n - ${e.fileName}').join('')}', textAlign: TextAlign.left, ), ), - actions: [ TextButton( onPressed: () { @@ -127,55 +112,52 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { onPressed: () { showDialog( context: context, - builder: (context) { - return ConfirmDialog( - title: 'Bestätigen', - content: 'Bist du sicher, dass du ${conflictingFiles.map((e) => e.fileName).toList()} überschreiben möchtest?', - onConfirm: () { - Navigator.pop(context, true); - }, - confirmButton: 'Ja', - cancelButton: 'Nein', - ); - } + builder: (context) => ConfirmDialog( + title: 'Bestätigen?', + content: 'Bist du sicher, dass du ${conflictingFiles.length} Dateien überschreiben möchtest?', + onConfirm: () { + Navigator.pop(context, true); + }, + confirmButton: 'Ja', + cancelButton: 'Nein', + ), ); }, child: const Text('Überschreiben', textAlign: TextAlign.center), ), ], - ); - } + ) ); if(!replaceFiles) { - for (var element in conflictingFiles) { - element.isConflicting = true; - } setState(() { _isUploading = false; _overallProgressValue = 0.0; _infoText = ''; + for (var element in conflictingFiles) { + element.isConflicting = true; + } }); return; } } } - List<String> uploadetFilePaths = []; + var uploadetFilePaths = <String>[]; for (var file in _uploadableFiles) { - String fileName = file.fileName; - String filePath = file.filePath; + var fileName = file.fileName; + var filePath = file.filePath; if(widget.uniqueNames) fileName = '${fileName.split('.').first}-${const Uuid().v4()}.${fileName.split('.').last}'; - String fullRemotePath = '${widget.remotePath}/$fileName'; + var fullRemotePath = '${widget.remotePath}/$fileName'; setState(() { _infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}'; }); - HttpClientResponse uploadTask = await webdavClient.putFile( + var uploadTask = await webdavClient.putFile( File(filePath), FileStat.statSync(filePath), PathUri.parse(fullRemotePath), @@ -211,8 +193,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { } @override - Widget build(BuildContext context) { - return Scaffold( + Widget build(BuildContext context) => Scaffold( appBar: AppBar( title: const Text('Dateien hochladen'), automaticallyImplyLeading: false, @@ -318,13 +299,25 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { onPressed: () => uploadFiles(override: widget.uniqueNames), child: const Text('Hochladen'), ), - child: Stack( - alignment: Alignment.center, - children: [ - CircularProgressIndicator(value: _overallProgressValue), - Center(child: Text(_infoText)), - ], + child: Visibility( + visible: _infoText.length < 5, + replacement: Row( + children: [ + Text(_infoText), + const SizedBox(width: 15), + CircularProgressIndicator(value: _overallProgressValue), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator(value: _overallProgressValue), + Center(child: Text(_infoText)), + ], + ), ), + + ), ], ), @@ -333,5 +326,4 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { ), ), ); - } -} \ No newline at end of file +} diff --git a/lib/view/pages/talk/components/chatTextfield.dart b/lib/view/pages/talk/components/chatTextfield.dart index 791db03..a39be26 100644 --- a/lib/view/pages/talk/components/chatTextfield.dart +++ b/lib/view/pages/talk/components/chatTextfield.dart @@ -35,7 +35,7 @@ class _ChatTextfieldState extends State<ChatTextfield> { void share(String shareFolder, List<String> filePaths) { for (var element in filePaths) { - String fileName = element.split(Platform.pathSeparator).last; + var fileName = element.split(Platform.pathSeparator).last; FileSharingApi().share(FileSharingApiParams( shareType: 10, shareWith: widget.sendToToken, @@ -44,10 +44,10 @@ class _ChatTextfieldState extends State<ChatTextfield> { } } - void mediaUpload(List<String>? paths) async { + Future<void> mediaUpload(List<String>? paths) async { if (paths == null) return; - String shareFolder = 'MarianumMobile'; + var shareFolder = 'MarianumMobile'; WebdavApi.webdav.then((webdav) { webdav.mkcol(PathUri.parse('/$shareFolder')); }); -- 2.30.2 From cf4dea566e125656d31f279daa38c056c37f2d11 Mon Sep 17 00:00:00 2001 From: Pupsi28 <larslukasneuhaus@gmx.de> Date: Mon, 8 Apr 2024 22:39:28 +0200 Subject: [PATCH 6/6] solved pr comments; picking multiple Images from Gallery is now possible --- lib/view/pages/files/files.dart | 4 ++-- lib/view/pages/files/filesUploadDialog.dart | 20 +------------------ .../pages/talk/components/chatTextfield.dart | 4 ++-- lib/widget/filePick.dart | 8 ++++++++ 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index a39bdbe..8068408 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -219,8 +219,8 @@ class _FilesState extends State<Files> { leading: const Icon(Icons.add_a_photo_outlined), title: const Text('Aus Gallerie hochladen'), onTap: () { - FilePick.galleryPick().then((value) { - if(value != null) mediaUpload([value.path]); + FilePick.multipleGalleryPick().then((value) { + if(value != null) mediaUpload(value.map((e) => e.path).toList()); }); Navigator.of(context).pop(); }, diff --git a/lib/view/pages/files/filesUploadDialog.dart b/lib/view/pages/files/filesUploadDialog.dart index 794d9a7..72803c2 100644 --- a/lib/view/pages/files/filesUploadDialog.dart +++ b/lib/view/pages/files/filesUploadDialog.dart @@ -170,7 +170,6 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { ); if(uploadTask.statusCode < 200 || uploadTask.statusCode > 299) { - // error code setState(() { _isUploading = false; _overallProgressValue = 0.0; @@ -241,7 +240,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { value: currentFile._uploadProgress, borderRadius: const BorderRadius.all(Radius.circular(2)), ) : null, - leading: Container( + trailing: Container( width: 24, height: 24, padding: EdgeInsets.zero, @@ -256,23 +255,6 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> { }); } }, - icon: const Icon(Icons.close_outlined), - ), - ), - trailing: Container( - width: 24, - height: 24, - padding: EdgeInsets.zero, - child: IconButton( - tooltip: 'Namen löschen', - padding: EdgeInsets.zero, - onPressed: () { - if(!_isUploading) { - setState(() { - currentFile.fileName = ''; - }); - } - }, icon: const Icon(Icons.delete_outlined), ), ), diff --git a/lib/view/pages/talk/components/chatTextfield.dart b/lib/view/pages/talk/components/chatTextfield.dart index a39be26..3628006 100644 --- a/lib/view/pages/talk/components/chatTextfield.dart +++ b/lib/view/pages/talk/components/chatTextfield.dart @@ -111,8 +111,8 @@ class _ChatTextfieldState extends State<ChatTextfield> { leading: const Icon(Icons.image), title: const Text('Aus Gallerie auswählen'), onTap: () { - FilePick.galleryPick().then((value) { - if(value != null) mediaUpload([value.path]); + FilePick.multipleGalleryPick().then((value) { + if(value != null) mediaUpload(value.map((e) => e.path).toList()); }); Navigator.of(context).pop(); }, diff --git a/lib/widget/filePick.dart b/lib/widget/filePick.dart index a4d490d..b9d7dbb 100644 --- a/lib/widget/filePick.dart +++ b/lib/widget/filePick.dart @@ -13,6 +13,14 @@ class FilePick { return null; } + static Future<List<XFile>?> multipleGalleryPick() async { + final pickedImages = await _picker.pickMultiImage(); + if(pickedImages.isNotEmpty) { + return pickedImages; + } + return null; + } + static Future<List<String>?> documentPick() async { var result = await FilePicker.platform.pickFiles(allowMultiple: true); var paths = result?.files.nonNulls.map((e) => e.path).toList(); -- 2.30.2