🐛 Fix file upload in site
This commit is contained in:
@@ -126,6 +126,8 @@ class FileUploadDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
final pathController = useTextEditingController(text: '/');
|
||||||
final isUploading = useState(false);
|
final isUploading = useState(false);
|
||||||
final progressStates = useState<List<Map<String, dynamic>>>(
|
final progressStates = useState<List<Map<String, dynamic>>>(
|
||||||
selectedFiles
|
selectedFiles
|
||||||
@@ -142,7 +144,7 @@ class FileUploadDialog extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final uploadFile = useCallback((
|
final uploadFile = useCallback((
|
||||||
String filePath,
|
String basePath,
|
||||||
File file,
|
File file,
|
||||||
int index,
|
int index,
|
||||||
) async {
|
) async {
|
||||||
@@ -154,7 +156,13 @@ class FileUploadDialog extends HookConsumerWidget {
|
|||||||
siteFilesNotifierProvider((siteId: site.id, path: null)).notifier,
|
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]['status'] = 'completed';
|
||||||
progressStates.value[index]['progress'] = 1.0;
|
progressStates.value[index]['progress'] = 1.0;
|
||||||
@@ -168,6 +176,8 @@ class FileUploadDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
final uploadAllFiles = useCallback(
|
final uploadAllFiles = useCallback(
|
||||||
() async {
|
() async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
isUploading.value = true;
|
isUploading.value = true;
|
||||||
|
|
||||||
// Reset all progress states
|
// Reset all progress states
|
||||||
@@ -181,9 +191,7 @@ class FileUploadDialog extends HookConsumerWidget {
|
|||||||
// Upload files sequentially (could be made parallel if needed)
|
// Upload files sequentially (could be made parallel if needed)
|
||||||
for (int i = 0; i < selectedFiles.length; i++) {
|
for (int i = 0; i < selectedFiles.length; i++) {
|
||||||
final file = selectedFiles[i];
|
final file = selectedFiles[i];
|
||||||
// For now, upload to root. In a real implementation, you'd get this from a form field
|
await uploadFile(pathController.text, file, i);
|
||||||
final uploadPath = '/${file.path.split('/').last}';
|
|
||||||
await uploadFile(uploadPath, file, i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploading.value = false;
|
isUploading.value = false;
|
||||||
@@ -208,6 +216,8 @@ class FileUploadDialog extends HookConsumerWidget {
|
|||||||
selectedFiles,
|
selectedFiles,
|
||||||
onUploadComplete,
|
onUploadComplete,
|
||||||
context,
|
context,
|
||||||
|
formKey,
|
||||||
|
pathController,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -215,49 +225,151 @@ class FileUploadDialog extends HookConsumerWidget {
|
|||||||
titleText: 'Upload Files',
|
titleText: 'Upload Files',
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Form(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
key: formKey,
|
||||||
children: [
|
child: Column(
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'Ready to upload ${selectedFiles.length} file${selectedFiles.length == 1 ? '' : 's'}:',
|
children: [
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
// Upload path field
|
||||||
),
|
TextFormField(
|
||||||
const Gap(16),
|
controller: pathController,
|
||||||
...selectedFiles.map((file) {
|
decoration: const InputDecoration(
|
||||||
final index = selectedFiles.indexOf(file);
|
labelText: 'Upload Path',
|
||||||
final progressState = progressStates.value[index];
|
hintText: '/ (root) or /assets/images/',
|
||||||
final fileName = file.path.split('/').last;
|
border: OutlineInputBorder(
|
||||||
final fileSize = file.lengthSync();
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
final fileSizeText =
|
),
|
||||||
fileSize < 1024 * 1024
|
),
|
||||||
? '${(fileSize / 1024).toStringAsFixed(1)} KB'
|
validator: (value) {
|
||||||
: '${(fileSize / (1024 * 1024)).toStringAsFixed(1)} MB';
|
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(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.description,
|
Symbols.description,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
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),
|
const Gap(8),
|
||||||
Expanded(
|
LinearProgressIndicator(
|
||||||
child: Text(
|
value: progressState['progress'],
|
||||||
fileName,
|
backgroundColor:
|
||||||
style: Theme.of(context).textTheme.bodyMedium
|
Theme.of(context).colorScheme.surfaceVariant,
|
||||||
?.copyWith(fontWeight: FontWeight.w500),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
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(
|
Text(
|
||||||
fileSizeText,
|
'Will upload to: ${pathController.text.endsWith('/') ? pathController.text : '${pathController.text}/'}$fileName',
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall?.copyWith(
|
).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(
|
return ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
padding: EdgeInsets.zero,
|
||||||
itemCount: files.length,
|
itemCount: files.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final file = files[index];
|
final file = files[index];
|
||||||
@@ -1177,8 +1235,12 @@ class _FileItem extends HookConsumerWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
elevation: 0,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
file.isDirectory ? Symbols.folder : Symbols.description,
|
file.isDirectory ? Symbols.folder : Symbols.description,
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
|
|||||||
Reference in New Issue
Block a user