feat: add download button to all file views

Adds a dedicated download button (blob-based, works on iOS WebApp)
to file cards, file preview modal, and image lightbox. Previously
only "open in tab" was available which doesn't work for non-browser
file types like .gpx on iOS.

Fixes #462
This commit is contained in:
Maurice
2026-04-08 18:36:51 +02:00
parent 2d17ec60db
commit 009b9f838a
+30 -1
View File
@@ -1,7 +1,7 @@
import ReactDOM from 'react-dom'
import { useState, useCallback, useRef, useEffect } from 'react'
import { useDropzone } from 'react-dropzone'
import { Upload, Trash2, ExternalLink, X, FileText, FileImage, File, MapPin, Ticket, StickyNote, Star, RotateCcw, Pencil, Check, ChevronLeft, ChevronRight } from 'lucide-react'
import { Upload, Trash2, ExternalLink, Download, X, FileText, FileImage, File, MapPin, Ticket, StickyNote, Star, RotateCcw, Pencil, Check, ChevronLeft, ChevronRight } from 'lucide-react'
import { useToast } from '../shared/Toast'
import { useTranslation } from '../../i18n'
import { filesApi } from '../../api/client'
@@ -30,6 +30,18 @@ function formatSize(bytes) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
async function triggerDownload(url: string, filename: string) {
const authUrl = await getAuthUrl(url, 'download')
const res = await fetch(authUrl)
const blob = await res.blob()
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = filename
document.body.appendChild(a)
a.click()
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove() }, 100)
}
function formatDateWithLocale(dateStr, locale) {
if (!dateStr) return ''
try {
@@ -113,6 +125,12 @@ function ImageLightbox({ files, initialIndex, onClose }: ImageLightboxProps) {
title={t('files.openTab')}>
<ExternalLink size={16} />
</button>
<button
onClick={() => triggerDownload(file.url, file.original_name)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'rgba(255,255,255,0.7)', display: 'flex', padding: 4 }}
title={t('files.download') || 'Download'}>
<Download size={16} />
</button>
<button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'rgba(255,255,255,0.7)', display: 'flex', padding: 4 }}>
<X size={18} />
</button>
@@ -514,6 +532,10 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
onMouseEnter={e => e.currentTarget.style.color = 'var(--text-primary)'} onMouseLeave={e => e.currentTarget.style.color = 'var(--text-faint)'}>
<ExternalLink size={14} />
</button>
<button onClick={() => triggerDownload(file.url, file.original_name)} title={t('files.download') || 'Download'} style={{ padding: 6, background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', borderRadius: 6, display: 'flex' }}
onMouseEnter={e => e.currentTarget.style.color = 'var(--text-primary)'} onMouseLeave={e => e.currentTarget.style.color = 'var(--text-faint)'}>
<Download size={14} />
</button>
{can('file_delete', trip) && <button onClick={() => handleDelete(file.id)} title={t('common.delete')} style={{ padding: 6, background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', borderRadius: 6, display: 'flex' }}
onMouseEnter={e => e.currentTarget.style.color = '#ef4444'} onMouseLeave={e => e.currentTarget.style.color = 'var(--text-faint)'}>
<Trash2 size={14} />
@@ -734,6 +756,13 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
onMouseLeave={e => (e.currentTarget as HTMLElement).style.color = 'var(--text-muted)'}>
<ExternalLink size={13} /> {t('files.openTab')}
</button>
<button
onClick={() => triggerDownload(previewFile.url, previewFile.original_name)}
style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--text-muted)', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'none', padding: '4px 8px', borderRadius: 6, transition: 'color 0.15s' }}
onMouseEnter={e => (e.currentTarget as HTMLElement).style.color = 'var(--text-primary)'}
onMouseLeave={e => (e.currentTarget as HTMLElement).style.color = 'var(--text-muted)'}>
<Download size={13} /> {t('files.download') || 'Download'}
</button>
<button onClick={() => setPreviewFile(null)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', display: 'flex', padding: 4, borderRadius: 6, transition: 'color 0.15s' }}
onMouseEnter={e => e.currentTarget.style.color = 'var(--text-primary)'}