feat: add Office, PDF, and PPTX previews

This commit is contained in:
HuanLinOTO
2026-08-10 16:46:01 +08:00
parent 4060bd53e1
commit a6befc8b6b
23 changed files with 5158 additions and 11 deletions
+46 -5
View File
@@ -13,11 +13,16 @@ import { EditorState } from '@codemirror/state'
import { EditorView as CodeMirrorView, keymap, lineNumbers } from '@codemirror/view'
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
import { IconCheckOutline16, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import { api, mediaUrl, type SessionScope } from './api.ts'
import { api, downloadUrl, mediaUrl, type SessionScope } from './api.ts'
import { languageForPath } from './lang.ts'
import { cmSurfaceTheme, CmThemeCompartment } from './cm-themes.ts'
import { isDarkScheme, subscribeColorScheme } from './theme.ts'
import { t } from './locales.ts'
import { officeKindForExt } from './office-types.ts'
import { DocxView, XlsxView } from './office-view.tsx'
import { isPdfExt } from './pdf-types.ts'
import { PdfView } from './PdfView.tsx'
import { PptxView } from './PptxView.tsx'
import css from './sidebar.module.css'
const IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico', '.avif']
@@ -26,8 +31,8 @@ const MD_EXT = ['.md', '.markdown']
type EditorLoad =
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'ready'; kind: 'text' | 'image' | 'md'; content: string; truncated: boolean }
| { status: 'binary' }
| { status: 'ready'; kind: 'text' | 'image' | 'md' | 'docx' | 'xlsx' | 'pptx' | 'pdf'; content: string; truncated: boolean }
| { status: 'binary' } // unknown binary / OLE legacy formats → download button
/** Previewable files (rendered output vs source editing). */
type ViewMode = 'preview' | 'edit'
@@ -57,9 +62,26 @@ export function EditorView(props: { scope: SessionScope; path: string; title: st
setDraft(null)
setDirty(false)
setSaveState('idle')
// Office preview short-circuit: the host's readText would NUL-probe every
// Office file as binary (a wasted round-trip), and .docx/.xlsx have their
// own renderers below. Detect by extension and skip fsRead entirely —
// the renderer fetches bytes through the media route itself.
const ext = extOfPath(path)
if (isPdfExt(ext)) {
setLoad({ status: 'ready', kind: 'pdf', content: '', truncated: false })
return
}
const officeKind = officeKindForExt(ext)
if (officeKind === 'docx' || officeKind === 'xlsx' || officeKind === 'pptx') {
setLoad({ status: 'ready', kind: officeKind, content: '', truncated: false })
return
}
if (officeKind === 'download-only') {
setLoad({ status: 'binary' })
return
}
api.fsRead(scope, path).then((result) => {
if (cancelled) return
const ext = extOfPath(path)
if (result.kind === 'binary') {
setLoad({ status: 'binary' })
return
@@ -207,7 +229,14 @@ export function EditorView(props: { scope: SessionScope; path: string; title: st
</div>
{load.status === 'loading' && <div className={css.editorPlaceholder}>{t('loading')}</div>}
{load.status === 'error' && <div className={css.editorError}>{load.message}</div>}
{load.status === 'binary' && <div className={css.editorPlaceholder}>{t('binary')}</div>}
{load.status === 'binary' && (
<div className={css.editorBinary}>
<span className={css.editorBinaryNotice}>{t('binaryNoPreview')}</span>
<a className={css.editorDownloadLink} href={downloadUrl(scope, path)} download>
{t('downloadToView')}
</a>
</div>
)}
{editable && (
<>
{load.truncated && mode === 'edit' && <div className={css.editorBanner}>{t('truncation')}</div>}
@@ -222,6 +251,18 @@ export function EditorView(props: { scope: SessionScope; path: string; title: st
<img className={css.editorImage} src={mediaUrl(scope, path)} alt={title} />
</div>
)}
{load.status === 'ready' && load.kind === 'docx' && (
<DocxView scope={scope} path={path} title={title} />
)}
{load.status === 'ready' && load.kind === 'xlsx' && (
<XlsxView scope={scope} path={path} title={title} />
)}
{load.status === 'ready' && load.kind === 'pptx' && (
<PptxView scope={scope} path={path} title={title} />
)}
{load.status === 'ready' && load.kind === 'pdf' && (
<PdfView scope={scope} path={path} title={title} />
)}
{previewable && mode === 'preview' && (
<div className={css.editorMd}>
<MarkdownText text={draft ?? load.content} />