259 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:flutter/material.dart';
 | |
| import 'package:firebase_storage/firebase_storage.dart';
 | |
| import 'package:flutter_dropzone/flutter_dropzone.dart';
 | |
| import 'package:path/path.dart' as p;
 | |
| 
 | |
| class DropzoneUploadWidget extends StatefulWidget {
 | |
|   final List<Map<String, String>> uploadedFiles;
 | |
|   final ValueChanged<List<Map<String, String>>> onFilesChanged;
 | |
|   final bool isLoading;
 | |
|   final String? error;
 | |
|   final String? success;
 | |
|   final double width;
 | |
|   final double height;
 | |
| 
 | |
|   const DropzoneUploadWidget({
 | |
|     super.key,
 | |
|     required this.uploadedFiles,
 | |
|     required this.onFilesChanged,
 | |
|     this.isLoading = false,
 | |
|     this.error,
 | |
|     this.success,
 | |
|     this.width = 400,
 | |
|     this.height = 200,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   State<DropzoneUploadWidget> createState() => _DropzoneUploadWidgetState();
 | |
| }
 | |
| 
 | |
| class _DropzoneUploadWidgetState extends State<DropzoneUploadWidget> {
 | |
|   DropzoneViewController? _dropzoneController;
 | |
|   bool _isDropzoneHighlighted = false;
 | |
|   bool _isLoading = false;
 | |
|   String? _error;
 | |
|   String? _success;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _isLoading = widget.isLoading;
 | |
|     _error = widget.error;
 | |
|     _success = widget.success;
 | |
|   }
 | |
| 
 | |
|   Future<void> _handleFiles(List<dynamic> files) async {
 | |
|     setState(() => _isLoading = true);
 | |
|     try {
 | |
|       List<Map<String, String>> newFiles = List.from(widget.uploadedFiles);
 | |
|       for (final file in files) {
 | |
|         final name = await _dropzoneController!.getFilename(file);
 | |
|         final bytes = await _dropzoneController!.getFileData(file);
 | |
|         final ref = FirebaseStorage.instance.ref().child(
 | |
|             'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
 | |
|         final uploadTask = await ref.putData(bytes);
 | |
|         final url = await uploadTask.ref.getDownloadURL();
 | |
|         if (!newFiles.any((f) => f['name'] == name && f['url'] == url)) {
 | |
|           newFiles.add({'name': name, 'url': url});
 | |
|         }
 | |
|             }
 | |
|       widget.onFilesChanged(newFiles);
 | |
|       setState(() {
 | |
|         _success = "Fichier(s) ajouté(s) !";
 | |
|         _error = null;
 | |
|       });
 | |
|     } catch (e) {
 | |
|       setState(() {
 | |
|         _error = 'Erreur lors de l\'upload : $e';
 | |
|         _success = null;
 | |
|       });
 | |
|     } finally {
 | |
|       setState(() {
 | |
|         _isLoading = false;
 | |
|         _isDropzoneHighlighted = false;
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return SizedBox(
 | |
|       width: widget.width,
 | |
|       height: widget.height,
 | |
|       child: Container(
 | |
|         decoration: BoxDecoration(
 | |
|           border: Border.all(
 | |
|             color: _isDropzoneHighlighted ? Colors.blue : Colors.grey,
 | |
|             width: 2,
 | |
|           ),
 | |
|           borderRadius: BorderRadius.circular(8),
 | |
|           color: _isDropzoneHighlighted ? Colors.blue.withOpacity(0.1) : null,
 | |
|         ),
 | |
|         child: Stack(
 | |
|           children: [
 | |
|             DropzoneView(
 | |
|               onCreated: (controller) => _dropzoneController = controller,
 | |
|               onDropFiles: (files) async {
 | |
|                 if (files == null) return;
 | |
|                 await _handleFiles(files);
 | |
|               },
 | |
|               onHover: () => setState(() => _isDropzoneHighlighted = true),
 | |
|               onLeave: () => setState(() => _isDropzoneHighlighted = false),
 | |
|             ),
 | |
|             Positioned.fill(
 | |
|               child: IgnorePointer(
 | |
|                 child: widget.uploadedFiles.isEmpty
 | |
|                     ? Column(
 | |
|                         mainAxisAlignment: MainAxisAlignment.center,
 | |
|                         crossAxisAlignment: CrossAxisAlignment.center,
 | |
|                         children: [
 | |
|                           const Icon(Icons.cloud_upload,
 | |
|                               size: 48, color: Colors.grey),
 | |
|                           const SizedBox(height: 12),
 | |
|                           const Text(
 | |
|                             'Glissez-déposez des fichiers ici ou cliquez sur "Ajouter"',
 | |
|                             textAlign: TextAlign.center,
 | |
|                             style: TextStyle(color: Colors.grey, fontSize: 16),
 | |
|                           ),
 | |
|                           const SizedBox(height: 16),
 | |
|                         ],
 | |
|                       )
 | |
|                     : null,
 | |
|               ),
 | |
|             ),
 | |
|             if (widget.uploadedFiles.isNotEmpty)
 | |
|               Padding(
 | |
|                 padding: const EdgeInsets.all(8.0),
 | |
|                 child: _buildFileListAndButton(),
 | |
|               ),
 | |
|             if (widget.uploadedFiles.isEmpty)
 | |
|               Positioned(
 | |
|                 bottom: 16,
 | |
|                 left: 0,
 | |
|                 right: 0,
 | |
|                 child: Center(
 | |
|                   child: SizedBox(
 | |
|                     width: 160,
 | |
|                     child: ElevatedButton.icon(
 | |
|                       icon: const Icon(Icons.attach_file, size: 18),
 | |
|                       label: const Text('Ajouter'),
 | |
|                       style: ElevatedButton.styleFrom(
 | |
|                         padding: const EdgeInsets.symmetric(
 | |
|                             horizontal: 8, vertical: 8),
 | |
|                         minimumSize: const Size(80, 36),
 | |
|                         textStyle: const TextStyle(fontSize: 14),
 | |
|                       ),
 | |
|                       onPressed: _isLoading
 | |
|                           ? null
 | |
|                           : () async {
 | |
|                               final files =
 | |
|                                   await _dropzoneController?.pickFiles();
 | |
|                               if (files != null) {
 | |
|                                 await _handleFiles(files);
 | |
|                               }
 | |
|                             },
 | |
|                     ),
 | |
|                   ),
 | |
|                 ),
 | |
|               ),
 | |
|             if (_isLoading)
 | |
|               const Positioned.fill(
 | |
|                 child: ColoredBox(
 | |
|                   color: Color.fromARGB(80, 255, 255, 255),
 | |
|                   child: Center(
 | |
|                     child: SizedBox(
 | |
|                         width: 32,
 | |
|                         height: 32,
 | |
|                         child: CircularProgressIndicator()),
 | |
|                   ),
 | |
|                 ),
 | |
|               ),
 | |
|             if (_error != null)
 | |
|               Positioned(
 | |
|                 bottom: 0,
 | |
|                 left: 0,
 | |
|                 right: 0,
 | |
|                 child: Padding(
 | |
|                   padding: EdgeInsets.only(top: 8.0),
 | |
|                   child:
 | |
|                       Text(_error!, style: const TextStyle(color: Colors.red)),
 | |
|                 ),
 | |
|               ),
 | |
|             if (_success != null)
 | |
|               Positioned(
 | |
|                 bottom: 0,
 | |
|                 left: 0,
 | |
|                 right: 0,
 | |
|                 child: Padding(
 | |
|                   padding: const EdgeInsets.only(top: 32.0),
 | |
|                   child: Text(_success!,
 | |
|                       style: const TextStyle(color: Colors.green)),
 | |
|                 ),
 | |
|               ),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildFileListAndButton() {
 | |
|     return Column(
 | |
|       crossAxisAlignment: CrossAxisAlignment.start,
 | |
|       children: [
 | |
|         ...widget.uploadedFiles.map((file) {
 | |
|           final fileName = file['name']!;
 | |
|           final ext = p.extension(fileName).toLowerCase();
 | |
|           IconData icon;
 | |
|           if ([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]
 | |
|               .contains(ext)) {
 | |
|             icon = Icons.image;
 | |
|           } else if (ext == ".pdf") {
 | |
|             icon = Icons.picture_as_pdf;
 | |
|           } else if ([".txt", ".md", ".csv", ".json", ".xml"].contains(ext)) {
 | |
|             icon = Icons.description;
 | |
|           } else {
 | |
|             icon = Icons.attach_file;
 | |
|           }
 | |
|           return ListTile(
 | |
|             leading: Icon(icon, color: Colors.blueGrey),
 | |
|             title: Text(fileName, overflow: TextOverflow.ellipsis),
 | |
|             trailing: IconButton(
 | |
|               icon: const Icon(Icons.close),
 | |
|               onPressed: _isLoading
 | |
|                   ? null
 | |
|                   : () {
 | |
|                       final newFiles =
 | |
|                           List<Map<String, String>>.from(widget.uploadedFiles)
 | |
|                             ..remove(file);
 | |
|                       widget.onFilesChanged(newFiles);
 | |
|                     },
 | |
|             ),
 | |
|             contentPadding: EdgeInsets.zero,
 | |
|             dense: true,
 | |
|           );
 | |
|         }),
 | |
|         SizedBox(
 | |
|           width: 160,
 | |
|           child: ElevatedButton.icon(
 | |
|             icon: const Icon(Icons.attach_file, size: 18),
 | |
|             label: const Text('Ajouter'),
 | |
|             style: ElevatedButton.styleFrom(
 | |
|               padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
 | |
|               minimumSize: const Size(80, 36),
 | |
|               textStyle: const TextStyle(fontSize: 14),
 | |
|             ),
 | |
|             onPressed: _isLoading
 | |
|                 ? null
 | |
|                 : () async {
 | |
|                     final files = await _dropzoneController?.pickFiles();
 | |
|                     if (files != null) {
 | |
|                       await _handleFiles(files);
 | |
|                     }
 | |
|                   },
 | |
|           ),
 | |
|         ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | 
