From 5c138ca85126e6cdf39c75088c1d88429e23269f Mon Sep 17 00:00:00 2001 From: zemul Date: Sun, 16 Aug 2026 22:32:17 +0800 Subject: [PATCH] catalog: add dsh-video-preview (video previewer) to the file-previewer recommended plugins (#126) * catalog: add dsh-video-preview to the file-previewer recommended plugins Adds the video preview plugin (dsh-video-preview) to the "add preview plugin" modal catalog, with zh/en descriptions. Data integrity is guarded by tests/plugin-list.spec.ts. * test: make add-plugin-modal window.open count catalog-driven The test pinned the viewer catalog to a single entry (2 "Open:" buttons). Adding a second recommended plugin (dsh-video-preview) renders 4 buttons and broke the hardcoded assertion. Derive the expected count from builtinViewerPlugins so adding/removing plugins keeps the test honest. --- src/client/locales.ts | 2 ++ src/client/plugins-viewers.ts | 7 +++++++ tests/add-plugin-modal.spec.tsx | 23 +++++++++++++++-------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/client/locales.ts b/src/client/locales.ts index 1ec4604..b92e122 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -228,6 +228,7 @@ export const zh = { pluginOfficeDesc: '为 better-sidebar 编辑器提供 Office 三件套预览(.docx / .xlsx / .pptx),把重型 Office 渲染库拆出主包、按需安装', pluginSentinelDesc: '条件驱动的 agent 唤醒系统:文件/进程/端口/HTTP/命令/webhook 传感器,条件达成自动唤醒休眠会话;注册「哨兵」Tab 展示服务器全局监控表', pluginSidebarQaDesc: '基于 better-sidebar 的划选提问tab分页: 对话划选 → 右侧面板提问 → 同工作区独立追问会话(❓追问·主题):快速无思考模型压缩主对话上下文后与引文一起注入,不打断主对话;追问可嵌套、可继续、可归档', + pluginVideoPreviewDesc: '在 better-sidebar 编辑器内联预览视频文件(.mp4/.webm/.mov/.mkv/.avi 等),自带支持 HTTP Range(206)的 /video 宿主路由,可拖动进度条、不受 20MB mediaLimit 限制', } /** The en dictionary (key-set-equal to zh, enforced by the type annotation). */ @@ -449,6 +450,7 @@ export const en: Record = { pluginOfficeDesc: 'Office-suite preview (.docx / .xlsx / .pptx) for the better-sidebar editor, keeping the heavy Office render libraries out of the core bundle', pluginSentinelDesc: 'Condition-driven agent wakeup: file/process/port/http/command/webhook sensors wake dormant sessions when conditions fire; registers a "Sentinel" tab with the server-wide watch table', pluginSidebarQaDesc: 'Select-and-ask: Select conversation text → ask in the right-side panel → a dedicated follow-up session (❓追问) in the same workspace; a fast no-thinking model compresses the main context and injects it with the quote, without interrupting the main conversation. Follow-ups nest, continue, and archive', + pluginVideoPreviewDesc: 'Inline video preview (.mp4/.webm/.mov/.mkv/.avi etc.) for the better-sidebar editor, backed by a dedicated /video host route with HTTP Range (206) support — scrubbing works and files are not capped by the 20MB mediaLimit', } /** diff --git a/src/client/plugins-viewers.ts b/src/client/plugins-viewers.ts index db131ce..8fe8706 100644 --- a/src/client/plugins-viewers.ts +++ b/src/client/plugins-viewers.ts @@ -20,4 +20,11 @@ export const builtinViewerPlugins: readonly PluginEntry[] = [ description: () => t('pluginOfficeDesc'), install: 'cd ~/.dsh && dsh plugin --profile web add @huanlin/dsh-plugin-better-sidebar-plugin-office', }, + { + id: 'dsh-video-preview', + name: '视频预览插件', + url: 'https://github.com/zemul/dsh-video-preview', + description: () => t('pluginVideoPreviewDesc'), + install: 'cd ~/.dsh && dsh plugin --profile web add dsh-video-preview', + }, ] diff --git a/tests/add-plugin-modal.spec.tsx b/tests/add-plugin-modal.spec.tsx index b1ec09b..6aedab3 100644 --- a/tests/add-plugin-modal.spec.tsx +++ b/tests/add-plugin-modal.spec.tsx @@ -155,7 +155,7 @@ describe('PluginListBody copy click (interactive)', () => { vi.restoreAllMocks() }) - it('both the name button and the jump button open the repo in a NEW browser tab (window.open, not a link)', () => { + it('the name and jump buttons open each catalog entry in a NEW browser tab (window.open, not a link)', () => { const store = createSidebarStore() const service = createBetterSidebarService(store) const opened: Array<[string, string, string]> = [] @@ -171,14 +171,21 @@ describe('PluginListBody copy click (interactive)', () => { act(() => { root.render(createElement(PluginListBody, { service, kind: 'viewer' })) }) + // Two window.open buttons per catalog entry — the name button and the + // jump button. The expected count is catalog-driven so adding or + // removing recommended plugins keeps the test honest instead of + // pinning a hardcoded total. const jumpButtons = [...container.querySelectorAll('button[aria-label^="Open:"]')] - expect(jumpButtons).toHaveLength(2) // the name + the jump button - act(() => { (jumpButtons[0] as HTMLButtonElement).click() }) - act(() => { (jumpButtons[1] as HTMLButtonElement).click() }) - expect(opened).toEqual([ - [officeEntry.url, '_blank', 'noopener'], - [officeEntry.url, '_blank', 'noopener'], - ]) + expect(jumpButtons).toHaveLength(builtinViewerPlugins.length * 2) + for (const button of jumpButtons) { + act(() => { (button as HTMLButtonElement).click() }) + } + expect(opened).toEqual( + builtinViewerPlugins.flatMap((entry) => [ + [entry.url, '_blank', 'noopener'], + [entry.url, '_blank', 'noopener'], + ]), + ) act(() => { root.unmount() }) container.remove() } finally {