mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
feat(web): add download button to file viewer (#926)
Adds a download icon button to the file viewer toolbar (next to copy-path). Clicking it decodes the existing base64 file content into a Blob and triggers a browser download — no new backend endpoint required. Works for text, binary, and image files. Button is hidden until the file has loaded successfully. Closes #924 via [HAPI](https://hapi.run) Co-authored-by: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -310,6 +310,7 @@ export default {
|
||||
'file.page.unknownPath': 'Unknown path',
|
||||
'file.page.copyPath': 'Copy path',
|
||||
'file.page.copyContent': 'Copy file content',
|
||||
'file.page.download': 'Download file',
|
||||
'file.page.tab.diff': 'Diff',
|
||||
'file.page.tab.file': 'File',
|
||||
'file.page.missingPath': 'No file path provided.',
|
||||
|
||||
@@ -314,6 +314,7 @@ export default {
|
||||
'file.page.unknownPath': '未知路径',
|
||||
'file.page.copyPath': '复制路径',
|
||||
'file.page.copyContent': '复制文件内容',
|
||||
'file.page.download': '下载文件',
|
||||
'file.page.tab.diff': 'Diff',
|
||||
'file.page.tab.file': '文件',
|
||||
'file.page.missingPath': '未提供文件路径。',
|
||||
|
||||
@@ -36,6 +36,44 @@ function decodePath(value: string): string {
|
||||
return decoded.ok ? decoded.text : value
|
||||
}
|
||||
|
||||
function DownloadIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={props.className}
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function triggerDownload(fileName: string, base64Content: string, mimeType: string | null) {
|
||||
const byteChars = atob(base64Content)
|
||||
const byteArray = new Uint8Array(byteChars.length)
|
||||
for (let i = 0; i < byteChars.length; i++) {
|
||||
byteArray[i] = byteChars.charCodeAt(i)
|
||||
}
|
||||
const blob = new Blob([byteArray], { type: mimeType ?? 'application/octet-stream' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function BackIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
@@ -206,6 +244,8 @@ export default function FilePage() {
|
||||
&& decodedContent.length > 0
|
||||
&& contentSizeBytes <= MAX_COPYABLE_FILE_BYTES
|
||||
|
||||
const canDownload = fileContentResult?.success === true && Boolean(fileContentResult.content)
|
||||
|
||||
const [displayMode, setDisplayMode] = useState<'diff' | 'file'>('diff')
|
||||
|
||||
useEffect(() => {
|
||||
@@ -260,6 +300,16 @@ export default function FilePage() {
|
||||
>
|
||||
{pathCopied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
{canDownload ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => triggerDownload(fileName, fileContentResult!.content!, imageMimeType)}
|
||||
className="shrink-0 rounded p-1 text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] transition-colors"
|
||||
title={t('file.page.download')}
|
||||
>
|
||||
<DownloadIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user