import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/publication_site.dart'; import 'package:island/pods/site_files.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; class FileUploadDialog extends HookConsumerWidget { final List selectedFiles; final SnPublicationSite site; final VoidCallback onUploadComplete; final List? relativePaths; const FileUploadDialog({ super.key, required this.selectedFiles, required this.site, required this.onUploadComplete, this.relativePaths, }); @override Widget build(BuildContext context, WidgetRef ref) { final formKey = useMemoized(() => GlobalKey()); final pathController = useTextEditingController(text: '/'); final isUploading = useState(false); final progressStates = useState>>( selectedFiles .map( (file) => { 'fileName': relativePaths?[selectedFiles.indexOf(file)] ?? file.path.split('/').last, 'progress': 0.0, 'status': 'pending', // 'pending', 'uploading', 'completed', 'error' 'error': null, }, ) .toList(), ); // Calculate overall progress final overallProgress = progressStates.value.isNotEmpty ? progressStates.value .map((e) => e['progress'] as double) .reduce((a, b) => a + b) / progressStates.value.length : 0.0; final overallStatus = progressStates.value.isEmpty ? 'pending' : progressStates.value.every((e) => e['status'] == 'completed') ? 'completed' : progressStates.value.any((e) => e['status'] == 'error') ? 'error' : progressStates.value.any((e) => e['status'] == 'uploading') ? 'uploading' : 'pending'; final uploadFile = useCallback(( String basePath, File file, int index, ) async { try { progressStates.value[index]['status'] = 'uploading'; progressStates.value = [...progressStates.value]; final siteFilesNotifier = ref.read( siteFilesNotifierProvider((siteId: site.id, path: null)).notifier, ); final fileName = relativePaths?[index] ?? file.path.split('/').last; final uploadPath = basePath.endsWith('/') ? '$basePath$fileName' : '$basePath/$fileName'; await siteFilesNotifier.uploadFile(file, uploadPath); progressStates.value[index]['status'] = 'completed'; progressStates.value[index]['progress'] = 1.0; progressStates.value = [...progressStates.value]; } catch (e) { progressStates.value[index]['status'] = 'error'; progressStates.value[index]['error'] = e.toString(); progressStates.value = [...progressStates.value]; } }, [ref, site.id, progressStates]); final uploadAllFiles = useCallback( () async { if (!formKey.currentState!.validate()) return; isUploading.value = true; // Reset all progress states for (int i = 0; i < progressStates.value.length; i++) { progressStates.value[i]['status'] = 'pending'; progressStates.value[i]['progress'] = 0.0; progressStates.value[i]['error'] = null; } progressStates.value = [...progressStates.value]; // Upload files sequentially (could be made parallel if needed) for (int i = 0; i < selectedFiles.length; i++) { final file = selectedFiles[i]; await uploadFile(pathController.text, file, i); } isUploading.value = false; // Close dialog if all uploads completed successfully if (progressStates.value.every( (state) => state['status'] == 'completed', )) { if (context.mounted) { showSnackBar('All files uploaded successfully'); onUploadComplete(); Navigator.of(context).pop(); } } }, [ uploadFile, isUploading, progressStates, selectedFiles, onUploadComplete, context, formKey, pathController, ], ); return SheetScaffold( titleText: 'Upload Files', child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Form( key: formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Upload path field TextFormField( controller: pathController, decoration: const InputDecoration( labelText: 'Upload Path', hintText: '/ (root) or /assets/images/', border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter an upload path'; } if (!value.startsWith('/') && value != '/') { return 'Path must start with /'; } if (value.contains(' ')) { return 'Path cannot contain spaces'; } if (value.contains('//')) { return 'Path cannot have consecutive slashes'; } return null; }, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(16), Card( child: Column( children: [ // Overall progress Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${(overallProgress * 100).toStringAsFixed(0)}% completed', style: Theme.of(context).textTheme.titleSmall, ), const Gap(8), LinearProgressIndicator(value: overallProgress), const Gap(8), Text( _getOverallStatusText(overallStatus), style: Theme.of(context).textTheme.bodySmall, ), ], ), ), // Divider const Divider(height: 0), // File list in expansion ExpansionTile( title: Text('${selectedFiles.length} files to upload'), initiallyExpanded: selectedFiles.length <= 10, children: selectedFiles.map((file) { final index = selectedFiles.indexOf(file); final progressState = progressStates.value[index]; final displayName = progressState['fileName'] as String; return ListTile( leading: _getStatusIcon( progressState['status'] as String, ), title: Text(displayName), subtitle: Text( 'Size: ${(file.lengthSync() / 1024).toStringAsFixed(1)} KB', ), dense: true, ); }).toList(), ), ], ), ), const Gap(24), Row( children: [ Expanded( child: OutlinedButton( onPressed: isUploading.value ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), ), const Gap(12), Expanded( child: FilledButton( onPressed: isUploading.value ? null : uploadAllFiles, child: Text( isUploading.value ? 'Uploading...' : 'Upload ${selectedFiles.length} File${selectedFiles.length == 1 ? '' : 's'}', ), ), ), ], ), ], ), ), ), ); } Icon _getStatusIcon(String status) { switch (status) { case 'completed': return const Icon(Symbols.check_circle, color: Colors.green); case 'uploading': return const Icon(Symbols.sync); case 'error': return const Icon(Symbols.error, color: Colors.red); default: return const Icon(Symbols.pending); } } String _getOverallStatusText(String status) { switch (status) { case 'completed': return 'All uploads completed'; case 'error': return 'Some uploads failed'; case 'uploading': return 'Uploading in progress'; default: return 'Ready to upload'; } } }