Add proxy downloads and bulk file operations
- Implement proxy download endpoint for better mobile Safari compatibility - Add bulk selection with checkboxes in file browser - Add bulk actions bar for downloading/deleting selected files - Replace direct S3 downloads with server-proxied downloads - Fix download issues on mobile devices by using proper link handling
This commit is contained in:
@@ -63,14 +63,52 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Bulk Actions Bar (shown when items selected) -->
|
||||
<v-row v-if="selectedItems.length > 0" class="mb-4">
|
||||
<v-col>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
closable
|
||||
@click:close="selectedItems = []"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span>{{ selectedItems.length }} item(s) selected</span>
|
||||
<div>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="downloadSelected"
|
||||
prepend-icon="mdi-download"
|
||||
class="mr-2"
|
||||
>
|
||||
Download
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="confirmDeleteSelected"
|
||||
prepend-icon="mdi-delete"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- File List -->
|
||||
<v-card>
|
||||
<v-data-table
|
||||
v-model="selectedItems"
|
||||
:headers="headers"
|
||||
:items="filteredFiles"
|
||||
:loading="loading"
|
||||
:items-per-page="25"
|
||||
class="elevation-0"
|
||||
show-select
|
||||
item-value="name"
|
||||
>
|
||||
<template v-slot:item.displayName="{ item }">
|
||||
<div
|
||||
@@ -295,6 +333,7 @@ const toast = useToast();
|
||||
// Data
|
||||
const files = ref<FileItem[]>([]);
|
||||
const filteredFiles = ref<FileItem[]>([]);
|
||||
const selectedItems = ref<string[]>([]);
|
||||
const searchQuery = ref('');
|
||||
const loading = ref(false);
|
||||
const uploading = ref(false);
|
||||
@@ -460,19 +499,17 @@ const createNewFolder = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Download file
|
||||
// Download file (using proxy for Safari compatibility)
|
||||
const downloadFile = async (file: FileItem) => {
|
||||
downloadingFiles.value[file.name] = true;
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/files/download', {
|
||||
params: { fileName: file.name },
|
||||
});
|
||||
// Use proxy download endpoint for better mobile compatibility
|
||||
const proxyUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(file.name)}`;
|
||||
|
||||
// For mobile Safari compatibility, use a link element
|
||||
// Create a link element
|
||||
const link = document.createElement('a');
|
||||
link.href = response.url;
|
||||
link.target = '_blank';
|
||||
link.href = proxyUrl;
|
||||
|
||||
// Extract clean filename for download
|
||||
let filename = file.displayName;
|
||||
@@ -481,16 +518,94 @@ const downloadFile = async (file: FileItem) => {
|
||||
}
|
||||
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// Clean up
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(link);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
toast.error('Failed to generate download link');
|
||||
toast.error('Failed to download file');
|
||||
} finally {
|
||||
downloadingFiles.value[file.name] = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Download selected files
|
||||
const downloadSelected = async () => {
|
||||
const selectedFiles = files.value.filter(file => selectedItems.value.includes(file.name) && !file.isFolder);
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
toast.warning('Please select files to download (folders cannot be downloaded)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Download files sequentially with a small delay
|
||||
for (const file of selectedFiles) {
|
||||
await downloadFile(file);
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Small delay between downloads
|
||||
}
|
||||
|
||||
selectedItems.value = [];
|
||||
};
|
||||
|
||||
// Confirm delete selected
|
||||
const confirmDeleteSelected = () => {
|
||||
const itemsToDelete = files.value.filter(file => selectedItems.value.includes(file.name));
|
||||
|
||||
if (itemsToDelete.length === 0) return;
|
||||
|
||||
const hasFolder = itemsToDelete.some(item => item.isFolder);
|
||||
const message = hasFolder
|
||||
? `Are you sure you want to delete ${itemsToDelete.length} item(s)? This includes folders with all their contents.`
|
||||
: `Are you sure you want to delete ${itemsToDelete.length} file(s)?`;
|
||||
|
||||
if (confirm(message)) {
|
||||
deleteSelected();
|
||||
}
|
||||
};
|
||||
|
||||
// Delete selected items
|
||||
const deleteSelected = async () => {
|
||||
const itemsToDelete = files.value.filter(file => selectedItems.value.includes(file.name));
|
||||
|
||||
deleting.value = true;
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
try {
|
||||
for (const item of itemsToDelete) {
|
||||
try {
|
||||
await $fetch('/api/files/delete', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
fileName: item.name,
|
||||
isFolder: item.isFolder,
|
||||
},
|
||||
});
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0 && errorCount === 0) {
|
||||
toast.success(`${successCount} item(s) deleted successfully`);
|
||||
} else if (successCount > 0 && errorCount > 0) {
|
||||
toast.warning(`${successCount} item(s) deleted, ${errorCount} failed`);
|
||||
} else {
|
||||
toast.error('Failed to delete items');
|
||||
}
|
||||
|
||||
selectedItems.value = [];
|
||||
await loadFiles();
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Confirm delete
|
||||
const confirmDelete = (file: FileItem) => {
|
||||
fileToDelete.value = file;
|
||||
|
||||
Reference in New Issue
Block a user