feat(runtime): add native office preview and download plugin
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
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
|
||||
};
|
||||
} });
|
||||
Reference in New Issue
Block a user