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.unknownPath': 'Unknown path',
|
||||||
'file.page.copyPath': 'Copy path',
|
'file.page.copyPath': 'Copy path',
|
||||||
'file.page.copyContent': 'Copy file content',
|
'file.page.copyContent': 'Copy file content',
|
||||||
|
'file.page.download': 'Download file',
|
||||||
'file.page.tab.diff': 'Diff',
|
'file.page.tab.diff': 'Diff',
|
||||||
'file.page.tab.file': 'File',
|
'file.page.tab.file': 'File',
|
||||||
'file.page.missingPath': 'No file path provided.',
|
'file.page.missingPath': 'No file path provided.',
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ export default {
|
|||||||
'file.page.unknownPath': '未知路径',
|
'file.page.unknownPath': '未知路径',
|
||||||
'file.page.copyPath': '复制路径',
|
'file.page.copyPath': '复制路径',
|
||||||
'file.page.copyContent': '复制文件内容',
|
'file.page.copyContent': '复制文件内容',
|
||||||
|
'file.page.download': '下载文件',
|
||||||
'file.page.tab.diff': 'Diff',
|
'file.page.tab.diff': 'Diff',
|
||||||
'file.page.tab.file': '文件',
|
'file.page.tab.file': '文件',
|
||||||
'file.page.missingPath': '未提供文件路径。',
|
'file.page.missingPath': '未提供文件路径。',
|
||||||
|
|||||||
@@ -36,6 +36,44 @@ function decodePath(value: string): string {
|
|||||||
return decoded.ok ? decoded.text : value
|
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 }) {
|
function BackIcon(props: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -206,6 +244,8 @@ export default function FilePage() {
|
|||||||
&& decodedContent.length > 0
|
&& decodedContent.length > 0
|
||||||
&& contentSizeBytes <= MAX_COPYABLE_FILE_BYTES
|
&& contentSizeBytes <= MAX_COPYABLE_FILE_BYTES
|
||||||
|
|
||||||
|
const canDownload = fileContentResult?.success === true && Boolean(fileContentResult.content)
|
||||||
|
|
||||||
const [displayMode, setDisplayMode] = useState<'diff' | 'file'>('diff')
|
const [displayMode, setDisplayMode] = useState<'diff' | 'file'>('diff')
|
||||||
|
|
||||||
useEffect(() => {
|
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" />}
|
{pathCopied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user