added upload with multiple files #61

Merged
MineTec merged 7 commits from develop-uploadMultipleFiles into develop 2024-04-09 08:21:55 +00:00
186 changed files with 1241 additions and 550 deletions
Showing only changes of commit b4defb9eda - Show all commits

View File

@ -1,9 +1,11 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:loader_overlay/loader_overlay.dart'; import 'package:loader_overlay/loader_overlay.dart';
import 'package:nextcloud/nextcloud.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:provider/provider.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart'; import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
@ -15,8 +17,8 @@ import '../../../storage/base/settingsProvider.dart';
import '../../../widget/loadingSpinner.dart'; import '../../../widget/loadingSpinner.dart';
import '../../../widget/placeholderView.dart'; import '../../../widget/placeholderView.dart';
import '../../../widget/filePick.dart'; import '../../../widget/filePick.dart';
import 'fileUploadDialog.dart';
import 'fileElement.dart'; import 'fileElement.dart';
import 'filesUploadDialog.dart';
class Files extends StatefulWidget { class Files extends StatefulWidget {
final List<String> path; final List<String> path;
@ -90,7 +92,8 @@ class _FilesState extends State<Files> {
ListFilesCache( ListFilesCache(
path: widget.path.isEmpty ? '/' : widget.path.join('/'), 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 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()); d.files.removeWhere((element) => element.name.isEmpty || element.name == widget.path.lastOrNull());
setState(() { setState(() {
data = d; 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),
Pupsi marked this conversation as resolved
Review

funktioniert das _query callback ohne die klammern()?

funktioniert das _query callback ohne die klammern()?
Review

wofüg irst das leere return hier?

wofüg irst das leere return hier?
);
return;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<CacheableFile> files = data?.sortBy( List<CacheableFile> files = data?.sortBy(
@ -149,7 +164,7 @@ class _FilesState extends State<Files> {
children: [ children: [
Icon(SortOptions.getOption(key).icon, color: Theme.of(context).colorScheme.onSurface), Icon(SortOptions.getOption(key).icon, color: Theme.of(context).colorScheme.onSurface),
const SizedBox(width: 15), const SizedBox(width: 15),
Text(SortOptions.getOption(key).displayName) Text(SortOptions.getOption(key).displayName),
], ],
) )
)).toList(); )).toList();
@ -204,7 +219,6 @@ class _FilesState extends State<Files> {
leading: const Icon(Icons.upload_file), leading: const Icon(Icons.upload_file),
title: const Text('Aus Dateien hochladen'), title: const Text('Aus Dateien hochladen'),
onTap: () { onTap: () {
context.loaderOverlay.show();
FilePick.documentPick().then(mediaUpload); FilePick.documentPick().then(mediaUpload);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@ -215,9 +229,8 @@ class _FilesState extends State<Files> {
leading: const Icon(Icons.add_a_photo_outlined), leading: const Icon(Icons.add_a_photo_outlined),
title: const Text('Aus Gallerie hochladen'), title: const Text('Aus Gallerie hochladen'),
onTap: () { onTap: () {
context.loaderOverlay.show();
FilePick.galleryPick().then((value) { FilePick.galleryPick().then((value) {
mediaUpload(value?.path); if(value != null) mediaUpload([value.path]);
}); });
Navigator.of(context).pop(); 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);
}
} }

View File

@ -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;
Pupsi marked this conversation as resolved
Review

wofür ist hier der Parameter List<String> uploadedFilePaths?

wofür ist hier der Parameter `List<String> uploadedFilePaths`?
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 = [];
Pupsi marked this conversation as resolved
Review

variable nicht leer initialisieren und durch addAll popularisieren

du kannst die variable hier late setzen und im initState setzen

variable nicht leer initialisieren und durch addAll popularisieren du kannst die variable hier late setzen und im initState setzen
bool _isUploading = false;
double _progressValue = 0.0;
Pupsi marked this conversation as resolved
Review

passenderer Variablenname _overallProgressValue
damit klar ist das sie den Gesamtfortschritt wiederspiegelt

passenderer Variablenname `_overallProgressValue` damit klar ist das sie den Gesamtfortschritt wiederspiegelt
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(
Pupsi marked this conversation as resolved
Review

ungenutzer code raus

ungenutzer code raus
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 {
Pupsi marked this conversation as resolved
Review

dein ehemaliger name upload ist in diesem kontext doch besser, vielleicht auch uploadFiles

dein ehemaliger name `upload` ist in diesem kontext doch besser, vielleicht auch `uploadFiles`
setState(() {
_isUploading = true;
_infoText = 'Vorbereiten';
});
for (var element in _uploadableFiles) {
setState(() {
Pupsi marked this conversation as resolved
Review

setState für jede datei auszuführen ist unnötig und ressourcenintensiv.

Mit setState auf zeile 70 verbinden und inline schreiben mit

_uploadableFiles.foreach(f => f.isConflicting = false)

setState für jede datei auszuführen ist unnötig und ressourcenintensiv. Mit setState auf zeile 70 verbinden und inline schreiben mit `_uploadableFiles.foreach(f => f.isConflicting = false)`
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) {
Pupsi marked this conversation as resolved
Review

umschreiben in fluente schreibweise

conflictingFiles = _uploadableFiles.where(...)
umschreiben in fluente schreibweise ``` conflictingFiles = _uploadableFiles.where(...) ```
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(() {
Pupsi marked this conversation as resolved
Review

kommentar raus

kommentar raus
_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',
Pupsi marked this conversation as resolved
Review

das ist sehr verwirrend....

bitte nur die tonne rechts, die das element löscht.

Der "Name" sollte nicht per button löschbar sein.

das ist sehr verwirrend.... bitte nur die tonne rechts, die das element löscht. Der "Name" sollte nicht per button löschbar sein.
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)),
],
),
),
],
),
),
],
),
),
);
}
}

View File

@ -1,10 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:nextcloud/nextcloud.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:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart'; import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart';
import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart'; import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart';
@ -15,7 +14,7 @@ import '../../../../model/chatList/chatProps.dart';
import '../../../../storage/base/settingsProvider.dart'; import '../../../../storage/base/settingsProvider.dart';
import '../../../../widget/filePick.dart'; import '../../../../widget/filePick.dart';
import '../../../../widget/focusBehaviour.dart'; import '../../../../widget/focusBehaviour.dart';
import '../../files/fileUploadDialog.dart'; import '../../files/filesUploadDialog.dart';
class ChatTextfield extends StatefulWidget { class ChatTextfield extends StatefulWidget {
final String sendToToken; final String sendToToken;
@ -34,34 +33,40 @@ class _ChatTextfieldState extends State<ChatTextfield> {
Provider.of<ChatProps>(context, listen: false).run(); Provider.of<ChatProps>(context, listen: false).run();
} }
void mediaUpload(String? path) async { void share(String shareFolder, List<String> filePaths) {
context.loaderOverlay.hide(); for (var element in filePaths) {
String fileName = element.split(Platform.pathSeparator).last;
if(path == null) { FileSharingApi().share(FileSharingApiParams(
return; 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'; String shareFolder = 'MarianumMobile';
WebdavApi.webdav.then((webdav) { WebdavApi.webdav.then((webdav) {
webdav.mkcol(PathUri.parse('/$shareFolder')); webdav.mkcol(PathUri.parse('/$shareFolder'));
}); });
showDialog(context: context, builder: (context) => FileUploadDialog( pushScreen(
doShowFinish: false, context,
fileName: filename, withNavBar: false,
localPath: path, screen: FilesUploadDialog(
remotePath: [shareFolder], filePaths: paths,
onUploadFinished: () { remotePath: shareFolder,
FileSharingApi().share(FileSharingApiParams( onUploadFinished: (uploadedFilePaths) {
shareType: 10, share(shareFolder, uploadedFilePaths);
shareWith: widget.sendToToken, },
path: '$shareFolder/$filename', uniqueNames: true,
)).then((value) => _query()); ),
}, );
), barrierDismissible: false);
} }
void setDraft(String text) { void setDraft(String text) {
if(text.isNotEmpty) { if(text.isNotEmpty) {
settings.val(write: true).talkSettings.drafts[widget.sendToToken] = text; settings.val(write: true).talkSettings.drafts[widget.sendToToken] = text;
@ -98,7 +103,6 @@ class _ChatTextfieldState extends State<ChatTextfield> {
leading: const Icon(Icons.file_open), leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'), title: const Text('Aus Dateien auswählen'),
onTap: () { onTap: () {
context.loaderOverlay.show();
FilePick.documentPick().then(mediaUpload); FilePick.documentPick().then(mediaUpload);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@ -109,9 +113,8 @@ class _ChatTextfieldState extends State<ChatTextfield> {
leading: const Icon(Icons.image), leading: const Icon(Icons.image),
title: const Text('Aus Gallerie auswählen'), title: const Text('Aus Gallerie auswählen'),
onTap: () { onTap: () {
context.loaderOverlay.show();
FilePick.galleryPick().then((value) { FilePick.galleryPick().then((value) {
mediaUpload(value?.path); if(value != null) mediaUpload([value.path]);
}); });
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },

View File

@ -13,8 +13,9 @@ class FilePick {
return null; return null;
} }
static Future<String?> documentPick() async { static Future<List<String>?> documentPick() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(); FilePickerResult? result = await FilePicker.platform.pickFiles(allowMultiple: true);
return result?.files.single.path; List<String?>? paths = result?.files.nonNulls.map((e) => e.path).toList();
return paths?.nonNulls.toList();
} }
} }