wallpaper-engine-dsh 0.1.9 → 1.0.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
@@ -2,19 +2,25 @@
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 and web wallpapers render in place — with crossfade transitions, liquid-glass panels, rotation lists, search, a resource monitor, and a bilingual UI.
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.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Renderable-only browsing** — video and web wallpapers render live and are the only kinds listed: scene and application wallpapers are Wallpaper Engine's private `.pkg` scene packages, which no browser can render, so they are filtered out instead of offered as degraded stills. A video the browser cannot decode falls back to its preview image until you refresh.
10
- - **Search** — filter the grid instantly by title or workshop id.
11
- - **Rotation lists** — any number of carousel lists, each with its own wallpapers, interval (1–120 min) and order (sequence/random). Your first playable Wallpaper Engine playlist is imported automatically; others can be imported into any list.
12
- - **Four live sliders** — Wallpaper blur, Scrim, Borders, Glass — all instant and persisted.
13
- - **Legibility on any wallpaper** — the veil opposes the text colour (white veil in light theme, black in dark) and a Smart veil toggle samples the wallpaper's brightness every few seconds, automatically strengthening the veil when a scene gets too bright or too dark for the current theme. Dropdowns, menus, and dialogs keep DSH's opaque surfaces untouched.
14
- - **Manual crop** — pick a base layout (Cover/Contain/Stretch/Original), then frame it yourself: **Adjust** opens the live wallpaper fullscreen where you drag to pan and scroll to zoom; **Reset crop** restores it.
15
- - **Resource monitor** — FPS readout with a low-framerate hint, plus auto-pause when the tab is hidden or the battery runs low (both optional).
16
- - **Bilingual** — the settings UI follows the shell's Language preference (Chinese/English), with a manual override in the picker.
17
- - **Crossfade** — wallpaper switches and clearing fade smoothly instead of snapping.
9
+ **The library, rendered live.** The grid lists every wallpaper the browser can genuinely render — video and web wallpapers. Scene and application wallpapers are Wallpaper Engine's private `.pkg` scene packages; since no browser can execute them, they are filtered out rather than offered as degraded stills. Everything listed plays for real, badged Video or Web, and a search box narrows large libraries by title or workshop id instantly.
10
+
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; it also strengthens the border contrast and the glass frosting on the same evidence. Dropdowns, menus, and dialogs sit on DSH's opaque surfaces and are never affected.
12
+
13
+ **Manual framing.** Pick a base layout — Cover, Contain, Stretch, or Original — then frame the shot yourself. **Adjust** opens the live wallpaper fullscreen: scroll to zoom in (up to 4×), drag to pan within the zoomed framing — the crop is clamped so the image always fills the screen with no exposed edges. Esc or Done finishes, the framing persists per selection, and **Reset crop** restores the default in one click.
14
+
15
+ **Four live sliders.** Wallpaper blur, Scrim, Borders, and Glass — every change lands instantly and persists. Wallpaper blur uses clipped overscan, so blurring never warps or shifts the image; a whisper of dither noise keeps smooth gradients band-free; and at zero blur the media bypasses the filter pipeline entirely for bit-exact pixels.
16
+
17
+ **Rotation lists.** Build any number of carousel lists, each with its own wallpapers, interval (1–120 min), and order (sequence or random). Your first playable Wallpaper Engine playlist is imported automatically on first run; any other playlist can be imported into any list.
18
+
19
+ **Resource monitor.** A live FPS readout with a low-framerate hint, plus optional auto-pause when the tab is hidden or the battery runs low. `prefers-reduced-motion` starts videos paused.
20
+
21
+ **Bilingual.** The UI follows the shell's Language setting (Chinese/English) with a manual override in the picker.
22
+
23
+ **Resilient playback.** Switches and clears crossfade smoothly. A video the browser cannot decode retries once with fresh tokens, then falls back to its preview still with a note in the status row — and the next Refresh retries the live media. Inventory rebuilds re-issue URLs in the background without ever interrupting a playing stream.
18
24
 
19
25
  ## Install
20
26
 
@@ -34,29 +40,29 @@ Restart `dsh web`, then open **Settings → General → Wallpaper Engine**.
34
40
  ## Usage
35
41
 
36
42
  1. Pick a wallpaper from the thumbnail grid — it fades in behind the app. Use the search box to narrow large libraries.
37
- 2. **Pause/Play** controls video playback; **Close** fades the wallpaper out; **Refresh** rescans the library (new workshop subscriptions appear without reloading the page).
38
- 3. Tune the blend with the sliders. On busy wallpapers, raise **Scrim** and **Borders** until text is comfortable; the UI follows DSH's light/dark theme automatically. The **Fit** row sets the base layout for video and still wallpapers — **Adjust** then lets you pan and zoom the crop directly on the canvas.
39
- 4. Create rotation lists with **New**, fill them from the grid or import a WE playlist, then enable **Auto-rotate**.
40
- 5. The resource row shows the current frame rate and offers two auto-pause switches. `prefers-reduced-motion` starts videos paused.
41
- 6. The Language control switches the picker between Chinese and English; on Auto it follows the shell setting.
43
+ 2. **Pause/Play** controls video playback; **Close** fades the wallpaper out; **Refresh** rescans the library — new workshop subscriptions appear without reloading the page, and any demoted wallpaper retries its live media.
44
+ 3. Tune the blend with the sliders. On busy wallpapers, raise **Scrim** and **Borders** until text is comfortable; Smart veil handles it automatically when enabled. The UI follows DSH's light/dark theme on its own.
45
+ 4. Use the **Fit** row for the base layout, then **Adjust** to pan and zoom the crop directly on the canvas.
46
+ 5. Create rotation lists with **New**, fill them from the grid or import a WE playlist, then enable **Auto-rotate**.
47
+ 6. The resource row shows the current frame rate and the two auto-pause switches.
42
48
 
43
- Your selection persists in the browser's `localStorage`.
49
+ Your selection and every knob persist in the browser's `localStorage`.
44
50
 
45
51
  ## How it works
46
52
 
47
53
  The bundle has two halves:
48
54
 
49
- - A **host plugin** discovers the Wallpaper Engine install (Steam registry entry, `libraryfolders.vdf`, standard probe paths across Windows/macOS/Linux), enumerates projects from the default/my-projects folders and the workshop, and serves a JSON inventory plus the media bytes over same-origin routes:
55
+ - A **host plugin** discovers the Wallpaper Engine install (Steam registry entry, `libraryfolders.vdf`, standard probe paths across Windows/macOS/Linux), enumerates projects from the default/my-projects folders and the workshop, keeps only the browser-renderable kinds, and serves a JSON inventory plus the media bytes over same-origin routes:
50
56
  - `GET /we-background/inventory[?refresh=1]`
51
57
  - `GET /we-background/media/<token>[/asset…]` — with HTTP Range support for video seeking; web wallpapers can fetch their bundled sub-assets
52
58
  - `GET /we-background/preview/<token>`
53
- - A **browser plugin** renders the chosen wallpaper on a fixed layer behind the app frame, contributes the settings UI, and keeps every effect on DSH design tokens so themes apply cleanly. Wallpaper blur is clipped overscan (no content warping) with a whisper of dither noise to keep gradients band-free; a video that repeatedly fails to decode (an unsupported container) falls back to its preview still, and the next Refresh retries the live media.
59
+ - A **browser plugin** renders the chosen wallpaper on a fixed layer behind the app frame, contributes the settings UI, and keeps every effect on DSH design tokens so themes apply cleanly.
54
60
 
55
- The host answers from a 30-second inventory cache with stable per-wallpaper tokens (an in-flight stream is never killed by a background rebuild); discovery is resolved asynchronously and cached for ten minutes. The bundle registers no model tools and adds no prompt text.
61
+ Media serving is defensive by design: tokens are unguessable 72-bit values, stable across inventory rebuilds (an in-flight stream is never killed by a background refresh) and re-minted only on an explicit refresh; sub-asset paths are contained lexically and re-verified against the real filesystem so symlinks cannot escape their wallpaper directory; HTML and SVG responses carry a `sandbox` CSP so they can never execute against the DSH origin; and responses that would come up short destroy themselves rather than truncate silently. The host answers from a 30-second inventory cache, resolves discovery asynchronously off the request path, and caches it for ten minutes. The bundle registers no model tools and adds no prompt text.
56
62
 
57
63
  ## Limitations
58
64
 
59
- - Scene and application wallpapers are not listed at all: they are Wallpaper Engine's private `.pkg` scene packages, and their live animation can only be rendered by Wallpaper Engine itself on the desktop.
65
+ - 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.
60
66
  - Web wallpapers run isolated from the page's storage, so ones that persist state across loads won't keep it.
61
67
  - The glass effect reads DSH design tokens; if a future shell redesign renames them, frosting degrades to plain transparency.
62
68
 
@@ -64,7 +70,7 @@ The host answers from a 30-second inventory cache with stable per-wallpaper toke
64
70
 
65
71
  ```sh
66
72
  npm install # builds lib/client.js via the prepare hook
67
- npm test # unit tests for the pure core (node:test)
73
+ npm test # unit tests for the pure core + wire-level host integration
68
74
  npm run build # regenerate lib/client.js from src/client.js
69
75
  npm run verify # boot the emitted bundle in a vm sandbox and assert behaviour
70
76
  ```
package/README.zh.md CHANGED
@@ -2,19 +2,25 @@
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
 
9
- - **只列可原生渲染的壁纸** —— 视频与网页壁纸动态渲染,且是列表中仅有的两类:场景/应用壁纸是 Wallpaper Engine 私有的 `.pkg` 场景包,浏览器无法渲染,因此直接从列表中过滤,而不是以降级的静态图充数。浏览器解不了的视频会临时退到预览图,下次刷新即重试原媒体。
10
- - **搜索** —— 按标题或创意工坊 ID 即时筛选网格。
11
- - **轮播列表** —— 可建任意多个列表,各自拥有壁纸集合、切换间隔(1–120 分钟)和顺序(顺序/随机)。首次运行自动导入第一个可播放的 Wallpaper Engine 播放列表,其余列表可随时导入。
12
- - **四个实时滑杆** —— 壁纸模糊、暗化、边框、玻璃,即时生效并持久保存。
13
- - **任意壁纸都可读** —— 遮罩颜色与文字颜色相对(浅色主题提亮、深色主题压暗);"智能可视"开关每隔几秒采样一次壁纸亮度,画面过亮或过暗时自动加强遮罩。下拉菜单、弹窗等浮层保持 DSH 原生不透明表面,不受影响。
14
- - **手动裁剪** —— 先选基础排布(填充/适应/拉伸/原始),再自己取景:点「调整画面」把壁纸全屏展开,拖动平移、滚轮缩放,「重置裁剪」一键还原。
15
- - **资源监控** —— 帧率读数与低帧率提示,页面隐藏或电量不足时自动暂停(均可开关)。
16
- - **中英双语** —— 设置界面跟随 DSH 的"语言"偏好自动切换,也可在面板内手动指定。
17
- - **交叉淡入** —— 切换与关闭都平滑过渡,不生硬跳变。
9
+ **只列真正渲染得动的壁纸。** 网格中列出的是浏览器能真实渲染的全部类型——视频与网页壁纸。场景/应用壁纸是 Wallpaper Engine 私有的 `.pkg` 场景包,浏览器无法执行,因此直接从列表中过滤,而不是以降级的静态图充数。列出的每张都真实播放,按 视频/网页 标注类型;搜索框按标题或创意工坊 ID 即时筛选大库。
10
+
11
+ **任意壁纸都可读。** 文字可读靠的是结构保障,不是运气。遮罩颜色始终与文字颜色相对——深色主题压黑、浅色主题提亮——无论纯黑还是纯白的壁纸都吞不掉界面。在此基础上,"智能可视"每隔几秒采样一次壁纸亮度,画面偏离当前主题的舒适区时自动加强遮罩。下拉菜单、弹窗等浮层坐在 DSH 原生的不透明表面上,从不受影响。
12
+
13
+ **手动取景。** 先选基础排布——填充、适应、拉伸、原始——然后自己构图:点「调整画面」把壁纸全屏展开,滚轮放大(最高 4×)、拖动在放大的画面内平移——裁剪范围经过钳制,画面永远铺满屏幕、不露边。Esc 或「完成」退出,取景随选择持久保存,「重置裁剪」一键还原。
14
+
15
+ **四个实时滑杆。** 壁纸模糊、暗化、边框、玻璃——每一步调节即时生效并持久保存。壁纸模糊采用裁边式超采样,模糊不变形、不偏移;叠加微量抖动噪声消除渐变色带;模糊归零时媒体完全绕开滤镜管线,像素逐位精确。
16
+
17
+ **轮播列表。** 可建任意多个列表,各自拥有壁纸集合、切换间隔(1–120 分钟)和顺序(顺序/随机)。首次运行自动导入第一个可播放的 Wallpaper Engine 播放列表,其余列表可随时导入到任意列表。
18
+
19
+ **资源监控。** 实时帧率读数与低帧率提示,页面隐藏或电量不足时可选自动暂停。系统开启 `prefers-reduced-motion` 时视频默认暂停。
20
+
21
+ **中英双语。** 界面跟随 DSH 的"语言"设置(中/英),面板内亦可手动指定。
22
+
23
+ **播放有韧性。** 切换与关闭都平滑淡入淡出。浏览器解不了的视频先换新令牌重试一次,仍失败则退到预览图并在状态行注明——下次「刷新」自动重试原媒体。后台重建清单时静默更换 URL,绝不掐断正在播放的流。
18
24
 
19
25
  ## 安装
20
26
 
@@ -22,54 +28,54 @@
22
28
  dsh plugin --profile web add wallpaper-engine-dsh
23
29
  ```
24
30
 
25
- 或从源码检出安装(便于开发,改动即时生效):
31
+ 或从源码检出安装(便于开发,改动即时生效):
26
32
 
27
33
  ```sh
28
34
  git clone https://github.com/Weilv-D/wallpaper-engine-dsh.git
29
35
  dsh plugin --profile web add link:<克隆文件夹的绝对路径>
30
36
  ```
31
37
 
32
- 重启 `dsh web`,打开 **设置 → 通用 → 壁纸背景 (Wallpaper Engine)**。
38
+ 重启 `dsh web`,打开 **设置 → 通用 → 壁纸背景 (Wallpaper Engine)**。
33
39
 
34
40
  ## 使用
35
41
 
36
- 1. 从缩略图网格选择壁纸,它会在界面背后淡入。壁纸库较大时用搜索框快速定位。
37
- 2. **暂停/播放** 控制视频播放;**关闭** 淡出壁纸;**刷新** 重新扫描壁纸库,新订阅的创意工坊壁纸无需刷新页面即可出现。
38
- 3. 用滑杆调节融合效果。壁纸太花时调高 **暗化** 和 **边框** 直到文字清晰;界面跟随 DSH 明暗主题自动适配。**排布** 一行决定视频与静态壁纸的基础摆放(填充/适应/拉伸/原始),点「调整画面」可在画布上直接拖动平移、滚轮缩放取景。
39
- 4. 用 **新建** 创建轮播列表,从网格挑选或导入 WE 播放列表,然后开启 **自动轮转**。
40
- 5. 资源行显示当前帧率,并提供两个自动暂停开关。系统开启 `prefers-reduced-motion` 时视频默认暂停。
41
- 6. "语言"控件在中英文之间切换面板;选"自动"则跟随 DSH 的语言设置。
42
+ 1. 从缩略图网格选择壁纸,它会在界面背后淡入。壁纸库较大时用搜索框快速定位。
43
+ 2. **暂停/播放** 控制视频播放;**关闭** 淡出壁纸;**刷新** 重新扫描壁纸库——新订阅的创意工坊壁纸无需重载页面即可出现,已降级的壁纸也会重试原媒体。
44
+ 3. 用滑杆调节融合效果。壁纸太花时调高 **暗化** 和 **边框** 直到文字清晰;开启智能可视后这些自动完成。界面跟随 DSH 明暗主题,无需手动适配。
45
+ 4. **排布** 一行决定基础摆放,点「调整画面」在画布上直接拖动平移、滚轮缩放取景。
46
+ 5. 用 **新建** 创建轮播列表,从网格挑选或导入 WE 播放列表,然后开启 **自动轮转**。
47
+ 6. 资源行显示当前帧率与两个自动暂停开关。
42
48
 
43
- 选择持久化在浏览器的 `localStorage` 中。
49
+ 你的选择与每一项调节都持久化在浏览器的 `localStorage` 中。
44
50
 
45
51
  ## 工作原理
46
52
 
47
- bundle 分为两半:
53
+ bundle 分为两半:
48
54
 
49
- - **宿主插件** 负责发现 Wallpaper Engine 安装位置(Steam 注册表项、`libraryfolders.vdf`、Windows/macOS/Linux 标准探测路径),从默认项目、我的项目和创意工坊目录枚举壁纸,并通过同源路由提供 JSON 清单与媒体数据:
55
+ - **宿主插件** 负责发现 Wallpaper Engine 安装位置(Steam 注册表项、`libraryfolders.vdf`、Windows/macOS/Linux 标准探测路径),从默认项目、我的项目和创意工坊目录枚举壁纸,只保留浏览器可渲染的类型,并通过同源路由提供 JSON 清单与媒体数据:
50
56
  - `GET /we-background/inventory[?refresh=1]`
51
- - `GET /we-background/media/<token>[/资源…]` —— 支持 HTTP Range,视频可拖动进度;网页壁纸可加载其打包的子资源
57
+ - `GET /we-background/media/<token>[/资源…]` —— 支持 HTTP Range,视频可拖动进度;网页壁纸可加载其打包的子资源
52
58
  - `GET /we-background/preview/<token>`
53
- - **浏览器插件** 把选中的壁纸渲染在应用框架之下的固定层,提供设置界面,所有视觉效果都读 DSH 设计令牌,主题切换自然生效。壁纸模糊采用裁边式超采样(内容不变形),叠加微量抖动噪声消除渐变色带;反复解码失败的视频(不支持的容器)退到预览图,下次刷新重试原媒体。
59
+ - **浏览器插件** 把选中的壁纸渲染在应用框架之下的固定层,提供设置界面,所有视觉效果都读 DSH 设计令牌,主题切换自然生效。
54
60
 
55
- 清单由宿主缓存 30 秒,媒体令牌按壁纸稳定复用(后台重建不会掐断播放中的流);安装发现异步执行并缓存 10 分钟。本插件不注册模型工具,不占用 prompt。
61
+ 媒体服务在每一层都有防御:令牌是不可猜测的 72 位随机值,跨清单重建保持稳定(后台刷新绝不掐断播放中的流),仅在显式刷新时重铸;子资源路径先做词法包含检查,再对照真实文件系统复核,符号链接逃不出壁纸目录;HTML 与 SVG 响应带 `sandbox` CSP,永远无法以 DSH 源的身份执行;会短斤缺两的响应自我销毁,而不是悄悄截断。清单由宿主缓存 30 秒,安装发现在请求路径之外异步解析并缓存 10 分钟。本插件不注册模型工具,不占用 prompt。
56
62
 
57
63
  ## 限制
58
64
 
59
- - 场景与应用壁纸完全不出现在列表里:它们是 Wallpaper Engine 私有的 `.pkg` 场景包,动态渲染只能由桌面端的 Wallpaper Engine 完成。
60
- - 网页壁纸与页面存储隔离,需要跨加载保存状态的壁纸无法保留状态。
61
- - 玻璃效果读 DSH 设计令牌;若未来外壳重构重命名令牌,磨砂会退化为普通半透明。
65
+ - 场景与应用壁纸不出现在列表中:它们是 Wallpaper Engine 私有的 `.pkg` 场景包,动态渲染只能由桌面端的 Wallpaper Engine 完成。
66
+ - 网页壁纸与页面存储隔离,需要跨加载保存状态的壁纸无法保留状态。
67
+ - 玻璃效果读 DSH 设计令牌;若未来外壳重构重命名令牌,磨砂会退化为普通半透明。
62
68
 
63
69
  ## 开发
64
70
 
65
71
  ```sh
66
72
  npm install # prepare 钩子自动构建 lib/client.js
67
- npm test # 纯核心层单元测试(node:test)
73
+ npm test # 纯核心单元测试 + 线级宿主集成测试
68
74
  npm run build # 从 src/client.js 重新生成 lib/client.js
69
75
  npm run verify # 在 vm 沙箱中启动产物并断言行为
70
76
  ```
71
77
 
72
- 目录结构:`lib/core.js`(纯逻辑,有测试)、`lib/index.js`(宿主插件)、`src/client.js`(浏览器源码)→ `lib/client.js`(构建产物,请勿手改)。`npm run prepublishOnly` 执行完整门禁:test → build → verify。
78
+ 目录结构:`lib/core.js`(纯逻辑,有测试)、`lib/index.js`(宿主插件)、`src/client.js`(浏览器源码)→ `lib/client.js`(构建产物,请勿手改)。`npm run prepublishOnly` 执行完整门禁:test → build → verify。
73
79
 
74
80
  ## 发布
75
81
 
package/lib/client.js CHANGED
@@ -271,7 +271,9 @@ window.__ModuleLoader__.load({
271
271
  autoScrim: o.autoScrim !== false,
272
272
  lang: o.lang === "zh" || o.lang === "en" ? o.lang : "auto",
273
273
  fit: FIT_MODES.indexOf(o.fit) >= 0 ? o.fit : DEFAULTS.fit,
274
- zoom: clampNum(o.zoom, 0.25, 4, DEFAULTS.zoom),
274
+ zoom: Math.max(1, clampNum(o.zoom, 0.25, 4, DEFAULTS.zoom)),
275
+ // Pan may have been persisted with a larger headroom than the zoom
276
+ // allows (older builds let zoom go below 1) — clampCrop re-seats it.
275
277
  offsetX: clampNum(o.offsetX, -200, 200, DEFAULTS.offsetX),
276
278
  offsetY: clampNum(o.offsetY, -200, 200, DEFAULTS.offsetY),
277
279
  };
@@ -298,6 +300,9 @@ window.__ModuleLoader__.load({
298
300
  inventory: { installDir: null, wallpapers: [], total: 0, portableCount: 0, playlists: [], error: null },
299
301
  loaded: false,
300
302
  };
303
+ // Older builds allowed zoom < 1 and unclamped pans — re-seat any persisted
304
+ // crop into the gap-free envelope before the first render.
305
+ clampCrop();
301
306
 
302
307
  const listeners = new Set();
303
308
  function emit() { for (const fn of [...listeners]) fn(); }
@@ -724,6 +729,18 @@ window.__ModuleLoader__.load({
724
729
  // The layer normally sits at z-index -2 behind the entire app, so it can
725
730
  // never receive pointer events. Adjust mode moves the LIVE layer node into a
726
731
  // topmost transparent overlay — the user edits the real thing, WYSIWYG.
732
+ //
733
+ // Crop geometry invariants: the media box covers the viewport exactly at
734
+ // zoom 1; zooming creates headroom, panning spends it. Zoom floors at 1
735
+ // and pan is clamped to ±(zoom-1)/2·100 %, so a gap-free fullscreen image
736
+ // is guaranteed at every setting.
737
+ function clampCrop() {
738
+ selection.zoom = Math.round(Math.min(4, Math.max(1, selection.zoom)) * 100) / 100;
739
+ const pan = (selection.zoom - 1) * 50;
740
+ selection.offsetX = Math.min(pan, Math.max(-pan, selection.offsetX));
741
+ selection.offsetY = Math.min(pan, Math.max(-pan, selection.offsetY));
742
+ }
743
+
727
744
  let adjustOverlay = null;
728
745
  let adjustCleanup = null;
729
746
 
@@ -780,11 +797,9 @@ window.__ModuleLoader__.load({
780
797
  // content tracks the cursor 1:1 at any blur setting.
781
798
  const w = (overlay.clientWidth || 1) + 2 * selection.wallpaperBlur;
782
799
  const h = (overlay.clientHeight || 1) + 2 * selection.wallpaperBlur;
783
- // True clamping (clampNum is a validate-or-fallback for persisted data).
784
- selection.offsetX = Math.min(200, Math.max(-200,
785
- selection.offsetX + ((e.clientX - drag.x) / w) * 100));
786
- selection.offsetY = Math.min(200, Math.max(-200,
787
- selection.offsetY + ((e.clientY - drag.y) / h) * 100));
800
+ selection.offsetX += ((e.clientX - drag.x) / w) * 100;
801
+ selection.offsetY += ((e.clientY - drag.y) / h) * 100;
802
+ clampCrop(); // gap-free guarantee: pan never outruns the zoom headroom
788
803
  drag = { x: e.clientX, y: e.clientY };
789
804
  applyEffects();
790
805
  };
@@ -802,12 +817,12 @@ window.__ModuleLoader__.load({
802
817
  };
803
818
  const onWheel = (e) => {
804
819
  if (typeof e.preventDefault === "function") e.preventDefault();
805
- // True clamping to [0.25, 4] — clampNum is a validate-or-fallback for
806
- // persisted data and would REJECT an out-of-range zoom instead of
807
- // pinning it to the boundary.
808
- const next = Math.min(4, Math.max(0.25,
809
- selection.zoom * Math.exp(-(e.deltaY || 0) * 0.0012)));
810
- selection.zoom = Math.round(next * 100) / 100;
820
+ // Crop semantics: zoom IN only (1–4×). Zooming below cover would float a
821
+ // shrunken box in empty space — that is not a crop. Tightening zoom also
822
+ // re-clamps the pan into the new headroom, so zooming back to 1 pulls
823
+ // the framing home instead of leaving edge gaps.
824
+ selection.zoom *= Math.exp(-(e.deltaY || 0) * 0.0012);
825
+ clampCrop();
811
826
  applyEffects();
812
827
  persistSelection();
813
828
  };
@@ -1830,6 +1845,12 @@ window.__ModuleLoader__.load({
1830
1845
  const inject = ["slots"];
1831
1846
 
1832
1847
  function apply(ctx) {
1848
+ // A plugin UPDATE re-applies this module WITHOUT re-evaluating it: the old
1849
+ // fiber's cleanup has already set `disposed = true`, which would make the
1850
+ // new fiber's loadInventory return forever (blank picker, no wallpaper).
1851
+ // Every apply starts a fresh lifecycle.
1852
+ disposed = false;
1853
+
1833
1854
  // 0. Language: follow the shell's locale service when present, and react
1834
1855
  // to its change events. Everything is optional — without the service
1835
1856
  // the browser language decides, and the manual override always wins.
package/lib/index.js CHANGED
@@ -228,10 +228,11 @@ export function createRouteRegistrar(webServer, opts = {}) {
228
228
  res.setHeader('X-Content-Type-Options', 'nosniff');
229
229
  if (mime === 'text/html' || mime === 'image/svg+xml') {
230
230
  // Script-capable documents (web wallpapers, SVG previews) get a
231
- // sandboxed CSP even on direct top-level navigation, so they can
232
- // never execute against the DSH origin. The client's iframe sandbox
233
- // already covers embedding; this is the server-side backstop.
234
- res.setHeader('Content-Security-Policy', 'sandbox');
231
+ // sandbox CSP even on direct top-level navigation, so they can
232
+ // never execute against the DSH origin. `allow-scripts` keeps the
233
+ // wallpaper itself functional — inside the sandbox the origin is
234
+ // opaque either way, so the isolation goal is unchanged.
235
+ res.setHeader('Content-Security-Policy', 'sandbox allow-scripts');
235
236
  }
236
237
 
237
238
  const range = core.parseRangeHeader(req.headers.range, st.size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallpaper-engine-dsh",
3
- "version": "0.1.9",
3
+ "version": "1.0.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",