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 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 { final List _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 result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses; List 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 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)), ], ), ), ], ), ), ], ), ), ); } }