wallpaper-engine-dsh 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ A DSH bundle that turns your local **Wallpaper Engine** library into the live ba
10
10
 
11
11
  **Legibility on any wallpaper.** Text stays readable by construction, not by luck. The veil always opposes the text colour — black in the dark theme, white in the light theme — so neither a pitch-black nor a pure-white wallpaper can swallow the UI. On top of that, a Smart veil samples the wallpaper's brightness every few seconds and automatically strengthens the veil when a scene drifts too bright (or too dark) for the current theme (web wallpapers' pixels are unreadable inside the sandbox, so their preview still stands in as the sample). While a wallpaper is active, dropdown menus, the slash-command popup, the queue-dock panel, the settings dialog, and the image lightbox all render as frosted glass: the shell's own menu primitive carries an isolation property that silently kills its internal blur layer, so the plugin applies the blur on the surface element itself and deepens the menu fill, keeping overlay text readable over any wallpaper. Everything reverts to the host look when the wallpaper is off.
12
12
 
13
- **Manual framing.** Pick a base layout — Cover, Contain, Stretch, Original, or Free — then frame the shot yourself. **Adjust** opens the live wallpaper fullscreen: scroll to zoom, drag to pan. In the first four layouts the crop is clamped so the image always fills the screen with no exposed edges; **Free** places the wallpaper at natural size on a black canvas where it can be dragged anywhere, zoomed from 0.1× to 4×, and rotated to any angle (±90° buttons or Alt+drag), with just enough kept on screen that it can never be dragged out of sight. **Mirror** flips the wallpaper horizontally and works in every layout — a flip never exposes an edge. Esc or Done finishes, the framing persists, and **Reset crop** restores the default in one click. Web wallpapers take the same select and the same zoom/pan/rotate framing — the transform applies to the iframe directly.
13
+ **Manual framing.** Pick a base layout — Cover, Contain, Stretch, Original, or Free — then frame the shot yourself. **Adjust** opens the live wallpaper fullscreen: scroll to zoom, drag to pan. In the seamless layouts the picture already fills the screen at 100% — zoom in with the wheel first and panning unlocks (the hint bar says so). In the first four layouts the crop is clamped so the image always fills the screen with no exposed edges; **Free** places the wallpaper at natural size on a black canvas where it can be dragged anywhere, zoomed from 0.1× to 4×, and rotated to any angle (±90° buttons or Alt+drag), with just enough kept on screen that it can never be dragged out of sight. **Mirror** flips the wallpaper horizontally and works in every layout — a flip never exposes an edge. Esc or Done finishes, the framing persists, and **Reset crop** restores the default in one click. Web wallpapers take the same select and the same zoom/pan/rotate framing — the transform applies to the iframe directly.
14
14
 
15
15
  **Four live sliders.** Wallpaper blur, Scrim, Borders, and Glass — every change lands instantly and persists. Blur renders in place: composition, layout, and crop stay identical at every blur level, and within one blur radius of the screen edge the picture fades softly into the veil beneath the UI chrome. A whisper of dither noise keeps smooth gradients band-free; at zero blur the media bypasses the filter pipeline entirely for bit-exact pixels. The Glass slider's colour melt is anchored to the shell's own menu material: at the 40px ceiling it reproduces the host recipe blur(40px) saturate(150%) exactly, so full glass reads as one system with the shell's native frost.
16
16
 
@@ -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
@@ -10,7 +10,7 @@
10
10
 
11
11
  **任何壁纸上文字都可读。** 遮罩颜色始终与文字颜色相反:深色主题用黑色遮罩,浅色主题用白色遮罩,因此过亮或过暗的壁纸都不会盖住文字。在此基础上,「智能可视」每隔几秒采样一次壁纸亮度,亮度过高或过低时自动加强遮罩(网页壁纸的像素在沙箱中读不到,改用其预览图作为采样依据)。壁纸激活时,下拉菜单、斜杠命令提示框、队列面板、设置弹窗与图片灯箱统一为磨砂玻璃:外壳的菜单原语自身带隔离属性,其内部模糊层在宿主原样下并不生效,插件改为在 surface 元素上施加模糊,并把菜单底色加深,保证浮层文字在任意壁纸上可读;关闭壁纸后恢复宿主原样。
12
12
 
13
- **手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮缩放,拖动平移。前四种排布下裁剪范围经过限制,画面始终铺满屏幕,不露出边缘;「自由」把壁纸按原始大小放在黑色画布上,可拖动到任意位置,缩放范围 0.1–4×,并可旋转任意角度(±90° 按钮或 Alt+拖动),同时始终保留一小块在屏幕内,不会拖丢。「镜像」水平翻转壁纸,所有排布下都可用——翻转不会破坏铺满。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸使用相同的排布下拉与缩放/平移/旋转取景(变换直接作用于 iframe)。
13
+ **手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮缩放,拖动平移。填充等无缝排布下,画面在 100% 倍率时已铺满屏幕,先滚轮放大后拖动平移才会生效(提示条会同步说明)。前四种排布下裁剪范围经过限制,画面始终铺满屏幕,不露出边缘;「自由」把壁纸按原始大小放在黑色画布上,可拖动到任意位置,缩放范围 0.1–4×,并可旋转任意角度(±90° 按钮或 Alt+拖动),同时始终保留一小块在屏幕内,不会拖丢。「镜像」水平翻转壁纸,所有排布下都可用——翻转不会破坏铺满。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸使用相同的排布下拉与缩放/平移/旋转取景(变换直接作用于 iframe)。
14
14
 
15
15
  **四个实时滑杆。** 壁纸模糊、暗化、边框、玻璃,每一步调节立即生效并保存。模糊在原位渲染:任意模糊度下,画面的比例、排布与取景完全一致;屏幕边缘一个模糊半径内的画面向遮罩过渡,被界面边饰覆盖,通常不可见。模糊时叠加轻微噪声,消除渐变色带;模糊为零时媒体完全绕过滤镜管线,像素保持不变。玻璃滑杆的「色彩熔融」在 40px 满档时正好等于外壳官方菜单材质的配方(blur(40px) saturate(150%)),满档玻璃与外壳原生磨砂观感一致。
16
16
 
@@ -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
@@ -102,6 +102,7 @@ window.__ModuleLoader__.load({
102
102
  adjustDone: "完成",
103
103
  adjustHint: "拖动平移 · 滚轮缩放 · Esc 或点「完成」退出",
104
104
  adjustHintFree: "拖动平移 · 滚轮缩放 · Alt+拖动旋转 · Esc 或点「完成」退出",
105
+ adjustHintZoom: "当前倍数下画面已铺满,先滚轮放大,即可拖动平移",
105
106
  mirror: "镜像",
106
107
  rotateCcw: "⟲",
107
108
  rotateCw: "⟳",
@@ -186,6 +187,7 @@ window.__ModuleLoader__.load({
186
187
  adjustDone: "Done",
187
188
  adjustHint: "Drag to pan · wheel to zoom · Esc or Done to exit",
188
189
  adjustHintFree: "Drag to pan · wheel to zoom · Alt+drag to rotate · Esc or Done to exit",
190
+ adjustHintZoom: "The picture already fills the screen — zoom in with the wheel to unlock panning",
189
191
  mirror: "Mirror",
190
192
  rotateCcw: "⟲",
191
193
  rotateCw: "⟳",
@@ -727,32 +729,64 @@ window.__ModuleLoader__.load({
727
729
  });
728
730
  }
729
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
+
730
759
  function attachMediaError(media, sel) {
731
- media.addEventListener("error", () => {
732
- const id = sel.id;
733
- const count = (mediaFailures.get(id) || 0) + 1;
734
- mediaFailures.set(id, count);
735
- const w = selection.inventory.wallpapers.find((x) => x.id === id);
736
- if (count === 1) {
737
- // Maybe just a dead token: refetch fresh URLs and remount. Throttled
738
- // PER WALLPAPER — a global window would strand a video whose first
739
- // error lands soon after an unrelated recovery (no remount, no second
740
- // error, blank layer until a manual refresh).
741
- const now = Date.now();
742
- if (now - (lastRecoveryAt.get(id) || 0) < 10000) return;
743
- lastRecoveryAt.set(id, now);
744
- forceRemount = true;
745
- loadInventory(true);
746
- } else if (w && w.preview && !demotedToPreview.has(id)) {
747
- // The media itself is unusable here — fall back to the still preview.
748
- demotedToPreview.add(id);
749
- applySelection(id);
750
- } else {
751
- // Unrenderable here with no preview to fall back on (or the entry has
752
- // vanished from the inventory entirely): clear the selection rather
753
- // than leave a dead element on the layer.
754
- applySelection("");
755
- }
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);
756
790
  });
757
791
  }
758
792
 
@@ -829,7 +863,15 @@ window.__ModuleLoader__.load({
829
863
  bar.className = "webg-adjust-bar";
830
864
  const hint = document.createElement("span");
831
865
  hint.className = "webg-adjust-hint";
832
- hint.textContent = isFreeFit() ? S().adjustHintFree : S().adjustHint;
866
+ const updateHint = () => {
867
+ // Pan feedback: in the gap-free fits the pan headroom is (zoom-1)*50,
868
+ // so at zoom 1 dragging is pinned BY DESIGN - say so instead of
869
+ // leaving a silent no-op that reads as "pan is broken".
870
+ hint.textContent = isFreeFit() ? S().adjustHintFree
871
+ : selection.zoom > 1 ? S().adjustHint
872
+ : S().adjustHintZoom;
873
+ };
874
+ updateHint();
833
875
  const done = document.createElement("button");
834
876
  done.type = "button";
835
877
  done.className = "webg-btn webg-btn--primary";
@@ -901,6 +943,7 @@ window.__ModuleLoader__.load({
901
943
  clampCrop(); // mode envelope: gap-free pan headroom, or free keep-visible
902
944
  drag = { x: e.clientX, y: e.clientY };
903
945
  applyEffects();
946
+ updateHint();
904
947
  };
905
948
  const endPointer = () => {
906
949
  if (rot) { rot = null; persistSelection(); return; }
@@ -921,6 +964,7 @@ window.__ModuleLoader__.load({
921
964
  selection.zoom *= Math.exp(-(e.deltaY || 0) * 0.0012);
922
965
  clampCrop();
923
966
  applyEffects();
967
+ updateHint();
924
968
  persistSelection();
925
969
  };
926
970
  const onKey = (e) => { if (e.key === "Escape") exitAdjust(); };
@@ -1896,12 +1940,18 @@ window.__ModuleLoader__.load({
1896
1940
  .webg-adjust {
1897
1941
  position: fixed; inset: 0; z-index: 2147483000;
1898
1942
  cursor: grab; touch-action: none;
1943
+ /* Desktop: the shell titlebar strip is a window-drag region and the
1944
+ union drag region ignores z-order - without no-drag, drags starting
1945
+ near the top of the adjust overlay move the WINDOW instead of
1946
+ panning, and the bar's Done button stops receiving clicks. */
1947
+ -webkit-app-region: no-drag;
1948
+ app-region: no-drag;
1899
1949
  }
1900
1950
  .webg-adjust:active { cursor: grabbing; }
1901
1951
  .webg-adjust .webg-layer { position: absolute; z-index: 0; }
1902
1952
  .webg-adjust-bar {
1903
1953
  position: absolute; top: 18px; left: 50%; transform: translateX(-50%);
1904
- z-index: 1; cursor: default;
1954
+ z-index: 1; cursor: default; -webkit-app-region: no-drag; app-region: no-drag;
1905
1955
  display: flex; align-items: center; gap: 12px;
1906
1956
  padding: 8px 8px 8px 14px; border-radius: 12px;
1907
1957
  background: var(--dsw-alias-bg-overlay, rgba(30, 32, 38, 0.85));
@@ -1984,9 +2034,9 @@ window.__ModuleLoader__.load({
1984
2034
  Same on the queue dock panel (:before carries the blur). With a
1985
2035
  wallpaper active, menus showed sharp content through a 45% fill.
1986
2036
  An element's OWN backdrop-filter ignores its own isolation, so
1987
- the filter moves onto the surface element; fill deepened 72%. */
1988
- body[${ACTIVE_ATTR}] { --dsw-menu-surface-fill: rgba(67, 69, 74, 0.72); }
1989
- body[${ACTIVE_ATTR}]:not([${DARK_ATTR}]) { --dsw-menu-surface-fill: rgba(248, 249, 250, 0.72); }
2037
+ the filter moves onto the surface element; fill deepened 85%. */
2038
+ body[${ACTIVE_ATTR}] { --dsw-menu-surface-fill: rgba(67, 69, 74, 0.85); }
2039
+ body[${ACTIVE_ATTR}]:not([${DARK_ATTR}]) { --dsw-menu-surface-fill: rgba(248, 249, 250, 0.85); }
1990
2040
  body[${ACTIVE_ATTR}] [data-menu-material],
1991
2041
  body[${ACTIVE_ATTR}] [data-composer-dock] [class*="_panel"] {
1992
2042
  -webkit-backdrop-filter: blur(var(--webg-blur, 16px)) saturate(var(--webg-saturate, 1.2)) brightness(var(--webg-glass-brightness, 1.04)) contrast(1.01);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallpaper-engine-dsh",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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",