dsh-remotion 0.6.0 → 0.7.0

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 (282) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.en.md +2 -0
  3. package/README.md +2 -0
  4. package/docs/WORKBENCH.md +29 -0
  5. package/lib/client.js +402 -106
  6. package/package.json +4 -2
  7. package/scripts/build-client.mjs +19 -4
  8. package/skills/remotion-best-practices/SKILL.md +54 -54
  9. package/skills/remotion-best-practices/agents/openai.yaml +7 -7
  10. package/skills/remotion-best-practices/assets/remotion-icon.svg +4 -4
  11. package/skills/remotion-best-practices/remotion-captions/REFERENCE.md +36 -36
  12. package/skills/remotion-best-practices/remotion-captions/agents/openai.yaml +7 -7
  13. package/skills/remotion-best-practices/remotion-captions/assets/remotion-icon.svg +4 -4
  14. package/skills/remotion-best-practices/remotion-captions/display-captions.md +190 -190
  15. package/skills/remotion-best-practices/remotion-captions/import-srt-captions.md +73 -73
  16. package/skills/remotion-best-practices/remotion-captions/transcribe-captions.md +70 -70
  17. package/skills/remotion-best-practices/remotion-create/REFERENCE.md +85 -85
  18. package/skills/remotion-best-practices/remotion-create/agents/openai.yaml +7 -7
  19. package/skills/remotion-best-practices/remotion-create/assets/remotion-icon.svg +4 -4
  20. package/skills/remotion-best-practices/remotion-create/tailwind.md +11 -11
  21. package/skills/remotion-best-practices/remotion-create/video-layout.md +9 -9
  22. package/skills/remotion-best-practices/remotion-docs/REFERENCE.md +46 -46
  23. package/skills/remotion-best-practices/remotion-docs/agents/openai.yaml +7 -7
  24. package/skills/remotion-best-practices/remotion-docs/assets/remotion-icon.svg +4 -4
  25. package/skills/remotion-best-practices/remotion-interactivity/REFERENCE.md +315 -315
  26. package/skills/remotion-best-practices/remotion-interactivity/agents/openai.yaml +7 -7
  27. package/skills/remotion-best-practices/remotion-interactivity/assets/remotion-icon.svg +4 -4
  28. package/skills/remotion-best-practices/remotion-maps/REFERENCE.md +36 -36
  29. package/skills/remotion-best-practices/remotion-maps/agents/openai.yaml +7 -7
  30. package/skills/remotion-best-practices/remotion-maps/assets/remotion-icon.svg +4 -4
  31. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/TECHNIQUE.md +89 -89
  32. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/CesiumFlythrough.tsx +284 -284
  33. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/cesium-path.json +299 -299
  34. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/city-path.json +12 -12
  35. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/example-Root.tsx +39 -39
  36. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/flight-path.ts +35 -35
  37. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/sample-river.geojson +215 -215
  38. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/references/3d-data-sources.md +43 -43
  39. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/references/3d-flyover-architecture.md +128 -128
  40. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/references/3d-troubleshooting.md +57 -57
  41. package/skills/remotion-best-practices/remotion-maps/techniques/cesium/scripts/prep-cesium-path.mjs +161 -161
  42. package/skills/remotion-best-practices/remotion-maps/techniques/mapbox/TECHNIQUE.md +449 -449
  43. package/skills/remotion-best-practices/remotion-maps/techniques/mapbox/references/render-stability.md +64 -64
  44. package/skills/remotion-best-practices/remotion-maps/techniques/maplibre/TECHNIQUE.md +484 -484
  45. package/skills/remotion-best-practices/remotion-maps/techniques/maplibre/references/render-stability.md +64 -64
  46. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/TECHNIQUE.md +80 -80
  47. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/CountryLabel.tsx +70 -70
  48. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/MapTilerVectorElement.ts +80 -80
  49. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/RiverReveal.tsx +363 -363
  50. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/example-Root.tsx +22 -22
  51. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/sample-data/country-meta.json +7535 -7535
  52. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/sample-data/yarlung-flow.json +605 -605
  53. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/tokens.ts +34 -34
  54. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/references/map-data-sources.md +91 -91
  55. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/references/map-explainer-architecture.md +129 -129
  56. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/references/map-geo-prep.md +85 -85
  57. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/references/render-stability.md +64 -64
  58. package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/scripts/prep-geo.mjs +202 -202
  59. package/skills/remotion-best-practices/remotion-maps/techniques/static-map/TECHNIQUE.md +37 -37
  60. package/skills/remotion-best-practices/remotion-markup/3d.md +86 -86
  61. package/skills/remotion-best-practices/remotion-markup/REFERENCE.md +358 -358
  62. package/skills/remotion-best-practices/remotion-markup/agents/openai.yaml +7 -7
  63. package/skills/remotion-best-practices/remotion-markup/assets/remotion-icon.svg +4 -4
  64. package/skills/remotion-best-practices/remotion-markup/audio-visualization.md +198 -198
  65. package/skills/remotion-best-practices/remotion-markup/audio.md +169 -169
  66. package/skills/remotion-best-practices/remotion-markup/calculate-metadata.md +136 -136
  67. package/skills/remotion-best-practices/remotion-markup/compositions.md +123 -123
  68. package/skills/remotion-best-practices/remotion-markup/connected-compositions.md +79 -79
  69. package/skills/remotion-best-practices/remotion-markup/cropping.md +34 -34
  70. package/skills/remotion-best-practices/remotion-markup/effects.md +251 -251
  71. package/skills/remotion-best-practices/remotion-markup/embedding-videos.md +171 -171
  72. package/skills/remotion-best-practices/remotion-markup/ffmpeg.md +38 -38
  73. package/skills/remotion-best-practices/remotion-markup/gifs.md +187 -187
  74. package/skills/remotion-best-practices/remotion-markup/google-fonts.md +76 -76
  75. package/skills/remotion-best-practices/remotion-markup/html-in-canvas.md +128 -128
  76. package/skills/remotion-best-practices/remotion-markup/images.md +73 -73
  77. package/skills/remotion-best-practices/remotion-markup/light-leaks.md +106 -106
  78. package/skills/remotion-best-practices/remotion-markup/local-fonts.md +69 -69
  79. package/skills/remotion-best-practices/remotion-markup/lottie.md +73 -73
  80. package/skills/remotion-best-practices/remotion-markup/measuring-dom-nodes.md +38 -38
  81. package/skills/remotion-best-practices/remotion-markup/measuring-text.md +146 -146
  82. package/skills/remotion-best-practices/remotion-markup/motion-blur.md +60 -60
  83. package/skills/remotion-best-practices/remotion-markup/multi-scene-video.md +81 -81
  84. package/skills/remotion-best-practices/remotion-markup/parameters.md +111 -111
  85. package/skills/remotion-best-practices/remotion-markup/remotion-maps/REFERENCE.md +36 -36
  86. package/skills/remotion-best-practices/remotion-markup/remotion-maps/agents/openai.yaml +7 -7
  87. package/skills/remotion-best-practices/remotion-markup/remotion-maps/assets/remotion-icon.svg +4 -4
  88. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/TECHNIQUE.md +89 -89
  89. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/CesiumFlythrough.tsx +284 -284
  90. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/cesium-path.json +299 -299
  91. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/city-path.json +12 -12
  92. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/example-Root.tsx +39 -39
  93. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/flight-path.ts +35 -35
  94. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/sample-river.geojson +215 -215
  95. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/references/3d-data-sources.md +43 -43
  96. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/references/3d-flyover-architecture.md +128 -128
  97. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/references/3d-troubleshooting.md +57 -57
  98. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/scripts/prep-cesium-path.mjs +161 -161
  99. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/mapbox/TECHNIQUE.md +449 -449
  100. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/mapbox/references/render-stability.md +64 -64
  101. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maplibre/TECHNIQUE.md +484 -484
  102. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maplibre/references/render-stability.md +64 -64
  103. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/TECHNIQUE.md +80 -80
  104. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/CountryLabel.tsx +70 -70
  105. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/MapTilerVectorElement.ts +80 -80
  106. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/RiverReveal.tsx +363 -363
  107. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/example-Root.tsx +22 -22
  108. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/sample-data/country-meta.json +7535 -7535
  109. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/sample-data/yarlung-flow.json +605 -605
  110. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/tokens.ts +34 -34
  111. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/references/map-data-sources.md +91 -91
  112. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/references/map-explainer-architecture.md +129 -129
  113. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/references/map-geo-prep.md +85 -85
  114. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/references/render-stability.md +64 -64
  115. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/scripts/prep-geo.mjs +202 -202
  116. package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/static-map/TECHNIQUE.md +37 -37
  117. package/skills/remotion-best-practices/remotion-markup/sequencing.md +169 -169
  118. package/skills/remotion-best-practices/remotion-markup/sfx.md +51 -51
  119. package/skills/remotion-best-practices/remotion-markup/silence-detection.md +71 -71
  120. package/skills/remotion-best-practices/remotion-markup/text-highlights.md +56 -56
  121. package/skills/remotion-best-practices/remotion-markup/timing.md +111 -111
  122. package/skills/remotion-best-practices/remotion-markup/transitions.md +239 -239
  123. package/skills/remotion-best-practices/remotion-markup/video-editing.md +90 -90
  124. package/skills/remotion-best-practices/remotion-markup/voiceover.md +99 -99
  125. package/skills/remotion-best-practices/remotion-multimedia/REFERENCE.md +20 -20
  126. package/skills/remotion-best-practices/remotion-multimedia/agents/openai.yaml +7 -7
  127. package/skills/remotion-best-practices/remotion-multimedia/assets/remotion-icon.svg +4 -4
  128. package/skills/remotion-best-practices/remotion-multimedia/get-audio-duration.md +56 -56
  129. package/skills/remotion-best-practices/remotion-multimedia/get-video-dimensions.md +66 -66
  130. package/skills/remotion-best-practices/remotion-multimedia/get-video-duration.md +58 -58
  131. package/skills/remotion-best-practices/remotion-render/REFERENCE.md +35 -35
  132. package/skills/remotion-best-practices/remotion-render/agents/openai.yaml +7 -7
  133. package/skills/remotion-best-practices/remotion-render/assets/remotion-icon.svg +4 -4
  134. package/skills/remotion-best-practices/remotion-render/transparent-videos.md +106 -106
  135. package/skills/remotion-best-practices/remotion-saas/REFERENCE.md +33 -33
  136. package/skills/remotion-best-practices/remotion-saas/agents/openai.yaml +7 -7
  137. package/skills/remotion-best-practices/remotion-saas/assets/remotion-icon.svg +4 -4
  138. package/skills/remotion-best-practices/remotion-saas/framework.md +19 -19
  139. package/skills/remotion-best-practices/remotion-saas/player.md +33 -33
  140. package/skills/remotion-best-practices/remotion-saas/rendering.md +69 -69
  141. package/skills/remotion-best-practices/remotion-studio/REFERENCE.md +24 -24
  142. package/skills/remotion-best-practices/remotion-studio/agents/openai.yaml +7 -7
  143. package/skills/remotion-best-practices/remotion-studio/assets/remotion-icon.svg +4 -4
  144. package/skills/remotion-best-practices/remotion-upgrade/REFERENCE.md +31 -31
  145. package/skills/remotion-best-practices/remotion-upgrade/agents/openai.yaml +7 -7
  146. package/skills/remotion-best-practices/remotion-upgrade/assets/remotion-icon.svg +4 -4
  147. package/skills/remotion-captions/SKILL.md +31 -31
  148. package/skills/remotion-captions/agents/openai.yaml +7 -7
  149. package/skills/remotion-captions/assets/remotion-icon.svg +4 -4
  150. package/skills/remotion-captions/display-captions.md +190 -190
  151. package/skills/remotion-captions/import-srt-captions.md +73 -73
  152. package/skills/remotion-captions/transcribe-captions.md +70 -70
  153. package/skills/remotion-create/SKILL.md +80 -80
  154. package/skills/remotion-create/agents/openai.yaml +7 -7
  155. package/skills/remotion-create/assets/remotion-icon.svg +4 -4
  156. package/skills/remotion-create/tailwind.md +11 -11
  157. package/skills/remotion-create/video-layout.md +9 -9
  158. package/skills/remotion-docs/SKILL.md +41 -41
  159. package/skills/remotion-docs/agents/openai.yaml +7 -7
  160. package/skills/remotion-docs/assets/remotion-icon.svg +4 -4
  161. package/skills/remotion-interactivity/SKILL.md +310 -310
  162. package/skills/remotion-interactivity/agents/openai.yaml +7 -7
  163. package/skills/remotion-interactivity/assets/remotion-icon.svg +4 -4
  164. package/skills/remotion-maps/SKILL.md +31 -31
  165. package/skills/remotion-maps/agents/openai.yaml +7 -7
  166. package/skills/remotion-maps/assets/remotion-icon.svg +4 -4
  167. package/skills/remotion-maps/techniques/cesium/TECHNIQUE.md +89 -89
  168. package/skills/remotion-maps/techniques/cesium/assets/CesiumFlythrough.tsx +284 -284
  169. package/skills/remotion-maps/techniques/cesium/assets/cesium-path.json +299 -299
  170. package/skills/remotion-maps/techniques/cesium/assets/city-path.json +12 -12
  171. package/skills/remotion-maps/techniques/cesium/assets/example-Root.tsx +39 -39
  172. package/skills/remotion-maps/techniques/cesium/assets/flight-path.ts +35 -35
  173. package/skills/remotion-maps/techniques/cesium/assets/sample-river.geojson +215 -215
  174. package/skills/remotion-maps/techniques/cesium/references/3d-data-sources.md +43 -43
  175. package/skills/remotion-maps/techniques/cesium/references/3d-flyover-architecture.md +128 -128
  176. package/skills/remotion-maps/techniques/cesium/references/3d-troubleshooting.md +57 -57
  177. package/skills/remotion-maps/techniques/cesium/scripts/prep-cesium-path.mjs +161 -161
  178. package/skills/remotion-maps/techniques/mapbox/TECHNIQUE.md +449 -449
  179. package/skills/remotion-maps/techniques/mapbox/references/render-stability.md +64 -64
  180. package/skills/remotion-maps/techniques/maplibre/TECHNIQUE.md +484 -484
  181. package/skills/remotion-maps/techniques/maplibre/references/render-stability.md +64 -64
  182. package/skills/remotion-maps/techniques/maptiler/TECHNIQUE.md +80 -80
  183. package/skills/remotion-maps/techniques/maptiler/assets/CountryLabel.tsx +70 -70
  184. package/skills/remotion-maps/techniques/maptiler/assets/MapTilerVectorElement.ts +80 -80
  185. package/skills/remotion-maps/techniques/maptiler/assets/RiverReveal.tsx +363 -363
  186. package/skills/remotion-maps/techniques/maptiler/assets/example-Root.tsx +22 -22
  187. package/skills/remotion-maps/techniques/maptiler/assets/sample-data/country-meta.json +7535 -7535
  188. package/skills/remotion-maps/techniques/maptiler/assets/sample-data/yarlung-flow.json +605 -605
  189. package/skills/remotion-maps/techniques/maptiler/assets/tokens.ts +34 -34
  190. package/skills/remotion-maps/techniques/maptiler/references/map-data-sources.md +91 -91
  191. package/skills/remotion-maps/techniques/maptiler/references/map-explainer-architecture.md +129 -129
  192. package/skills/remotion-maps/techniques/maptiler/references/map-geo-prep.md +85 -85
  193. package/skills/remotion-maps/techniques/maptiler/references/render-stability.md +64 -64
  194. package/skills/remotion-maps/techniques/maptiler/scripts/prep-geo.mjs +202 -202
  195. package/skills/remotion-maps/techniques/static-map/TECHNIQUE.md +37 -37
  196. package/skills/remotion-markup/3d.md +86 -86
  197. package/skills/remotion-markup/SKILL.md +353 -353
  198. package/skills/remotion-markup/agents/openai.yaml +7 -7
  199. package/skills/remotion-markup/assets/remotion-icon.svg +4 -4
  200. package/skills/remotion-markup/audio-visualization.md +198 -198
  201. package/skills/remotion-markup/audio.md +169 -169
  202. package/skills/remotion-markup/calculate-metadata.md +136 -136
  203. package/skills/remotion-markup/compositions.md +123 -123
  204. package/skills/remotion-markup/connected-compositions.md +79 -79
  205. package/skills/remotion-markup/cropping.md +34 -34
  206. package/skills/remotion-markup/effects.md +251 -251
  207. package/skills/remotion-markup/embedding-videos.md +171 -171
  208. package/skills/remotion-markup/ffmpeg.md +38 -38
  209. package/skills/remotion-markup/gifs.md +187 -187
  210. package/skills/remotion-markup/google-fonts.md +76 -76
  211. package/skills/remotion-markup/html-in-canvas.md +128 -128
  212. package/skills/remotion-markup/images.md +73 -73
  213. package/skills/remotion-markup/light-leaks.md +106 -106
  214. package/skills/remotion-markup/local-fonts.md +69 -69
  215. package/skills/remotion-markup/lottie.md +73 -73
  216. package/skills/remotion-markup/measuring-dom-nodes.md +38 -38
  217. package/skills/remotion-markup/measuring-text.md +146 -146
  218. package/skills/remotion-markup/motion-blur.md +60 -60
  219. package/skills/remotion-markup/multi-scene-video.md +81 -81
  220. package/skills/remotion-markup/parameters.md +111 -111
  221. package/skills/remotion-markup/remotion-maps/REFERENCE.md +36 -36
  222. package/skills/remotion-markup/remotion-maps/agents/openai.yaml +7 -7
  223. package/skills/remotion-markup/remotion-maps/assets/remotion-icon.svg +4 -4
  224. package/skills/remotion-markup/remotion-maps/techniques/cesium/TECHNIQUE.md +89 -89
  225. package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/CesiumFlythrough.tsx +284 -284
  226. package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/cesium-path.json +299 -299
  227. package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/city-path.json +12 -12
  228. package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/example-Root.tsx +39 -39
  229. package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/flight-path.ts +35 -35
  230. package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/sample-river.geojson +215 -215
  231. package/skills/remotion-markup/remotion-maps/techniques/cesium/references/3d-data-sources.md +43 -43
  232. package/skills/remotion-markup/remotion-maps/techniques/cesium/references/3d-flyover-architecture.md +128 -128
  233. package/skills/remotion-markup/remotion-maps/techniques/cesium/references/3d-troubleshooting.md +57 -57
  234. package/skills/remotion-markup/remotion-maps/techniques/cesium/scripts/prep-cesium-path.mjs +161 -161
  235. package/skills/remotion-markup/remotion-maps/techniques/mapbox/TECHNIQUE.md +449 -449
  236. package/skills/remotion-markup/remotion-maps/techniques/mapbox/references/render-stability.md +64 -64
  237. package/skills/remotion-markup/remotion-maps/techniques/maplibre/TECHNIQUE.md +484 -484
  238. package/skills/remotion-markup/remotion-maps/techniques/maplibre/references/render-stability.md +64 -64
  239. package/skills/remotion-markup/remotion-maps/techniques/maptiler/TECHNIQUE.md +80 -80
  240. package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/CountryLabel.tsx +70 -70
  241. package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/MapTilerVectorElement.ts +80 -80
  242. package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/RiverReveal.tsx +363 -363
  243. package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/example-Root.tsx +22 -22
  244. package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/sample-data/country-meta.json +7535 -7535
  245. package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/sample-data/yarlung-flow.json +605 -605
  246. package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/tokens.ts +34 -34
  247. package/skills/remotion-markup/remotion-maps/techniques/maptiler/references/map-data-sources.md +91 -91
  248. package/skills/remotion-markup/remotion-maps/techniques/maptiler/references/map-explainer-architecture.md +129 -129
  249. package/skills/remotion-markup/remotion-maps/techniques/maptiler/references/map-geo-prep.md +85 -85
  250. package/skills/remotion-markup/remotion-maps/techniques/maptiler/references/render-stability.md +64 -64
  251. package/skills/remotion-markup/remotion-maps/techniques/maptiler/scripts/prep-geo.mjs +202 -202
  252. package/skills/remotion-markup/remotion-maps/techniques/static-map/TECHNIQUE.md +37 -37
  253. package/skills/remotion-markup/sequencing.md +169 -169
  254. package/skills/remotion-markup/sfx.md +51 -51
  255. package/skills/remotion-markup/silence-detection.md +71 -71
  256. package/skills/remotion-markup/text-highlights.md +56 -56
  257. package/skills/remotion-markup/timing.md +111 -111
  258. package/skills/remotion-markup/transitions.md +239 -239
  259. package/skills/remotion-markup/video-editing.md +90 -90
  260. package/skills/remotion-markup/voiceover.md +99 -99
  261. package/skills/remotion-multimedia/SKILL.md +15 -15
  262. package/skills/remotion-multimedia/agents/openai.yaml +7 -7
  263. package/skills/remotion-multimedia/assets/remotion-icon.svg +4 -4
  264. package/skills/remotion-multimedia/get-audio-duration.md +56 -56
  265. package/skills/remotion-multimedia/get-video-dimensions.md +66 -66
  266. package/skills/remotion-multimedia/get-video-duration.md +58 -58
  267. package/skills/remotion-render/SKILL.md +30 -30
  268. package/skills/remotion-render/agents/openai.yaml +7 -7
  269. package/skills/remotion-render/assets/remotion-icon.svg +4 -4
  270. package/skills/remotion-render/transparent-videos.md +106 -106
  271. package/skills/remotion-saas/SKILL.md +28 -28
  272. package/skills/remotion-saas/agents/openai.yaml +7 -7
  273. package/skills/remotion-saas/assets/remotion-icon.svg +4 -4
  274. package/skills/remotion-saas/framework.md +19 -19
  275. package/skills/remotion-saas/player.md +33 -33
  276. package/skills/remotion-saas/rendering.md +69 -69
  277. package/skills/remotion-studio/SKILL.md +19 -19
  278. package/skills/remotion-studio/agents/openai.yaml +7 -7
  279. package/skills/remotion-studio/assets/remotion-icon.svg +4 -4
  280. package/skills/remotion-upgrade/SKILL.md +26 -26
  281. package/skills/remotion-upgrade/agents/openai.yaml +7 -7
  282. package/skills/remotion-upgrade/assets/remotion-icon.svg +4 -4
package/lib/client.js CHANGED
@@ -1,111 +1,407 @@
1
- // Shared original browser source. build-client substitutes the engine identity.
2
- window.__ModuleLoader__.load({ id: 'dsh-remotion', factory: require => {
3
- const React = require('react'), { useState, useEffect, useRef } = React, h = React.createElement;
4
- const engine = 'remotion', title = engine === 'hyperframes' ? 'HyperFrames' : 'Remotion', route = 'api/dsh-' + engine + '/workbench';
5
-
6
- function mediaError(error, en) {
7
- const message = typeof error === 'string' ? error : error?.message || String(error || '');
8
- if (!en || !/[\u3400-\u9fff]/.test(message)) return message;
9
- if (error?.status === 401) return 'Your session expired. Reopen DSH and try again.';
10
- if (/先归档|已达 30/.test(message)) return 'The 30 active project slots are full. Archive another project before creating or restoring one.';
11
- if (/已有预览或渲染/.test(message)) return 'Stop the active preview or cancel the render before archiving or restoring a project.';
12
- if (error?.status === 409 || /已.*更新|另一窗口|修订/.test(message)) return 'The project changed. Reopen the latest version and compare it with your preserved input.';
13
- if (/标题/.test(message)) return 'Enter a title up to 72 characters.';
14
- if (/时长/.test(message)) return 'Set a duration between 3 and 30 seconds.';
15
- if (/正文/.test(message)) return 'Use body text up to 360 characters.';
16
- if (/素材|PNG|JPEG|MP3|WAV|MP4/.test(message)) return 'Check the media format and file contents. Each asset must be ≤20 MB; a project supports 12 assets and 40 MB total, with one audio and one video background.';
17
- if (/任务|官方|FFmpeg|Chrome|环境/.test(message)) return 'The official task did not finish. Open the job log and check the render environment, then retry. Your project is preserved.';
18
- if (/工程记录|项目目录|文件/.test(message)) return 'The project files could not be read safely. Back them up and check their location and permissions before retrying.';
19
- return 'The operation failed. Check the DSH logs and reload the project before retrying; your input is preserved.';
1
+ // Generated from templates/client.js, templates/panels/ and templates/workbench-ui.js. Edit the sources.
2
+ (function () {
3
+ /* DSH workbench UI v1. MIT. Canonical source: dsh-suite/ui/workbench-ui.js.
4
+ * Vendored into each plugin: no Suite or extra runtime dependency is required.
5
+ * Styles are scoped to .dshw and never replace the host chrome or theme.
6
+ */
7
+ function createWorkbenchUI(React) {
8
+ const h = React.createElement;
9
+ const { useEffect, useId, useRef } = React;
10
+ const cx = (...values) => values.filter(Boolean).join(' ');
11
+ const paths = {
12
+ book: ['M4 4h6a3 3 0 0 1 2 2v15a4 4 0 0 0-4-2H4z', 'M20 4h-6a3 3 0 0 0-2 2v15a4 4 0 0 1 4-2h4z'],
13
+ calendar: ['M5 5h14a2 2 0 0 1 2 2v13H3V7a2 2 0 0 1 2-2Z', 'M7 3v4M17 3v4M3 10h18'],
14
+ mail: ['M3 5h18v14H3z', 'm3 6 9 7 9-7'],
15
+ rss: ['M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16', 'M4 19h.01'],
16
+ slides: ['M3 4h18v13H3z', 'M12 17v4M8 21h8'],
17
+ shield: ['m12 3 8 3v6c0 4-4 7-8 9-4-2-8-5-8-9V6z', 'm8 12 3 3 5-6'],
18
+ layers: ['M5 3h16v16H5z', 'M2 7v15h15'],
19
+ play: ['m9 6 9 6-9 6Z'],
20
+ video: ['M3 5h12v14H3z', 'm15 10 6-4v12l-6-4'],
21
+ search: ['M10.5 18a7.5 7.5 0 1 0 0-15 7.5 7.5 0 0 0 0 15Z', 'm16 16 5 5'],
22
+ plus: ['M12 5v14M5 12h14'],
23
+ refresh: ['M20 7a9 9 0 1 0 1 8', 'M20 2v5h-5'],
24
+ settings: ['m9 3-.7 2.4-2 .9L4 5.8 2.5 8.4l1.7 1.8v2.3L2.5 14 4 17l2.5-.4 1.9 1L9 20h3l.8-2.4 2-1 2.5.4 1.5-2.7-1.7-1.9v-2.2l1.7-1.8-1.5-2.6-2.5.4-2-1L12 3Z', 'M14 11.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z'],
25
+ close: ['m6 6 12 12M18 6 6 18'],
26
+ left: ['m14 5-7 7 7 7'],
27
+ right: ['m10 5 7 7-7 7'],
28
+ down: ['m5 9 7 7 7-7'],
29
+ filter: ['M4 5h16M7 12h10M10 19h4'],
30
+ download: ['M12 3v12m-5-5 5 5 5-5', 'M4 16v5h16v-5'],
31
+ upload: ['M12 16V4m-5 5 5-5 5 5', 'M4 16v5h16v-5'],
32
+ copy: ['M8 8h13v13H8z', 'M16 8V3H3v13h5'],
33
+ external: ['M14 3h7v7m0-7L10 14', 'M10 3H3v18h18v-7'],
34
+ file: ['M4 3h10l6 6v12H4z', 'M14 3v6h6M8 13h8M8 17h6'],
35
+ folder: ['M3 5h7l2 3h9v12H3z'],
36
+ edit: ['m15 4 5 5-10 10-6 1 1-6Z', 'm13 6 5 5'],
37
+ trash: ['M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7'],
38
+ more: ['M5 12h.01M12 12h.01M19 12h.01'],
39
+ info: ['M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z', 'M12 11v6M12 7h.01'],
40
+ check: ['m5 12 4 4L19 6'],
41
+ quote: ['M4 12h6v7H3v-7c0-4 2-6 5-7M15 12h6v7h-7v-7c0-4 2-6 5-7'],
42
+ star: ['m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-3-5.6 3 1.1-6.2L3 9.6l6.2-.9Z'],
43
+ reply: ['m9 5-7 7 7 7v-5h6c3 0 5 2 6 5 0-8-4-11-12-11Z'],
44
+ link: ['m10 13 4-4M8 16l-2 2a4 4 0 0 1-6-6l4-4a4 4 0 0 1 6 0', 'm14 8 2-2a4 4 0 0 1 6 6l-4 4a4 4 0 0 1-6 0'],
45
+ };
46
+ function Icon({ name = 'file', size = 20, ...props }) {
47
+ return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.75, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, focusable: false, ...props },
48
+ (paths[name] || paths.file).map((d, index) => h('path', { key: index, d })));
20
49
  }
21
-
22
- let cache = { project: null, fields: null, dirty: false, job: null, creating: false };
23
- const empty = () => ({ template: 'title', title: '', body: '', format: 'landscape', duration: 8 });
24
- const fieldsOf = p => ({ template: p.template, title: p.title, body: p.body, format: p.format, duration: p.duration });
25
- 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}}`;
26
- async function api(body) {
27
- 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) });
28
- let data; try { data = await response.json() } catch { throw new Error('无法读取插件响应,请检查是否已经重启 DSH。') }
29
- if (!response.ok || !data.ok) { const error = new Error(data.message || '操作未完成'); error.status = response.status; throw error; } return data.value;
50
+ function Button({ children, icon, variant, className, ...props }) {
51
+ return h('button', { type: 'button', ...props, className: cx('w-button', variant && 'w-' + variant, className) }, icon ? h(Icon, { name: icon, size: 18 }) : null, children);
52
+ }
53
+ function IconButton({ label, icon, ...props }) {
54
+ return h(Button, { ...props, icon, className: cx('w-icon-button', props.className), 'aria-label': label, title: label });
55
+ }
56
+ function Page({ title, subtitle, actions, children, workbench = false, className, rootRef, ...props }) {
57
+ return h('section', { ...props, ref: rootRef, className: cx('dshw', 'w-page', className), 'data-main': workbench ? 'true' : 'false' },
58
+ h('style', null, styles),
59
+ 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),
60
+ h('div', { className: 'w-body' }, children));
30
61
  }
31
- function urlFor(project, kind, extra = {}) { const url = new URL(route, document.baseURI); url.search = new URLSearchParams({ id: project.id, kind, ...extra }); return url.href; }
32
- function Workbench({ locale }) {
33
- const [lang, setLang] = useState('zh'), [projects, setProjects] = useState([]), [archived, setArchived] = useState([]), [archiveReview, setArchiveReview] = useState(false), [environment, setEnvironment] = useState(null), [templates, setTemplates] = useState([]);
34
- const [project, setProject] = useState(cache.project), [fields, setFields] = useState(cache.fields || empty()), [dirty, setDirty] = useState(cache.dirty), [creating, setCreating] = useState(cache.creating);
35
- const [job, setJob] = useState(cache.job), [busy, setBusy] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState(''), [pending, setPending] = useState(null), [conflict, setConflict] = useState(false);
36
- const busyRef = useRef(false), fileRef = useRef(null), latest = useRef({ project, fields, dirty }); latest.current = { project, fields, dirty };
37
- const t = (zh, en) => lang === 'en' ? en : zh, activeJob = job?.state === 'running', running = busy || activeJob && job.kind !== 'prepare';
38
- const stage = lang === 'en' ? ({ '下载环境': 'Preparing environment', '检查 FFmpeg': 'Checking FFmpeg', '下载官方依赖': 'Downloading official dependencies', '检查官方环境': 'Checking official CLI', '检查或下载 Chrome': 'Checking or downloading Chrome', '下载专用渲染浏览器': 'Downloading the official rendering browser', '准备修订快照': 'Preparing saved revision', '启动官方预览': 'Starting official preview', '官方预览已就绪': 'Official preview is ready', '检查合成': 'Checking composition', '渲染 MP4': 'Rendering MP4', '已完成': 'Completed', '已取消': 'Cancelled', '已取消;工程已保留': 'Cancelled; project preserved', '官方任务未完成,请展开运行记录后重试。工程已保留。': 'The official task failed. Open the job log and retry; your project is preserved.', '未找到 FFmpeg,请安装并加入 PATH,重启 DSH 后重试。工程已保留。': 'FFmpeg was not found. Install it, add it to PATH, then restart DSH and retry; your project is preserved.' }[job?.stage] || mediaError(job?.stage, true)) : job?.stage;
39
- useEffect(() => { const update = () => setLang(String(locale?.getSnapshot?.().active || '').startsWith('en') ? 'en' : 'zh'); update(); return locale?.subscribe?.(update); }, [locale]);
40
- useEffect(() => { cache = { project, fields, dirty, job, creating }; }, [project, fields, dirty, job, creating]);
41
- useEffect(() => { const exit = e => { if (latest.current.dirty) { e.preventDefault(); e.returnValue = ''; } }; window.addEventListener('beforeunload', exit); return () => window.removeEventListener('beforeunload', exit); }, []);
42
- async function refresh() {
43
- const data = await api({ action: 'list' }); setProjects(data.projects); setArchived(data.archivedProjects || []); setEnvironment(data.environment); setTemplates(data.templates);
44
- const current = latest.current, saved = !current.dirty && data.projects.find(row => row.id === current.project?.id);
45
- if (saved) { setProject(saved); setFields(fieldsOf(saved)); }
46
- setJob(previous => data.jobs.find(row => row.id === previous?.id) || data.jobs.filter(row => row.state === 'running').at(-1) || data.jobs.at(-1) || null);
62
+ function Tabs({ items, value, onChange, label, children }) {
63
+ const id = useId();
64
+ const active = items.some(item => item.id === value) ? value : items[0]?.id;
65
+ function keys(event, index) {
66
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
67
+ event.preventDefault();
68
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + items.length) % items.length;
69
+ const node = event.currentTarget.parentElement?.querySelectorAll('[role=tab]')[next];
70
+ if (node) { onChange(items[next].id); node.focus(); }
47
71
  }
48
- useEffect(() => { refresh().catch(e => setError(e)); }, []);
72
+ return h('div', { className: 'w-tabs-layout' },
73
+ h('div', { className: 'w-tabs', role: 'tablist', 'aria-label': label }, items.map((item, index) => h('button', {
74
+ type: 'button', role: 'tab', key: item.id, id: id + '-' + item.id, 'aria-controls': id + '-panel', 'aria-selected': active === item.id, tabIndex: active === item.id ? 0 : -1,
75
+ onClick: () => onChange(item.id), onKeyDown: event => keys(event, index),
76
+ }, item.label, item.count !== undefined ? h('span', { className: 'w-count' }, item.count) : null))),
77
+ h('div', { className: 'w-tab-panel', id: id + '-panel', role: 'tabpanel', 'aria-labelledby': id + '-' + active }, children));
78
+ }
79
+ function Status({ children, tone = 'info', className }) {
80
+ if (!children) return null;
81
+ 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));
82
+ }
83
+ function Empty({ title, children, action, icon = 'file' }) {
84
+ 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);
85
+ }
86
+ function Split({ list, detail, selected, onBack, backLabel, className, listWidth = 360 }) {
87
+ const ref = useRef(null);
88
+ useEffect(() => {
89
+ if (!selected || !ref.current || ref.current.clientWidth > 760) return;
90
+ const heading = ref.current.querySelector('.w-detail h2');
91
+ if (heading) { heading.tabIndex = -1; heading.focus({ preventScroll: true }); }
92
+ }, [selected]);
93
+ return h('div', { ref, className: cx('w-split', className), 'data-selected': selected ? 'true' : 'false', style: { '--w-list-width': listWidth + 'px' } },
94
+ h('div', { className: 'w-master' }, list),
95
+ h('div', { className: 'w-detail' }, selected ? h(Button, { className: 'w-back', icon: 'left', variant: 'ghost', onClick: onBack }, backLabel) : null, detail));
96
+ }
97
+ function Row({ title, meta, description, active, onClick, children, status, className, disabled }) {
98
+ return h('div', { className: cx('w-row', active && 'w-selected', className) },
99
+ children ? h('div', { className: 'w-row-select' }, children) : null,
100
+ h('button', { type: 'button', className: 'w-row-button', onClick, disabled, 'aria-pressed': !!active },
101
+ h('span', { className: 'w-row-head' }, h('strong', null, title), status ? h('span', { className: 'w-tag' }, status) : null),
102
+ meta ? h('span', { className: 'w-row-meta' }, meta) : null,
103
+ description ? h('span', { className: 'w-row-description' }, description) : null));
104
+ }
105
+ function Search({ label, value, onChange, ...props }) {
106
+ const id = useId();
107
+ return h('div', { className: 'w-search' }, h(Icon, { name: 'search', size: 18 }), h('input', { type: 'search', id, 'aria-label': label, placeholder: label, value, onChange, ...props }));
108
+ }
109
+ function Switch({ label, checked, onChange, disabled }) {
110
+ return h('label', { className: 'w-switch-label' }, h('span', null, label), h('input', { type: 'checkbox', role: 'switch', checked, disabled, onChange, 'aria-label': label }), h('span', { className: 'w-switch-track', 'aria-hidden': true }));
111
+ }
112
+ function Dialog({ title, children, onClose, closeLabel, className, busy = false }) {
113
+ const id = useId(), panel = useRef(null);
49
114
  useEffect(() => {
50
- if (!job || job.state !== 'running') return;
51
- let live = true, polling = false;
52
- const timer = setInterval(async () => {
53
- if (polling) return; polling = true;
54
- try {
55
- const next = await api({ action: 'job', job: job.id }); if (!live) return;
56
- // Refresh output before the terminal state tears down this polling effect.
57
- if (next.state !== 'running') await refresh();
58
- else setJob(next);
59
- } catch (e) { if (live) { setError(e); setJob(null); } } finally { polling = false; }
60
- }, 1000);
61
- return () => { live = false; clearInterval(timer); };
62
- }, [job?.id, job?.state]);
63
- async function perform(fn) { if (busyRef.current) return; busyRef.current = true; setBusy(true); setError(''); setNotice(''); try { return await fn(); } catch (e) { setError(e); setConflict(e.status === 409); } finally { busyRef.current = false; setBusy(false); } }
64
- async function open(id) { const next = await api({ action: 'get', id }); setProject(next); setFields(fieldsOf(next)); setCreating(false); setDirty(false); setConflict(false); setPending(null); setArchiveReview(false); }
65
- function navigate(target) { if (dirty) { setPending(target); return; } perform(() => move(target)); }
66
- async function move(target) { if (target === 'new') { setProject(null); setFields(empty()); setCreating(true); } else if (target === 'list') { setProject(null); setCreating(false); await refresh(); } else await open(target); setDirty(false); setPending(null); }
67
- async function save() { const next = await api(project ? { action: 'update', id: project.id, revision: project.revision, fields } : { action: 'create', fields }); setProject(next); setFields(fieldsOf(next)); setCreating(false); setDirty(false); setConflict(false); setNotice(t('工程已保存。', 'Project saved.')); await refresh(); return next; }
68
- async function upload(event) { const file = event.target.files?.[0]; if (!file) return; await perform(async () => {
69
- if (!project || dirty) throw new Error(t('请先保存工程,再上传素材。', 'Save your project before uploading media.'));
70
- if (file.size > 20 * 1024 * 1024) throw new Error(t('单个素材不能超过 20 MB。', 'Each asset must be at most 20 MB.'));
71
- const response = await fetch(new URL(route + '/upload', document.baseURI), { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/octet-stream', 'x-dsh-media': '1', 'x-project-id': project.id, 'x-project-revision': String(project.revision), 'x-file-name': encodeURIComponent(file.name) }, body: file }); const result = await response.json(); if (!response.ok || !result.ok) { const e = new Error(result.message); e.status = response.status; throw e; } setProject(result.value); setNotice(t('素材副本已保存,原文件未改变。', 'A media copy was saved; your original is unchanged.'));
72
- }); event.target.value = ''; }
73
- async function start(action) { await perform(async () => { if (action !== 'prepare' && dirty) throw new Error(t('请先保存修改。', 'Save changes first.')); setJob(await api({ action, id: project?.id, revision: project?.revision })); }); }
74
- 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.')); }
75
- const render = project?.renders?.[0], stale = render && render.revision !== project.revision;
76
- if (!environment) return h('div', { className: 'dmw' }, h('style', null, css), h('h2', null, title + t(' 视频工作台', ' video workbench')),
77
- error ? h(React.Fragment, null, h('p', { className: 'notice error', role: 'alert' }, mediaError(error, lang === 'en')), h('button', { disabled: busy, onClick: () => perform(refresh) }, t('重新读取项目', 'Reload projects'))) : h('p', { role: 'status' }, t('正在读取工程与渲染环境…', 'Loading projects and render environment…')));
78
- return h('div', { className: 'dmw' }, h('style', null, css), h('h2', null, title + t(' 视频工作台', ' video workbench')),
79
- h('p', { className: 'muted' }, t('模板 → 素材 → 官方预览 → 本机 MP4。工程与素材保存在 DSH 本地数据目录。', 'Template → media → official preview → local MP4. Projects stay in the local DSH data folder.')),
80
- error && h('div', { className: 'notice error', role: 'alert' }, mediaError(error, lang === 'en')), notice && h('div', { className: 'notice', role: 'status' }, notice),
81
- archived.length > 0 && h('details', { className: 'card' }, h('summary', null, t('已归档工程', 'Archived projects') + ' (' + archived.length + ')'), h('p', {className:'muted'}, t('归档保留素材与成品,不占当前 30 个工程名额。恢复后可以继续编辑。','Archives retain media and output, freeing an active project slot. Restore to edit again.')), archived.map(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'))))),
82
- project && !creating && h('div',{className:'bar'},h('button',{disabled:busy||dirty||activeJob,onClick:()=>setArchiveReview(true)},t('归档此工程','Archive this project'))),
83
- project && archiveReview && h('div',{className:'card',role:'alert'},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.')),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')))),
84
- pending && h('div', { className: 'card' }, h('p', null, t('有未保存内容,如何继续?', 'You have unsaved changes.')),
85
- 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')))),
86
- conflict && project && h('button', { disabled: running, onClick: () => { setPending(project.id); } }, t('重新打开最新版本(先处理当前输入)', 'Reopen latest version (review your current input first)')),
87
- h('details', { className: 'card', open: !environment?.ready }, h('summary', null, t('渲染环境', 'Render environment') + (environment?.ready ? t(' · 已准备', ' · ready') : t(' · 首次需下载', ' · first download required'))),
88
- 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.'))), engine === 'remotion' && h('p', null, h('a', { href: 'https://www.remotion.dev/license', target: '_blank', rel: 'noreferrer' }, t('查看 Remotion 使用许可证', 'Read the Remotion license'))),
89
- h('button', { disabled: busy || activeJob, onClick: () => start('prepare') }, environment?.ready ? t('重新检查 / 修复环境', 'Check / repair environment') : t('准备渲染环境', 'Prepare render environment'))),
90
- job && h('div', { className: job.state === 'failed' ? 'card notice error' : 'card', role: job.state === 'failed' ? 'alert' : 'status' }, h('strong', null, t('任务状态:', 'Job status: ') + stage), job.url && h('p', null, h('a', { className: 'button', href: job.url, target: '_blank', rel: 'noreferrer' }, t('打开官方 Studio 预览', 'Open official Studio preview'))),
91
- job.state === 'running' && h('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')),
92
- h('details', null, h('summary', null, t('查看运行记录', 'Show job log')), h('pre', null, job.lines.join('\n')))),
93
- !project && !creating ? h(React.Fragment, null, h('div', { className: 'bar' }, h('button', { className: 'primary', disabled: running, onClick: () => navigate('new') }, t('新建视频', 'New video')), h('button', { disabled: running, onClick: () => perform(refresh) }, t('刷新项目', 'Refresh projects'))),
94
- projects.length ? h('div', { className: 'projects' }, projects.map(p => h('button', { key: p.id, disabled: running, onClick: () => navigate(p.id) }, h('strong', null, p.title), h('div', { className: 'muted' }, p.duration + t(' 秒 · 修订 ', ' sec · revision ') + p.revision)))) : h('div', { className: 'card' }, h('h3', null, t('还没有视频工程', 'No video projects yet')), h('p', null, t('点击“新建视频”,先选模板并保存。环境准备可以稍后进行。', 'Choose New video, select a template and save. You can prepare the render environment later.')))) :
95
- h(React.Fragment, null, h('div', { className: 'bar' }, h('button', { disabled: running, onClick: () => navigate('list') }, t('返回项目', 'Back to projects')), project && h('span', { className: 'muted' }, t('修订 ', 'Revision ') + project.revision), dirty && h('span', null, t('有未保存修改', 'Unsaved changes'))),
96
- h('div', { className: 'card' }, h('h3', null, t('1. 模板与内容', '1. Template and content')),
97
- h('label', { htmlFor: engine + '-template' }, t('模板', 'Template')), h('select', { id: engine + '-template', disabled: running, value: fields.template, onChange: e => { setFields({ ...fields, template: e.target.value }); setDirty(true); } }, templates.map(row => h('option', { key: row.id, value: row.id }, lang === 'en' ? row.en : row.zh))),
98
- h('label', { htmlFor: engine + '-title' }, t('标题(最多 72 字符)', 'Title (up to 72 characters)')), h('input', { id: engine + '-title', value: fields.title, disabled: running, maxLength: 72, onChange: e => { setFields({ ...fields, title: e.target.value }); setDirty(true); } }),
99
- h('label', { htmlFor: engine + '-body' }, t('正文(轮播时每行对应一页)', 'Body (one line per slideshow page)')), h('textarea', { id: engine + '-body', value: fields.body, disabled: running, maxLength: 360, rows: 4, onChange: e => { setFields({ ...fields, body: e.target.value }); setDirty(true); } }),
100
- h('div', { className: 'row' }, h('div', null, h('label', { htmlFor: engine + '-format' }, t('画幅', 'Canvas')), h('select', { id: engine + '-format', value: fields.format, disabled: running, onChange: e => { setFields({ ...fields, format: e.target.value }); setDirty(true); } }, ['landscape','portrait','square'].map((key, i) => h('option', { key, value: key }, [t('横屏 16:9', 'Landscape 16:9'),t('竖屏 9:16', 'Portrait 9:16'),t('正方形', 'Square')][i])))), h('div', null, h('label', { htmlFor: engine + '-duration' }, t('时长(3–30 秒)', 'Duration (3–30 seconds)')), h('input', { id: engine + '-duration', type: 'number', min: 3, max: 30, value: fields.duration, disabled: running, onChange: e => { setFields({ ...fields, duration: Number(e.target.value) }); setDirty(true); } }))),
101
- h('div', { className: 'bar' }, h('button', { className: 'primary', disabled: running || project && !dirty, onClick: () => perform(save) }, t('保存工程', 'Save project')), project && h('button', { disabled: running || !dirty, onClick: () => { setFields(fieldsOf(project)); setDirty(false); setError(''); } }, t('取消修改', 'Discard changes')))),
102
- project && h('div', { className: 'card' }, h('h3', null, t('2. 素材', '2. Media')), h('p', { className: 'muted' }, t('PNG/JPEG 图片、一个背景音频和一个静音背景视频;每个 ≤20 MB,总共 ≤40 MB。轮播按上传顺序,每张至少两秒。', 'PNG/JPEG images, one background audio and one muted background video; each ≤20 MB, total ≤40 MB. Slideshow follows upload order, at least two seconds per image.')),
103
- h('label', { htmlFor: engine + '-asset' }, t('上传素材副本', 'Upload a media copy')), h('input', { ref: fileRef, id: engine + '-asset', type: 'file', accept: '.png,.jpg,.jpeg,.mp3,.wav,.mp4', disabled: running || dirty, onChange: upload }),
104
- h('div', { className: 'assets' }, project.assets.map(a => h('div', { className: 'asset', key: a.id }, a.kind === 'image' && h('img', { src: urlFor(project, 'asset', { asset: a.id }), alt: a.name }), h('span', null, a.name + ' · ' + Math.ceil(a.bytes / 1024) + ' KB'), h('button', { disabled: running || dirty, onClick: () => perform(async () => { setProject(await api({ action: 'removeAsset', id: project.id, revision: project.revision, asset: a.id })); }) }, t('移除上传副本', 'Remove uploaded copy')))))),
105
- project && h('div', { className: 'card' }, h('h3', null, t('3. 预览与成品', '3. Preview and output')), h('p', { className: 'muted' }, t('保存后在官方 Studio 检查画面。关闭本次预览后再编辑或导出。MP4 按已保存修订生成;渲染不会占用前台窗口。', 'Check the saved project in official Studio. Stop this preview before editing or exporting. MP4 uses the saved revision and renders in the background.')),
106
- h('div', { className: 'bar' }, h('button', { disabled: running || dirty || !environment?.ready, onClick: () => start('preview') }, t('启动官方预览', 'Start official preview')), h('button', { className: 'primary', disabled: running || dirty || !environment?.ready, onClick: () => start('render') }, t('导出 MP4', 'Export MP4')), h('a', { className: 'button', href: urlFor(project, 'source'), download: '' }, t('备份可编辑工程', 'Back up editable project'))),
107
- render ? h(React.Fragment, null, h('p', { className: stale ? 'notice error' : 'muted' }, t('此 MP4 来自修订 ', 'This MP4 uses revision ') + render.revision + (stale ? t(',工程已有更新,请重新导出。', '; your project has newer changes. Export again.') : t(',请播放检查最终画面。', '. Play it to check the final output.'))), h('video', { controls: true, preload: 'metadata', src: urlFor(project, 'mp4', { render: render.id }) }), h('p', null, h('a', { className: 'button', href: urlFor(project, 'mp4', { render: render.id, download: '1' }), download: '' }, t('下载这份 MP4', 'Download this MP4')))) : h('p', { className: 'muted' }, t('尚无 MP4 成品。预览成功后点击“导出 MP4”。', 'No MP4 yet. Check the preview, then choose Export MP4.')))));
115
+ const previous = document.activeElement;
116
+ panel.current?.querySelector('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]')?.focus();
117
+ return () => { if (previous?.isConnected) previous.focus({ preventScroll: true }); };
118
+ }, []);
119
+ function keydown(event) {
120
+ if (event.key === 'Escape' && !event.isComposing) { event.preventDefault(); event.stopPropagation(); if (!busy) onClose(); return; }
121
+ if (event.key !== 'Tab') return;
122
+ const controls = [...panel.current.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), a[href], [tabindex="0"]')].filter(node => node.getClientRects().length);
123
+ const first = controls[0], last = controls.at(-1);
124
+ if (!controls.length) { event.preventDefault(); panel.current.focus(); }
125
+ else if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
126
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
127
+ }
128
+ return h('div', { className: 'w-overlay', onClick: event => { if (!busy && event.target === event.currentTarget) onClose(); } },
129
+ h('section', { ref: panel, className: cx('w-dialog', className), role: 'dialog', 'aria-modal': true, 'aria-labelledby': id, tabIndex: -1, onKeyDown: keydown },
130
+ 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)));
108
131
  }
109
- function apply(ctx) { let locale; try { locale = ctx.get?.('locale') || ctx.locale; } catch {} ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-' + engine, order: engine === 'hyperframes' ? 62 : 63, label: () => title, inject: () => ({ locale }) }, Workbench)); }
110
- return { inject: ['slots','locale'], apply };
111
- } });
132
+ function register(ctx, { id, label, icon, component, inject, order = 50 }) {
133
+ // The two slots are optional. Existing settings and manual tools still load
134
+ // in hosts without a workbench, while RC.3 owns navigation/pinning itself.
135
+ ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: id, inject: () => ({ ...(inject?.() || {}), workbench: true }) }, component));
136
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', id, order, label }, function WorkbenchIcon({ size }) { return h(Icon, { name: icon, size }); }));
137
+ }
138
+ const styles = `
139
+ .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}
140
+ .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%}
141
+ .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)}
142
+ .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}
143
+ .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}
144
+ .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}
145
+ .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%}
146
+ @container dsh-workbench (max-width:1000px){.w-editor-grid{grid-template-columns:160px minmax(0,1fr)}.w-editor-inspector{grid-column:1/-1;border-inline-start:0;border-top:1px solid var(--w-border)}.w-header-actions{max-width:60%}.w-split{grid-template-columns:minmax(240px,32%) minmax(0,1fr)}}
147
+ @container dsh-workbench (max-width:760px){.w-page-header{gap:14px;flex-wrap:wrap;padding-bottom:18px}.w-heading{flex:1;min-width:170px}.dshw h1{font-size:24px}.w-header-actions{max-width:100%;justify-content:flex-start}.w-subtitle{font-size:13px}.w-tabs{gap:12px}.dshw .w-tabs>button{padding:10px 4px 12px}.w-split{display:block;min-height:360px}.w-split[data-selected=false]>.w-detail{display:none}.w-split[data-selected=true]>.w-master{display:none}.w-master{border-inline-end:0}.w-detail{padding:20px 0}.w-back{display:inline-flex!important;align-self:flex-start;margin:0 0 16px!important}.w-detail h2{font-size:22px}.w-master-toolbar{padding:16px 0}.dshw .w-row-button{padding:16px 10px}.w-detail-footer{position:static}.w-detail-footer .w-muted{flex-basis:100%}.w-editor-grid{display:flex;flex-direction:column}.w-editor-list{border-inline-end:0;border-bottom:1px solid var(--w-border);padding:16px 0}.w-editor-canvas{padding:16px 0;background:var(--w-bg)}.w-editor-inspector{padding:20px 0}.w-dialog-header,.w-dialog-body{padding:16px}.w-button{min-height:40px}.w-definition{grid-template-columns:80px minmax(0,1fr)}}
148
+ @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%}}
149
+ @media(prefers-reduced-motion:reduce){.dshw *,.dshw *:before{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
150
+ `;
151
+ return { Icon, Button, IconButton, Page, Tabs, Status, Empty, Split, Row, Search, Switch, Dialog, register, styles };
152
+ }
153
+
154
+ // Editable browser entry. Build with node scripts/build-client.mjs.
155
+ // Shared original browser source. build-client substitutes the engine identity.
156
+ window.__ModuleLoader__.load({ id: 'dsh-remotion', factory: require => {
157
+ const React = require('react'), { useState, useEffect, useRef } = React, h = React.createElement;
158
+ const W = createWorkbenchUI(React);
159
+ function MediaCanvas({ project, fields, render, dirty, stale, job, running, ready, busy, start, perform, setNotice, t }) {
160
+ const previewing = job?.kind === 'preview' && job.state === 'running';
161
+ const ratio = fields.format === 'portrait' ? '9 / 16' : fields.format === 'square' ? '1 / 1' : '16 / 9';
162
+ return h('div', { className: 'media-preview-workspace' },
163
+ h('div', { className: 'w-toolbar' }, h(W.Button, { icon: 'play', disabled: !project || running || dirty || !ready, title: !ready ? t('先在“环境”页准备渲染工具。', 'Prepare the renderer in Environment first.') : undefined, onClick: () => start('preview') }, t('启动 Studio 预览', 'Start Studio preview')),
164
+ h(W.Button, { disabled: busy || !previewing, onClick: () => perform(async () => { await api({ action: 'cancel', job: job.id }); setNotice(t('已请求停止;正在关闭本次任务。', 'Stopping this job.')); }) }, t('停止预览', 'Stop preview')),
165
+ previewing && job.url ? h('a', { className: 'w-button', href: job.url, target: '_blank', rel: 'noopener noreferrer' }, t('打开 Studio', 'Open Studio')) : null),
166
+ render ? h('div', { className: 'media-player-wrap' }, h('video', { key: render.id, controls: true, preload: 'metadata', src: urlFor(project, 'mp4', { render: render.id }), 'aria-label': t('已导出视频', 'Exported video') })) :
167
+ h('div', { className: 'w-preview-empty media-empty-preview', style: { aspectRatio: ratio, width: fields.format === 'portrait' ? 'min(100%, 240px)' : fields.format === 'square' ? 'min(100%, 440px)' : 'min(100%, 960px)', maxHeight: 'none', minHeight: '0px', alignSelf: 'center' } }, h(W.Icon, { name: 'video', size: 32 }), h('strong', null, t('导出 MP4 后可在这里播放', 'Play the MP4 here after exporting')), h('p', { className: 'w-muted' }, t('Studio 预览会在单独页面打开。', 'Studio preview opens in a separate page.'))),
168
+ h('p', { className: 'w-muted media-render-caption' }, render ? dirty ? t('此 MP4 反映已保存修订;当前编辑尚未保存。', 'This MP4 reflects a saved revision. Current edits are not saved.') : t('此 MP4 来自修订 ', 'This MP4 uses revision ') + render.revision + (stale ? t(',工程已有更新,请重新导出。', '; your project has newer changes. Export again.') : t(',请播放检查最终画面。', '. Play it to check the final output.')) : t('尚未导出本次修改。', 'No export for these changes yet.')),
169
+ render ? h('a', { className: 'w-button', href: urlFor(project, 'mp4', { render: render.id, download: '1' }), download: '' }, t('下载这份 MP4', 'Download this MP4')) : null);
170
+ }
171
+
172
+ function MediaAssets({ project, dirty, running, fileRef, upload, perform, setProject, t }) {
173
+ const [remove, setRemove] = useState(null);
174
+ if (!project) return h(W.Empty, { title: t('先保存工程,再添加素材', 'Save the project before adding media'), icon: 'video' }, t('填写标题并点击上方“保存工程”。', 'Enter a title and choose Save project above.'));
175
+ return h('section', { className: 'media-assets-view' },
176
+ h('div', { className: 'w-toolbar' }, h('h3', null, t('工程素材', 'Project media') + ' · ' + project.assets.length), h(W.Button, { icon: 'upload', disabled: running || dirty, onClick: () => fileRef.current?.click() }, t('上传素材', 'Upload media'))),
177
+ h('p', { className: 'w-muted' }, t('PNG/JPEG 图片、一个背景音频和一个静音背景视频;每个 ≤20 MB,总共 ≤40 MB。轮播按上传顺序,每张至少两秒。', 'PNG/JPEG images, one background audio and one muted background video; each ≤20 MB, total ≤40 MB. Slideshow follows upload order, at least two seconds per image.')),
178
+ dirty ? h(W.Status, null, t('请先保存修改,再上传或移除素材副本。', 'Save changes before uploading or removing media copies.')) : null,
179
+ h('input', { ref: fileRef, type: 'file', accept: '.png,.jpg,.jpeg,.mp3,.wav,.mp4', disabled: running || dirty, onChange: upload, 'aria-label': t('上传素材副本', 'Upload a media copy'), style: { display: 'none' } }),
180
+ project.assets.length ? h('ul', { className: 'media-asset-list' }, project.assets.map(asset => h('li', { key: asset.id }, asset.kind === 'image' ? h('img', { src: urlFor(project, 'asset', { asset: asset.id }), alt: asset.name }) : h(W.Icon, { name: asset.kind === 'video' ? 'video' : 'file', size: 28 }),
181
+ h('div', null, h('strong', null, asset.name), h('p', { className: 'w-muted' }, asset.kind + ' · ' + Math.ceil(asset.bytes / 1024) + ' KB')),
182
+ h(W.Button, { variant: 'ghost', icon: 'trash', disabled: running || dirty, onClick: () => setRemove(asset) }, t('移除上传副本', 'Remove uploaded copy'))))) : h(W.Empty, { title: t('还没有上传素材', 'No media uploaded yet'), icon: 'video' }, t('标题卡可以直接导出。需要图片或配乐时,再上传自己的素材。', 'Title cards can be exported as they are. Upload your own media when you need images or music.')),
183
+ remove ? h(W.Dialog, { title: t('移除素材副本', 'Remove media copy'), closeLabel: t('取消', 'Cancel'), busy: running, onClose: () => setRemove(null) }, h('p', null, remove.name), h('p', { className: 'w-muted' }, t('仅移除本工程中的上传副本,原文件保留。', 'Only the uploaded project copy is removed. Your original file is preserved.')), h('div', { className: 'w-toolbar' }, h(W.Button, { variant: 'danger', disabled: running || dirty, onClick: () => perform(async () => { setProject(await api({ action: 'removeAsset', id: project.id, revision: project.revision, asset: remove.id })); setRemove(null); }) }, t('确认移除副本', 'Confirm removal')), h(W.Button, { disabled: running, onClick: () => setRemove(null) }, t('取消', 'Cancel')))) : null);
184
+ }
185
+
186
+ function MediaExports({ project, dirty, running, activeJob, busy, setArchiveReview, t }) {
187
+ if (!project) return h(W.Empty, { title: t('工程尚未保存', 'Project not saved yet'), icon: 'video' }, t('保存后才能导出视频和可编辑工程。', 'Save the project to export video and editable source.'));
188
+ return h('section', { className: 'media-exports-view' },
189
+ h('h3', null, t('已导出成品', 'Exported videos')),
190
+ h('p', { className: 'w-muted' }, t('每份 MP4 对应已保存的工程修订。修改内容后,需要重新导出。', 'Each MP4 corresponds to a saved project revision. Export again after editing.')),
191
+ project.renders.length ? h('ul', { className: 'media-export-list' }, project.renders.map(render => h('li', { key: render.id },
192
+ h('div', null, h('strong', null, t('修订 ', 'Revision ') + render.revision), h('p', { className: 'w-muted' }, new Date(render.created).toLocaleString() + ' · ' + Math.ceil(render.bytes / 1024) + ' KB'), render.revision === project.revision && !dirty ? h('span', { className: 'w-tag' }, t('当前已保存版本', 'Current saved version')) : null),
193
+ h('a', { className: 'w-button', href: urlFor(project, 'mp4', { render: render.id, download: '1' }), download: '' }, t('下载 MP4', 'Download MP4'))))) : h(W.Empty, { title: t('尚无 MP4 成品', 'No MP4 yet'), icon: 'video' }, t('先准备环境、保存并预览工程,再点击“导出 MP4”。', 'Prepare the renderer, save and preview the project, then choose Export MP4.')),
194
+ h('section', { className: 'w-section' }, h('h3', null, t('工程备份', 'Editable project backup')), h('a', { className: 'w-button', href: urlFor(project, 'source'), download: '' }, t('下载可编辑工程', 'Download editable project'))),
195
+ h('section', { className: 'w-section' }, h('h3', null, t('归档', 'Archive')), h('p', { className: 'w-muted' }, t('归档保留素材与成品,不占当前 30 个工程名额。', 'Archiving preserves media and exports and frees an active project slot.')), h(W.Button, { disabled: busy || dirty || activeJob, onClick: () => setArchiveReview(true) }, t('归档此工程', 'Archive this project'))));
196
+ }
197
+
198
+ const MEDIA_WORKBENCH_CSS = `
199
+ .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}
200
+ @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}}
201
+ @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}}
202
+ @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))}}
203
+ `;
204
+
205
+ const engine = 'remotion', title = engine === 'hyperframes' ? 'HyperFrames' : 'Remotion', route = 'api/dsh-' + engine + '/workbench';
206
+ function mediaError(error, en) {
207
+ const message = typeof error === 'string' ? error : error?.message || String(error || '');
208
+ if (!en || !/[\u3400-\u9fff]/.test(message))
209
+ return message;
210
+ if (error?.status === 401)
211
+ return 'Your session expired. Reopen DSH and try again.';
212
+ if (/先归档|已达 30/.test(message))
213
+ return 'The 30 active project slots are full. Archive another project before creating or restoring one.';
214
+ if (/已有预览或渲染/.test(message))
215
+ return 'Stop the active preview or cancel the render before archiving or restoring a project.';
216
+ if (error?.status === 409 || /已.*更新|另一窗口|修订/.test(message))
217
+ return 'The project changed. Reopen the latest version and compare it with your preserved input.';
218
+ if (/标题/.test(message))
219
+ return 'Enter a title up to 72 characters.';
220
+ if (/时长/.test(message))
221
+ return 'Set a duration between 3 and 30 seconds.';
222
+ if (/正文/.test(message))
223
+ return 'Use body text up to 360 characters.';
224
+ if (/素材|PNG|JPEG|MP3|WAV|MP4/.test(message))
225
+ return 'Check the media format and file contents. Each asset must be ≤20 MB; a project supports 12 assets and 40 MB total, with one audio and one video background.';
226
+ if (/任务|官方|FFmpeg|Chrome|环境/.test(message))
227
+ return 'The official task did not finish. Open the job log and check the render environment, then retry. Your project is preserved.';
228
+ if (/工程记录|项目目录|文件/.test(message))
229
+ return 'The project files could not be read safely. Back them up and check their location and permissions before retrying.';
230
+ return 'The operation failed. Check the DSH logs and reload the project before retrying; your input is preserved.';
231
+ }
232
+ let cache = { project: null, fields: null, dirty: false, job: null, creating: false };
233
+ const empty = () => ({ template: 'title', title: '', body: '', format: 'landscape', duration: 8 });
234
+ const fieldsOf = p => ({ template: p.template, title: p.title, body: p.body, format: p.format, duration: p.duration });
235
+ 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}}`;
236
+ async function api(body) {
237
+ 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) });
238
+ let data;
239
+ try {
240
+ data = await response.json();
241
+ }
242
+ catch {
243
+ throw new Error('无法读取插件响应,请检查是否已经重启 DSH。');
244
+ }
245
+ if (!response.ok || !data.ok) {
246
+ const error = new Error(data.message || '操作未完成');
247
+ error.status = response.status;
248
+ throw error;
249
+ }
250
+ return data.value;
251
+ }
252
+ function urlFor(project, kind, extra = {}) { const url = new URL(route, document.baseURI); url.search = new URLSearchParams({ id: project.id, kind, ...extra }); return url.href; }
253
+ function Workbench({locale,workbench=false}={}) {
254
+ const [editorTab,setEditorTab]=useState('content'),[libraryTab,setLibraryTab]=useState('projects');const controlId=React.useId();
255
+ const [lang, setLang] = useState('zh'), [projects, setProjects] = useState([]), [archived, setArchived] = useState([]), [archiveReview, setArchiveReview] = useState(false), [environment, setEnvironment] = useState(null), [templates, setTemplates] = useState([]);
256
+ const [project, setProject] = useState(cache.project), [fields, setFields] = useState(cache.fields || empty()), [dirty, setDirty] = useState(cache.dirty), [creating, setCreating] = useState(cache.creating);
257
+ const [job, setJob] = useState(cache.job), [busy, setBusy] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState(''), [pending, setPending] = useState(null), [conflict, setConflict] = useState(false);
258
+ const busyRef = useRef(false), fileRef = useRef(null), latest = useRef({ project, fields, dirty });
259
+ latest.current = { project, fields, dirty };
260
+ const t = (zh, en) => lang === 'en' ? en : zh, activeJob = job?.state === 'running', running = busy || activeJob && job.kind !== 'prepare';
261
+ const stage = lang === 'en' ? ({ '下载环境': 'Preparing environment', '检查 FFmpeg': 'Checking FFmpeg', '下载官方依赖': 'Downloading official dependencies', '检查官方环境': 'Checking official CLI', '检查或下载 Chrome': 'Checking or downloading Chrome', '下载专用渲染浏览器': 'Downloading the official rendering browser', '准备修订快照': 'Preparing saved revision', '启动官方预览': 'Starting official preview', '官方预览已就绪': 'Official preview is ready', '检查合成': 'Checking composition', '渲染 MP4': 'Rendering MP4', '已完成': 'Completed', '已取消': 'Cancelled', '已取消;工程已保留': 'Cancelled; project preserved', '官方任务未完成,请展开运行记录后重试。工程已保留。': 'The official task failed. Open the job log and retry; your project is preserved.', '未找到 FFmpeg,请安装并加入 PATH,重启 DSH 后重试。工程已保留。': 'FFmpeg was not found. Install it, add it to PATH, then restart DSH and retry; your project is preserved.' }[job?.stage] || mediaError(job?.stage, true)) : job?.stage;
262
+ useEffect(() => { const update = () => setLang(String(locale?.getSnapshot?.().active || '').startsWith('en') ? 'en' : 'zh'); update(); return locale?.subscribe?.(update); }, [locale]);
263
+ useEffect(() => { cache = { project, fields, dirty, job, creating }; }, [project, fields, dirty, job, creating]);
264
+ useEffect(() => { const exit = e => { if (latest.current.dirty) {
265
+ e.preventDefault();
266
+ e.returnValue = '';
267
+ } }; window.addEventListener('beforeunload', exit); return () => window.removeEventListener('beforeunload', exit); }, []);
268
+ async function refresh() {
269
+ const data = await api({ action: 'list' });
270
+ setProjects(data.projects);
271
+ setArchived(data.archivedProjects || []);
272
+ setEnvironment(data.environment);
273
+ setTemplates(data.templates);
274
+ const current = latest.current, saved = !current.dirty && data.projects.find(row => row.id === current.project?.id);
275
+ if (saved) {
276
+ setProject(saved);
277
+ setFields(fieldsOf(saved));
278
+ }
279
+ setJob(previous => data.jobs.find(row => row.id === previous?.id) || data.jobs.filter(row => row.state === 'running').at(-1) || data.jobs.at(-1) || null);
280
+ }
281
+ useEffect(() => { refresh().catch(e => setError(e)); }, []);
282
+ useEffect(() => {
283
+ if (!job || job.state !== 'running')
284
+ return;
285
+ let live = true, polling = false;
286
+ const timer = setInterval(async () => {
287
+ if (polling)
288
+ return;
289
+ polling = true;
290
+ try {
291
+ const next = await api({ action: 'job', job: job.id });
292
+ if (!live)
293
+ return;
294
+ // Refresh output before the terminal state tears down this polling effect.
295
+ if (next.state !== 'running')
296
+ await refresh();
297
+ else
298
+ setJob(next);
299
+ }
300
+ catch (e) {
301
+ if (live) {
302
+ setError(e);
303
+ setJob(null);
304
+ }
305
+ }
306
+ finally {
307
+ polling = false;
308
+ }
309
+ }, 1000);
310
+ return () => { live = false; clearInterval(timer); };
311
+ }, [job?.id, job?.state]);
312
+ async function perform(fn) { if (busyRef.current)
313
+ return; busyRef.current = true; setBusy(true); setError(''); setNotice(''); try {
314
+ return await fn();
315
+ }
316
+ catch (e) {
317
+ setError(e);
318
+ setConflict(e.status === 409);
319
+ }
320
+ finally {
321
+ busyRef.current = false;
322
+ setBusy(false);
323
+ } }
324
+ async function open(id) { const next = await api({ action: 'get', id }); setProject(next); setFields(fieldsOf(next)); setCreating(false); setDirty(false); setConflict(false); setPending(null); setArchiveReview(false); }
325
+ function navigate(target) { if (dirty) {
326
+ setPending(target);
327
+ return;
328
+ } perform(() => move(target)); }
329
+ async function move(target) { if (target === 'new') {
330
+ setProject(null);
331
+ setFields(empty());
332
+ setCreating(true);setEditorTab('content');
333
+ }
334
+ else if (target === 'list') {
335
+ setProject(null);
336
+ setCreating(false);
337
+ await refresh();
338
+ }
339
+ else
340
+ await open(target); setDirty(false); setPending(null); }
341
+ async function save() { const next = await api(project ? { action: 'update', id: project.id, revision: project.revision, fields } : { action: 'create', fields }); setProject(next); setFields(fieldsOf(next)); setCreating(false); setDirty(false); setConflict(false); setNotice(t('工程已保存。', 'Project saved.')); await refresh(); return next; }
342
+ async function upload(event) {
343
+ const file = event.target.files?.[0];
344
+ if (!file)
345
+ return;
346
+ await perform(async () => {
347
+ if (!project || dirty)
348
+ throw new Error(t('请先保存工程,再上传素材。', 'Save your project before uploading media.'));
349
+ if (file.size > 20 * 1024 * 1024)
350
+ throw new Error(t('单个素材不能超过 20 MB。', 'Each asset must be at most 20 MB.'));
351
+ const response = await fetch(new URL(route + '/upload', document.baseURI), { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/octet-stream', 'x-dsh-media': '1', 'x-project-id': project.id, 'x-project-revision': String(project.revision), 'x-file-name': encodeURIComponent(file.name) }, body: file });
352
+ const result = await response.json();
353
+ if (!response.ok || !result.ok) {
354
+ const e = new Error(result.message);
355
+ e.status = response.status;
356
+ throw e;
357
+ }
358
+ setProject(result.value);
359
+ setNotice(t('素材副本已保存,原文件未改变。', 'A media copy was saved; your original is unchanged.'));
360
+ });
361
+ event.target.value = '';
362
+ }
363
+ async function start(action) { await perform(async () => { if (action !== 'prepare' && dirty)
364
+ throw new Error(t('请先保存修改。', 'Save changes first.')); setJob(await api({ action, id: project?.id, revision: project?.revision })); }); }
365
+ 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.')); }
366
+ const render=project?.renders?.[0],stale=render && (dirty || render.revision!==project.revision);
367
+ const canSave=fields.title.trim().length>0 && fields.title.length<=72 && Number.isInteger(fields.duration) && fields.duration>=3 && fields.duration<=30;
368
+ if(!environment) return 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'}));
369
+ return h(W.Page,{workbench,title,subtitle:t('编辑视频工程,预览并导出 MP4。','Edit video projects, preview and export MP4.'),className:'dmw media-workbench',actions:h(React.Fragment,null,
370
+ 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')),
371
+ 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)}),
372
+ project ? h(W.Button,{icon:'download',disabled:running || dirty || !environment.ready,onClick:()=>start('render')},t('导出 MP4','Export MP4')) : null)},
373
+ 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)')),
374
+ (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,
375
+ 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,
376
+ 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,
377
+ h('details',null,h('summary',null,t('查看运行记录','Show job log')),h('pre',null,job.lines.join('\n')))) : null,
378
+ 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')}]},
379
+ !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.'))),
380
+ engine === 'remotion' && h('p', null, h('a', { href: 'https://www.remotion.dev/license', target: '_blank', rel: 'noreferrer' }, t('查看 Remotion 使用许可证', 'Read the Remotion license'))),
381
+ 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.')),
382
+ archived.map(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'},projects.map(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.')) :
383
+ 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.'))),
384
+ engine === 'remotion' && h('p', null, h('a', { href: 'https://www.remotion.dev/license', target: '_blank', rel: 'noreferrer' }, t('查看 Remotion 使用许可证', 'Read the Remotion license'))),
385
+ 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}) :
386
+ h('div',{className:'media-editor'},h('section',{className:'media-content-fields'},h('h3',null,t('模板与内容','Template and content')),h('div',{className:'media-field'},h('label', { htmlFor: controlId+'-template' }, t('模板', 'Template')),h('select', { id: controlId+'-template', disabled: running, value: fields.template, onChange: e => { setFields({ ...fields, template: e.target.value }); setDirty(true); } }, templates.map(row => h('option', { key: row.id, value: row.id }, lang === 'en' ? row.en : row.zh)))),
387
+ h('div',{className:'media-field'},h('label', { htmlFor: controlId+'-title' }, t('标题(最多 72 字符)', 'Title (up to 72 characters)')),h('input', { id: controlId+'-title', value: fields.title, disabled: running, maxLength: 72, onChange: e => { setFields({ ...fields, title: e.target.value }); setDirty(true); } })),
388
+ h('div',{className:'media-field'},h('label', { htmlFor: controlId+'-body' }, t('正文(轮播时每行对应一页)', 'Body (one line per slideshow page)')),h('textarea', { id: controlId+'-body', value: fields.body, disabled: running, maxLength: 360, rows: 4, onChange: e => { setFields({ ...fields, body: e.target.value }); setDirty(true); } })),dirty?h('p',{className:'w-muted'},t('有未保存修改;切换工程前会提醒。','Unsaved changes. You will be asked before switching projects.')):project?h('p',{className:'w-muted'},t('已保存 · 修订 ','Saved · revision ')+project.revision):null,project && h('button', { disabled: running || !dirty, onClick: () => { setFields(fieldsOf(project)); setDirty(false); setError(''); } }, t('取消修改', 'Discard changes'))),
389
+ h(MediaCanvas,{project,fields,render,dirty,stale,job,running,ready:environment.ready,busy,start,perform,setNotice,t}),
390
+ h('aside',{className:'media-properties'},h('h3',null,t('视频设置','Video settings')),h('div',{className:'media-field'},h('label', { htmlFor: controlId+'-format' }, t('画幅', 'Canvas')),h('select', { id: controlId+'-format', value: fields.format, disabled: running, onChange: e => { setFields({ ...fields, format: e.target.value }); setDirty(true); } }, ['landscape', 'portrait', 'square'].map((key, i) => h('option', { key, value: key }, [t('横屏 16:9', 'Landscape 16:9'), t('竖屏 9:16', 'Portrait 9:16'), t('正方形', 'Square')][i])))),
391
+ 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); } })),
392
+ 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),
393
+ 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))),
394
+ 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.')),
395
+ 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,
396
+ 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.')),
397
+ 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);
398
+ }
399
+ function apply(ctx) { let locale; try {
400
+ locale = ctx.get?.('locale') || ctx.locale;
401
+ }
402
+ catch { } W.register(ctx,{id:'dsh-'+engine+'.workbench',label:()=>title,icon:engine==='hyperframes'?'layers':'play',component:Workbench,inject:()=>({locale}),order:engine==='hyperframes'?65:66});
403
+ ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-' + engine, order: engine === 'hyperframes' ? 62 : 63, label: () => title, inject: () => ({ locale }) }, Workbench)); }
404
+ return { inject: ['slots', 'locale'], apply };
405
+ } });
406
+
407
+ })();