dsh-remotion 0.10.3 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.10.4 (2026-10-30)
4
+
5
+ - 视频工作台:导出前新增检查清单(标题、画幅与时长、素材数、已保存修订、渲染环境、上一份成品对应的修订);预览与导出任务显示真实步骤进度,失败后可就同一修订直接重试;同一工程在别处更新时不再提供会导致反复冲突的「保存后继续」,改为显式重新载入或保留输入;保存/导出按钮不可用时给出具体原因;空工程列表提供「新建视频」入口。
6
+ - Pre-export checklist, step progress with in-place retry, conflict-safe save flow, disabled-button reasons and an empty-state action in the video workbench.
7
+
8
+ - 同步共享 workbench-ui v1.1:Page 外的 Empty / Dialog / Status 自带样式与 `.dshw` 主题作用域,Status 新增 warn 色调。
9
+ - Sync workbench-ui v1.1: Empty / Dialog / Status now carry their own styles and `.dshw` theme scope outside Page, and Status gains a warn tone.
10
+
3
11
  ## 0.10.3 (2026-10-30)
4
12
 
5
13
  - 重整中英文首页:用途、三步流程图、安装入口与操作示例;详细配置与边界集中到完整说明。
package/docs/USAGE.en.md CHANGED
@@ -30,7 +30,9 @@ Open Settings → Remotion in DSH Web or Desktop.
30
30
  2. Upload local PNG/JPEG copies, one MP3/WAV background audio and one muted MP4 background video. Limits: 20 MB each, 40 MB total. Slideshow follows upload order and needs at least two seconds per image.
31
31
  3. Explicitly prepare the environment. It downloads official CLI 4.0.539; installed Chrome is reused, with a browser download only when needed. You can continue editing during preparation. Check the [official Remotion license](https://www.remotion.dev/license) for your use case.
32
32
  4. Start official Studio and inspect the picture. Stop this preview before editing or exporting; no window opens automatically.
33
- 5. Export, play and download MP4. Editing preserves earlier MP4s with a revision warning; export again for a new output.
33
+ 5. Review the pre-export check (title, canvas and duration, media count, saved revision, renderer, which revision the last MP4 used), then play and download the MP4 in the workbench. Later edits preserve earlier MP4s and display a revision warning. Export again for a new final output.
34
+
35
+ Preview and export jobs show their current step (for example "Step 3 / 4 · Rendering MP4"), a collapsible job log and a cancel button. A failed job keeps the project and offers "Retry this task" without re-entering input. A project changed elsewhere is never overwritten silently: the banner offers "Reload the latest version (discard my input)" or "Keep my input for now", and the unsaved-changes dialog blocks "Save and continue" so the same conflict cannot repeat. When Save or Export is unavailable, hover the button to read the exact reason (unsaved changes, renderer not prepared, title or duration out of range).
34
36
 
35
37
  The editable ZIP includes source, data and media. Extract it, run npm install and npm run preview / render. Original template code is MIT; engines retain their own licenses. ZIP import is not yet available in settings.
36
38
 
package/docs/USAGE.md CHANGED
@@ -30,7 +30,9 @@ dsh plugin --profile web remove dsh-remotion
30
30
  2. 上传 PNG/JPEG、一个背景音频(MP3/WAV)、一个静音背景视频(MP4)的副本。每个文件最多 20 MB、总计 40 MB;轮播按上传顺序,每张至少两秒。
31
31
  3. 首次点击「准备渲染环境」。此时下载固定版本官方 CLI 4.0.539,优先使用已安装的 Chrome,没有时再下载浏览器;可以继续编辑工程。下载失败保留工程,查看运行记录后重试。使用前确认 [Remotion 官方许可证](https://www.remotion.dev/license)适合你的用途。
32
32
  4. 启动官方 Studio,检查画面。停止本次预览后再编辑或导出;不会自动打开或占用前台窗口。
33
- 5. 导出 MP4、在工作台播放并下载。工程更新后保留旧成品并显示旧修订提醒;再次导出即可生成新成片。
33
+ 5. 导出 MP4 前先看导出前检查(标题、画幅与时长、素材数、已保存修订、渲染环境、上一份成品对应哪次修订),确认后在工作台播放并下载。工程更新后保留旧成品并显示旧修订提醒;再次导出即可生成新成片。
34
+
35
+ 预览与导出任务在顶部就地显示当前步骤(如「步骤 3 / 4 · 渲染 MP4」)、可展开的运行记录和取消按钮。任务失败时工程保留,可直接「重试本次任务」,无需重新填写。同一工程在别处被更新时不会静默覆盖:提示已在别处更新,可「重新载入最新版本(放弃我的输入)」或「保留我的输入,稍后处理」;此时未保存对话框里的「保存后继续」会被禁用,避免反复冲突。导出、保存按钮不可用时,悬停可看到具体原因(未保存修改、环境未准备、标题或时长不合规)。
34
36
 
35
37
  「备份可编辑工程」包含源代码、项目数据和素材。解压后按 README 使用 npm install、npm run preview / render;原模板代码 MIT,渲染器保留自己的许可证。
36
38
 
package/docs/WORKBENCH.md CHANGED
@@ -14,6 +14,16 @@ Update the plugin, restart DSH and open its native sidebar panel (or More if it
14
14
 
15
15
  The UI follows host language and theme without translating user content. Narrow master/detail views have a Back action; editors adapt their columns to available space. Destructive actions require confirmation and edits are protected.
16
16
 
17
+ ## 任务与冲突 / Jobs and conflicts
18
+
19
+ 预览与导出任务在编辑页顶部就地显示阶段文案、当前步骤(步骤 N / M)、取消按钮和可展开的运行记录。失败时就地提供「重试本次任务」,复用同一已保存修订,无需重新填写;步骤名来自官方 CLI 的真实阶段,进度不虚构百分比。导出按钮先打开导出前检查,列出标题、画幅与时长、素材数、已保存修订、渲染环境和上一份成品对应的修订,确认后才开始渲染。
20
+
21
+ Jobs render in place above the editor: stage text, a step counter (step N of M), cancel and a collapsible log. A failure offers Retry for the same saved revision instead of discarding input. Steps map to the official CLI stages, so progress is never a fabricated percentage. Export opens a pre-export check listing title, canvas and duration, media count, saved revision, renderer state and which revision the last MP4 used.
22
+
23
+ 同一工程在两个窗口保存时,后保存的一方看到冲突提示,而不是静默覆盖:可重新载入最新版本(放弃当前输入)或保留输入稍后处理;冲突状态下未保存对话框禁用「保存后继续」。
24
+
25
+ When two windows save the same project, the later one sees a conflict instead of silently overwriting: reload the latest version (discarding input) or keep the input for later. While a conflict is open, the unsaved-changes dialog disables Save and continue.
26
+
17
27
  ## 源码 / Source
18
28
 
19
29
  编辑 templates/client.js、templates/panels/*.js 与 templates/workbench-ui.js,运行 pnpm run build。lib/client.js 是确定性产物,请随源码提交。公共组件的规范源位于 dsh-suite/ui/workbench-ui.js,Suite 的同步脚本显式分发副本;单个仓库可独立构建和安装。
package/lib/client.js CHANGED
@@ -494,7 +494,7 @@ function createWorkbenchUI(React) {
494
494
  }
495
495
  function Page({ title, subtitle, actions, children, workbench = false, className, rootRef, ...props }) {
496
496
  return h('section', { ...props, ref: rootRef, className: cx('dshw', 'w-page', className), 'data-main': workbench ? 'true' : 'false' },
497
- h('style', null, styles),
497
+ h(Styles, { key: 'styles' }),
498
498
  h('header', { className: 'w-page-header' }, h('div', { className: 'w-heading' }, h('h1', null, title), subtitle ? h('p', { className: 'w-subtitle' }, subtitle) : null), actions ? h('div', { className: 'w-header-actions' }, actions) : null),
499
499
  h('div', { className: 'w-body' }, children));
500
500
  }
@@ -517,10 +517,10 @@ function createWorkbenchUI(React) {
517
517
  }
518
518
  function Status({ children, tone = 'info', className }) {
519
519
  if (!children) return null;
520
- return h('div', { className: cx('w-status', 'w-status-' + tone, className), role: tone === 'error' ? 'alert' : 'status', 'aria-live': tone === 'error' ? 'assertive' : 'polite' }, h(Icon, { name: tone === 'success' ? 'check' : 'info', size: 18 }), h('div', null, children));
520
+ return h('div', { className: cx('dshw', 'w-status', 'w-status-' + tone, className), role: tone === 'error' ? 'alert' : 'status', 'aria-live': tone === 'error' ? 'assertive' : 'polite' }, h(Icon, { name: tone === 'success' ? 'check' : 'info', size: 18 }), h('div', null, children));
521
521
  }
522
522
  function Empty({ title, children, action, icon = 'file' }) {
523
- return h('div', { className: 'w-empty' }, h(Icon, { name: icon, size: 32 }), h('h2', null, title), children ? h('div', { className: 'w-muted' }, children) : null, action ? h('div', { className: 'w-toolbar' }, action) : null);
523
+ return h('div', { className: 'dshw w-empty' }, h(Styles, { key: 'styles' }), h(Icon, { name: icon, size: 32 }), h('h2', null, title), children ? h('div', { className: 'w-muted' }, children) : null, action ? h('div', { className: 'w-toolbar' }, action) : null);
524
524
  }
525
525
  function Split({ list, detail, selected, onBack, backLabel, className, listWidth = 360 }) {
526
526
  const ref = useRef(null);
@@ -564,7 +564,8 @@ function createWorkbenchUI(React) {
564
564
  else if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
565
565
  else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
566
566
  }
567
- return h('div', { className: 'w-overlay', onClick: event => { if (!busy && event.target === event.currentTarget) onClose(); } },
567
+ return h('div', { className: 'dshw w-overlay', onClick: event => { if (!busy && event.target === event.currentTarget) onClose(); } },
568
+ h(Styles, { key: 'styles' }),
568
569
  h('section', { ref: panel, className: cx('w-dialog', className), role: 'dialog', 'aria-modal': true, 'aria-labelledby': id, tabIndex: -1, onKeyDown: keydown },
569
570
  h('header', { className: 'w-dialog-header' }, h('h2', { id }, title), h(IconButton, { icon: 'close', label: closeLabel, disabled: busy, onClick: onClose })), h('div', { className: 'w-dialog-body' }, children)));
570
571
  }
@@ -578,7 +579,7 @@ function createWorkbenchUI(React) {
578
579
  .dshw{--w-bg:var(--dsw-alias-bg-layer-1,#fff);--w-soft:var(--dsw-alias-bg-layer-2,#f7f8fa);--w-text:var(--dsw-alias-label-primary,#18202c);--w-muted:var(--dsw-alias-label-secondary,#667085);--w-border:var(--dsw-alias-border-l3,#e7e9ef);--w-accent:var(--dsh-plugin-workbench-accent,#4f6bdb);--w-selected:color-mix(in srgb,var(--w-accent) 10%,var(--w-bg));--w-primary:var(--w-text);--w-on-primary:var(--w-bg);container-type:inline-size;container-name:dsh-workbench;color:var(--w-text);background:var(--w-bg);font:14px/1.65 var(--dsh-font-family-content,system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif);min-width:0;max-width:100%;box-sizing:border-box;text-align:start;color-scheme:inherit}
579
580
  .dshw *,.dshw *:before,.dshw *:after{box-sizing:border-box}.dshw h1,.dshw h2,.dshw h3,.dshw h4,.dshw p{margin:0}.dshw h1{font-size:30px;line-height:1.25;letter-spacing:-.025em;font-weight:650}.dshw h2{font-size:24px;line-height:1.35;font-weight:650;letter-spacing:-.015em;overflow-wrap:anywhere}.dshw h3,.dshw h4{font-size:16px;line-height:1.4;font-weight:600}.dshw a{color:var(--w-accent);text-underline-offset:3px;overflow-wrap:anywhere}.dshw button,.dshw input,.dshw select,.dshw textarea{font:inherit;color:inherit;max-width:100%}.dshw input:not([type=checkbox]):not([type=radio]):not([type=file]),.dshw select,.dshw textarea{background:var(--w-bg);border:1px solid var(--w-border);border-radius:8px;padding:8px 11px;min-width:0;line-height:1.5}.dshw input:not([type=checkbox]):not([type=radio]):not([type=file]),.dshw select{min-height:38px}.dshw textarea{resize:vertical}.dshw input[type=checkbox],.dshw input[type=radio]{width:16px;height:16px;flex-shrink:0;accent-color:var(--w-accent)}.dshw :focus-visible{outline:2px solid var(--w-accent);outline-offset:3px}.dshw button:disabled,.dshw input:disabled,.dshw select:disabled,.dshw textarea:disabled{opacity:.5;cursor:not-allowed}.dshw label{font-size:13px}.dshw fieldset{min-width:0;border:1px solid var(--w-border);border-radius:8px}.dshw summary{cursor:pointer;font-weight:550;list-style:none;display:flex;align-items:center;gap:8px}.dshw summary::-webkit-details-marker{display:none}.dshw summary:after{content:'';display:block;margin-inline-start:auto;width:7px;height:7px;border-inline-end:1.75px solid currentColor;border-bottom:1.75px solid currentColor;transform:rotate(45deg);flex-shrink:0}.dshw details[open]>summary:after{transform:rotate(225deg)}.dshw details>summary+*{margin-top:12px}.dshw pre{white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.7 ui-monospace,SFMono-Regular,Consolas,monospace;max-width:100%;background:var(--w-soft);border-radius:6px;padding:14px}.dshw img,.dshw video,.dshw iframe{max-width:100%}
580
581
  .w-page{display:flex;flex-direction:column;position:relative;padding:0;min-height:540px;gap:0}.w-page[data-main=true]{height:100%;min-height:0;padding:calc(24px + var(--dsh-frame-top-clearance,0px)) 28px 0 max(28px,var(--dsh-frame-leading-clearance,0px));overflow:auto}.w-page-header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;padding-bottom:22px;flex-shrink:0}.w-heading{min-width:0}.w-subtitle{margin-top:8px!important;font-size:14px;color:var(--w-muted);line-height:1.6}.w-header-actions,.w-toolbar,.w-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;min-width:0}.w-header-actions{justify-content:flex-end}.w-body,.w-tabs-layout{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0}.w-body{overflow:auto}.w-tab-panel{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:auto}.w-tabs{display:flex;align-items:stretch;gap:20px;border-bottom:1px solid var(--w-border);flex-shrink:0;overflow-x:auto;scrollbar-width:thin}.dshw .w-tabs>button{border:0;border-bottom:2px solid transparent;background:transparent;padding:11px 8px 13px;white-space:nowrap;font-size:14px;font-weight:550;cursor:pointer;color:var(--w-muted);display:flex;align-items:center;gap:7px}.dshw .w-tabs>button[aria-selected=true]{border-bottom-color:var(--w-accent);color:var(--w-accent)}.w-count,.w-tag{font-size:12px;line-height:1.5;display:inline-flex;align-items:center;flex-shrink:0;background:var(--w-soft);border-radius:5px;padding:1px 6px;color:var(--w-muted)}.w-tabs [aria-selected=true] .w-count,.w-selected .w-tag{color:var(--w-accent);background:var(--w-selected)}
581
- .dshw .w-button{display:inline-flex;justify-content:center;align-items:center;gap:7px;background:var(--w-bg);border:1px solid var(--w-border);border-radius:8px;padding:8px 12px;min-height:36px;line-height:1.4;font-size:14px;font-weight:500;cursor:pointer;text-decoration:none;white-space:normal}.dshw .w-button:not(:disabled):hover{background:var(--w-soft)}.dshw .w-primary{background:var(--w-primary);border-color:var(--w-primary);color:var(--w-on-primary)}.dshw .w-primary:not(:disabled):hover{background:var(--w-primary);opacity:.9}.dshw .w-ghost{background:transparent;border-color:transparent}.dshw .w-danger{color:var(--dsw-alias-state-error-primary,#b43c3c)}.dshw .w-icon-button{width:36px;min-width:36px;padding:8px}.dshw .w-button svg{flex-shrink:0}.w-muted{color:var(--w-muted);font-size:12px;line-height:1.6}.w-section{padding:24px 0;border-bottom:1px solid var(--w-border);min-width:0}.w-section>h3{margin-bottom:12px}.w-section>p+p{margin-top:8px}.w-fields{display:grid;gap:16px;min-width:0}.w-field{display:grid;gap:6px;min-width:0}.w-field>input,.w-field>select,.w-field>textarea{width:100%}.w-status{display:flex;align-items:flex-start;gap:8px;padding:11px 13px;margin:12px 0;border:1px solid var(--w-border);border-radius:8px;background:var(--w-soft);font-size:13px;overflow-wrap:anywhere;flex-shrink:0}.w-status>svg{flex-shrink:0;margin-top:2px}.w-status-error{color:var(--dsw-alias-state-error-primary,#ad3636)}.w-status-success{color:var(--dsw-alias-state-success-primary,#24794a)}.w-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:14px;max-width:580px;padding:48px 24px;margin:auto;min-height:280px;overflow-wrap:anywhere}.w-empty>svg{color:var(--w-muted)}.w-empty h2{font-size:20px}.w-empty .w-muted{font-size:14px}.w-empty .w-toolbar{justify-content:center}
582
+ .dshw .w-button{display:inline-flex;justify-content:center;align-items:center;gap:7px;background:var(--w-bg);border:1px solid var(--w-border);border-radius:8px;padding:8px 12px;min-height:36px;line-height:1.4;font-size:14px;font-weight:500;cursor:pointer;text-decoration:none;white-space:normal}.dshw .w-button:not(:disabled):hover{background:var(--w-soft)}.dshw .w-primary{background:var(--w-primary);border-color:var(--w-primary);color:var(--w-on-primary)}.dshw .w-primary:not(:disabled):hover{background:var(--w-primary);opacity:.9}.dshw .w-ghost{background:transparent;border-color:transparent}.dshw .w-danger{color:var(--dsw-alias-state-error-primary,#b43c3c)}.dshw .w-icon-button{width:36px;min-width:36px;padding:8px}.dshw .w-button svg{flex-shrink:0}.w-muted{color:var(--w-muted);font-size:12px;line-height:1.6}.w-section{padding:24px 0;border-bottom:1px solid var(--w-border);min-width:0}.w-section>h3{margin-bottom:12px}.w-section>p+p{margin-top:8px}.w-fields{display:grid;gap:16px;min-width:0}.w-field{display:grid;gap:6px;min-width:0}.w-field>input,.w-field>select,.w-field>textarea{width:100%}.w-status{display:flex;align-items:flex-start;gap:8px;padding:11px 13px;margin:12px 0;border:1px solid var(--w-border);border-radius:8px;background:var(--w-soft);font-size:13px;overflow-wrap:anywhere;flex-shrink:0}.w-status>svg{flex-shrink:0;margin-top:2px}.w-status-error{color:var(--dsw-alias-state-error-primary,#ad3636)}.w-status-success{color:var(--dsw-alias-state-success-primary,#24794a)}.w-status-warn{color:var(--dsw-alias-state-warning-primary,#8a5a00)}.w-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:14px;max-width:580px;padding:48px 24px;margin:auto;min-height:280px;overflow-wrap:anywhere}.w-empty>svg{color:var(--w-muted)}.w-empty h2{font-size:20px}.w-empty .w-muted{font-size:14px}.w-empty .w-toolbar{justify-content:center}
582
583
  .w-split{display:grid;grid-template-columns:minmax(240px,var(--w-list-width)) minmax(0,1fr);min-height:460px;flex:1;min-width:0}.w-master{min-width:0;border-inline-end:1px solid var(--w-border);overflow:auto;padding-bottom:24px}.w-detail{min-width:0;padding:26px 28px;overflow:auto;overflow-wrap:anywhere;display:flex;flex-direction:column}.w-master-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:20px 16px}.w-search{display:flex;align-items:center;gap:8px;border:1px solid var(--w-border);border-radius:8px;padding:0 10px;min-width:0;flex:1;background:var(--w-bg)}.w-search>svg{color:var(--w-muted);flex-shrink:0}.dshw .w-search>input{border:0!important;background:transparent!important;padding:8px 0!important;width:100%;outline-offset:0}.w-row{display:flex;gap:0;min-width:0;position:relative;border-bottom:1px solid var(--w-border);border-inline-start:3px solid transparent}.w-row-select{display:flex;align-items:flex-start;padding:21px 0 0 14px}.dshw .w-row-button{display:flex;flex-direction:column;align-items:stretch;gap:7px;text-align:start;width:100%;padding:18px 18px 18px 16px;border:0;background:transparent;color:inherit;min-width:0;cursor:pointer;line-height:1.5}.dshw .w-row-button:hover{background:var(--w-soft)}.w-row-head{display:flex;align-items:flex-start;gap:10px;justify-content:space-between;min-width:0}.w-row-head>strong{font-size:14px;line-height:1.5;font-weight:600;overflow-wrap:anywhere}.w-row-meta,.w-row-description{color:var(--w-muted);font-size:12px;line-height:1.6;overflow-wrap:anywhere}.w-row-description{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.w-selected{background:var(--w-selected);border-inline-start-color:var(--w-accent)}.w-back{display:none!important}.w-detail-title{margin-bottom:12px!important}.w-detail-meta{color:var(--w-muted);font-size:13px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.w-detail-footer{padding:24px 0;display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:auto;border-top:1px solid var(--w-border)}.w-detail-footer .w-muted{flex:1;min-width:160px}.w-definition{display:grid;grid-template-columns:minmax(80px,120px) minmax(0,1fr);gap:8px 16px;font-size:14px;margin:20px 0}.w-definition dt{color:var(--w-muted)}.w-definition dd{margin:0;min-width:0;overflow-wrap:anywhere}
583
584
  .w-switch-label{display:inline-flex;align-items:center;gap:8px;font-size:13px;position:relative;cursor:pointer}.w-switch-label>input{position:absolute;opacity:0;inset-inline-end:0;width:36px!important;height:22px!important;margin:0;cursor:pointer}.w-switch-track{width:36px;height:22px;border-radius:12px;display:block;background:var(--w-border);border:1px solid var(--w-border);pointer-events:none;padding:2px;transition:background .15s}.w-switch-track:before{content:'';display:block;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px #0002;transition:transform .15s}.w-switch-label>input:checked+.w-switch-track{background:var(--w-accent);border-color:var(--w-accent)}.w-switch-label>input:checked+.w-switch-track:before{transform:translateX(14px)}.w-switch-label>input:focus-visible+.w-switch-track{outline:2px solid var(--w-accent);outline-offset:3px}.w-switch-label:has(>input:disabled){opacity:.5;cursor:not-allowed}
584
585
  .w-overlay{position:fixed;inset:0;background:#0005;display:flex;justify-content:flex-end;z-index:1000;padding:12px;overflow:auto}.w-dialog{width:min(620px,100%);max-height:100%;display:flex;flex-direction:column;min-height:0;background:var(--w-bg);color:var(--w-text);border:1px solid var(--w-border);border-radius:12px;box-shadow:0 18px 70px #0003}.w-dialog-header{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:20px 24px;border-bottom:1px solid var(--w-border);flex-shrink:0}.w-dialog-header>h2{font-size:20px}.w-dialog-body{overflow:auto;padding:20px 24px;min-height:0}.w-editor-grid{display:grid;grid-template-columns:210px minmax(0,1fr) 300px;min-height:500px;flex:1}.w-editor-list{padding:20px 12px;border-inline-end:1px solid var(--w-border);min-width:0}.w-editor-canvas{background:var(--w-soft);padding:20px;min-width:0;display:flex;flex-direction:column;gap:14px}.w-editor-inspector{padding:24px;border-inline-start:1px solid var(--w-border);min-width:0;overflow:auto}.w-editor-inspector h3{margin-bottom:18px}.w-preview-empty{display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;gap:12px;aspect-ratio:16/9;min-height:180px;background:var(--w-soft);border:1px solid var(--w-border);border-radius:8px;padding:24px;color:var(--w-muted)}.w-content-pad{padding:24px 0}.w-stack{display:flex;flex-direction:column;gap:16px;min-width:0}.w-scroll-x{overflow:auto;max-width:100%}
@@ -587,7 +588,8 @@ function createWorkbenchUI(React) {
587
588
  @media(max-width:700px){.w-page[data-main=true]{padding:calc(16px + var(--dsh-frame-top-clearance,0px)) 16px 0 max(16px,var(--dsh-frame-leading-clearance,0px))}.w-overlay{padding:8px}.w-dialog{width:100%}}
588
589
  @media(prefers-reduced-motion:reduce){.dshw *,.dshw *:before{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
589
590
  `;
590
- return { Icon, Button, IconButton, Page, Tabs, Status, Empty, Split, Row, Search, Switch, Dialog, register, styles };
591
+ function Styles() { return h('style', null, styles); }
592
+ return { Icon, Button, IconButton, Page, Tabs, Status, Empty, Split, Row, Search, Switch, Dialog, Styles, register, styles };
591
593
  }
592
594
 
593
595
  // Editable browser entry. Build with node scripts/build-client.mjs.
@@ -684,7 +686,7 @@ function MediaExports({ project, dirty, running, activeJob, busy, setArchiveRevi
684
686
  }
685
687
 
686
688
  const MEDIA_WORKBENCH_CSS = `
687
- .media-workbench.dmw{--fg:var(--w-text);--fg-muted:var(--w-muted);--border:var(--w-border);--bg:var(--w-bg);--bg-surface:var(--w-bg);padding:0}.media-workbench.dmw[data-main=true]{padding:calc(24px + var(--dsh-frame-top-clearance,0px)) 28px 0 max(28px,var(--dsh-frame-leading-clearance,0px))}.media-workbench .media-editor{display:grid;grid-template-columns:260px minmax(0,1fr) 220px;min-height:500px;flex:1}.media-workbench .media-content-fields{border-inline-end:1px solid var(--w-border);padding:24px 20px 24px 0;min-width:0}.media-workbench .media-properties{border-inline-start:1px solid var(--w-border);padding:24px 0 24px 20px;min-width:0}.media-workbench .media-content-fields h3,.media-workbench .media-properties h3{margin-bottom:20px}.media-workbench .media-field{display:grid;gap:7px;margin:0 0 18px;font-size:13px}.media-workbench .media-field label{margin:0;color:var(--w-muted)}.media-workbench .media-field input,.media-workbench .media-field select,.media-workbench .media-field textarea{width:100%;font-size:14px}.media-workbench .media-field textarea{min-height:160px}.media-workbench .media-preview-workspace{display:flex;flex-direction:column;align-items:stretch;gap:16px;padding:24px;min-width:0}.media-workbench .media-preview-workspace>.w-toolbar{justify-content:flex-start}.media-workbench .media-empty-preview{margin:auto 0;max-height:600px;min-height:180px;width:100%;justify-content:center;background:var(--w-soft);border-radius:8px}.media-workbench .media-player-wrap{display:flex;justify-content:center;align-items:center;min-height:220px;flex:1;min-width:0}.media-workbench .media-player-wrap video{width:100%;height:auto;max-height:calc(100dvh - 360px);object-fit:contain;border-radius:8px;background:#14171c}.media-workbench .media-render-caption{margin-top:0}.media-workbench .media-preview-workspace>.w-button{align-self:flex-start}.media-workbench .media-properties .w-section{padding:20px 0}.media-workbench .media-properties .w-button{width:100%;margin-top:10px}.media-workbench .media-properties p{margin:12px 0}.media-workbench .media-job{border-bottom:1px solid var(--w-border);padding:14px 0;font-size:13px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}.media-workbench .media-job details{width:100%}.media-workbench .media-job pre{font-size:12px;max-height:240px}.media-workbench .media-project-list{max-width:900px;padding:12px 0 24px}.media-workbench .media-project-list .w-row-button{padding:20px 16px;border:0;border-radius:0;background:transparent}.media-workbench .media-assets-view,.media-workbench .media-exports-view,.media-workbench .media-environment{padding:24px 0;width:min(900px,100%)}.media-workbench .media-assets-view>.w-muted,.media-workbench .media-exports-view>.w-muted{margin-top:12px}.media-workbench .media-asset-list,.media-workbench .media-export-list{list-style:none;padding:0;margin:20px 0;display:flex;flex-direction:column}.media-workbench .media-asset-list>li,.media-workbench .media-export-list>li{display:flex;align-items:center;gap:16px;flex-wrap:wrap;min-width:0;padding:18px 0;border-bottom:1px solid var(--w-border)}.media-workbench .media-asset-list>li>div,.media-workbench .media-export-list>li>div{flex:1;min-width:180px;overflow-wrap:anywhere}.media-workbench .media-asset-list img{width:80px;height:60px;object-fit:contain;border-radius:6px;background:var(--w-soft)}.media-workbench .media-export-list p{margin-top:5px}.media-workbench .media-export-list .w-tag{margin-top:8px}.media-workbench .media-environment>p{margin:14px 0}.media-workbench .media-environment>button{margin-top:12px}.media-workbench.dmw button.primary{background:var(--w-primary);border-color:var(--w-primary);color:var(--w-on-primary)}.media-workbench .card{border:0;border-bottom:1px solid var(--w-border);border-radius:0;padding:20px 0;background:transparent;box-shadow:none}.media-workbench .notice{background:var(--w-soft);color:var(--w-text);border:1px solid var(--w-border)}.media-workbench .notice.error{color:var(--dsw-alias-state-error-primary,#ad3636)}.media-workbench .media-project-list .bar{justify-content:space-between;border-bottom:1px solid var(--w-border);padding:16px 0}.media-workbench .media-exports-view .w-section h3{margin-bottom:12px}.media-workbench .media-environment button{border:1px solid var(--w-border);border-radius:8px;background:var(--w-primary);color:var(--w-on-primary);padding:8px 12px;font-size:14px;min-height:36px}.media-workbench .media-properties a{font-size:12px}.media-workbench :focus-visible{outline:2px solid var(--w-accent);outline-offset:3px}
689
+ .media-workbench.dmw{--fg:var(--w-text);--fg-muted:var(--w-muted);--border:var(--w-border);--bg:var(--w-bg);--bg-surface:var(--w-bg);padding:0}.media-workbench.dmw[data-main=true]{padding:calc(24px + var(--dsh-frame-top-clearance,0px)) 28px 0 max(28px,var(--dsh-frame-leading-clearance,0px))}.media-workbench .media-editor{display:grid;grid-template-columns:260px minmax(0,1fr) 220px;min-height:500px;flex:1}.media-workbench .media-content-fields{border-inline-end:1px solid var(--w-border);padding:24px 20px 24px 0;min-width:0}.media-workbench .media-properties{border-inline-start:1px solid var(--w-border);padding:24px 0 24px 20px;min-width:0}.media-workbench .media-content-fields h3,.media-workbench .media-properties h3{margin-bottom:20px}.media-workbench .media-field{display:grid;gap:7px;margin:0 0 18px;font-size:13px}.media-workbench .media-field label{margin:0;color:var(--w-muted)}.media-workbench .media-field input,.media-workbench .media-field select,.media-workbench .media-field textarea{width:100%;font-size:14px}.media-workbench .media-field textarea{min-height:160px}.media-workbench .media-preview-workspace{display:flex;flex-direction:column;align-items:stretch;gap:16px;padding:24px;min-width:0}.media-workbench .media-preview-workspace>.w-toolbar{justify-content:flex-start}.media-workbench .media-empty-preview{margin:auto 0;max-height:600px;min-height:180px;width:100%;justify-content:center;background:var(--w-soft);border-radius:8px}.media-workbench .media-player-wrap{display:flex;justify-content:center;align-items:center;min-height:220px;flex:1;min-width:0}.media-workbench .media-player-wrap video{width:100%;height:auto;max-height:calc(100dvh - 360px);object-fit:contain;border-radius:8px;background:#14171c}.media-workbench .media-render-caption{margin-top:0}.media-workbench .media-preview-workspace>.w-button{align-self:flex-start}.media-workbench .media-properties .w-section{padding:20px 0}.media-workbench .media-properties .w-button{width:100%;margin-top:10px}.media-workbench .media-properties p{margin:12px 0}.media-workbench .media-job{border-bottom:1px solid var(--w-border);padding:14px 0;font-size:13px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}.media-workbench .media-job details{width:100%}.media-workbench .media-job pre{font-size:12px;max-height:240px}.media-workbench .media-job progress{width:180px;height:10px;accent-color:var(--w-accent)}.media-workbench .media-job[data-state=failed]>strong{color:var(--dsw-alias-state-error-primary,#ad3636)}.media-workbench .media-job[data-state=done]>strong{color:var(--dsw-alias-state-success-primary,#24794a)}.media-workbench .media-export-checks{list-style:none;padding:0;margin:14px 0;display:grid;gap:6px;font-size:13px;overflow-wrap:anywhere}.media-workbench .media-project-list{max-width:900px;padding:12px 0 24px}.media-workbench .media-project-list .w-row-button{padding:20px 16px;border:0;border-radius:0;background:transparent}.media-workbench .media-assets-view,.media-workbench .media-exports-view,.media-workbench .media-environment{padding:24px 0;width:min(900px,100%)}.media-workbench .media-assets-view>.w-muted,.media-workbench .media-exports-view>.w-muted{margin-top:12px}.media-workbench .media-asset-list,.media-workbench .media-export-list{list-style:none;padding:0;margin:20px 0;display:flex;flex-direction:column}.media-workbench .media-asset-list>li,.media-workbench .media-export-list>li{display:flex;align-items:center;gap:16px;flex-wrap:wrap;min-width:0;padding:18px 0;border-bottom:1px solid var(--w-border)}.media-workbench .media-asset-list>li>div,.media-workbench .media-export-list>li>div{flex:1;min-width:180px;overflow-wrap:anywhere}.media-workbench .media-asset-list img{width:80px;height:60px;object-fit:contain;border-radius:6px;background:var(--w-soft)}.media-workbench .media-export-list p{margin-top:5px}.media-workbench .media-export-list .w-tag{margin-top:8px}.media-workbench .media-environment>p{margin:14px 0}.media-workbench .media-environment>button{margin-top:12px}.media-workbench.dmw button.primary{background:var(--w-primary);border-color:var(--w-primary);color:var(--w-on-primary)}.media-workbench .card{border:0;border-bottom:1px solid var(--w-border);border-radius:0;padding:20px 0;background:transparent;box-shadow:none}.media-workbench .notice{background:var(--w-soft);color:var(--w-text);border:1px solid var(--w-border)}.media-workbench .notice.error{color:var(--dsw-alias-state-error-primary,#ad3636)}.media-workbench .media-project-list .bar{justify-content:space-between;border-bottom:1px solid var(--w-border);padding:16px 0}.media-workbench .media-exports-view .w-section h3{margin-bottom:12px}.media-workbench .media-environment button{border:1px solid var(--w-border);border-radius:8px;background:var(--w-primary);color:var(--w-on-primary);padding:8px 12px;font-size:14px;min-height:36px}.media-workbench .media-properties a{font-size:12px}.media-workbench :focus-visible{outline:2px solid var(--w-accent);outline-offset:3px}
688
690
  @container dsh-workbench (max-width:1050px){.media-workbench .media-editor{grid-template-columns:240px minmax(0,1fr)}.media-workbench .media-properties{grid-column:1/-1;border-inline-start:0;border-top:1px solid var(--w-border);padding:20px 0;display:grid;grid-template-columns:1fr 1fr;gap:20px}.media-workbench .media-properties>h3{grid-column:1/-1;margin:0}.media-workbench .media-properties>.w-section{padding:0;border:0}.media-workbench .media-preview-workspace{padding-inline-end:0}}
689
691
  @container dsh-workbench (max-width:760px){.media-workbench .media-editor{display:flex;flex-direction:column}.media-workbench .media-content-fields{border-inline-end:0;padding:20px 0}.media-workbench .media-preview-workspace{order:-1;padding:20px 0}.media-workbench .media-properties{display:block}.media-workbench .media-properties>.w-section{padding:20px 0;border-top:1px solid var(--w-border)}.media-workbench .media-empty-preview{max-height:440px}.media-workbench .media-player-wrap video{max-height:480px}.media-workbench .media-asset-list>li>div{min-width:120px}}
690
692
  @media(max-width:700px){.media-workbench.dmw[data-main=true]{padding:calc(16px + var(--dsh-frame-top-clearance,0px)) 16px 0 max(16px,var(--dsh-frame-leading-clearance,0px))}}
@@ -724,6 +726,9 @@ const MEDIA_WORKBENCH_CSS = `
724
726
  let cache = { project: null, fields: null, dirty: false, job: null, creating: false };
725
727
  const empty = () => ({ template: 'title', title: '', body: '', format: 'landscape', duration: 8 });
726
728
  const fieldsOf = p => ({ template: p.template, title: p.title, body: p.body, format: p.format, duration: p.duration });
729
+ // Official stage names are the only progress signal the engines expose; map them to steps instead of inventing percentages.
730
+ const jobSteps = { prepare: ['下载环境', '检查 FFmpeg', '下载官方依赖', '检查官方环境', '检查或下载 Chrome', '下载专用渲染浏览器'], preview: ['准备修订快照', '启动官方预览', '官方预览已就绪'], render: ['准备修订快照', '检查合成', '渲染 MP4', '已完成'] };
731
+ const jobStepOf = step => { const steps = jobSteps[step?.kind] || [], index = steps.indexOf(step?.stage); return { index: index < 0 ? 0 : index, total: steps.length || 1 }; };
727
732
  const css = `.dmw{font:14px/1.6 system-ui,sans-serif;color:var(--fg,#142D39);min-width:0;padding:8px}.dmw *{box-sizing:border-box}.dmw h2{margin:0;font-size:23px}.dmw h3{font-size:17px;margin:8px 0}.dmw .muted{color:var(--fg-muted,#677881);font-size:13px}.dmw .bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0}.dmw button,.dmw .button{font:inherit;color:inherit;background:var(--bg-surface,#fff);border:1px solid var(--border,#c8d6d9);border-radius:9px;padding:8px 12px;cursor:pointer;white-space:normal;text-decoration:none}.dmw button.primary{background:#df5639;border-color:#df5639;color:white}.dmw button:disabled{opacity:.45;cursor:default}.dmw input:not([type=file]),.dmw textarea,.dmw select{font:inherit;color:inherit;width:100%;border:1px solid var(--border,#b9cccd);border-radius:8px;padding:9px;background:var(--bg,#fff)}.dmw label{display:block;margin:10px 0 4px}.dmw .card{border:1px solid var(--border,#ccd7d6);border-radius:12px;padding:14px;margin:12px 0;min-width:0;background:var(--bg-surface,#fff)}.dmw .notice{border-radius:8px;padding:10px;margin:10px 0;background:#eef5ed;color:#28573e;overflow-wrap:anywhere}.dmw .error{background:#fff0ec;color:#943b27}.dmw .row{display:grid;grid-template-columns:1fr 1fr;gap:14px}.dmw .projects{display:grid;gap:8px}.dmw .projects button{text-align:left;width:100%}.dmw img{width:90px;height:70px;object-fit:contain}.dmw .assets{display:flex;flex-direction:column;gap:8px}.dmw .asset{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.dmw video{width:100%;max-height:400px;background:#142D39;border-radius:10px}.dmw pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;margin:8px 0;max-height:220px;overflow:auto}.dmw :focus-visible{outline:3px solid #79CDB4;outline-offset:2px}[role=dialog]:has(.dmw){width:min(1160px,calc(100vw - 40px))!important;max-width:1160px!important}@media(max-width:700px){.dmw{padding:3px}.dmw .row{grid-template-columns:1fr}[role=dialog]:has(.dmw){width:100%!important;max-width:100%!important;flex-direction:column!important}[role=dialog]:has(.dmw)>nav+div{min-width:0;width:100%}[role=dialog]:has(.dmw)>nav+div>div:has(.dmw){padding:12px!important;min-width:0}[role=dialog]:has(.dmw) nav{width:100%!important;flex-basis:auto!important;border-right:0;max-height:170px;overflow:auto}[role=dialog]:has(.dmw) nav>div:has(>button){display:flex;flex-direction:row!important;flex-wrap:wrap;gap:4px}}`;
728
733
  async function api(body) {
729
734
  const response = await fetch(new URL(route, document.baseURI), { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', 'x-dsh-media': '1' }, body: JSON.stringify(body) });
@@ -746,10 +751,10 @@ const MEDIA_WORKBENCH_CSS = `
746
751
  const [editorTab,setEditorTab]=useState('content'),[libraryTab,setLibraryTab]=useState('projects');const controlId=React.useId();
747
752
  const [lang, setLang] = useState('zh'), [projects, setProjects] = useState([]), [archived, setArchived] = useState([]), [archiveReview, setArchiveReview] = useState(false), [environment, setEnvironment] = useState(null), [templates, setTemplates] = useState([]);
748
753
  const [project, setProject] = useState(cache.project), [fields, setFields] = useState(cache.fields || empty()), [dirty, setDirty] = useState(cache.dirty), [creating, setCreating] = useState(cache.creating);
749
- const [job, setJob] = useState(cache.job), [busy, setBusy] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState(''), [pending, setPending] = useState(null), [conflict, setConflict] = useState(false);
754
+ const [job, setJob] = useState(cache.job), [busy, setBusy] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState(''), [pending, setPending] = useState(null), [conflict, setConflict] = useState(false), [exportReview, setExportReview] = useState(false);
750
755
  const busyRef = useRef(false), fileRef = useRef(null), latest = useRef({ project, fields, dirty });
751
756
  latest.current = { project, fields, dirty };
752
- const t = (zh, en) => lang === 'en' ? en : zh, activeJob = job?.state === 'running', running = busy || activeJob && job.kind !== 'prepare';
757
+ const t = (zh, en) => lang === 'en' ? en : zh, activeJob = job?.state === 'running', running = busy || activeJob && job.kind !== 'prepare', jobStep = jobStepOf(job);
753
758
  // Update protection: dsh-remotion
754
759
  __stardustUpdatesGuard(React, "dsh-remotion", { dirty, busy: busy || activeJob, retainOnClose: true });
755
760
 
@@ -860,21 +865,26 @@ __stardustUpdatesGuard(React, "dsh-remotion", { dirty, busy: busy || activeJob,
860
865
  async function archiveProject() { await api({ action: 'archive', id: project.id, revision: project.revision, confirmed: true }); setArchiveReview(false); setProject(null); setCreating(false); setDirty(false); await refresh(); setNotice(t('工程已归档,素材和成品均保留,可在下方恢复。', 'Project archived. Media and output are preserved; restore it below.')); }
861
866
  const render=project?.renders?.[0],stale=render && (dirty || render.revision!==project.revision);
862
867
  const canSave=fields.title.trim().length>0 && fields.title.length<=72 && Number.isInteger(fields.duration) && fields.duration>=3 && fields.duration<=30;
868
+ const saveHint=!fields.title.trim()?t('先填写标题再保存。','Enter a title before saving.'):fields.title.length>72?t('标题最多 72 字符。','Titles are limited to 72 characters.'):!(Number.isInteger(fields.duration)&&fields.duration>=3&&fields.duration<=30)?t('时长需在 3–30 秒之间。','Set a duration between 3 and 30 seconds.'):project&&!dirty?t('没有需要保存的修改。','There is nothing to save yet.'):null;
869
+ const exportHint=!environment?.ready?t('先在“环境”页准备渲染工具,再导出。','Prepare the renderer in Environment before exporting.'):dirty?t('先保存修改;导出使用已保存的修订。','Save your changes first; exports use the saved revision.'):t('先检查导出内容,再开始渲染。','Review the export summary before rendering.');
863
870
  if(!environment) return __stardustUpdatesInlinePage(React,h(W.Page,{workbench,title,subtitle:t('编辑视频工程,预览并导出 MP4。','Edit video projects, preview and export MP4.'),className:'dmw media-workbench'},h('style',null,MEDIA_WORKBENCH_CSS),error?h(W.Empty,{title:t('暂时无法读取工程','Projects are unavailable'),action:h(W.Button,{disabled:busy,onClick:()=>perform(refresh)},t('重新读取项目','Reload projects'))},mediaError(error,lang==='en')):h(W.Empty,{title:t('正在读取工程与渲染环境…','Loading projects and render environment…'),icon:'video'})),"dsh-remotion");
864
871
  return __stardustUpdatesInlinePage(React,h(W.Page,{workbench,title,subtitle:t('编辑视频工程,预览并导出 MP4。','Edit video projects, preview and export MP4.'),className:'dmw media-workbench',actions:h(React.Fragment,null,
865
872
  project || creating ? h(W.Button,{icon:'left',disabled:running,onClick:()=>navigate('list')},t('返回项目','Back to projects')) : h(W.Button,{icon:'plus',variant:'primary',disabled:running,onClick:()=>navigate('new')},t('新建视频','New video')),
866
- project || creating ? h(W.Button,{variant:'primary',disabled:running || !canSave || !!project && !dirty,onClick:()=>perform(save)},t('保存工程','Save project')) : h(W.IconButton,{icon:'refresh',label:t('刷新项目','Refresh projects'),disabled:running,onClick:()=>perform(refresh)}),
867
- project ? h(W.Button,{icon:'download',disabled:running || dirty || !environment.ready,onClick:()=>start('render')},t('导出 MP4','Export MP4')) : null)},
868
- h('style',null,css),h('style',null,MEDIA_WORKBENCH_CSS),h(W.Status,{tone:'error'},error && mediaError(error,lang==='en')),h(W.Status,{tone:'success'},notice),conflict && project && h('button', { disabled: running, onClick: () => { setPending(project.id); } }, t('重新打开最新版本(先处理当前输入)', 'Reopen latest version (review your current input first)')),
873
+ project || creating ? h(W.Button,{variant:'primary',disabled:running || !canSave || !!project && !dirty,title:saveHint || undefined,onClick:()=>perform(save)},t('保存工程','Save project')) : h(W.IconButton,{icon:'refresh',label:t('刷新项目','Refresh projects'),disabled:running,onClick:()=>perform(refresh)}),
874
+ project ? h(W.Button,{icon:'download',disabled:running || dirty || !environment.ready,title:exportHint,onClick:()=>setExportReview(true)},t('导出 MP4','Export MP4')) : null)},
875
+ h('style',null,css),h('style',null,MEDIA_WORKBENCH_CSS),h(W.Status,{tone:'error'},conflict ? t('工程已在别处更新,当前输入尚未保存。先复制需要的修改,再重新载入最新修订。', 'The project changed elsewhere and your input is not saved yet. Copy what you need, then reload the latest revision.') : error && mediaError(error,lang==='en')),h(W.Status,{tone:'success'},notice),conflict && project ? h('div',{className:'w-toolbar'},h(W.Button,{icon:'refresh',disabled:running,onClick:()=>setPending(project.id)},t('重新载入最新版本(放弃我的输入)','Reload the latest version (discard my input)')),h(W.Button,{variant:'ghost',disabled:running,onClick:()=>setConflict(false)},t('保留我的输入,稍后处理','Keep my input for now'))) : null,
869
876
  (project || creating) && !environment.ready ? h(W.Status,null,t('首次导出前需要准备本地渲染环境;内容可以先编辑和保存。','Prepare the local renderer before your first export. You can edit and save content now.'),h(W.Button,{variant:'ghost',onClick:()=>setEditorTab('environment')},t('查看环境','View environment'))) : null,
870
- job ? h('div',{className:'media-job',role:job.state==='failed'?'alert':'status'},h('strong',null,t('任务状态:','Job status: ')+stage),job.url?h('a',{className:'w-button',href:job.url,target:'_blank',rel:'noopener noreferrer'},t('打开官方 Studio 预览','Open official Studio preview')):null,
877
+ job ? h('div',{className:'media-job',role:job.state==='failed'?'alert':'status','data-state':job.state,'aria-live':job.state==='failed'?'assertive':'polite'},h('strong',null,t('任务状态:','Job status: ')+stage),job.state==='running'?h('span',{className:'w-muted'},t('步骤 '+(jobStep.index+1)+' / '+jobStep.total,'Step '+(jobStep.index+1)+' / '+jobStep.total)):null,
878
+ job.state==='running'?h('progress',{className:'media-job-progress',max:jobStep.total,value:jobStep.index+1,'aria-label':t('任务步骤进度','Job step progress')}):null,
879
+ job.url?h('a',{className:'w-button',href:job.url,target:'_blank',rel:'noopener noreferrer'},t('打开官方 Studio 预览','Open official Studio preview')):null,
871
880
  job.state==='running'?h(W.Button,{disabled:busy,onClick:()=>perform(async()=>{await api({action:'cancel',job:job.id});setNotice(t('已请求停止;正在关闭本次任务。','Stopping this job.'));})},job.kind==='preview'?t('停止预览,返回编辑','Stop preview and return to editing'):t('取消任务','Cancel job')):null,
881
+ job.state==='failed'?h(W.Button,{variant:'primary',icon:'refresh',disabled:running || dirty || job.kind!=='prepare' && !project,title:dirty?t('先保存修改,再用同一修订重试。','Save your changes before retrying the same revision.'):job.kind!=='prepare' && !project?t('先在工程库打开工程,再重试本次任务。','Open the project from the list before retrying this task.'):t('重试失败的官方任务;工程已保留。','Retry the failed official task; your project is preserved.'),onClick:()=>start(job.kind==='prepare'?'prepare':job.kind)},t('重试本次任务','Retry this task')):null,
872
882
  h('details',null,h('summary',null,t('查看运行记录','Show job log')),h('pre',null,job.lines.join('\n')))) : null,
873
883
  h(W.Tabs,{label:t('视频工作台视图','Video workbench views'),value:project || creating ? editorTab : libraryTab,onChange:project || creating ? setEditorTab : setLibraryTab,items:project || creating ? [{id:'content',label:t('内容与预览','Content and preview')},{id:'assets',label:t('素材','Media')},{id:'exports',label:t('导出记录','Exports')},{id:'environment',label:t('环境','Environment')}] : [{id:'projects',label:t('当前工程','Projects'),count:projects.length},{id:'archives',label:t('已归档','Archived'),count:archived.length},{id:'environment',label:t('环境','Environment')}]},
874
884
  !project && !creating ? libraryTab === 'environment' ? h('section',{className:'media-environment'},h('h3',null,t('渲染环境','Render environment')+(environment.ready?t(' · 已准备',' · ready'):t(' · 首次需下载',' · first download required'))),h('p', { className: 'muted' }, t(environment?.hint, 'First use downloads the official CLI. ' + (engine === 'hyperframes' ? 'Windows uses the official rendering browser; other platforms reuse installed Chrome when available. FFmpeg must be in PATH.' : 'Installed Chrome is reused; a browser is downloaded when needed. Remotion retains its official license.'))),
875
885
  engine === 'remotion' && h('p', null, h('a', { href: 'https://www.remotion.dev/license', target: '_blank', rel: 'noreferrer' }, t('查看 Remotion 使用许可证', 'Read the Remotion license'))),
876
886
  h('button', { disabled: busy || activeJob, onClick: () => start('prepare') }, environment?.ready ? t('重新检查 / 修复环境', 'Check / repair environment') : t('准备渲染环境', 'Prepare render environment'))) : libraryTab === 'archives' ? archived.length ? h('section',{className:'media-project-list'},h('p', { className: 'muted' }, t('归档保留素材与成品,不占当前 30 个工程名额。恢复后可以继续编辑。', 'Archives retain media and output, freeing an active project slot. Restore to edit again.')),
877
- h(ProjectLibrary,{items:archived,scope:'archives',language:lang,t,searchHint:t('搜索归档工程','Search archived projects'),searchText:p=>[p.title,p.body,p.template].join(' '),updatedAt:p=>p.updated,renderItem:p=>h('div', { className: 'bar', key: p.id }, h('strong', null, p.title), h('button', { disabled: busy || dirty || activeJob, onClick: () => perform(async () => { await api({ action: 'restore', id: p.id, revision: p.revision, confirmed: true }); await refresh(); await open(p.id); setNotice(t('工程已恢复。', 'Project restored.')); }) }, t('恢复工程', 'Restore project')))})) : h(W.Empty,{title:t('没有归档工程','No archived projects'),icon:'folder'}) : projects.length ? h('div',{className:'media-project-list'},h(ProjectLibrary,{items:projects,language:lang,t,searchHint:t('搜索标题、正文或模板','Search titles, text or templates'),searchText:p=>[p.title,p.body,p.template,templates.find(row=>row.id===p.template)?.[lang==='en'?'en':'zh']].join(' '),updatedAt:p=>p.updated,renderItem:p=>h(W.Row,{key:p.id,title:p.title,disabled:running,meta:p.duration+t(' 秒 · 修订 ',' sec · revision ')+p.revision,description:(templates.find(row=>row.id===p.template)?.[lang==='en'?'en':'zh'] || p.template),onClick:()=>{setEditorTab('content');navigate(p.id);}})})) : h(W.Empty,{title:t('还没有视频工程','No video projects yet'),icon:'video'},t('点击“新建视频”,选择模板并保存。环境可以稍后准备。','Choose New video, select a template and save. The renderer can be prepared later.')) :
887
+ h(ProjectLibrary,{items:archived,scope:'archives',language:lang,t,searchHint:t('搜索归档工程','Search archived projects'),searchText:p=>[p.title,p.body,p.template].join(' '),updatedAt:p=>p.updated,renderItem:p=>h('div', { className: 'bar', key: p.id }, h('strong', null, p.title), h('button', { disabled: busy || dirty || activeJob, onClick: () => perform(async () => { await api({ action: 'restore', id: p.id, revision: p.revision, confirmed: true }); await refresh(); await open(p.id); setNotice(t('工程已恢复。', 'Project restored.')); }) }, t('恢复工程', 'Restore project')))})) : h(W.Empty,{title:t('没有归档工程','No archived projects'),icon:'folder'}) : projects.length ? h('div',{className:'media-project-list'},h(ProjectLibrary,{items:projects,language:lang,t,searchHint:t('搜索标题、正文或模板','Search titles, text or templates'),searchText:p=>[p.title,p.body,p.template,templates.find(row=>row.id===p.template)?.[lang==='en'?'en':'zh']].join(' '),updatedAt:p=>p.updated,renderItem:p=>h(W.Row,{key:p.id,title:p.title,disabled:running,meta:p.duration+t(' 秒 · 修订 ',' sec · revision ')+p.revision,description:(templates.find(row=>row.id===p.template)?.[lang==='en'?'en':'zh'] || p.template),onClick:()=>{setEditorTab('content');navigate(p.id);}})})) : h(W.Empty,{title:t('还没有视频工程','No video projects yet'),icon:'video',action:h(W.Button,{variant:'primary',icon:'plus',disabled:running,onClick:()=>navigate('new')},t('新建视频','New video'))},t('点击“新建视频”,选择模板并保存。环境可以稍后准备。','Choose New video, select a template and save. The renderer can be prepared later.')) :
878
888
  editorTab === 'environment' ? h('section',{className:'media-environment'},h('h3',null,t('渲染环境','Render environment')+(environment.ready?t(' · 已准备',' · ready'):t(' · 首次需下载',' · first download required'))),h('p', { className: 'muted' }, t(environment?.hint, 'First use downloads the official CLI. ' + (engine === 'hyperframes' ? 'Windows uses the official rendering browser; other platforms reuse installed Chrome when available. FFmpeg must be in PATH.' : 'Installed Chrome is reused; a browser is downloaded when needed. Remotion retains its official license.'))),
879
889
  engine === 'remotion' && h('p', null, h('a', { href: 'https://www.remotion.dev/license', target: '_blank', rel: 'noreferrer' }, t('查看 Remotion 使用许可证', 'Read the Remotion license'))),
880
890
  h('button', { disabled: busy || activeJob, onClick: () => start('prepare') }, environment?.ready ? t('重新检查 / 修复环境', 'Check / repair environment') : t('准备渲染环境', 'Prepare render environment'))) : editorTab === 'assets' ? h(MediaAssets,{project,dirty,running,fileRef,upload,perform,setProject,t}) : editorTab === 'exports' ? h(MediaExports,{project,dirty,running,activeJob,busy,setArchiveReview,t}) :
@@ -886,8 +896,15 @@ h('div',{className:'media-field'},h('label', { htmlFor: controlId+'-body' }, t('
886
896
  h('div',{className:'media-field'},h('label', { htmlFor: controlId+'-duration' }, t('时长(3–30 秒)', 'Duration (3–30 seconds)')),h('input', { id: controlId+'-duration', type: 'number', min: 3, max: 30, value: fields.duration, disabled: running, onChange: e => { setFields({ ...fields, duration: Number(e.target.value) }); setDirty(true); } })),
887
897
  h('section',{className:'w-section'},h('p',null,t('已上传素材 ','Uploaded media ')+(project?.assets.length || 0)),h(W.Button,{icon:'upload',disabled:!project || running || dirty,onClick:()=>setEditorTab('assets')},t('上传素材','Upload media')),project?h('a',{className:'w-button',href:urlFor(project,'source'),download:''},t('下载可编辑工程','Download editable project')):null),
888
898
  h('p',{className:'w-muted'},t('导出使用本地 ','Exports use the local ')+title+t(' 引擎。',' engine.')),h('p',{className:'w-muted'},t('图片、音频和无声视频;单文件 20 MB,工程合计 40 MB。','Images, audio and muted video; 20 MB per file, 40 MB per project.')),engine==='remotion'?h('a',{href:'https://www.remotion.dev/license',target:'_blank',rel:'noopener noreferrer'},t('Remotion 许可说明','Remotion license')):null))),
899
+ project && exportReview ? h(W.Dialog,{title:t('导出前检查','Check before export'),closeLabel:t('取消导出','Cancel export'),busy:running,onClose:()=>setExportReview(false)},
900
+ h('p', null, t('确认后启动官方渲染,生成新 MP4;工程内容不会被修改。', 'Rendering starts after you confirm and produces a new MP4. Your project is not modified.')),
901
+ h('ul', { className: 'media-export-checks' }, h('li', null, t('标题:', 'Title: ') + project.title), h('li', null, t('画幅与时长:', 'Canvas and duration: ') + ({ landscape: t('横屏 16:9', 'Landscape 16:9'), portrait: t('竖屏 9:16', 'Portrait 9:16'), square: t('正方形', 'Square') }[project.format] || project.format) + ' · ' + project.duration + t(' 秒', ' sec')), h('li', null, t('素材:', 'Media: ') + project.assets.length + t(' 个', ' items')), h('li', null, t('修订:', 'Revision: ') + project.revision), h('li', null, t('渲染环境:', 'Renderer: ') + (environment.ready ? t('已准备', 'ready') : t('未准备', 'not ready'))), h('li', null, t('上次导出:', 'Last export: ') + (render ? t('修订 ', 'revision ') + render.revision : t('尚未导出', 'none yet')))),
902
+ !project.assets.length ? h(W.Status, null, t('没有上传素材,本次只渲染模板内容。', 'No media uploaded; this export renders the template only.')) : null,
903
+ render && render.revision !== project.revision ? h(W.Status, null, t('上一份 MP4 对应修订 ', 'The last MP4 uses revision ') + render.revision + t(',本次会生成修订 ', '; this export creates revision ') + project.revision + t(' 的新成品;旧成品保留在导出记录中。', '. Older files stay in Exports.')) : null,
904
+ h('div', { className: 'w-toolbar' }, h(W.Button, { variant: 'primary', icon: 'download', disabled: running || dirty || !environment.ready, title: exportHint, onClick: () => { setExportReview(false); start('render'); } }, t('开始导出', 'Start export')), h(W.Button, { disabled: running, onClick: () => setExportReview(false) }, t('取消导出', 'Cancel export')))) : null,
889
905
  pending ? h(W.Dialog,{title:t('有未保存的修改','Unsaved changes'),closeLabel:t('留在当前','Stay here'),busy:running,onClose:()=>setPending(null)},h('p', null, t('有未保存内容,如何继续?', 'You have unsaved changes.')),
890
- h('div', { className: 'bar' }, h('button', { disabled: running, onClick: () => perform(async () => { await save(); await move(pending); }) }, t('保存后继续', 'Save and continue')), h('button', { disabled: running, onClick: () => perform(() => move(pending)) }, t('放弃修改并继续', 'Discard and continue')), h('button', { onClick: () => setPending(null) }, t('留在当前', 'Stay here')))) : null,
906
+ conflict ? h(W.Status, { tone: 'error' }, t('此工程已在别处更新,直接保存会再次冲突。请选择放弃修改并载入最新版本,或留在当前复制需要的输入。', 'This project changed elsewhere, so saving again would conflict. Discard and load the latest revision, or stay here to copy your input.')) : null,
907
+ h('div', { className: 'w-toolbar' }, h(W.Button, { variant: 'primary', disabled: running || conflict, title: conflict ? t('工程已更新,无法直接保存。', 'The project changed, so saving is blocked.') : undefined, onClick: () => perform(async () => { await save(); await move(pending); }) }, t('保存后继续', 'Save and continue')), h(W.Button, { disabled: running, onClick: () => perform(() => move(pending)) }, t('放弃修改并继续', 'Discard and continue')), h(W.Button, { variant: 'ghost', onClick: () => setPending(null) }, t('留在当前', 'Stay here')))) : null,
891
908
  project && archiveReview ? h(W.Dialog,{title:t('归档此工程','Archive this project'),closeLabel:t('取消归档','Cancel archive'),busy:busy || activeJob,onClose:()=>setArchiveReview(false)},h('p', null, t('归档后将从当前项目列表移走;工程、上传素材和已导出 MP4 全部保留,可随时恢复。未保存修改需先处理。', 'Archiving removes this project from the active list. The project, uploaded media and exported MP4s are preserved and can be restored. Save or discard pending edits first.')),
892
909
  h('div', { className: 'bar' }, h('button', { disabled: busy || dirty || activeJob, onClick: () => perform(archiveProject) }, t('确认归档', 'Confirm archive')), h('button', { disabled: busy, onClick: () => setArchiveReview(false) }, t('取消归档', 'Cancel archive')))) : null),"dsh-remotion");
893
910
  }
@@ -897,7 +914,7 @@ h('div', { className: 'bar' }, h('button', { disabled: busy || dirty || activeJo
897
914
  catch { } W.register(ctx,{id:'dsh-'+engine+'.workbench',label:()=>title,icon:engine==='hyperframes'?'layers':'play',component:Workbench,inject:()=>({locale}),order:engine==='hyperframes'?65:66});
898
915
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-' + engine, order: engine === 'hyperframes' ? 62 : 63, label: () => title, inject: () => ({ locale }) }, Workbench)); }
899
916
  return { inject: ['slots', 'locale'], apply };
900
- } },{"name":"dsh-remotion","version":"0.1.0","hasWorkbench":true,"packageVersion":"0.10.3","label":"Remotion","labelEn":"Remotion","suiteComponent":true}));
917
+ } },{"name":"dsh-remotion","version":"0.1.0","hasWorkbench":true,"packageVersion":"0.10.4","label":"Remotion","labelEn":"Remotion","suiteComponent":true}));
901
918
 
902
919
  })();
903
920
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-remotion",
3
- "version": "0.10.3",
4
- "description": "在 DSH 里把 React 工程做成视频。 首次使用需准备官方渲染环境 欢迎提交 issues 和 PR。",
3
+ "version": "0.10.4",
4
+ "description": "DSH React 视频工作台:官方 Remotion 技能、工程检索、模板、素材排序、Studio 预览与 MP4 导出。欢迎提交 issues 和 PR。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -29,8 +29,8 @@
29
29
  "scripts": {
30
30
  "build": "tsc -p tsconfig.json && node scripts/build-client.mjs && node scripts/build-updates.mjs",
31
31
  "typecheck": "tsc -p tsconfig.json --noEmit",
32
- "test": "pnpm run build && node --test \"test/*.test.mjs\"",
33
- "prepublishOnly": "pnpm run build"
32
+ "test": "npm run build && node --test \"test/*.test.mjs\"",
33
+ "prepublishOnly": "npm run build"
34
34
  },
35
35
  "dsh": {
36
36
  "bundle": {