wallpaper-engine-dsh 2.1.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | [中文](README.zh.md)
4
4
 
5
- A DSH bundle that turns your local **Wallpaper Engine** library into the live background of the DSH web GUI (`dsh web`). Video wallpapers play behind the chat, web wallpapers render in place, and everything in between — crossfade transitions, liquid-glass panels, a legibility veil that adapts to any wallpaper, manual framing, rotation lists, search, and a bilingual UI — is controlled from a single settings row.
5
+ A DSH bundle that turns your local **Wallpaper Engine** library into the live background of the DSH web GUI (`dsh web`). Video wallpapers play behind the chat, web wallpapers render in place, and everything in between — crossfade transitions, liquid-glass panels, a legibility veil that adapts to any wallpaper, manual framing, rotation lists, search, and a bilingual UI — is controlled from its own dedicated settings section.
6
6
 
7
7
  ## Features
8
8
 
@@ -35,7 +35,7 @@ git clone https://github.com/Weilv-D/wallpaper-engine-dsh.git
35
35
  dsh plugin --profile web add link:<absolute path to the cloned folder>
36
36
  ```
37
37
 
38
- Restart `dsh web`, then open **Settings → General → Wallpaper Engine**.
38
+ Restart `dsh web`, then open **Settings → Wallpaper Engine**.
39
39
 
40
40
  ## Usage
41
41
 
@@ -73,6 +73,7 @@ Media serving is defensive by design: tokens are unguessable 72-bit values, stab
73
73
  - Scene and application wallpapers are not listed: they are Wallpaper Engine's private `.pkg` scene packages, and their live animation can only be rendered by Wallpaper Engine itself on the desktop.
74
74
  - Web wallpapers run isolated from the page's storage, so ones that persist state across loads won't keep it.
75
75
  - Web wallpapers with sound may stay silent until you interact with the page — browsers gate audio in sandboxed documents on a user gesture, and wallpapers that rely on Wallpaper Engine's system-audio feed (visualizers) have no signal to draw from.
76
+ - Video codecs are bounded by what the browser can decode. H.264/AVC, VP8/VP9, and AV1 play natively; workshop videos compressed with H.265/HEVC cannot be software-decoded on most machines — previously a solid black screen, now detected as "playing but zero decoded frames" and automatically demoted to the preview still with a status note. Transcoding to H.264 mp4 or VP9 webm restores full playback.
76
77
  - The glass effect reads DSH design tokens; if a future shell redesign renames them, frosting degrades to plain transparency.
77
78
 
78
79
  ## Development
package/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | [中文](README.zh.md)
4
4
 
5
- 一个 DSH bundle:把本机 **Wallpaper Engine** 壁纸库变成 DSH web GUI(`dsh web`)的动态背景。视频壁纸在聊天界面背后播放,网页壁纸直接渲染。交叉淡入、液态玻璃面板、随壁纸自适应的可读性遮罩、手动取景、轮播列表、搜索与中英双语界面,全部集中在一个设置行里。
5
+ 一个 DSH bundle:把本机 **Wallpaper Engine** 壁纸库变成 DSH web GUI(`dsh web`)的动态背景。视频壁纸在聊天界面背后播放,网页壁纸直接渲染。交叉淡入、液态玻璃面板、随壁纸自适应的可读性遮罩、手动取景、轮播列表、搜索与中英双语界面,集中在一个专门的设置分区里。
6
6
 
7
7
  ## 功能
8
8
 
@@ -35,7 +35,7 @@ git clone https://github.com/Weilv-D/wallpaper-engine-dsh.git
35
35
  dsh plugin --profile web add link:<克隆文件夹的绝对路径>
36
36
  ```
37
37
 
38
- 重启 `dsh web`,打开 **设置 → 通用 → 壁纸背景 (Wallpaper Engine)**。
38
+ 重启 `dsh web`,打开 **设置 → 壁纸背景 (Wallpaper Engine)**。
39
39
 
40
40
  ## 使用
41
41
 
@@ -73,6 +73,7 @@ bundle 分为两半:
73
73
  - 场景与应用壁纸不出现在列表中:它们是 Wallpaper Engine 私有的 `.pkg` 场景包,动态渲染只能由桌面端的 Wallpaper Engine 完成。
74
74
  - 网页壁纸与页面存储隔离,需要跨加载保存状态的壁纸无法保留状态。
75
75
  - 带声音的网页壁纸可能在你与页面交互前保持静默——浏览器对沙箱文档的音频有自动播放限制;依赖 Wallpaper Engine 系统音频输入的壁纸(如音频可视化)在浏览器中没有信号可画。
76
+ - 视频编码受浏览器解码能力约束。浏览器可以直接播放 H.264/AVC、VP8/VP9 与 AV1;创意工坊中用 H.265/HEVC 压缩的视频在多数机器上无法软解——之前表现为只有一片黑色,现在检测到「声称在播放但解不出任何帧」时会自动降级为预览图并在状态行说明。手工转码为 H.264 的 mp4 或 VP9 的 webm 即可完整播放。
76
77
  - 玻璃效果读 DSH 设计令牌;若未来外壳重构重命名令牌,磨砂会退化为普通半透明。
77
78
 
78
79
  ## 开发
package/lib/client.js CHANGED
@@ -729,32 +729,64 @@ window.__ModuleLoader__.load({
729
729
  });
730
730
  }
731
731
 
732
+ function handleMediaFailure(sel) {
733
+ const id = sel.id;
734
+ const count = (mediaFailures.get(id) || 0) + 1;
735
+ mediaFailures.set(id, count);
736
+ const w = selection.inventory.wallpapers.find((x) => x.id === id);
737
+ if (count === 1) {
738
+ // Maybe just a dead token: refetch fresh URLs and remount. Throttled
739
+ // PER WALLPAPER - a global window would strand a video whose first
740
+ // error lands soon after an unrelated recovery (no remount, no second
741
+ // error, blank layer until a manual refresh).
742
+ const now = Date.now();
743
+ if (now - (lastRecoveryAt.get(id) || 0) < 10000) return;
744
+ lastRecoveryAt.set(id, now);
745
+ forceRemount = true;
746
+ loadInventory(true);
747
+ } else if (w && w.preview && !demotedToPreview.has(id)) {
748
+ // The media itself is unusable here - fall back to the still preview.
749
+ demotedToPreview.add(id);
750
+ applySelection(id);
751
+ } else {
752
+ // Unrenderable here with no preview to fall back on (or the entry has
753
+ // vanished from the inventory entirely): clear the selection rather
754
+ // than leave a dead element on the layer.
755
+ applySelection("");
756
+ }
757
+ }
758
+
732
759
  function attachMediaError(media, sel) {
733
- media.addEventListener("error", () => {
734
- const id = sel.id;
735
- const count = (mediaFailures.get(id) || 0) + 1;
736
- mediaFailures.set(id, count);
737
- const w = selection.inventory.wallpapers.find((x) => x.id === id);
738
- if (count === 1) {
739
- // Maybe just a dead token: refetch fresh URLs and remount. Throttled
740
- // PER WALLPAPER — a global window would strand a video whose first
741
- // error lands soon after an unrelated recovery (no remount, no second
742
- // error, blank layer until a manual refresh).
743
- const now = Date.now();
744
- if (now - (lastRecoveryAt.get(id) || 0) < 10000) return;
745
- lastRecoveryAt.set(id, now);
746
- forceRemount = true;
747
- loadInventory(true);
748
- } else if (w && w.preview && !demotedToPreview.has(id)) {
749
- // The media itself is unusable here — fall back to the still preview.
750
- demotedToPreview.add(id);
751
- applySelection(id);
752
- } else {
753
- // Unrenderable here with no preview to fall back on (or the entry has
754
- // vanished from the inventory entirely): clear the selection rather
755
- // than leave a dead element on the layer.
756
- applySelection("");
757
- }
760
+ media.addEventListener("error", () => handleMediaFailure(sel));
761
+ if (sel.type === "video") attachZeroFrameDetector(media, sel);
762
+ }
763
+
764
+ // Zero-decoded-frames detector: Chromium can report a video as PLAYING
765
+ // without ever decoding a frame - no error event, currentTime advancing,
766
+ // picture permanently black. Typical with HEVC/H.265 content when the
767
+ // machine has no usable hardware decode. getVideoPlaybackQuality exposes
768
+ // the truth (totalVideoFrames stays 0); a genuinely dark wallpaper is
769
+ // unaffected because its frames DO decode. Wired into the same recovery
770
+ // ladder as a media error: first hit retries with fresh tokens, second
771
+ // hit demotes to the preview still (or clears without one).
772
+ function attachZeroFrameDetector(media, sel) {
773
+ media.addEventListener("playing", () => {
774
+ // One probe ~2s into claimed playback: enough for even a slow
775
+ // hardware pipeline to produce its first frames.
776
+ schedule(() => {
777
+ // Stale guard: crossfade may have swapped the element out already,
778
+ // and the probe must never run while the user paused playback.
779
+ const layer = document.getElementById(LAYER_ID);
780
+ if (!layer || layer.querySelector("video") !== media) return;
781
+ if (media.paused || !(media.currentTime > 0)) return;
782
+ const q = typeof media.getVideoPlaybackQuality === "function"
783
+ ? media.getVideoPlaybackQuality() : null;
784
+ const frames = q ? q.totalVideoFrames
785
+ : (typeof media.webkitDecodedFrameCount === "number"
786
+ ? media.webkitDecodedFrameCount : null);
787
+ if (frames === null) return; // no probe available -> cannot detect
788
+ if (frames === 0) handleMediaFailure(sel);
789
+ }, 2000);
758
790
  });
759
791
  }
760
792
 
@@ -2076,7 +2108,7 @@ window.__ModuleLoader__.load({
2076
2108
  /* padding-top lifts the first row off the settings-section divider so the
2077
2109
  search/language row breathes; gap keeps every row evenly spaced.
2078
2110
  Type scale follows the shell: 14px body → 13px controls → 12px hints. */
2079
- .webg-picker { display: flex; flex-direction: column; gap: 12px; padding-top: 10px; }
2111
+ .webg-picker { display: flex; flex-direction: column; gap: 12px; padding-top: 10px; max-width: 900px; }
2080
2112
  .webg-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
2081
2113
  .webg-hint { font-size: 12px; opacity: 0.7; color: var(--dsw-alias-label-tertiary, inherit); }
2082
2114
  .webg-error {
@@ -2298,7 +2330,7 @@ window.__ModuleLoader__.load({
2298
2330
 
2299
2331
  // ── Plugin exports ──────────────────────────────────────────────────────────
2300
2332
  // The client runner (dsh-cordis-client-runner) rejects ctx.get() of any
2301
- // service this plugin does not declare. slots registers the settings row,
2333
+ // service this plugin does not declare. slots registers the settings section,
2302
2334
  // locale drives the bilingual UI, theme drives the veil/scheme reactions.
2303
2335
  const inject = ["slots", "locale", "theme"];
2304
2336
 
@@ -2503,11 +2535,15 @@ window.__ModuleLoader__.load({
2503
2535
  });
2504
2536
  }
2505
2537
 
2506
- // 4. Settings row (this slot is NOT the overlay; safe to register into).
2538
+ // 4. Own settings section — NOT a row inside General. The settings dialog
2539
+ // enumerates every registered settings.section slot for its nav
2540
+ // (settings-general and settings-plugins register theirs the same way),
2541
+ // so the picker renders as a full page with its own sidebar entry,
2542
+ // ordered after the built-in sections (general 0, plugins 15).
2507
2543
  if (ctx.slots) {
2508
- ctx.slots.inject("settings.general.item", () =>
2544
+ ctx.slots.inject("settings.section", () =>
2509
2545
  ctx.slots.register(
2510
- { name: "settings.general.item", id: "we-background", order: 500, label: () => S().settingsLabel },
2546
+ { name: "settings.section", id: "we-background", order: 20, label: () => S().settingsLabel },
2511
2547
  () => React.createElement(WallpaperPicker),
2512
2548
  ),
2513
2549
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallpaper-engine-dsh",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Wallpaper Engine library as the DSH web GUI background — video/web/still wallpapers, rotation lists, search, resource monitor, bilingual UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",