diff --git a/lib/screens/creators/sites/site_detail.dart b/lib/screens/creators/sites/site_detail.dart index 2932ed4d..d57ac82d 100644 --- a/lib/screens/creators/sites/site_detail.dart +++ b/lib/screens/creators/sites/site_detail.dart @@ -126,6 +126,8 @@ class FileUploadDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + final pathController = useTextEditingController(text: '/'); final isUploading = useState(false); final progressStates = useState>>( selectedFiles @@ -142,7 +144,7 @@ class FileUploadDialog extends HookConsumerWidget { ); final uploadFile = useCallback(( - String filePath, + String basePath, File file, int index, ) async { @@ -154,7 +156,13 @@ class FileUploadDialog extends HookConsumerWidget { siteFilesNotifierProvider((siteId: site.id, path: null)).notifier, ); - await siteFilesNotifier.uploadFile(file, filePath); + final fileName = 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; @@ -168,6 +176,8 @@ class FileUploadDialog extends HookConsumerWidget { final uploadAllFiles = useCallback( () async { + if (!formKey.currentState!.validate()) return; + isUploading.value = true; // Reset all progress states @@ -181,9 +191,7 @@ class FileUploadDialog extends HookConsumerWidget { // Upload files sequentially (could be made parallel if needed) for (int i = 0; i < selectedFiles.length; i++) { final file = selectedFiles[i]; - // For now, upload to root. In a real implementation, you'd get this from a form field - final uploadPath = '/${file.path.split('/').last}'; - await uploadFile(uploadPath, file, i); + await uploadFile(pathController.text, file, i); } isUploading.value = false; @@ -208,6 +216,8 @@ class FileUploadDialog extends HookConsumerWidget { selectedFiles, onUploadComplete, context, + formKey, + pathController, ], ); @@ -215,49 +225,151 @@ class FileUploadDialog extends HookConsumerWidget { titleText: 'Upload Files', child: SingleChildScrollView( padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ready to upload ${selectedFiles.length} file${selectedFiles.length == 1 ? '' : 's'}:', - style: Theme.of(context).textTheme.titleMedium, - ), - const Gap(16), - ...selectedFiles.map((file) { - final index = selectedFiles.indexOf(file); - final progressState = progressStates.value[index]; - final fileName = file.path.split('/').last; - final fileSize = file.lengthSync(); - final fileSizeText = - fileSize < 1024 * 1024 - ? '${(fileSize / 1024).toStringAsFixed(1)} KB' - : '${(fileSize / (1024 * 1024)).toStringAsFixed(1)} MB'; + 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(20), + Text( + 'Ready to upload ${selectedFiles.length} file${selectedFiles.length == 1 ? '' : 's'}:', + style: Theme.of(context).textTheme.titleMedium, + ), + const Gap(16), + ...selectedFiles.map((file) { + final index = selectedFiles.indexOf(file); + final progressState = progressStates.value[index]; + final fileName = file.path.split('/').last; + final fileSize = file.lengthSync(); + final fileSizeText = + fileSize < 1024 * 1024 + ? '${(fileSize / 1024).toStringAsFixed(1)} KB' + : '${(fileSize / (1024 * 1024)).toStringAsFixed(1)} MB'; - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Symbols.description, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Symbols.description, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Text( + fileName, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + fileSizeText, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + if (progressState['status'] == 'uploading') ...[ const Gap(8), - Expanded( - child: Text( - fileName, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith(fontWeight: FontWeight.w500), - overflow: TextOverflow.ellipsis, + LinearProgressIndicator( + value: progressState['progress'], + backgroundColor: + Theme.of(context).colorScheme.surfaceVariant, + ), + const Gap(4), + Text( + 'Uploading... ${(progressState['progress'] * 100).toStringAsFixed(0)}%', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.primary, ), ), + ] else if (progressState['status'] == 'completed') ...[ + const Gap(8), + Row( + children: [ + Icon( + Symbols.check_circle, + color: Colors.green, + size: 16, + ), + const Gap(4), + Text( + 'Completed', + style: TextStyle( + color: Colors.green, + fontSize: 12, + ), + ), + ], + ), + ] else if (progressState['status'] == 'error') ...[ + const Gap(8), + Row( + children: [ + Icon(Symbols.error, color: Colors.red, size: 16), + const Gap(4), + Expanded( + child: Text( + progressState['error'] ?? 'Upload failed', + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + // Show the final upload path when not uploading + if (!isUploading.value && + progressState['status'] != 'uploading') ...[ + const Gap(8), Text( - fileSizeText, + 'Will upload to: ${pathController.text.endsWith('/') ? pathController.text : '${pathController.text}/'}$fileName', style: Theme.of( context, ).textTheme.bodySmall?.copyWith( @@ -268,92 +380,38 @@ class FileUploadDialog extends HookConsumerWidget { ), ), ], - ), - if (progressState['status'] == 'uploading') ...[ - const Gap(8), - LinearProgressIndicator( - value: progressState['progress'], - backgroundColor: - Theme.of(context).colorScheme.surfaceVariant, - ), - const Gap(4), - Text( - 'Uploading... ${(progressState['progress'] * 100).toStringAsFixed(0)}%', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ] else if (progressState['status'] == 'completed') ...[ - const Gap(8), - Row( - children: [ - Icon( - Symbols.check_circle, - color: Colors.green, - size: 16, - ), - const Gap(4), - Text( - 'Completed', - style: TextStyle( - color: Colors.green, - fontSize: 12, - ), - ), - ], - ), - ] else if (progressState['status'] == 'error') ...[ - const Gap(8), - Row( - children: [ - Icon(Symbols.error, color: Colors.red, size: 16), - const Gap(4), - Expanded( - child: Text( - progressState['error'] ?? 'Upload failed', - style: TextStyle( - color: Colors.red, - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), ], - ], - ), - ), - ); - }), - 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'}', ), ), - ), - ], - ), - ], + ); + }), + 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'}', + ), + ), + ), + ], + ), + ], + ), ), ), ); @@ -1133,7 +1191,7 @@ class _FileManagementSection extends HookConsumerWidget { return ListView.builder( shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, itemCount: files.length, itemBuilder: (context, index) { final file = files[index]; @@ -1177,8 +1235,12 @@ class _FileItem extends HookConsumerWidget { final theme = Theme.of(context); return Card( - margin: const EdgeInsets.only(bottom: 8), + color: Theme.of(context).colorScheme.surfaceContainerHigh, + elevation: 0, child: ListTile( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), leading: Icon( file.isDirectory ? Symbols.folder : Symbols.description, color: theme.colorScheme.primary,