dsh-remotion 0.8.1 → 0.8.2

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.en.md +3 -1
  3. package/README.md +3 -1
  4. package/docs/USAGE.en.md +3 -3
  5. package/docs/USAGE.md +3 -3
  6. package/lib/client.js +60 -58
  7. package/lib/media-templates.d.ts +3 -3
  8. package/lib/media-templates.js +1 -1
  9. package/package.json +2 -2
  10. package/scripts/sync-skills.mjs +3 -2
  11. package/skills/UPSTREAM.json +297 -0
  12. package/skills/remotion-best-practices/SKILL.md +58 -4
  13. package/skills/remotion-best-practices/remotion-captions/REFERENCE.md +7 -1
  14. package/skills/remotion-best-practices/remotion-captions/display-captions.md +126 -147
  15. package/skills/remotion-best-practices/remotion-captions/import-srt-captions.md +3 -3
  16. package/skills/remotion-best-practices/remotion-captions/transcribe-captions.md +146 -44
  17. package/skills/remotion-best-practices/remotion-create/REFERENCE.md +16 -26
  18. package/skills/remotion-best-practices/remotion-docs/REFERENCE.md +1 -1
  19. package/skills/remotion-best-practices/remotion-interactivity/REFERENCE.md +70 -53
  20. package/skills/remotion-best-practices/remotion-interactivity/inline-effects.md +42 -0
  21. package/skills/remotion-best-practices/remotion-interactivity/timeline-structure.md +215 -0
  22. package/skills/remotion-best-practices/remotion-maps/REFERENCE.md +1 -1
  23. package/skills/remotion-best-practices/remotion-maps/techniques/mapbox/TECHNIQUE.md +3 -3
  24. package/skills/remotion-best-practices/remotion-maps/techniques/maplibre/TECHNIQUE.md +3 -3
  25. package/skills/remotion-best-practices/remotion-markup/REFERENCE.md +83 -113
  26. package/skills/remotion-best-practices/remotion-markup/arabic-text.md +88 -0
  27. package/skills/remotion-best-practices/remotion-markup/audio.md +35 -14
  28. package/skills/remotion-best-practices/remotion-markup/color-correction.md +190 -0
  29. package/skills/remotion-best-practices/remotion-markup/compositions.md +89 -34
  30. package/skills/remotion-best-practices/remotion-markup/connected-compositions.md +55 -69
  31. package/skills/remotion-best-practices/remotion-markup/cropping.md +2 -0
  32. package/skills/remotion-best-practices/remotion-markup/effects.md +34 -5
  33. package/skills/remotion-best-practices/remotion-markup/embedding-videos.md +20 -13
  34. package/skills/remotion-best-practices/remotion-markup/ffmpeg.md +2 -2
  35. package/skills/remotion-best-practices/remotion-markup/html-in-canvas.md +5 -27
  36. package/skills/remotion-best-practices/remotion-markup/images.md +5 -7
  37. package/skills/remotion-best-practices/remotion-markup/light-leaks.md +2 -4
  38. package/skills/remotion-best-practices/remotion-markup/motion-blur.md +2 -2
  39. package/skills/remotion-best-practices/remotion-markup/multi-scene-video.md +19 -79
  40. package/skills/remotion-best-practices/remotion-markup/remotion-maps/REFERENCE.md +1 -1
  41. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/mapbox/TECHNIQUE.md +3 -3
  42. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maplibre/TECHNIQUE.md +3 -3
  43. package/skills/remotion-best-practices/remotion-markup/sequencing.md +21 -162
  44. package/skills/remotion-best-practices/remotion-markup/sfx.md +2 -2
  45. package/skills/remotion-best-practices/remotion-markup/silence-detection.md +2 -2
  46. package/skills/remotion-best-practices/remotion-markup/timing-props.md +90 -0
  47. package/skills/remotion-best-practices/remotion-markup/transitions.md +121 -74
  48. package/skills/remotion-best-practices/remotion-markup/video-editing.md +127 -75
  49. package/skills/remotion-best-practices/remotion-markup/voiceover.md +6 -2
  50. package/skills/remotion-best-practices/remotion-multimedia/REFERENCE.md +1 -1
  51. package/skills/remotion-best-practices/remotion-multimedia/get-audio-duration.md +8 -3
  52. package/skills/remotion-best-practices/remotion-multimedia/get-video-dimensions.md +9 -3
  53. package/skills/remotion-best-practices/remotion-multimedia/get-video-duration.md +6 -3
  54. package/skills/remotion-best-practices/remotion-render/REFERENCE.md +1 -1
  55. package/skills/remotion-best-practices/remotion-saas/REFERENCE.md +1 -1
  56. package/skills/remotion-best-practices/remotion-studio/REFERENCE.md +11 -3
  57. package/skills/remotion-best-practices/remotion-upgrade/REFERENCE.md +1 -1
  58. package/skills/remotion-captions/SKILL.md +11 -1
  59. package/skills/remotion-captions/display-captions.md +126 -147
  60. package/skills/remotion-captions/import-srt-captions.md +3 -3
  61. package/skills/remotion-captions/transcribe-captions.md +146 -44
  62. package/skills/remotion-create/SKILL.md +20 -26
  63. package/skills/remotion-docs/SKILL.md +5 -1
  64. package/skills/remotion-interactivity/SKILL.md +74 -53
  65. package/skills/remotion-interactivity/inline-effects.md +42 -0
  66. package/skills/remotion-interactivity/timeline-structure.md +215 -0
  67. package/skills/remotion-maps/SKILL.md +5 -1
  68. package/skills/remotion-maps/techniques/mapbox/TECHNIQUE.md +3 -3
  69. package/skills/remotion-maps/techniques/maplibre/TECHNIQUE.md +3 -3
  70. package/skills/remotion-markup/SKILL.md +87 -113
  71. package/skills/remotion-markup/arabic-text.md +88 -0
  72. package/skills/remotion-markup/audio.md +35 -14
  73. package/skills/remotion-markup/color-correction.md +190 -0
  74. package/skills/remotion-markup/compositions.md +89 -34
  75. package/skills/remotion-markup/connected-compositions.md +55 -69
  76. package/skills/remotion-markup/cropping.md +2 -0
  77. package/skills/remotion-markup/effects.md +34 -5
  78. package/skills/remotion-markup/embedding-videos.md +20 -13
  79. package/skills/remotion-markup/ffmpeg.md +2 -2
  80. package/skills/remotion-markup/html-in-canvas.md +5 -27
  81. package/skills/remotion-markup/images.md +5 -7
  82. package/skills/remotion-markup/light-leaks.md +2 -4
  83. package/skills/remotion-markup/motion-blur.md +2 -2
  84. package/skills/remotion-markup/multi-scene-video.md +19 -79
  85. package/skills/remotion-markup/remotion-maps/REFERENCE.md +1 -1
  86. package/skills/remotion-markup/remotion-maps/techniques/mapbox/TECHNIQUE.md +3 -3
  87. package/skills/remotion-markup/remotion-maps/techniques/maplibre/TECHNIQUE.md +3 -3
  88. package/skills/remotion-markup/sequencing.md +21 -162
  89. package/skills/remotion-markup/sfx.md +2 -2
  90. package/skills/remotion-markup/silence-detection.md +2 -2
  91. package/skills/remotion-markup/timing-props.md +90 -0
  92. package/skills/remotion-markup/transitions.md +121 -74
  93. package/skills/remotion-markup/video-editing.md +127 -75
  94. package/skills/remotion-markup/voiceover.md +6 -2
  95. package/skills/remotion-multimedia/SKILL.md +5 -1
  96. package/skills/remotion-multimedia/get-audio-duration.md +8 -3
  97. package/skills/remotion-multimedia/get-video-dimensions.md +9 -3
  98. package/skills/remotion-multimedia/get-video-duration.md +6 -3
  99. package/skills/remotion-render/SKILL.md +5 -1
  100. package/skills/remotion-saas/SKILL.md +5 -1
  101. package/skills/remotion-studio/SKILL.md +15 -3
  102. package/skills/remotion-upgrade/SKILL.md +5 -1
package/CHANGELOG.md CHANGED
@@ -1,7 +1,13 @@
1
- # 更新记录
1
+ #- 同步官方 Remotion 4.0.539 的 12 个技能及引用资源,补充 DSH 包边界,避免独立技能更新命令改到其他安装。
2
+ 更新记录
2
3
 
3
4
  [返回简介](README.md) · [使用说明](docs/USAGE.md) · [验证记录](docs/VALIDATION.md)
4
5
 
6
+ ## 0.8.2 (2026-10-27)
7
+
8
+ - 更新控件移至原有工作台或原生插件详情页底部;更新记录与选项原地展开,移除独立更新菜单与页面。
9
+ - 保留原页面操作与草稿,自动安装默认关闭;安装前确认,遇到未保存修改或运行任务时暂停。
10
+
5
11
  ## 0.8.1 (2026-10-27)
6
12
 
7
13
  - Keep this component's own update entry visible when installed transitively through Suite. Confirm whole-Suite update scope before opening its controls.
package/README.en.md CHANGED
@@ -12,6 +12,8 @@ Feedback and contributions are welcome: report [issues](https://github.com/STARD
12
12
 
13
13
  Open the new native sidebar workbench after updating and restarting DSH. Settings remains available. See the [workbench guide and validation boundaries](docs/WORKBENCH.md).
14
14
 
15
+ Bundled skills: official Remotion 4.0.539, twelve skills and their reference resources.
16
+
15
17
  ## What it does
16
18
 
17
19
  - A settings workbench for templates, editable content, media uploads, official Studio preview and local MP4 export.
@@ -53,7 +55,7 @@ Detailed configuration, tool arguments and troubleshooting are in the [usage gui
53
55
 
54
56
  ## Plugin updates
55
57
 
56
- Requires DSH 0.2.1-rc.3 or newer. Each plugin provides its own check, release-note and confirmed-install controls on its page or in settings. Preferences are independent. Automatic checks are on by default; **automatic installation is off by default**. Explicitly enabled automatic updates only install compatible stable patches while idle with no unsaved changes. Disable, defer or ignore reminders per plugin. DSH is never restarted or upgraded automatically, and local-source installs are preserved. Suite components clearly direct updates through Suite.
58
+ Requires DSH 0.2.1-rc.3 or newer. Update controls sit at the bottom of the existing workbench, or native plugin details for plugins without a workbench. Checks, release notes and preferences expand in place, with no separate update page or settings menu. Automatic checks are on by default; **automatic installation is off by default**. Only explicit opt-in permits compatible stable patches while idle with no unsaved changes. Preferences are independent, with per-plugin disable, deferral and ignore controls. No forced DSH restart or upgrade, and local-source installations stay unchanged. Suite components explain and operate on the whole Suite in the same footer.
57
59
 
58
60
  Older releases need one manual update in native plugin management to acquire these controls. Publishing to npm does not replace an existing user installation.
59
61
 
package/README.md CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  更新并重启 DSH 后,可从原生侧栏打开新版工作台,原设置入口继续保留。详见[工作台说明与验收范围](docs/WORKBENCH.md)。
14
14
 
15
+ 技能内容:官方 Remotion 4.0.539,12 个技能及完整引用资源。
16
+
15
17
  ## 功能
16
18
 
17
19
  - 设置页视频工作台:选模板、编辑内容、上传素材,在官方 Studio 预览并导出本机 MP4。
@@ -53,7 +55,7 @@ dsh plugin --profile desktop add dsh-remotion
53
55
 
54
56
  ## 插件更新
55
57
 
56
- 需要 DSH 0.2.1-rc.3 或更新版本。每个插件在自己的页面或设置项提供检查、更新记录与确认安装入口,开关独立保存。自动检查默认开启;**自动安装默认关闭**,只有明确授权后,兼容的稳定补丁才会在空闲且没有未保存修改时安装。可单独关闭、推迟或忽略提醒;不会强制重启 DSH、升级 DSH 本体或替换本地源码安装。Suite 安装的组件会明确提示通过 Suite 整包更新。
58
+ 需要 DSH 0.2.1-rc.3 或更新版本。更新控件直接位于原有工作台内容末尾;没有工作台的插件,位于原生插件详情页底部。检查、更新记录和偏好在原地展开,不增加独立更新页面或设置菜单。自动检查默认开启;**自动安装默认关闭**,明确授权后才会在空闲且没有未保存修改时安装兼容的稳定补丁。开关按插件独立保存,可关闭、推迟或忽略提醒;不会强制重启 DSH、升级 DSH 本体或替换本地源码安装。Suite 组件在当前页面底部明确说明并操作 Suite 整包更新。
57
59
 
58
60
  旧版用户先在官方插件管理中手动更新一次,之后才会获得此入口。发布到 npm 不会直接替换用户已经安装的版本。
59
61
 
package/docs/USAGE.en.md CHANGED
@@ -28,7 +28,7 @@ Open Settings → Remotion in DSH Web or Desktop.
28
28
 
29
29
  1. Create a video. Select title card, product card or slideshow; set title/body, canvas and 3–30 seconds, then save.
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
- 3. Explicitly prepare the environment. It downloads official CLI 4.0.532; 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.
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
33
  5. Export, play and download MP4. Editing preserves earlier MP4s with a revision warning; export again for a new output.
34
34
 
@@ -40,7 +40,7 @@ This is template editing, without a full timeline, transcription, TTS or cloud r
40
40
 
41
41
  ## Contents
42
42
 
43
- - **remotion-best-practices** (router hub) + 11 domain skills (captions/create/docs/interactivity/maps/markup/multimedia/render/saas/studio/upgrade), synced from official Remotion v4.0.529
43
+ - **remotion-best-practices** (router hub) + 11 domain skills (captions/create/docs/interactivity/maps/markup/multimedia/render/saas/studio/upgrade), synced from official Remotion v4.0.539
44
44
 
45
45
  ## Requirements
46
46
 
@@ -48,7 +48,7 @@ Node.js `^22.19.0 || >=24.0.0` + npx (npm registry); rendering needs ffmpeg (gui
48
48
 
49
49
  ## Porting notes
50
50
 
51
- Synced from the official `remotion-dev/skills` checkout at v4.0.529 (2026-09-25): upstream skill bodies are vendored as-is, including the embedded reference copies under `remotion-best-practices/` and `remotion-markup/`, with `scripts/sync-skills.mjs` making the port repeatable and checkable. Only the frontmatter is ours (`name`/`description` plus the upstream `version`).
51
+ Synced from official `remotion-dev/skills` v4.0.539 commit `30d4553ecd46fbc5212fd0ff006ae43af522158b` (2026-10-27). All twelve skills and embedded references are updated. Frontmatter and a short DSH package context are adapted so DSH owns bundled skill updates and existing project dependencies remain separate. `scripts/sync-skills.mjs --check` validates the port; resource hashes are recorded in `skills/UPSTREAM.json`.
52
52
 
53
53
  ## Multi-harness
54
54
 
package/docs/USAGE.md CHANGED
@@ -28,7 +28,7 @@ dsh plugin --profile web remove dsh-remotion
28
28
 
29
29
  1. 新建视频,选择标题卡、产品介绍或图文轮播,填写标题/正文、画幅和 3–30 秒时长,明确保存。
30
30
  2. 上传 PNG/JPEG、一个背景音频(MP3/WAV)、一个静音背景视频(MP4)的副本。每个文件最多 20 MB、总计 40 MB;轮播按上传顺序,每张至少两秒。
31
- 3. 首次点击「准备渲染环境」。此时下载固定版本官方 CLI 4.0.532,优先使用已安装的 Chrome,没有时再下载浏览器;可以继续编辑工程。下载失败保留工程,查看运行记录后重试。使用前确认 [Remotion 官方许可证](https://www.remotion.dev/license)适合你的用途。
31
+ 3. 首次点击「准备渲染环境」。此时下载固定版本官方 CLI 4.0.539,优先使用已安装的 Chrome,没有时再下载浏览器;可以继续编辑工程。下载失败保留工程,查看运行记录后重试。使用前确认 [Remotion 官方许可证](https://www.remotion.dev/license)适合你的用途。
32
32
  4. 启动官方 Studio,检查画面。停止本次预览后再编辑或导出;不会自动打开或占用前台窗口。
33
33
  5. 导出 MP4、在工作台播放并下载。工程更新后保留旧成品并显示旧修订提醒;再次导出即可生成新成片。
34
34
 
@@ -44,7 +44,7 @@ dsh plugin --profile web remove dsh-remotion
44
44
 
45
45
  ## 技能内容
46
46
 
47
- - **remotion-best-practices**(总纲路由)+ 11 个领域技能(captions/create/docs/interactivity/maps/markup/multimedia/render/saas/studio/upgrade),同步自 Remotion 官方 v4.0.529
47
+ - **remotion-best-practices**(总纲路由)+ 11 个领域技能(captions/create/docs/interactivity/maps/markup/multimedia/render/saas/studio/upgrade),同步自 Remotion 官方 v4.0.539
48
48
 
49
49
  ## 依赖
50
50
 
@@ -52,7 +52,7 @@ Node.js `^22.19.0 || >=24.0.0` + npx(npm registry);渲染需 ffmpeg(Remo
52
52
 
53
53
  ## 移植说明
54
54
 
55
- 技能同步自官方 `remotion-dev/skills` 检出 v4.0.529(2026-09-25):上游正文原样引入(含 `remotion-best-practices/`、`remotion-markup/` 下的内嵌引用副本),仅把 frontmatter 换成本包的 `name`/`description` + 上游 `version`;`scripts/sync-skills.mjs` 让移植可复现、可校验(`--check`)。
55
+ 技能同步自官方 `remotion-dev/skills` 的 v4.0.539 提交 `30d4553ecd46fbc5212fd0ff006ae43af522158b`(2026-10-27)。12 个技能及内嵌引用资源同步更新;仅整理 frontmatter 并增加 DSH 包边界说明,技能文件由 DSH 插件更新管理,不执行独立技能更新命令去改其他安装。`scripts/sync-skills.mjs --check` 可核对移植正文,`skills/UPSTREAM.json` 记录资源摘要。
56
56
 
57
57
  ## 跨平台使用
58
58
 
package/lib/client.js CHANGED
@@ -173,16 +173,33 @@ function __stardustUpdatesGuard(React, name, { dirty = false, busy = false, reta
173
173
  for (const listener of pool.listeners) listener();
174
174
  }, []);
175
175
  }
176
- function __StardustPluginUpdateButton({ React, name }) {
176
+ function __StardustPluginUpdatesFooter({ React, name }) {
177
177
  const [, render] = React.useState(0), pool = __stardustUpdatesGuards();
178
178
  React.useEffect(() => { const fn = () => render(value => value + 1); pool.viewListeners.add(fn); return () => pool.viewListeners.delete(fn); }, []);
179
179
  const client = [...pool.clients].find(client => client.has(name));
180
180
  if (!client) return null;
181
- const available = client.row(name)?.available;
182
- return React.createElement(client.Button, { icon: 'refresh', variant: available ? undefined : 'ghost', onClick: () => client.open(name), 'data-plugin-update': name }, available ? client.t('有更新', 'Update available') : client.t('插件更新', 'Update plugin'));
181
+ return React.createElement(client.Footer, { name });
183
182
  }
184
- function __stardustUpdatesPage(React, props, name) {
185
- return { ...props, actions: React.createElement(React.Fragment, null, props.actions, React.createElement(__StardustPluginUpdateButton, { React, name })) };
183
+ function __stardustUpdatesInlinePage(React, element, name) {
184
+ return React.cloneElement(element, null, element.props.children, React.createElement(__StardustPluginUpdatesFooter, { key: 'plugin-updates', React, name }));
185
+ }
186
+ function __stardustUpdatesDetailName(subject, rows) {
187
+ if (!subject || !['bundle', 'row'].includes(subject.kind)) return null;
188
+ if (subject.kind === 'row' && subject.pkg?.name === '@stardustlc/dsh-suite') {
189
+ const component = rows.find(row => row.bundledBy === subject.pkg.name && (subject.row?.moduleName === row.name || subject.row?.moduleName?.startsWith(row.name + '/')));
190
+ if (component) return component.name;
191
+ }
192
+ return rows.some(row => row.name === subject.pkg?.name) ? subject.pkg.name : null;
193
+ }
194
+ function __stardustUpdatesNativeFormState(document) {
195
+ let dirty = false, busy = false;
196
+ for (const scope of document.querySelectorAll('[data-plugin-detail],[data-plugin-row-detail]')) for (const button of scope.querySelectorAll('button')) {
197
+ if (!button.getClientRects().length || button.closest('.plugin-updates-footer')) continue;
198
+ const label = (button.getAttribute('aria-label') || button.textContent || '').trim();
199
+ if (/^(?:正在保存|保存中|Saving)(?:[.…\s]|$)/i.test(label) || button.getAttribute('aria-busy') === 'true') busy = true;
200
+ if (!button.disabled && button.getAttribute('aria-disabled') !== 'true' && /^(?:保存(?:更改|设置|配置)?|Save(?: changes| settings| configuration)?)(?:\s+(?:Ctrl|⌘|Enter|↵).*)?$/i.test(label)) dirty = true;
201
+ }
202
+ return { dirty, busy };
186
203
  }
187
204
  function __stardustUpdatesDisplayRows(next, known) {
188
205
  for (const row of next.rows) known.set(row.name, row);
@@ -244,8 +261,8 @@ function __stardustUpdatesJoin(host, React, require, participant) {
244
261
  }
245
262
  function __stardustUpdatesClient(ctx, React, require, shared) {
246
263
  const h = React.createElement, W = createWorkbenchUI(React), route = new URL('api/stardustlc/plugin-updates', document.baseURI);
247
- const { useState, useEffect } = React, id = crypto.randomUUID(), listeners = new Set(), pool = __stardustUpdatesGuards(), requests = new Set(), sections = new Map(), notified = new Set(), knownRows = new Map();
248
- let alive = true, registered = false, state = null, lastInteraction = Date.now(), inPoll = false, activeName = null, manualOperation = false;
264
+ const { useState, useEffect } = React, id = crypto.randomUUID(), listeners = new Set(), pool = __stardustUpdatesGuards(), requests = new Set(), notified = new Set(), knownRows = new Map();
265
+ let alive = true, registered = false, state = null, lastInteraction = Date.now(), inPoll = false, manualOperation = false;
249
266
  const language = () => String(ctx.locale?.getSnapshot?.().active || '').startsWith('en') ? 'en' : 'zh';
250
267
  const t = (zh, en) => language() === 'en' ? en : zh;
251
268
  const preferences = name => state?.preferences.packages[name] || { checkEnabled: true, autoInstall: false, deferredUntil: 0, ignoredVersion: null };
@@ -265,12 +282,12 @@ function __stardustUpdatesClient(ctx, React, require, shared) {
265
282
  finally { clearTimeout(timeout); requests.delete(controller); }
266
283
  }
267
284
  function pageState(manual = manualOperation) {
268
- const values = [...pool.values.values()];
269
- const dialog = [...document.querySelectorAll('[role="dialog"],dialog[open]')].some(node => node.getClientRects().length && !(manual && (node.closest('.plugin-updates-page') || node.querySelector('.plugin-updates-page'))));
270
- return { action: 'heartbeat', id, dirty: values.some(value => value.dirty), busy: values.some(value => value.busy) || dialog, idleForMs: Math.min(86400000, Math.max(0, Date.now() - lastInteraction)) };
285
+ const values = [...pool.values.values()], nativeForm = __stardustUpdatesNativeFormState(document);
286
+ const dialog = [...document.querySelectorAll('[role="dialog"],dialog[open]')].some(node => node.getClientRects().length && !(manual && (node.closest('.plugin-updates-footer') || node.querySelector('.plugin-updates-footer'))));
287
+ return { action: 'heartbeat', id, dirty: nativeForm.dirty || values.some(value => value.dirty), busy: nativeForm.busy || values.some(value => value.busy) || dialog, idleForMs: Math.min(86400000, Math.max(0, Date.now() - lastInteraction)) };
271
288
  }
272
289
  const heartbeat = () => api(pageState());
273
- function interaction() { lastInteraction = Date.now(); }
290
+ function interaction() { lastInteraction = Date.now(); if (registered && alive) void heartbeat().catch(() => {}); }
274
291
  const guardChanged = () => { if (registered && alive) void heartbeat().catch(() => {}); };
275
292
  for (const event of ['pointerdown', 'keydown', 'input']) document.addEventListener(event, interaction, { capture: true, passive: true });
276
293
  pool.listeners.add(guardChanged);
@@ -304,8 +321,14 @@ function __stardustUpdatesClient(ctx, React, require, shared) {
304
321
  function jobLabel(job) {
305
322
  return ({ verifying: t('正在核对版本', 'Verifying release'), installing: t('正在安装', 'Installing'), cancelling: t('正在请求取消', 'Requesting cancellation'), complete: t('已更新', 'Updated'), 'restart-required': t('已安装,等待你手动重启', 'Installed; manual restart needed'), restored: t('更新未成功,已恢复原版本', 'Update failed; previous version restored'), 'restore-failed': t('恢复未成功,请检查官方插件管理', 'Recovery failed; check native plugin management'), unknown: t('结果未确认,请检查官方插件管理', 'Result unconfirmed; check native plugin management'), cancelled: t('已取消', 'Cancelled'), failed: t('更新未成功', 'Update failed') })[job.state] || job.state;
306
323
  }
307
- function Panel({ name, compact = false }) {
308
- const snapshot = useSnapshot(), [error, setError] = useState(''), [working, setWorking] = useState(false), [checking, setChecking] = useState(false), [dialog, setDialog] = useState(null), [notes, setNotes] = useState(null), [loadingNotes, setLoadingNotes] = useState(false);
324
+ function FooterFrame({ name, label, subtitle, actions, children }) {
325
+ return h('section', { className: 'dshw plugin-updates-footer', 'data-plugin-update-footer': name, 'aria-label': label + t(' · 版本与更新', ' · Version and updates') },
326
+ h('style', null, W.styles),
327
+ h('style', null, '.plugin-updates-footer{box-sizing:border-box;flex-shrink:0;margin-top:28px;padding-top:18px;border-top:1px solid var(--dsw-alias-border-l3,#e5e7eb);min-width:0;max-width:100%;color:var(--dsw-alias-label-primary,#202124)}.plugin-updates-footer .updates-head{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:12px}.plugin-updates-footer h2{font-size:14px;line-height:22px;margin:0;font-weight:600}.plugin-updates-footer .updates-subtitle,.plugin-updates-footer .updates-version{font:12px/1.6 ui-monospace,monospace;overflow-wrap:anywhere;margin:4px 0}.plugin-updates-footer .updates-options{margin-top:12px}.plugin-updates-footer .updates-options>summary{cursor:pointer;font-size:12px;line-height:22px;color:var(--dsw-alias-label-secondary,#657080)}.plugin-updates-footer .updates-options>summary:focus-visible{outline:2px solid var(--dsw-focus-ring-color,#6b79df);outline-offset:4px}.plugin-updates-footer .updates-settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr));gap:16px;padding:14px 0 0}.plugin-updates-footer .updates-note{margin:5px 0 0;font-size:12px;line-height:1.6}.plugin-updates-footer>.w-section{margin:10px 0 0;padding:0;border:0}.plugin-updates-footer .w-toolbar{margin-top:10px!important}.plugin-updates-footer .w-switch-label{max-width:100%;gap:12px;align-items:flex-start}.plugin-updates-footer .w-switch-track{flex-shrink:0}.plugin-updates-footer .updates-notes{margin-top:12px;padding:12px;border:1px solid var(--dsw-alias-border-l3,#e5e7eb);border-radius:10px}.plugin-updates-footer .updates-notes pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:240px;overflow:auto;font:12px/1.7 ui-monospace,monospace}.plugin-updates-footer .w-overlay{z-index:1002}@media(max-width:600px){.plugin-updates-footer .updates-head{gap:8px}.plugin-updates-footer .w-toolbar{gap:6px;flex-wrap:wrap}.plugin-updates-footer .w-button{max-width:100%}}'),
328
+ h('div', { className: 'updates-head' }, h('div', null, h('h2', null, t('版本与更新', 'Version and updates')), h('p', { className: 'updates-subtitle w-muted' }, subtitle)), actions), children);
329
+ }
330
+ function Panel({ name }) {
331
+ const snapshot = useSnapshot(), [error, setError] = useState(''), [working, setWorking] = useState(false), [checking, setChecking] = useState(false), [dialog, setDialog] = useState(null), [notes, setNotes] = useState(null), [notesOpen, setNotesOpen] = useState(false), [loadingNotes, setLoadingNotes] = useState(false);
309
332
  const row = rowFor(name), original = originalRow(name), target = row?.name || name, prefs = preferences(target), job = snapshot?.job?.name === target ? snapshot.job : null;
310
333
  async function perform(callback) { setError(''); setWorking(true); try { await callback(); await refresh(); } catch (error) { setError(error.message); await refresh().catch(() => {}); } finally { setWorking(false); } }
311
334
  async function save(changes, consent) { await api({ action: 'preferences', name: target, revision: state.preferences.revision, changes, consent }); }
@@ -325,52 +348,41 @@ function __stardustUpdatesClient(ctx, React, require, shared) {
325
348
  });
326
349
  }
327
350
  async function showNotes() {
328
- setDialog('notes'); setNotes(null); setLoadingNotes(true); setError('');
351
+ if (notesOpen) { setNotesOpen(false); return; }
352
+ setNotesOpen(true); setNotes(null); setLoadingNotes(true); setError('');
329
353
  try { setNotes(await api({ action: 'notes', name: target, version: row.release.version })); } catch (error) { setError(error.message); } finally { setLoadingNotes(false); }
330
354
  }
331
355
  if (!snapshot || !row) return h(W.Status, null, t('此插件尚未安装或已从当前 profile 移除。', 'This plugin is not installed in the current profile.'));
332
356
  const title = language() === 'en' ? original.labelEn : original.label;
333
- if (original.bundledBy) return h(W.Page, { title: title + t(' · 更新', ' · Updates'), subtitle: t('此组件随 Suite 安装。', 'This component is installed through Suite.'), className: 'plugin-updates-page' },
334
- h('p', { className: 'updates-version' }, original.name + ' · ' + original.version),
335
- h(W.Status, null, t('请更新 Suite 整包,组件版本会一起同步。不会单独再安装一份组件;自动安装默认关闭。', 'Update the whole Suite to synchronize component versions. No duplicate component will be installed. Automatic installation is off by default.')),
336
- h(W.Button, { icon: 'refresh', onClick: () => { activeName = original.bundledBy; notify(); } }, t('查看 Suite 更新设置', 'Open Suite update settings')));
337
- if (compact) return h('div', { className: 'dshw', style: { padding: 16 } }, h('style', null, W.styles), h('div', { className: 'w-stack' }, h('h3', null, title), h('p', { className: 'w-muted' }, original.name + ' · ' + original.version), h('p', null, t('这个插件的更新设置与安装入口。自动安装默认关闭。', 'Update settings and installation for this plugin. Automatic installation is off by default.')), h(W.Button, { icon: 'refresh', onClick: () => { activeName = name; notify(); } }, row.available ? t('查看可用更新', 'Review available update') : t('检查与更新', 'Check and update'))));
338
- const unconfirmed = ['unknown', 'restore-failed'].includes(snapshot.job?.state), pageBusy = pageState(true).busy || (snapshot.pages.clients || []).some(client => client.id !== id && client.busy), installLocked = working || activeJob() || unconfirmed || snapshot.updaterRestart || snapshot.preferenceError || !snapshot.activity.known || snapshot.activity.busy || snapshot.pages.dirty || pageBusy || !snapshot.pages.known;
357
+ const ownPage = pageState(true), unconfirmed = ['unknown', 'restore-failed'].includes(snapshot.job?.state), pageBusy = ownPage.busy || (snapshot.pages.clients || []).some(client => client.id !== id && client.busy), installLocked = working || activeJob() || unconfirmed || snapshot.updaterRestart || snapshot.preferenceError || !snapshot.activity.known || snapshot.activity.busy || snapshot.pages.dirty || ownPage.dirty || pageBusy || !snapshot.pages.known;
339
358
  const checkingThis = checking || snapshot.checkingNames?.includes(target);
340
359
  const status = row.sourceKind !== 'registry' ? t('本地 / 外部来源,保留原安装方式', 'Local / external source; keep original install method') : row.loadError ? t('加载异常,请先检查官方插件管理', 'Loading error; check native plugin management') : row.error ? t('检查失败,可以重试', 'Check failed; retry available') : row.release && !row.release.compatible ? t('新版本暂不兼容当前环境', 'Latest release is incompatible with this environment') : row.available ? (prefs.ignoredVersion === row.release.version ? t('已忽略提醒,仍可手动更新', 'Reminder ignored; manual update available') : t('有新版本', 'Update available')) : row.checkedAt ? t('当前已是最新稳定版', 'Latest stable version') : t('尚未检查', 'Not checked yet');
341
- return h(W.Page, { title: title + t(' · 更新', ' · Updates'), subtitle: target === '@stardustlc/dsh-suite' ? t('管理 Suite 整包及其 18 个组件。检查不会安装,也不会重启 DSH。', 'Manage the whole Suite and its 18 components. Checking does not install anything or restart DSH.') : t('只管理这个插件。检查不会安装,也不会重启 DSH。', 'Manage this plugin. Checking does not install anything or restart DSH.'), className: 'plugin-updates-page', actions: h(W.Button, { icon: 'refresh', disabled: checkingThis || row.sourceKind !== 'registry', onClick: check }, checkingThis ? t('正在检查…', 'Checking…') : t('检查更新', 'Check for updates')) },
342
- h('style', null, '.plugin-updates-page{min-height:0;padding:4px}.plugin-updates-page .updates-settings{display:grid;gap:24px;padding:20px 0}.plugin-updates-page .updates-note{margin-top:8px}.plugin-updates-page .updates-version{font:13px/1.7 ui-monospace,monospace;overflow-wrap:anywhere}.plugin-updates-page .w-overlay{z-index:1002}.plugin-updates-page .w-switch-label{max-width:100%}.plugin-updates-page .w-switch-track{flex-shrink:0}@media(max-width:700px){[data-shortcut-modal="settings"]:has(.plugin-updates-page){flex-direction:column!important;width:calc(100vw - 16px)!important;max-width:calc(100vw - 16px)!important}[data-shortcut-modal="settings"]:has(.plugin-updates-page)>nav{width:100%!important;flex-basis:auto!important;flex-shrink:0;max-height:180px;overflow:auto;border-right:0!important}[data-shortcut-modal="settings"]:has(.plugin-updates-page)>nav>div:has(>button){display:flex;flex-wrap:wrap;gap:4px}[data-shortcut-modal="settings"]:has(.plugin-updates-page)>div{min-width:0!important;width:100%!important}.plugin-updates-page .w-switch-label{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}.plugin-updates-page .w-switch-label>span:first-child{min-width:0}.plugin-updates-page .w-switch-label>input{inset-inline-end:0}}'),
360
+ return h(FooterFrame, { name, label: title, subtitle: original.name + ' · ' + original.version, actions: h(W.Button, { icon: 'refresh', disabled: checkingThis || row.sourceKind !== 'registry', onClick: check }, checkingThis ? t('正在检查…', 'Checking…') : target === '@stardustlc/dsh-suite' ? t('检查整包更新', 'Check Suite updates') : t('检查更新', 'Check for updates')) },
343
361
  original.bundledBy && h(W.Status, null, t('此插件随 Suite 安装。以下设置与安装作用于 Suite 整包,以保持组件版本一致。单独安装的插件可以独立更新。', 'This plugin was installed through Suite. These controls update Suite as a package so its components stay consistent. Independently installed plugins update individually.')),
344
- h('section', { className: 'w-section' }, h('p', { className: 'updates-version' }, target + ' · ' + row.version, row.release?.version && row.release.version !== row.version ? ' → ' + row.release.version : ''), h('p', { className: 'w-muted updates-note' }, status), !row.enabled && h('p', { className: 'w-muted' }, t('当前已停用,更新后仍保持停用。', 'Currently disabled; it will remain disabled after updating.')), row.checkedAt && h('p', { className: 'w-muted' }, t('上次检查:', 'Last checked: ') + new Date(row.checkedAt).toLocaleString(language() === 'en' ? 'en-US' : 'zh-CN')),
345
- h('div', { className: 'w-toolbar', style: { marginTop: 16 } }, row.available && row.sourceKind === 'registry' && h(W.Button, { variant: 'primary', icon: 'download', disabled: installLocked || row.loadError, onClick: () => setDialog('install') }, t('更新此插件', 'Update this plugin')), row.release && h(W.Button, { variant: 'ghost', onClick: showNotes }, t('更新记录', 'Release notes')), row.available && h(W.Button, { variant: 'ghost', disabled: working, onClick: () => perform(() => api({ action: 'later', name: target, revision: state.preferences.revision })) }, t('明天再提醒', 'Remind me tomorrow')), row.available && prefs.ignoredVersion !== row.release.version && h(W.Button, { variant: 'ghost', disabled: working, onClick: () => perform(() => api({ action: 'ignore', name: target, revision: state.preferences.revision, version: row.release.version })) }, t('忽略此版本提醒', 'Ignore this reminder')))),
346
- h('section', { className: 'updates-settings', 'aria-label': t('这个插件的更新偏好', 'Update preferences for this plugin') },
362
+ h('section', { className: 'w-section' }, (original.bundledBy || row.available) && h('p', { className: 'updates-version' }, target + ' · ' + row.version, row.release?.version && row.release.version !== row.version ? ' → ' + row.release.version : ''), h('p', { className: 'w-muted updates-note' }, status), !row.enabled && h('p', { className: 'w-muted' }, t('当前已停用,更新后仍保持停用。', 'Currently disabled; it will remain disabled after updating.')), row.checkedAt && h('p', { className: 'w-muted updates-note' }, t('上次检查:', 'Last checked: ') + new Date(row.checkedAt).toLocaleString(language() === 'en' ? 'en-US' : 'zh-CN')),
363
+ h('div', { className: 'w-toolbar' }, row.available && row.sourceKind === 'registry' && h(W.Button, { variant: 'primary', icon: 'download', disabled: installLocked || row.loadError, onClick: () => setDialog('install') }, target === '@stardustlc/dsh-suite' ? t('更新 Suite 整包', 'Update whole Suite') : t('更新此插件', 'Update this plugin')), row.release && h(W.Button, { variant: 'ghost', onClick: showNotes, 'aria-expanded': notesOpen }, t('更新记录', 'Release notes')), row.available && h(W.Button, { variant: 'ghost', disabled: working, onClick: () => perform(() => api({ action: 'later', name: target, revision: state.preferences.revision })) }, t('明天再提醒', 'Remind me tomorrow')), row.available && prefs.ignoredVersion !== row.release.version && h(W.Button, { variant: 'ghost', disabled: working, onClick: () => perform(() => api({ action: 'ignore', name: target, revision: state.preferences.revision, version: row.release.version })) }, t('忽略此版本提醒', 'Ignore this reminder')))),
364
+ notesOpen && h('section', { className: 'updates-notes', 'aria-label': t('更新记录', 'Release notes') }, h('strong', null, target + ' · ' + (notes?.version || row.release?.version || '')), loadingNotes ? h('p', null, t('正在读取…', 'Loading…')) : notes?.text ? h('pre', null, notes.text) : h('p', null, t('未找到此版本的独立更新记录。', 'No separate notes were found for this version.')), notes?.url && h('a', { href: notes.url, target: '_blank', rel: 'noreferrer noopener' }, t('查看仓库更新记录', 'View repository changelog'))),
365
+ h('details', { className: 'updates-options' }, h('summary', null, t('更新选项', 'Update options'), ' · ', prefs.checkEnabled ? t('自动检查:开', 'Checks: on') : t('自动检查:关', 'Checks: off'), ' · ', prefs.autoInstall ? t('自动安装:开', 'Install: on') : t('自动安装:关', 'Install: off')), h('section', { className: 'updates-settings', 'aria-label': t('这个插件的更新偏好', 'Update preferences for this plugin') },
347
366
  h('div', null, h(W.Switch, { label: t('自动检查这个插件的更新', 'Automatically check this plugin'), checked: prefs.checkEnabled, disabled: working || !!snapshot.preferenceError || row.sourceKind !== 'registry', onChange: event => perform(() => save({ checkEnabled: event.target.checked })) }), h('p', { className: 'w-muted updates-note' }, t('默认开启,每天最多一次。只影响这个插件,关闭后仍可手动检查。', 'On by default, at most once daily. This affects only this plugin; manual checks remain available.'))),
348
- h('div', null, h(W.Switch, { label: t('空闲时自动安装这个插件的补丁', 'Install patches for this plugin while idle'), checked: prefs.autoInstall, disabled: working || !!snapshot.preferenceError || row.sourceKind !== 'registry', onChange: event => event.target.checked ? setDialog('automatic') : perform(() => save({ autoInstall: false })) }), h('p', { className: 'w-muted updates-note' }, t('默认关闭。明确开启后,仅空闲且没有未保存修改时安装兼容补丁。其他插件的开关不变。', 'Off by default. Explicitly enable compatible patches while idle with no unsaved changes. Other plugins’ settings stay unchanged.')))),
367
+ h('div', null, h(W.Switch, { label: t('空闲时自动安装这个插件的补丁', 'Install patches for this plugin while idle'), checked: prefs.autoInstall, disabled: working || !!snapshot.preferenceError || row.sourceKind !== 'registry', onChange: event => event.target.checked ? setDialog('automatic') : perform(() => save({ autoInstall: false })) }), h('p', { className: 'w-muted updates-note' }, t('默认关闭。明确开启后,仅空闲且没有未保存修改时安装兼容补丁。其他插件的开关不变。', 'Off by default. Explicitly enable compatible patches while idle with no unsaved changes. Other plugins’ settings stay unchanged.'))))),
349
368
  error && h(W.Status, { tone: 'error' }, message(error)), snapshot.preferenceError && h(W.Status, { tone: 'error' }, message('preferences-unreadable')),
350
369
  snapshot.pendingRestart && h(W.Status, null, snapshot.updaterRestart ? message('restart-required') : t('已有更新等待手动重启后生效。你可以继续工作,自动安装已暂停。', 'Some updates need a manual restart. Continue your work; automatic installation is paused.')),
351
370
  prefs.autoInstall && snapshot.autoReady === false && h(W.Status, null, message('page-not-ready')),
352
371
  snapshot.pages.clients.some(client => !client.fresh) && h(W.Status, null, h('p', null, t('有窗口已断开,无法确认是否还有未保存修改。仍在使用的窗口请先保存并刷新;已关闭的窗口可以手动重新确认。', 'A disconnected window may contain unsaved changes. Save and refresh windows still in use. If they are closed, you can confirm that manually.')), h(W.Button, { onClick: () => setDialog('windows') }, t('重新确认已关闭的窗口', 'Confirm closed windows'))),
353
- snapshot.pages.dirty && h(W.Status, null, message('page-dirty')), (snapshot.activity.busy || pageBusy) && h(W.Status, null, message(snapshot.activity.busy ? 'busy' : 'page-busy')),
372
+ (snapshot.pages.dirty || ownPage.dirty) && h(W.Status, null, message('page-dirty')), (snapshot.activity.busy || pageBusy) && h(W.Status, null, message(snapshot.activity.busy ? 'busy' : 'page-busy')),
354
373
  prefs.deferredUntil > Date.now() && h(W.Status, null, t('此插件已推迟提醒和自动安装 24 小时,仍可手动更新。', 'Reminders and automatic installation for this plugin are deferred for 24 hours. Manual updates remain available.')),
355
374
  unconfirmed && h(W.Status, { tone: 'error' }, message('operation-unconfirmed')),
356
375
  job && h('section', { className: 'w-section', 'aria-label': t('这个插件的更新进度', 'Update progress for this plugin') }, h('h3', null, jobLabel(job)), h('p', { className: 'w-muted' }, job.from + ' → ' + job.to + (job.mode === 'automatic' ? t(' · 自动补丁更新', ' · Automatic patch') : '')), (job.reason || job.outcome?.result?.error?.code) && h(W.Status, { tone: 'error' }, message(job.reason || job.outcome.result.error.code)), activeJob() && h(W.Button, { disabled: job.state === 'cancelling', onClick: () => perform(() => api({ action: 'cancel', id: job.id })) }, t('取消更新', 'Cancel update')), h('p', { className: 'w-muted updates-note' }, t('安装开始后,取消由官方包管理器处理;已落盘的写入以实际结果或恢复状态为准。', 'After installation starts, the native package manager handles cancellation. Completed writes and recovery are reported as they occur.'))),
357
- dialog && h(W.Dialog, { title: dialog === 'windows' ? t('断开的窗口都已关闭?', 'Are the disconnected windows closed?') : dialog === 'automatic' ? t('允许这个插件自动安装补丁?', 'Allow automatic patches for this plugin?') : dialog === 'notes' ? t('更新记录', 'Release notes') : t('确认更新此插件', 'Confirm this plugin update'), closeLabel: t('关闭', 'Close'), onClose: () => setDialog(null) },
358
- target === '@stardustlc/dsh-suite' && dialog !== 'notes' && h('p', null, t('此处操作的是 Suite 整包,包含 18 个组件。独立安装的其他插件不受影响。', 'This action applies to the whole Suite, including its 18 components. Other independently installed plugins are unaffected.')),
376
+ dialog && h(W.Dialog, { title: dialog === 'windows' ? t('断开的窗口都已关闭?', 'Are the disconnected windows closed?') : dialog === 'automatic' ? t('允许这个插件自动安装补丁?', 'Allow automatic patches for this plugin?') : t('确认更新此插件', 'Confirm this plugin update'), closeLabel: t('关闭', 'Close'), onClose: () => setDialog(null) },
377
+ target === '@stardustlc/dsh-suite' && h('p', null, t('此处操作的是 Suite 整包,包含 18 个组件。独立安装的其他插件不受影响。', 'This action applies to the whole Suite, including its 18 components. Other independently installed plugins are unaffected.')),
359
378
  dialog === 'windows' ? h('div', { className: 'w-stack' }, h('p', null, t('只清除超过 45 秒未回应的其他窗口记录。请先确认它们已经关闭;仍在使用的窗口请保存修改并刷新。当前或仍在回应的窗口保护会保留。', 'Only other windows silent for over 45 seconds are cleared. Confirm that they are closed; save and refresh windows still in use. Current and responsive windows remain protected.')), h('div', { className: 'w-toolbar' }, h(W.Button, { onClick: () => setDialog(null) }, t('取消', 'Cancel')), h(W.Button, { onClick: () => { setDialog(null); void perform(() => api({ ...pageState(true), action: 'reconfirm-windows', confirmation: 'other-stale-windows-are-closed' })); } }, t('已关闭,重新确认状态', 'They are closed; reconfirm')))) :
360
379
  dialog === 'automatic' ? h('div', { className: 'w-stack' }, h('p', null, (language() === 'en' ? row.labelEn : row.label) + ' · ' + target), h('p', null, t('你正在主动启用这个插件的自动安装。只处理兼容的稳定补丁;有任务、未保存修改、未知窗口或等待重启时暂停。', 'You are explicitly enabling automatic installation for this plugin. Only compatible stable patches qualify; running tasks, unsaved changes, unknown windows or a pending restart pause it.')), h('p', null, t('不会自动重启 DSH,其他插件不会跟着开启。可随时关闭。', 'DSH will not restart automatically, and other plugins will not be enabled for automatic updates. Turn it off anytime.')), h('div', { className: 'w-toolbar' }, h(W.Button, { onClick: () => setDialog(null) }, t('保持关闭', 'Keep off')), h(W.Button, { variant: 'primary', onClick: () => { setDialog(null); void perform(() => save({ checkEnabled: true, autoInstall: true }, 'enable-idle-patch-updates')); } }, t('允许此插件自动安装补丁', 'Allow patches for this plugin')))) :
361
- dialog === 'notes' ? h('div', { className: 'w-stack' }, h('p', null, target + ' · ' + row.release.version), loadingNotes ? h('p', null, t('正在读取…', 'Loading…')) : notes?.text ? h('pre', null, notes.text) : h('p', null, t('未找到此版本的独立更新记录。', 'No separate notes were found for this version.')), notes?.url && h('a', { href: notes.url, target: '_blank', rel: 'noreferrer noopener' }, t('查看仓库更新记录', 'View repository changelog'))) :
362
- h('div', { className: 'w-stack' }, h('p', { className: 'updates-version' }, target + ': ' + row.version + ' → ' + row.release.version), h('p', null, t('通过 DSH 官方插件管理器安装。需要重启时,由你手动决定时间;停用状态会保留。', 'Install through DSH’s native plugin manager. Choose when to restart if needed; disabled status is preserved.')), h('p', { className: 'w-muted' }, t('来源:npm 官方 registry,网络不可达时回退 npmmirror。安装前再次核对版本与完整性。', 'Source: official npm registry, with npmmirror fallback if unreachable. Version and integrity are checked again before installation.')), h('div', { className: 'w-toolbar' }, h(W.Button, { onClick: () => setDialog(null) }, t('取消', 'Cancel')), h(W.Button, { variant: 'primary', onClick: install }, t('确认安装此版本', 'Install this version')))))
380
+ h('div', { className: 'w-stack' }, h('p', { className: 'updates-version' }, target + ': ' + row.version + ' → ' + row.release.version), h('p', null, t('通过 DSH 官方插件管理器安装。需要重启时,由你手动决定时间;停用状态会保留。', 'Install through DSH’s native plugin manager. Choose when to restart if needed; disabled status is preserved.')), h('p', { className: 'w-muted' }, t('来源:npm 官方 registry,网络不可达时回退 npmmirror。安装前再次核对版本与完整性。', 'Source: official npm registry, with npmmirror fallback if unreachable. Version and integrity are checked again before installation.')), (ownPage.dirty || snapshot.pages.dirty) && h(W.Status, null, message('page-dirty')), h('div', { className: 'w-toolbar' }, h(W.Button, { onClick: () => setDialog(null) }, t('取消', 'Cancel')), h(W.Button, { variant: 'primary', disabled: installLocked || row.loadError, onClick: install }, t('确认安装此版本', 'Install this version')))))
363
381
  );
364
382
  }
365
- function Overlay() {
366
- useSnapshot();
367
- const row = activeName && originalRow(activeName);
368
- if (!row) return null;
369
- const content = h('div', { className: 'dshw' }, h('style', null, W.styles), h(W.Dialog, { title: (language() === 'en' ? row.labelEn : row.label) + t(' · 更新设置', ' · Update settings'), closeLabel: t('关闭更新设置', 'Close update settings'), onClose: () => { activeName = null; notify(); } }, h(Panel, { key: activeName, name: activeName })));
370
- const settings = document.querySelector('[data-shortcut-modal="settings"]');
371
- // Keep the dialog inside the native settings focus boundary when its
372
- // per-plugin entry was opened there. The shared fiber survives HMR.
373
- return settings ? require('react-dom').createPortal(content, settings) : content;
383
+ function NativeFooter({ subject }) {
384
+ const snapshot = useSnapshot(), name = __stardustUpdatesDetailName(subject, snapshot?.rows || []);
385
+ return name ? h(Panel, { name }) : null;
374
386
  }
375
387
  function Reminder() {
376
388
  useSnapshot();
@@ -379,26 +391,16 @@ function __stardustUpdatesClient(ctx, React, require, shared) {
379
391
  let Toast; try { Toast = require('@deepseek-ai/dsh-client-ui-primitives').Toast; } catch { return null; }
380
392
  if (!Toast) return null;
381
393
  const key = row.name + '@' + row.release.version;
382
- return h(Toast, { key, text: (language() === 'en' ? row.labelEn : row.label) + t('有新版本,可在它的页面或设置中查看。', ' has an update. Review it on its page or in settings.'), icon: h(W.Icon, { name: 'refresh', size: 18 }), holdMs: 7000, onDone: () => { notified.add(key); notify(); } });
394
+ return h(Toast, { key, text: (language() === 'en' ? row.labelEn : row.label) + t('有新版本,可在页面底部查看。', ' has an update. Review it at the bottom of its page.'), icon: h(W.Icon, { name: 'refresh', size: 18 }), holdMs: 7000, onDone: () => { notified.add(key); notify(); } });
383
395
  }
384
396
  function register() {
385
397
  if (!alive) return;
386
398
  if (!registered) {
387
399
  registered = true; pool.clients.add(client);
388
- ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'stardustlc.plugin-update-dialog' }, Overlay));
400
+ ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({ name: 'plugins.detail.section', id: 'stardustlc.plugin-update-footer', order: 1000 }, NativeFooter));
389
401
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'stardustlc.plugin-update-reminder' }, Reminder));
390
402
  notify();
391
403
  }
392
- for (const row of state.rows) {
393
- const hasWorkbench = [...shared.refs.values()].some(participant => participant.name === row.name && participant.hasWorkbench);
394
- if (hasWorkbench) { if (sections.has(row.name)) { void sections.get(row.name).dispose(); sections.delete(row.name); } continue; }
395
- if (sections.has(row.name)) continue;
396
- const label = () => (language() === 'en' ? row.labelEn : row.label) + t(' · 更新', ' · Updates');
397
- const fiber = ctx.plugin({ name: 'plugin-update-settings-' + row.name, inject: ['slots', 'locale'], apply(section) {
398
- section.slots.inject('settings.section', () => section.slots.register({ name: 'settings.section', id: row.name + '.updates', label, order: 89 }, () => h(Panel, { name: row.name })));
399
- } });
400
- sections.set(row.name, fiber);
401
- }
402
404
  }
403
405
  async function poll() {
404
406
  if (inPoll || !alive || !shared.refs.size) return;
@@ -408,7 +410,7 @@ function __stardustUpdatesClient(ctx, React, require, shared) {
408
410
  finally { inPoll = false; }
409
411
  }
410
412
  const timer = setInterval(() => { void poll(); }, 15000), first = setTimeout(() => { void poll(); }, 100);
411
- const client = { t, Button: W.Button, has: name => registered && !!originalRow(name), row: rowFor, open(name) { activeName = name; notify(); }, dispose() {
413
+ const client = { t, Footer: Panel, has: name => registered && !!originalRow(name), row: rowFor, dispose() {
412
414
  close(); alive = false; clearTimeout(first); clearInterval(timer); pool.clients.delete(client);
413
415
  pool.listeners.delete(guardChanged); listeners.clear();
414
416
  for (const event of ['pointerdown', 'keydown', 'input']) document.removeEventListener(event, interaction, true);
@@ -790,11 +792,11 @@ __stardustUpdatesGuard(React, "dsh-remotion", { dirty, busy: busy || activeJob,
790
792
  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.')); }
791
793
  const render=project?.renders?.[0],stale=render && (dirty || render.revision!==project.revision);
792
794
  const canSave=fields.title.trim().length>0 && fields.title.length<=72 && Number.isInteger(fields.duration) && fields.duration>=3 && fields.duration<=30;
793
- if(!environment) return h(W.Page,__stardustUpdatesPage(React,{workbench,title,subtitle:t('编辑视频工程,预览并导出 MP4。','Edit video projects, preview and export MP4.'),className:'dmw media-workbench'},"dsh-remotion"),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'}));
794
- return h(W.Page,__stardustUpdatesPage(React,{workbench,title,subtitle:t('编辑视频工程,预览并导出 MP4。','Edit video projects, preview and export MP4.'),className:'dmw media-workbench',actions:h(React.Fragment,null,
795
+ 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");
796
+ 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,
795
797
  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')),
796
798
  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)}),
797
- project ? h(W.Button,{icon:'download',disabled:running || dirty || !environment.ready,onClick:()=>start('render')},t('导出 MP4','Export MP4')) : null)},"dsh-remotion"),
799
+ project ? h(W.Button,{icon:'download',disabled:running || dirty || !environment.ready,onClick:()=>start('render')},t('导出 MP4','Export MP4')) : null)},
798
800
  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)')),
799
801
  (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,
800
802
  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,
@@ -819,7 +821,7 @@ h('div',{className:'media-field'},h('label', { htmlFor: controlId+'-duration' },
819
821
  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.')),
820
822
  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,
821
823
  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.')),
822
- 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);
824
+ 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");
823
825
  }
824
826
  function apply(ctx) { let locale; try {
825
827
  locale = ctx.get?.('locale') || ctx.locale;
@@ -827,7 +829,7 @@ h('div', { className: 'bar' }, h('button', { disabled: busy || dirty || activeJo
827
829
  catch { } W.register(ctx,{id:'dsh-'+engine+'.workbench',label:()=>title,icon:engine==='hyperframes'?'layers':'play',component:Workbench,inject:()=>({locale}),order:engine==='hyperframes'?65:66});
828
830
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-' + engine, order: engine === 'hyperframes' ? 62 : 63, label: () => title, inject: () => ({ locale }) }, Workbench)); }
829
831
  return { inject: ['slots', 'locale'], apply };
830
- } },{"name":"dsh-remotion","version":"0.1.0","hasWorkbench":true,"packageVersion":"0.8.1","label":"Remotion","labelEn":"Remotion","suiteComponent":true}));
832
+ } },{"name":"dsh-remotion","version":"0.1.0","hasWorkbench":true,"packageVersion":"0.8.2","label":"Remotion","labelEn":"Remotion","suiteComponent":true}));
831
833
 
832
834
  })();
833
835
 
@@ -27,9 +27,9 @@ export interface MediaProject extends MediaFields {
27
27
  }>;
28
28
  }
29
29
  export declare const MEDIA_VERSIONS: {
30
- readonly hyperframes: "0.8.114";
31
- readonly remotion: "4.0.532";
32
- readonly gsap: "3.14.2";
30
+ readonly hyperframes: "0.8.207";
31
+ readonly remotion: "4.0.539";
32
+ readonly gsap: "3.15.0";
33
33
  };
34
34
  export declare const MEDIA_TEMPLATES: readonly [{
35
35
  readonly id: "title";
@@ -1,4 +1,4 @@
1
- export const MEDIA_VERSIONS = { hyperframes: '0.8.114', remotion: '4.0.532', gsap: '3.14.2' };
1
+ export const MEDIA_VERSIONS = { hyperframes: '0.8.207', remotion: '4.0.539', gsap: '3.15.0' };
2
2
  export const MEDIA_TEMPLATES = [
3
3
  { id: 'title', zh: '标题卡', en: 'Title card', description: '一句标题、一段介绍,适合开场与通知。' },
4
4
  { id: 'product', zh: '产品介绍', en: 'Product card', description: '深色画布与一张主图,适合产品亮点。' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remotion",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "DSH 视频插件:Remotion 技能、模板与素材工作台、官方 Studio 预览和本地 MP4 导出。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -61,7 +61,7 @@
61
61
  "fflate": "0.8.3",
62
62
  "proper-lockfile": "4.1.2",
63
63
  "yaml": "^2.9.0",
64
- "@stardustlc/dsh-updates": "0.1.1"
64
+ "@stardustlc/dsh-updates": "0.1.2"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^24.0.0",
@@ -6,7 +6,8 @@
6
6
  * The upstream skill bodies are the product content; only the frontmatter is
7
7
  * ours: an Agent Skills `name`/`description` plus the upstream `version`, with
8
8
  * every other upstream key (e.g. allowed-tools) dropped so DSH registration
9
- * stays on the documented keys.
9
+ * stays on the documented keys. A short DSH package context keeps standalone
10
+ * skill-upgrade commands from changing a different installation.
10
11
  *
11
12
  * Usage:
12
13
  * node scripts/sync-skills.mjs --upstream <path-to-remotion-dev/skills> [--check]
@@ -33,7 +34,7 @@ const portFrontmatter = (text, fallback) => {
33
34
  const name = /^name:\s*(.+)$/m.exec(match[1])?.[1]?.trim() ?? fallback.name
34
35
  const description = /^description:\s*(.+)$/m.exec(match[1])?.[1]?.trim() ?? fallback.description
35
36
  if (!version) throw new Error('Missing upstream version for ' + name)
36
- return ['---', 'name: ' + name, 'description: ' + description, 'version: ' + version, '---', '', text.slice(match[0].length).replace(/^\n+/, '')].join('\n')
37
+ return ['---', 'name: ' + name, 'description: ' + description, 'version: ' + version, '---', '', '## DSH package context', '', 'This skill is bundled with dsh-remotion. DSH plugin management owns updates to these skill files; do not run standalone `skills add`, `skills update`, or `remotion skills` commands to replace this bundle. Project dependencies and lockfiles are separate: preserve existing projects, and only upgrade their Remotion dependencies when the user requests it, then verify the project. Run commands in the user project, never in this installed plugin directory. The official instructions below apply within these package boundaries.', '', text.slice(match[0].length).replace(/^\n+/, '')].join('\n')
37
38
  }
38
39
 
39
40
  if (!values.check) rmSync(targetSkills, { recursive: true, force: true })