diff --git a/components/FilePreviewModal.vue b/components/FilePreviewModal.vue index aba8666..cc8712d 100644 --- a/components/FilePreviewModal.vue +++ b/components/FilePreviewModal.vue @@ -199,33 +199,51 @@ const handlePreviewError = (event: any) => { loading.value = false; }; -// Download file +// Download file (with special handling for Safari) const downloadFile = async () => { if (!props.file) return; try { - // Use proxy download endpoint for better mobile compatibility - const proxyUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}`; - - // Create a link element - const link = document.createElement('a'); - link.href = proxyUrl; - // Extract clean filename for download let filename = props.file.displayName; if (!filename.includes('.') && props.file.extension) { filename += '.' + props.file.extension; } - link.download = filename; - link.style.display = 'none'; - document.body.appendChild(link); - link.click(); + // Check if Safari (iOS or desktop) + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - // Clean up - setTimeout(() => { - document.body.removeChild(link); - }, 100); + if (isSafari) { + // For Safari, use location.href to force proper filename handling + const downloadUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}`; + window.location.href = downloadUrl; + } else { + // For other browsers, use blob approach + const response = await fetch(`/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}`); + + if (!response.ok) { + throw new Error('Failed to download file'); + } + + const blob = await response.blob(); + + // Create object URL from blob + const downloadUrl = URL.createObjectURL(blob); + + // Create a link element + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = filename; + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + + // Clean up + setTimeout(() => { + document.body.removeChild(link); + URL.revokeObjectURL(downloadUrl); + }, 100); + } } catch (err) { console.error('Failed to download file:', err); } diff --git a/pages/dashboard/file-browser.vue b/pages/dashboard/file-browser.vue index b415ae9..6ea733c 100644 --- a/pages/dashboard/file-browser.vue +++ b/pages/dashboard/file-browser.vue @@ -499,37 +499,58 @@ const createNewFolder = async () => { } }; -// Download file (using proxy for Safari compatibility) +// Download file (with special handling for Safari) const downloadFile = async (file: FileItem) => { downloadingFiles.value[file.name] = true; try { - // Use proxy download endpoint for better mobile compatibility - const proxyUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(file.name)}`; - - // Create a link element - const link = document.createElement('a'); - link.href = proxyUrl; - // Extract clean filename for download let filename = file.displayName; if (!filename.includes('.') && file.extension) { filename += '.' + file.extension; } - link.download = filename; - link.style.display = 'none'; - document.body.appendChild(link); - link.click(); + // Check if Safari (iOS or desktop) + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - // Clean up - setTimeout(() => { - document.body.removeChild(link); - }, 100); + if (isSafari) { + // For Safari, open in new window to force proper filename handling + const downloadUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(file.name)}`; + window.location.href = downloadUrl; + } else { + // For other browsers, use blob approach + const response = await fetch(`/api/files/proxy-download?fileName=${encodeURIComponent(file.name)}`); + + if (!response.ok) { + throw new Error('Failed to download file'); + } + + const blob = await response.blob(); + + // Create object URL from blob + const objectUrl = URL.createObjectURL(blob); + + // Create a link element + const link = document.createElement('a'); + link.href = objectUrl; + link.download = filename; + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + + // Clean up + setTimeout(() => { + document.body.removeChild(link); + URL.revokeObjectURL(objectUrl); + }, 100); + } } catch (error) { toast.error('Failed to download file'); } finally { - downloadingFiles.value[file.name] = false; + // Add delay for Safari to prevent immediate loading state removal + setTimeout(() => { + downloadingFiles.value[file.name] = false; + }, 1000); } }; diff --git a/server/api/files/proxy-download.ts b/server/api/files/proxy-download.ts index f6be459..80eb61b 100644 --- a/server/api/files/proxy-download.ts +++ b/server/api/files/proxy-download.ts @@ -41,7 +41,8 @@ export default defineEventHandler(async (event) => { // Set headers for download setHeader(event, 'Content-Type', contentType); - setHeader(event, 'Content-Disposition', `attachment; filename="${cleanFileName}"`); + // Use both filename and filename* for better compatibility + setHeader(event, 'Content-Disposition', `attachment; filename="${cleanFileName}"; filename*=UTF-8''${encodeURIComponent(cleanFileName)}`); // Content-Length header is set automatically by Nitro when returning a buffer // Return the file buffer