window.__ModuleLoader__.load({ id: 'dsh-preview-download', factory: require => { const React = require('react'); const { createElement: h, useState, useRef, useEffect } = React; const namespace = 'previewDownload'; function filename(path) { const name = path.replaceAll('\\', '/').split('/').pop() || 'download'; return Array.from(name, char => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 || '<>:"|?*'.includes(char) ? '_' : char).join(''); } async function readOriginal(remote, sessionId, path, signal) { signal.throwIfAborted(); const result = await remote.workspaceFiles.readBytes(sessionId, path, {}, signal); signal.throwIfAborted(); if (!result.ok) throw Object.assign(new Error('Download refused'), { code: result.error.code }); const file = result.value; if (!file.eof || file.offset !== 0 || !(file.data instanceof Uint8Array) || (file.bytes !== undefined && file.bytes !== file.data.byteLength)) { throw new Error('Incomplete file response'); } return file.data; } function Download({ absolutePath, download, t }) { const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const pending = useRef(null); useEffect(() => { setBusy(false); setError(''); return () => { pending.current?.abort(); pending.current = null; }; }, [absolutePath, download]); async function start() { if (pending.current) return; const controller = new AbortController(); pending.current = controller; setBusy(true); setError(''); try { const bytes = await download(absolutePath, controller.signal); controller.signal.throwIfAborted(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/octet-stream' })); const link = document.createElement('a'); link.href = url; link.download = filename(absolutePath); link.hidden = true; document.body.appendChild(link); try { link.click(); } finally { link.remove(); setTimeout(() => URL.revokeObjectURL(url), 60000); } } catch (cause) { if (!controller.signal.aborted) setError(t(cause?.code === 'workspace-file/too-large' ? 'tooLarge' : 'failed')); } finally { if (pending.current === controller) { pending.current = null; setBusy(false); } } } return h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px', flexShrink: 0 } }, h('button', { type: 'button', disabled: busy, onClick: start, title: t('download'), 'aria-label': t('download'), 'aria-busy': busy, 'data-preview-download': '', style: { color: 'inherit', background: 'transparent', border: '1px solid currentColor', borderRadius: '6px', padding: '4px 8px', font: 'inherit', cursor: busy ? 'wait' : 'pointer' } }, busy ? t('loading') : t('download')), error && h('span', { role: 'alert', style: { fontSize: '12px', maxWidth: '180px' } }, error)); } return { inject: ['slots', 'locale', 'remote.workspaceFiles'], apply(ctx) { ctx.effect(() => ctx.locale.register(namespace, { zh: { download: '下载原文件', loading: '下载中…', failed: '下载失败,请检查文件权限或重试。', tooLarge: '文件超过下载大小限制。' }, en: { download: 'Download original', loading: 'Downloading…', failed: 'Download failed. Check access or retry.', tooLarge: 'File exceeds the download size limit.' } })); ctx.effect(() => ctx.slots.inject('sidebar.right.tab.document.actions', () => ctx.slots.register({ name: 'sidebar.right.tab.document.actions', id: 'dsh-preview-download', locale: namespace, inject: sessionId => ({ download: (path, signal) => readOriginal(ctx.remote, sessionId, path, signal) }) }, Download))); }, // Pure helpers are also exercised by the offline regression suite. filename, readOriginal }; } });