From e8e6fd16f38cf3b51f59a714b4cffac2ee0cac8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Mon, 29 May 2023 16:07:05 +0200 Subject: [PATCH] File upload ui, creating folders, automatic reload --- .idea/libraries/Dart_Packages.xml | 4 +- .../webdav/queries/listFiles/listFiles.dart | 5 +- lib/screen/pages/files/fileUpload.dart | 72 ------ lib/screen/pages/files/fileUploadDialog.dart | 227 ++++++++++++++++++ lib/screen/pages/files/files.dart | 70 ++++-- lib/theming/darkAppTheme.dart | 8 +- 6 files changed, 284 insertions(+), 102 deletions(-) delete mode 100644 lib/screen/pages/files/fileUpload.dart create mode 100644 lib/screen/pages/files/fileUploadDialog.dart diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index bca37eb..005afa6 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -278,7 +278,7 @@ - @@ -1147,7 +1147,7 @@ - + diff --git a/lib/api/marianumcloud/webdav/queries/listFiles/listFiles.dart b/lib/api/marianumcloud/webdav/queries/listFiles/listFiles.dart index a9e6490..50618ab 100644 --- a/lib/api/marianumcloud/webdav/queries/listFiles/listFiles.dart +++ b/lib/api/marianumcloud/webdav/queries/listFiles/listFiles.dart @@ -12,8 +12,9 @@ class ListFiles extends WebdavApi { @override Future run() async { - //Set files = (await (await WebdavApi.webdav).ls(params.path)).map((e) => CacheableFile.fromDavFile(e)).toSet(); - Set files = (await (await WebdavApi.webdav).ls(params.path)).toWebDavFiles((await WebdavApi.webdav)).map((e) => CacheableFile.fromDavFile(e)).toSet(); + List davFiles = (await (await WebdavApi.webdav).ls(params.path)).toWebDavFiles((await WebdavApi.webdav)); + davFiles.removeWhere((element) => element.path == "/${params.path}/" || element.path == "/"); // somehow the current working folder is also listed, it is filtered here. + Set files = davFiles.map((e) => CacheableFile.fromDavFile(e)).toSet(); return ListFilesResponse(files); } diff --git a/lib/screen/pages/files/fileUpload.dart b/lib/screen/pages/files/fileUpload.dart deleted file mode 100644 index 84847ec..0000000 --- a/lib/screen/pages/files/fileUpload.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'dart:developer'; -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:flutter/material.dart'; -import 'package:marianum_mobile/api/marianumcloud/webdav/webdavApi.dart'; - -class FileUpload extends StatefulWidget { - final String localPath; - final String remotePath; - const FileUpload({Key? key, required this.localPath, required this.remotePath}) : super(key: key); - - @override - State createState() => _FileUploadState(); -} - -class _FileUploadState extends State { - CancelableOperation? cancelableOperation; - late File localFile; - - @override - void initState() { - super.initState(); - localFile = File(widget.localPath); - } - - bool isRunning() { - return cancelableOperation != null && !(cancelableOperation!.isCompleted); - } - - bool isComplete() { - return cancelableOperation?.isCompleted ?? false; - } - - void start() async { - cancelableOperation = CancelableOperation.fromFuture( - (await WebdavApi.webdav).upload(localFile.readAsBytesSync(), widget.remotePath).then((value) { - log("Upload done!"); - }), - onCancel: () => log("Upload cancelled"), - ); - - cancelableOperation!.then((e) { - setState(() {}); - }); - setState(() {}); - } - - void stop() { - cancelableOperation?.cancel(); - setState(() {}); - Navigator.of(context).pop(); - } - - - @override - Widget build(BuildContext context) { - List actions = List.empty(growable: true); - if(!isRunning() && !isComplete()) actions.add(TextButton(onPressed: start, child: const Text("Upload starten"))); - if(isRunning()) actions.add(TextButton(onPressed: stop, child: const Text("Upload Abbrechen"))); - if(isComplete()) actions.add(TextButton(onPressed: Navigator.of(context).pop, child: const Text("Fertig"))); - - return AlertDialog( - title: const Text("Hochladen"), - content: Center( - child: isRunning() ? const CircularProgressIndicator() : const Text("Datei hochladen"), - ), - actions: actions, - icon: const Icon(Icons.upload), - ); - } -} diff --git a/lib/screen/pages/files/fileUploadDialog.dart b/lib/screen/pages/files/fileUploadDialog.dart new file mode 100644 index 0000000..67a4000 --- /dev/null +++ b/lib/screen/pages/files/fileUploadDialog.dart @@ -0,0 +1,227 @@ +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 remotePath; + final String fileName; + final void Function() triggerReload; + const FileUploadDialog({Key? key, required this.localPath, required this.remotePath, required this.fileName, required this.triggerReload}) : super(key: key); + + @override + State createState() => _FileUploadDialogState(); +} + +class _FileUploadDialogState extends State { + 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 result = (await webdavClient.ls(widget.remotePath.join("/"))).responses; + if(result.any((element) => element.href!.endsWith("/$targetFileName"))) { + setState(() { + state = FileUploadState.conflict; + }); + return; + } else { + setState(() { + state = FileUploadState.upload; + }); + } + } + + Future uploadTask = webdavClient.upload(File(widget.localPath).readAsBytesSync(), fullRemotePath); + uploadTask.then((value) => Future.value(value)).catchError((e) { + setState(() { + state = FileUploadState.error; + }); + return null; + }); + + + cancelableOperation = CancelableOperation.fromFuture( + uploadTask, + onCancel: () => log("Upload cancelled"), + ); + + cancelableOperation!.then((value) { + log("Upload done!"); + 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.triggerReload(); + return AlertDialog( + icon: const Icon(Icons.upload), + 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 diff --git a/lib/screen/pages/files/files.dart b/lib/screen/pages/files/files.dart index b0e2a51..74a228a 100644 --- a/lib/screen/pages/files/files.dart +++ b/lib/screen/pages/files/files.dart @@ -5,20 +5,19 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:loader_overlay/loader_overlay.dart'; import 'package:marianum_mobile/api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart'; -import 'package:marianum_mobile/screen/pages/files/fileUpload.dart'; import 'package:marianum_mobile/widget/errorView.dart'; -import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart'; import '../../../api/marianumcloud/webdav/queries/listFiles/listFilesCache.dart'; import '../../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart'; import '../../../api/marianumcloud/webdav/webdavApi.dart'; import '../../../data/files/filesProps.dart'; import '../../../widget/filePick.dart'; +import 'fileUploadDialog.dart'; import 'fileElement.dart'; class Files extends StatefulWidget { - List path; - Files(this.path, {Key? key}) : super(key: key); + final List path; + const Files(this.path, {Key? key}) : super(key: key); @override State createState() => _FilesState(); @@ -83,10 +82,11 @@ class _FilesState extends State { void _query() { ListFilesCache( path: widget.path.isEmpty ? "/" : widget.path.join("/"), - onUpdate: (ListFilesResponse d) => { + onUpdate: (ListFilesResponse d) { + if(!context.mounted) return; // prevent setState when widget is possibly already disposed setState(() { data = d; - }), + }); } ); } @@ -100,12 +100,12 @@ class _FilesState extends State { appBar: AppBar( title: Text(widget.path.isNotEmpty ? widget.path.last : "Dateien"), actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () => { - // TODO implement search - }, - ), + // IconButton( + // icon: const Icon(Icons.search), + // onPressed: () => { + // // TODO implement search + // }, + // ), PopupMenuButton( icon: Icon(currentSortDirection ? Icons.text_rotate_up : Icons.text_rotation_down), itemBuilder: (context) { @@ -151,22 +151,44 @@ class _FilesState extends State { ], ), floatingActionButton: FloatingActionButton( + backgroundColor: Theme.of(context).primaryColor, onPressed: () { showDialog(context: context, builder: (context) { return SimpleDialog( children: [ ListTile( leading: const Icon(Icons.folder), - title: const Text("Neuer Ordner"), + title: const Text("Ordner erstellen"), onTap: () { - WebdavApi.webdav.then((webdav) { - webdav.mkdirs("/MarianumMobileTest"); + Navigator.of(context).pop(); + showDialog(context: context, builder: (context) { + var inputController = TextEditingController(); + return AlertDialog( + title: const Text("Neuer Ordner"), + content: TextField( + controller: inputController, + decoration: const InputDecoration( + labelText: "Name", + ), + ), + actions: [ + TextButton(onPressed: () { + Navigator.of(context).pop(); + }, child: const Text("Abbrechen")), + TextButton(onPressed: () { + WebdavApi.webdav.then((webdav) { + webdav.mkdirs("${widget.path.join("/")}/${inputController.text}").then((value) => _query()); + }); + Navigator.of(context).pop(); + }, child: const Text("Ordner erstellen")), + ], + ); }); }, ), ListTile( leading: const Icon(Icons.file_open), - title: const Text("Aus Dateien auswählen"), + title: const Text("Aus Dateien hochladen"), onTap: () { context.loaderOverlay.show(); FilePick.documentPick().then((value) { @@ -178,7 +200,7 @@ class _FilesState extends State { ), ListTile( leading: const Icon(Icons.image), - title: const Text("Aus Gallerie auswählen"), + title: const Text("Aus Gallerie hochladen"), onTap: () { context.loaderOverlay.show(); FilePick.galleryPick().then((value) { @@ -192,7 +214,7 @@ class _FilesState extends State { ); }); }, - child: const Icon(Icons.upload), + child: const Icon(Icons.add), ), body: data == null ? const Center(child: CircularProgressIndicator()) : data!.files.isEmpty ? const ErrorView(icon: Icons.folder_off_rounded, text: "Der Ordner ist leer") : LoaderOverlay( child: RefreshIndicator( @@ -203,7 +225,7 @@ class _FilesState extends State { child: ListView.builder( itemCount: files.length, itemBuilder: (context, index) { - CacheableFile file = files.toList().skip(index).first; + CacheableFile file = files.toList()[index]; return FileElement(file, widget.path); }, ), @@ -213,15 +235,13 @@ class _FilesState extends State { } void mediaUpload(String? path) async { + context.loaderOverlay.hide(); + if(path == null) { - context.loaderOverlay.hide(); return; } - context.loaderOverlay.show(); - File file = File(path); - var remotePath = "${widget.path.join("/")}/${file.path.split(Platform.pathSeparator).last}"; - PersistentNavBarNavigator.pushNewScreen(context, screen: FileUpload(localPath: path, remotePath: remotePath), withNavBar: false); - + var fileName = path.split(Platform.pathSeparator).last; + showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, triggerReload: () => _query()), barrierDismissible: false); } } diff --git a/lib/theming/darkAppTheme.dart b/lib/theming/darkAppTheme.dart index 0191f20..be3bf3e 100644 --- a/lib/theming/darkAppTheme.dart +++ b/lib/theming/darkAppTheme.dart @@ -13,7 +13,7 @@ class DarkAppTheme { surface: Colors.black, onSurface: Colors.white, - primary: Colors.black, + primary: Colors.white, onPrimary: Colors.white, secondary: Colors.grey, @@ -34,6 +34,12 @@ class DarkAppTheme { progressIndicatorTheme: const ProgressIndicatorThemeData( color: marianumRed, ), + iconButtonTheme: IconButtonThemeData( + style: ButtonStyle( + textStyle: MaterialStateProperty.all(const TextStyle(color: Colors.white)), + backgroundColor: MaterialStateProperty.all(Colors.white), + ) + ), ); } \ No newline at end of file