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/confirmDialog.dart'; import '../../../widget/focusBehaviour.dart'; class FilesUploadDialog extends StatefulWidget { final List filePaths; final String remotePath; final void Function(List uploadedFilePaths) onUploadFinished; final bool uniqueNames; const FilesUploadDialog({super.key, required this.filePaths, required this.remotePath, required this.onUploadFinished, this.uniqueNames = false}); @override State 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 { late List _uploadableFiles; bool _isUploading = false; double _overallProgressValue = 0.0; String _infoText = ''; @override void initState() { super.initState(); _uploadableFiles = widget.filePaths.map((filePath) { var fileName = filePath.split(Platform.pathSeparator).last; return UploadableFile(filePath, fileName); }).toList(); } void showHttpErrorCode(int httpErrorCode){ showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: const Text('Ein Fehler ist aufgetreten'), contentPadding: const EdgeInsets.all(10), content: Text('Error code: $httpErrorCode'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Schließen', textAlign: TextAlign.center), ), ], ) ); } Future uploadFiles({bool override = false}) async { setState(() { _isUploading = true; _infoText = 'Vorbereiten'; for (var file in _uploadableFiles) { file.isConflicting = false; } }); var webdavClient = await WebdavApi.webdav; if (!override) { 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) => 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.', textAlign: TextAlign.left, ) : SingleChildScrollView( child: Text( '${conflictingFiles.length} Dateien mit folgenden Namen existieren bereits: \n${conflictingFiles.map((e) => '\n - ${e.fileName}').join('')}', textAlign: TextAlign.left, ), ), actions: [ TextButton( onPressed: () { Navigator.pop(context, false); }, child: const Text('Bearbeiten', textAlign: TextAlign.center), ), TextButton( onPressed: () { showDialog( context: context, 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) { setState(() { _isUploading = false; _overallProgressValue = 0.0; _infoText = ''; for (var element in conflictingFiles) { element.isConflicting = true; } }); return; } } } var uploadetFilePaths = []; for (var file in _uploadableFiles) { var fileName = file.fileName; var filePath = file.filePath; if(widget.uniqueNames) fileName = '${fileName.split('.').first}-${const Uuid().v4()}.${fileName.split('.').last}'; var fullRemotePath = '${widget.remotePath}/$fileName'; setState(() { _infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}'; }); var uploadTask = await webdavClient.putFile( File(filePath), FileStat.statSync(filePath), PathUri.parse(fullRemotePath), onProgress: (progress) { setState(() { file._uploadProgress = progress; _overallProgressValue = ((progress + _uploadableFiles.indexOf(file)) / _uploadableFiles.length).toDouble(); }); }, ); if(uploadTask.statusCode < 200 || uploadTask.statusCode > 299) { setState(() { _isUploading = false; _overallProgressValue = 0.0; _infoText = ''; }); Navigator.of(context).pop(); showHttpErrorCode(uploadTask.statusCode); } else { uploadetFilePaths.add(fullRemotePath); } } setState(() { _isUploading = false; _overallProgressValue = 0.0; _infoText = ''; }); Navigator.of(context).pop(); widget.onUploadFinished(uploadetFilePaths); } @override Widget build(BuildContext context) => 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, trailing: 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.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: () => uploadFiles(override: widget.uniqueNames), child: const Text('Hochladen'), ), 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)), ], ), ), ), ], ), ), ], ), ), ); }