dsh-mpkg-wallpaper 3.7.0 → 3.7.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.en.md +335 -18
- package/README.md +337 -22
- package/THIRD-PARTY.md +135 -0
- package/icon.svg +5 -0
- package/lib/client.js +6470 -642
- package/lib/index.js +1347 -101
- package/lib/pkg-extract.js +870 -2
- package/lib/web-interaction.js +427 -0
- package/lib/web-wallpaper.js +1091 -0
- package/package.json +9 -2
package/README.en.md
CHANGED
|
@@ -36,13 +36,25 @@ A plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harn
|
|
|
36
36
|
- Appearance tab also has: floating cards, etc. (the clock is a runtime-compat item — old configs still show it, but there is no settings toggle)
|
|
37
37
|
|
|
38
38
|
**🧩 dsh-better-sidebar adaptation (shown when that plugin is detected)**
|
|
39
|
-
- When
|
|
40
|
-
- **Floating double-layer fix** (bsFloat):
|
|
39
|
+
- When dsh-better-sidebar is installed, an **adaptation section** appears in the **"Other" tab** (not "Appearance") with a master toggle + sub-toggles:
|
|
40
|
+
- **Floating double-layer fix** (bsFloat): 14px rounded shell with `overflow:hidden` (clips inner right angles / active-tab pill), transparent inner `pane/tabBar/terminalWrap`, **zero outer margin** (the panel's left/right are aligned by better-sidebar's own ResizeObserver — margins shift it 8px and leave a 3.6px sliver when collapsed), and the host resize strip is moved inside the panel (host default `top:-4px` gets half-clipped once the shell has rounded corners)
|
|
41
41
|
- **Reveal level** (bsReveal + bsRevealAlpha slider): how much wallpaper shows through the better-sidebar surface (higher = more transparent)
|
|
42
42
|
- **Follow theme / Aqua** (bsAlpha / bsAqua): better-sidebar panel follows the theme base / the unified-fog color
|
|
43
43
|
- **Bottom panel avoidance** (bsBottomAvoid): the bottom panel stays aligned with the DSH center column (handled by better-sidebar's own ResizeObserver — no manual offset)
|
|
44
44
|
- Font follow (bsFont) and other sub-toggles
|
|
45
45
|
- The host `/ping` endpoint auto-detects whether better-sidebar is installed; the section is hidden when it is not
|
|
46
|
+
- **Version-aware adaptation**: the host also reports the installed better-sidebar version (e.g. `0.19.1`) and the
|
|
47
|
+
client writes `body[data-mpw-bs-version]`, so version-specific rules can be gated with
|
|
48
|
+
`[data-mpw-bs-version^="…"]` (e.g. the 0.16+ floating-window reveal; 0.19 removed floating windows upstream, so
|
|
49
|
+
that rule simply stops matching). Panel-level rules additionally carry the 0.19 stable attribute hooks
|
|
50
|
+
`[data-dsh-bottom-panel]` / `[data-dsh-pane]`, so a CSS-module hash change cannot silently break them.
|
|
51
|
+
**2026-09-17: two "looks adapted but never actually ran" root causes were fixed** (`/ping` always returned a
|
|
52
|
+
`null` version; version probing only ran when the settings panel was opened). Criteria, real-machine evidence and
|
|
53
|
+
reproduction commands: [`docs/BETTER-SIDEBAR-COMPAT.md`](docs/BETTER-SIDEBAR-COMPAT.md); the 0.19.1 DOM contract:
|
|
54
|
+
[`docs/BETTER-SIDEBAR-DOM-CONTRACT-0.19.1.md`](docs/BETTER-SIDEBAR-DOM-CONTRACT-0.19.1.md).
|
|
55
|
+
Regression: `node tools/better-sidebar-compat-test.mjs` (41 assertions, including a mutation case that must turn
|
|
56
|
+
red when the old code shape comes back, plus an anchor canary against the installed version; wired into
|
|
57
|
+
`tools/check.sh` step 10).
|
|
46
58
|
|
|
47
59
|
**⏯️ Playback control & power saving**
|
|
48
60
|
- **Pause / Play button**: when the current wallpaper is a video/web type, the settings page shows a **Pause/Play** button (click to freeze the image, click again to resume). The paused state is **synced in real-time** (the button follows the actual video state); **adjusting unrelated settings (mute/brightness/blur etc.) does NOT trigger a replay** — the root cause ("video.src" compared as an absolute URL to a relative one → every settings apply reloaded the media source) has been fixed.
|
|
@@ -57,7 +69,7 @@ A plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harn
|
|
|
57
69
|
- Based on CSS `backdrop-filter`: semi-transparent + blur + edge highlight (**no longer the WebGL refraction version** — WebGL was removed in v3.6.0, see below). Four toggles:
|
|
58
70
|
- **lgTest (test mode)**: keeps only wallpaper + floating + layout, and overrides no DSH token (otherwise a translucent base makes the chat box transparent without blur)
|
|
59
71
|
- **lgComposer / lgSidebar / lgHeader**: add a liquid-glass overlay to the message-bubble area / sidebar / title bar respectively (the sidebar can only be semi-transparent + edge highlight because of the settings dialog's render hierarchy — it **cannot** use backdrop-filter, or it would squash the settings dialog into the sidebar — historical pitfall)
|
|
60
|
-
- **History**: early versions used real WebGL refraction (`lib/liquid-glass/` library + `liquid-glass-bundle.js` 107KB); v3.6.0 removed the WebGL runtime (unstable + large) in favor of pure CSS. `lib/liquid-glass/*.js
|
|
72
|
+
- **History**: early versions used real WebGL refraction (`lib/liquid-glass/` library + `liquid-glass-bundle.js` 107KB); v3.6.0 removed the WebGL runtime (unstable + large) in favor of pure CSS. `lib/liquid-glass/*.js` and `liquid-glass-bundle.js` still ship (`files: ["lib"]`) but are **no longer referenced by the client**; the host still serves the `/api/mpkg-wallpaper/lg` route (no callers). the host-side static route is a **live path** (`/api/mpkg-wallpaper/lg/<file>.js` really `readFileSync`s `lib/liquid-glass/<file>`), and the `tools/liquid-demo/` demo page now uses that same mount (P-122). `tools/liquid-demo/` stays in the repo only and is **not shipped** (`files` excludes `tools/`). So they are **not dead files**; removing them is a release-surface decision (review: `docs/LIQUID-GLASS-DEDUP.md`).
|
|
61
73
|
|
|
62
74
|
**🎬 Lens & picture**
|
|
63
75
|
- Lens zoom (10–2000%) & pan, brightness (50–150%), light sharpen, Deep diving background box
|
|
@@ -92,6 +104,9 @@ A plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harn
|
|
|
92
104
|
- **Scene cache is byte-bounded**: layer cache has a 128MB byte budget + count cap double-guard; scene.pkg is only read in full on a cache miss (stat-first)
|
|
93
105
|
- **Listeners/timers register once**: storage listener, 60s slot check, inline-style watcher, etc. are de-duplicated — repeated apply/RTC reconnects never accumulate
|
|
94
106
|
- **Lazy loading prevents OOM**: time-variation wallpapers extract only the current slot; hybrid streams large files with minimal memory
|
|
107
|
+
- **The audio list no longer waits for the whole package (2026-09-15, user report #1)**: a `scene.pkg` keeps its **directory table at the start of the file**, so "which audio tracks does this package have" needs only that table plus a 16-byte magic sniff per candidate entry (track bytes never enter memory). Two new entry points: `/custom-scene-audio?folder=` and `/library-scene-audio?ltoken=`, returning `{count,tracks:[{path,size,mime,refs}],stats}` (measured on the 22.5MB hina package: **2.3ms / 107KB read**; 0.5ms on a cache hit), and `/raw` now supports **Range/206** (it always returned 200 + the whole body before, so a renderer could not even fetch just the index). Measured with `tools/audio-scan-bench.mjs` (11 real packages, median of 3): whole-package read 1.7–379ms ⇒ index read 1.3–6.9ms cold / 0.4–0.9ms warm, with the track list **item-for-item identical** to this repository's own spec `docs/AUDIO-TRACK-SPEC.md` (11/11 packages, including FLAC/OggS/ID3/ftyp and scene.json layer refs). The track-detection/collection section was clean-room rewritten against that spec on 2026-09-16 (provenance: `THIRD-PARTY.md`).
|
|
108
|
+
- **Scene video probing is now "index-first" (2026-09-15, user report #1 ⑥c)**: `ensureSceneVideo` used to sit on the **critical path of applying a wallpaper** — `readFileSync` of the whole package plus full mipmap decompression of every `.tex` (the client even sets a 4s timeout for this step). It now reads only the **directory table** plus a **prefix** of each candidate entry (`.tex`: walk the header + first mipmap record and read the first 12 payload bytes to test `ftyp`; when mip0 is LZ4 only the first sequence is decompressed; standalone video entries win and are the only ones read; it stops after confirming 2 embedded videos), falling back to a full read whenever anything is uncertain. **The selection result, the on-disk cache file name (same hash formula) and its content sha256 are item-for-item identical to before.** Measured across 11 real packages: **2894ms → 532ms cold / 7ms warm**; the 7 no-video packages **1772ms → 18ms**; the 69.5MB Kel'thuzad-class package 771ms → 4ms; all 213 corpus `.tex` prefix decisions are correct. Bench `tools/scene-video-bench.mjs`, gate `tools/scene-video-test.mjs` (26 assertions).
|
|
109
|
+
⚠ **Which path**: this changes the **DSH plugin host** path (`/custom-scene-video-check`). The `:8899` renderer's 🔊 audio panel does **not** go through the plugin — its perceived improvement comes from renderer-side scheduling (see `docs/AUDIO-SCAN-FAST.md` §0).
|
|
95
110
|
- **Weak-device throttling**: heavy compositing (full-screen backdrop-filter over streaming video) is globally throttled; for extreme WebView combos, Edge / desktop browsers still give the best experience
|
|
96
111
|
|
|
97
112
|
**🌐 Browser compatibility (tested reference)**
|
|
@@ -112,7 +127,7 @@ A plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harn
|
|
|
112
127
|
| **mpkg (scene)** | 🟡 Partial | static-frame / layer composite / preview animation (below); slots with video textures auto-switch |
|
|
113
128
|
| **Time-variation wallpaper** | ✅ Full | multi-slot auto-switch + manual lock, lazy loading to avoid OOM |
|
|
114
129
|
| **Video (mp4/webm)** | ✅ Full | plays directly |
|
|
115
|
-
| **Web (HTML)** | 🟡 Experimental | sandboxed iframe; **web wallpapers with settings are wired into the plugin (below)**; **interactive-feature wallpapers not adapted yet** |
|
|
130
|
+
| **Web (HTML)** | 🟡 Experimental | **sandboxed iframe + a WE API shim injected before author scripts** (property / audio / media / slideshow callbacks work); **web wallpapers with settings are wired into the plugin (below)**; **interactive-feature wallpapers not adapted yet** |
|
|
116
131
|
| **Scene raw folder (scene.pkg)** | 🟡 Partial | same as mpkg scene |
|
|
117
132
|
| **Application (.exe)** | ❌ Excluded | safety: never read/executed |
|
|
118
133
|
|
|
@@ -146,12 +161,35 @@ The plugin offers these partial solutions (chosen automatically by scene content
|
|
|
146
161
|
|
|
147
162
|
## Web Wallpapers (Experimental)
|
|
148
163
|
|
|
149
|
-
- HTML wallpapers load full-screen in a **sandboxed iframe
|
|
164
|
+
- HTML wallpapers load full-screen in a **sandboxed iframe**; **webUrl is persisted** — auto-recovers after refresh / route changes / RTC reconnects without losing config (refresh the page manually if it ever freezes)
|
|
165
|
+
- **Two loading modes** (pick one in the confirm dialog; the mode is stored inside webUrl and survives reloads):
|
|
166
|
+
- **Sandbox mode (default, recommended)**: the host injects a **WE API shim** as the first `<script>` in the entry HTML's `<head>`
|
|
167
|
+
(`window.wallpaperPropertyListener`, `wallpaperRegisterAudioListener`, `wallpaperRegisterMedia*Listener`,
|
|
168
|
+
`wallpaperRequestRandomFileForProperty`, …), so wallpapers that rely on the WE API actually run. The iframe's `sandbox`
|
|
169
|
+
is **`allow-scripts` only** (opaque origin) ⇒ wallpaper scripts **cannot reach the DSH UI or local storage**.
|
|
170
|
+
Trade-off: the parent cannot read the frame's DOM either, so mute / speed / pause are executed in-frame by the shim.
|
|
171
|
+
- **Compatibility mode (same-origin)**: equivalent to the previous bare iframe
|
|
172
|
+
(`allow-scripts allow-same-origin allow-pointer-lock`). Required by the "Web wallpaper options" of Live2D-style
|
|
173
|
+
wallpapers (resolution / language / volume, written to the frame's same-origin `localStorage`).
|
|
174
|
+
- **Property / media wiring**: on mount the wallpaper's `project.json` `general.properties` defaults (overlaid with your
|
|
175
|
+
saved `propEdits`) are pushed to the shim; media files in the wallpaper folder become the slideshow pool
|
|
176
|
+
(`__mpw-list.json`). Mute / speed / pause are delivered in-frame via postMessage.
|
|
177
|
+
- **Type detection is content-first, not declaration-first**: `general.type` is only a hint —
|
|
178
|
+
a package that claims `web` but ships `scene.pkg` is treated as a **scene**, and one that claims `scene`/`video`
|
|
179
|
+
but only has `index.html` is treated as a **web** wallpaper (four states: web / scene / video / unknown;
|
|
180
|
+
`application/exe` is always excluded).
|
|
150
181
|
- **Risk preflight**: auto-classified during scan; badges shown in the list and confirm dialog:
|
|
151
|
-
- **⚠heavy animation**: Spine/L2D skeletal wallpapers — may freeze on low-end devices
|
|
182
|
+
- **⚠heavy animation**: Spine/L2D skeletal wallpapers — may freeze on low-end devices (use compatibility mode for these)
|
|
152
183
|
- **🌐external**: depends on external SDK/CDN (e.g. miHoYo event pages) — may fail to load
|
|
184
|
+
- **Author script errors never break the plugin**: the in-frame shim catches listener exceptions, global `error` events and
|
|
185
|
+
unhandled promise rejections, and rate-limits reports to the parent (`console.warn` + `/diag`).
|
|
153
186
|
- Tested: webm-video-based web wallpapers (light) work; Spine skeletal ones depend on device performance; **Live2D portraits with a `loadJson.json` are wired into the plugin options** (above)
|
|
154
187
|
|
|
188
|
+
> Details (detection table, per-attribute sandbox rationale, shim API / control protocol, file-URL rewriting,
|
|
189
|
+
> error boundary, known limitations, diff against the reference implementation):
|
|
190
|
+
> [`docs/WEB-WALLPAPER.md`](docs/WEB-WALLPAPER.md) (Chinese).
|
|
191
|
+
> Regression: `node tools/web-wallpaper-test.mjs` (wired into step 5 of `bash tools/check.sh`, all assertions green).
|
|
192
|
+
|
|
155
193
|
## Settings Tabs (8 total)
|
|
156
194
|
|
|
157
195
|
- **Source**: master switch, hybrid, mpkg file, image/video files, custom folder (can point at the workshop root), local library (Steam scan), switching/rotation, **time-variation slot lock**
|
|
@@ -163,6 +201,198 @@ The plugin offers these partial solutions (chosen automatically by scene content
|
|
|
163
201
|
- **Liquid glass**: lgTest / lgComposer / lgSidebar / lgHeader (CSS version, **experimental, not recommended**; with a separate demo page, see the "Liquid glass" section above)
|
|
164
202
|
- **Other**: power-saving 3-tier (hidden/blur/battery), new style/sharpen/round-compat, update check/apply, **backup & restore**, restore all defaults, submit feedback; the better-sidebar **adaptation section** appears here when that plugin is installed (clock is a runtime-compat item, no settings toggle)
|
|
165
203
|
|
|
204
|
+
## P-66 panel robustness fixes (2026-09-15)
|
|
205
|
+
|
|
206
|
+
> Context: the 9 UI issues reported in that round were confirmed to belong to the **webwallgl test bench (:8901)**,
|
|
207
|
+
> not to the DSH plugin panel. Under that scope this round keeps **only two genuine plugin bugs that are independent
|
|
208
|
+
> of that UI and independently reproducible**; every other interface change was reverted (after the revert
|
|
209
|
+
> `lib/client.js` is byte-identical to the synced copy). Regression: `node tools/panel-fixes-test.mjs`
|
|
210
|
+
> (wired into step 2 of `tools/check.sh`).
|
|
211
|
+
|
|
212
|
+
| Real bug | Root cause | Fix | Repro / assertion |
|
|
213
|
+
| --- | --- | --- | --- |
|
|
214
|
+
| **The render error boundary itself is broken, swallowing the real cause** | The outer `catch (err)` of `MpkgSectionImpl` called `h(...)`, but `h` is a `const` declared **inside the outer `try` block** (block scope is not visible in `catch`) → the boundary throws `h is not defined`, so the user sees "壁纸引擎设置区渲染异常:h is not defined" and the **real error is lost** | That catch now uses `react.createElement` | `node tools/panel-fixes-test.mjs --client <pre-fix copy>` → red (shows `h is not defined`); against the current code → green (shows the real error `boom-body`) |
|
|
215
|
+
| **zh/en dictionaries had different key sets** | `en` was missing 18 keys (`glass.title/desc`, `glassWindow*`, `glass.accent*`, `glass.color*`, `glass.alpha`, `glass.reset`, `flipX/Y*`, `themeColor*`, `rightSidebarBlur.overridden`) → the English UI printed raw keys; the 10 `clock.*` keys existed only in `en` → the Chinese UI showed English; plus two hardcoded Chinese strings (`"当前状态: "`, `"(已重挂)"`) | Both dictionaries now carry **identical key sets** (additions only; the exact count is printed by `node tools/panel-fixes-test.mjs`); the two hardcoded strings go through `t()` (visible Chinese text unchanged) | Same test: identical key sets / all static `t("k")` keys present in both / zero Chinese in the English render / zero raw keys in the Chinese render |
|
|
216
|
+
|
|
217
|
+
## Two on-device bugs fixed at the root (2026-09-16): header frost / right-side timeline
|
|
218
|
+
|
|
219
|
+
| Bug | Root cause (evidence) | Fix | Rollback switch | Regression |
|
|
220
|
+
| --- | --- | --- | --- | --- |
|
|
221
|
+
| **Header frost "never shows up"** | The first statement of `syncHeaderFrost()` calls `normalizeSection(...)`, but that helper was declared **inside another function's body** ⇒ every call threw `ReferenceError`, which the function's own `catch {}` swallowed ⇒ the frost layer was never injected and the diagnostic `reason` stayed empty (on-device diag: `injected:false`, `reason:""`) | Hoisted the normalisation helpers to **module scope**; the catch now writes the exception into `hdrFrostState.reason` (no more silence); the injected frost element and the header's translucent background are now applied **as a pair**, with the background colour coming from our own `--mpw-hdr-frost-bg`; when `wanted=false` we **clean up** instead of leaving a `blur:none` stub layer (a stub suppresses the pseudo-element fallback and kills frost entirely) | `?hdrfrost=legacy` (old gating), `?hdrfrost=off` (fully off), `?hdrblur=pseudo\|element` (A/B) | `node tools/frost-rail-test.mjs` |
|
|
222
|
+
| **Right-side timeline (turn-navigation rail) turns transparent with a wallpaper** | The rail marks are `.eGxaPq_mark::before`; their colours come from `--dsw-alias-border-l4` (`#00000029` / `#fff3`, i.e. 16%/20% alpha) and `--dsw-alias-label-*`. The plugin ① overrode `--dsw-alias-label-*` with `var(--mpw-aqua-ink, inherit)` and self-referencing fallbacks — DSH defines those tokens on `body`, not on `html`, so on `body` they became **guaranteed-invalid** ⇒ the active/preview marks' `background` resolved to unset = transparent; ② a bare `html body { --dsw-specific-sidebar-fill: transparent }` rewrote a host token **globally** and made the chat surfaces transparent ⇒ a 16%-alpha mark painted over the wallpaper is invisible | Token overrides are now **scoped** to an explicit whitelist of containers; the aqua/text-colour overrides are gated by `data-mpw-*` and no longer use `inherit` or self-references; the marks get a theme-derived contrast colour from **our own namespace**, applied only to whitelisted `.eGxaPq_*` nodes (host tokens untouched, no `!important`) | `?railink=off` (disable the contrast compensation), `?sbfill=wide` (restore the old global sidebar-fill override) | `node tools/frost-rail-test.mjs` |
|
|
223
|
+
|
|
224
|
+
> Details and the diagnostic field table: [`docs/HEADER-FROST.md`](docs/HEADER-FROST.md), [`docs/TIMELINE-RAIL-TOKEN.md`](docs/TIMELINE-RAIL-TOKEN.md).
|
|
225
|
+
|
|
226
|
+
## Style-scope guard: why this class of bug cannot come back (2026-09-17, MASTER-TODO §5 item 2)
|
|
227
|
+
|
|
228
|
+
Both bugs above share one mechanism: **nothing owned selector scope**, so a style edit could hit host UI
|
|
229
|
+
without anyone noticing locally. Gate step 12 turns that into a machine-checkable, red-on-regression rule
|
|
230
|
+
(`node tools/style-scope-guard.mjs`):
|
|
231
|
+
|
|
232
|
+
* **It uses the real artifact**: `buildCss` is never re-implemented. `tools/_stub.mjs` loads `lib/client.js`
|
|
233
|
+
in Node and calls the plugin's own `__mpwBuildCss(patch)` over **600+ setting combinations** (613 today:
|
|
234
|
+
defaults / each boolean alone / all 512 combinations of the 9 core switches / the `bsCompat` family /
|
|
235
|
+
numeric 0 and 100 / no wallpaper / lgTest), then parses every generated rule, including `@media` / `@supports` nesting.
|
|
236
|
+
* **Verdict**: every selector must hit our own markers (`.mpw*` / `[data-mpw*]` / `#mpw-*`) or a **registered**
|
|
237
|
+
host/third-party scope. The `bsCompat` block that deliberately targets third-party DOM
|
|
238
|
+
(`[data-dsh-better-sidebar] …`) is allowed **only because it is declared** in the allow-list with a reason and a
|
|
239
|
+
`docs/*.md:line` pointer (pointers are verified at runtime; a rotted pointer fails the gate). Bare element
|
|
240
|
+
selectors (`button{…}`), a bare `*`, `:root` overriding host tokens, host tokens set to transparent/inherit,
|
|
241
|
+
unregistered `!important` token overrides, touching the host turn-navigation rail without our own gate,
|
|
242
|
+
`[data-dsh-panel-host]`, or making the header border transparent ⇒ **red**.
|
|
243
|
+
* **It proves it can discriminate**: `node tools/style-scope-guard.mjs --selftest` copies `lib/client.js` into a
|
|
244
|
+
temp dir and injects 15 mutations (plus a negative control that must still pass), asserting RED/REVIEW/PASS for each.
|
|
245
|
+
* Criteria, the allow-list ledger and "how to register a new entry":
|
|
246
|
+
[`docs/STYLE-SCOPE-GUARD.md`](docs/STYLE-SCOPE-GUARD.md).
|
|
247
|
+
|
|
248
|
+
## Surface token namespace: top bar / sidebar / panels / timeline rail read one `--mpw-*` set (2026-09-18, MASTER-TODO §5 item 1 / P0-3)
|
|
249
|
+
|
|
250
|
+
The requirement reads: "the four surfaces must use **one token namespace** (`--mpw-*`) and
|
|
251
|
+
**never override host tokens** ⇒ structurally eliminate the class of bugs where we break a new host
|
|
252
|
+
feature." Gate step 12's second check makes that mechanical (`node tools/token-namespace-test.mjs`):
|
|
253
|
+
|
|
254
|
+
* **One source**: host tokens are consumed into `--mpw-surface-*` only inside `emitSurfaceTokens()`; the single
|
|
255
|
+
`body{…}` block in the output is the definition point for every surface value, and the four surfaces'
|
|
256
|
+
rules **only** write `var(--mpw-surface-*)`.
|
|
257
|
+
* **Why `body` and not `:root`**: DSH defines `--dsw-static-*` / `--dsw-alias-*` on **`body`** (not on `html`).
|
|
258
|
+
A `var()` inside a custom property is resolved **on the element where it is declared**, so declaring the
|
|
259
|
+
SSOT on `:root` makes it guaranteed-invalid and **inherit that invalidity to every descendant** (consumers
|
|
260
|
+
all fall back to `unset` = transparent). That is the very mechanism behind the historical "timeline rail went
|
|
261
|
+
transparent" bug; a dedicated assertion plus a mutation guard it.
|
|
262
|
+
* **Only one host-token override left**: `buildSidebarFillCss()` (`--dsw-specific-sidebar-fill`, scoped to the
|
|
263
|
+
sidebar allow-list, only in the "sidebar translucent" feature's respective state). The registry
|
|
264
|
+
(`HOST_OVERRIDE_REGISTRY`) demands token + selector + value shape + **activation condition** per entry:
|
|
265
|
+
all 39 override declarations in the output must be registered, and **none may appear in combinations where
|
|
266
|
+
the feature is off**.
|
|
267
|
+
* **Equivalence evidence**: using `git HEAD`'s `lib/client.js` as *before*, 606 setting combinations ×
|
|
268
|
+
light/dark × default/gated states are compared on the four surfaces' **effective values** (tiny cascade
|
|
269
|
+
model + recursive `var()` substitution) ⇒ 25,428 keys match exactly. This is a refactor, not a redesign.
|
|
270
|
+
* Inventory (which token belongs to which surface / which host token is consumed / the registry with reasons /
|
|
271
|
+
known deviations / how to add a token): [`docs/TOKEN-NAMESPACE.md`](docs/TOKEN-NAMESPACE.md).
|
|
272
|
+
|
|
273
|
+
## Switch-wiring audit: no more "the toggle clicks but nothing happens" (2026-09-18)
|
|
274
|
+
|
|
275
|
+
**Real incident**: the CSS for "Accent colour" and "Dark-background text readability" (aquaTextEnhance) was
|
|
276
|
+
wrapped together inside `if (aquaOn(section))`, so **turning on only those two switches generated no rules at
|
|
277
|
+
all** — the toggle was clickable, had no effect, and logged nothing (both blocks' own comments claimed they did
|
|
278
|
+
not depend on Aqua, contradicting the implementation — very hard to spot by reading). Fixed, plus a general
|
|
279
|
+
check: `node tools/switch-wiring-test.mjs` (gate step 2):
|
|
280
|
+
|
|
281
|
+
* every boolean switch must **change the `buildCss` output** in at least one of three contexts
|
|
282
|
+
(default / rich / all-others-on); runtime-only switches must be registered with a `reason`;
|
|
283
|
+
* non-boolean features (`accent` / `aquaTextEnhance` must change the output); `themeColor` is
|
|
284
|
+
"always-emitted CSS + runtime attribute gate", so the check asserts the gate rules exist instead;
|
|
285
|
+
* switches **proven dead** go into `KNOWN_DEAD` and are listed on every run (two-way assertion: fixing one
|
|
286
|
+
requires deleting its entry). That list drove the first fix: **`lgCss` (pure CSS/SVG liquid glass) never ran at
|
|
287
|
+
all** — the block referenced `bdSupported` while the `const` was declared after it, i.e. a same-scope TDZ
|
|
288
|
+
`ReferenceError` swallowed by the outer `catch { /* liquid glass failure must not affect other styles */ }`
|
|
289
|
+
(the catch is kept; it now only fires on real failures). The criteria are two-way: `lgCss:true` must emit the
|
|
290
|
+
glass block (`mix-blend-mode: screen` + `url(#mpw-lg-warp)`), `lgCss:false` must not, and the two must not be
|
|
291
|
+
byte-identical; a mutation restoring the TDZ order must turn all three red. The second entry is fixed too: **`sessionFollow` (new-chat button follows panel opacity)** — the setting page had a
|
|
292
|
+
toggle and copy but nothing read `section.sessionFollow`; implemented per the **user-visible copy** (on = follow
|
|
293
|
+
that opacity, off = **back to the host's original colour**), default unchanged, asserted in
|
|
294
|
+
`tools/switch-wiring-test.mjs` section A4 (three two-way assertions in both default and unified-blur contexts;
|
|
295
|
+
the "remove the read" mutation must go red). The third, `glassWindow`, stays registered and unchanged (copy
|
|
296
|
+
without implementation) — see [`docs/TOKEN-NAMESPACE.md`](docs/TOKEN-NAMESPACE.md) §3b. **`bsCompat` (the better-sidebar adaptation master switch) now defaults to on** (ruled 2026-09-18): the
|
|
297
|
+
bottom-panel float adaptation is settled on real devices, so a default of off meant nobody ever saw it.
|
|
298
|
+
Existing users are migrated **only if they never set it explicitly**; anyone who turned it off by hand is
|
|
299
|
+
**never overridden** (the write path stamps a `bsCompatUserSet` marker; the migration itself does not).
|
|
300
|
+
Asserted by `node tools/bs-compat-default-test.mjs` (gate step 10, 15 assertions + 3 mutations).
|
|
301
|
+
Liquid glass also gained a **`?lgcss=off` kill switch** now that it actually runs (registered in the renderer repo's diagnostics table;
|
|
302
|
+
`node tests/diag-flag-check.mjs` reports 149==149).
|
|
303
|
+
**The header's share of the refraction lives on a pseudo-element**
|
|
304
|
+
(`html body[data-mpw-hdr-frost-el] .wSkVaW_header::before`, `z-index:0`): putting it on `.wSkVaW_header` itself
|
|
305
|
+
turns the header into a **backdrop root** ⇒ floating panels inside the header lose their backdrop sampling
|
|
306
|
+
(frosting fails, text behind shows through sharply) — a real-device regression that `tools/css-matrix.mjs`
|
|
307
|
+
assertion 3 caught with 40 problems. A pseudo-element is not an ancestor of those panels, so sampling still
|
|
308
|
+
works; it is gated on the JS-injected frost layer existing (that layer already raises the header's direct
|
|
309
|
+
children to `z-index:1`, so `z-index:0` lands between background and content), and it is only emitted when the
|
|
310
|
+
header is frosted anyway (`(headerBlur || unifyTint) && headerBg`, matching css-matrix assertion 7).
|
|
311
|
+
Neither assertion was relaxed.
|
|
312
|
+
* discrimination proof: reverting either gate back under `aquaOn` must turn the audit red.
|
|
313
|
+
|
|
314
|
+
## Video-wallpaper transcoding: a **misjudgement** + resource caps (2026-09-17, item 1)
|
|
315
|
+
|
|
316
|
+
> User report: "I'm not using transcoding, my wallpaper is a **video-class mpkg**, the
|
|
317
|
+
> **decode FPS cap is unlimited** and **resolution is the original** — I changed nothing.
|
|
318
|
+
> Is this a bug?" Measured: an `ffmpeg -threads 1 -filter_threads 1 … -i
|
|
319
|
+
> ~/.dsh-mpkg-wallpaper/transcodes/src_1789….bin` process **resident, RSS ≈ 690MB** —
|
|
320
|
+
> the plugin was transcoding the wallpaper the user was **currently playing**.
|
|
321
|
+
|
|
322
|
+
**Verdict: the transcode was a misjudgement (bug).** Evidence: that `src_*.bin` is
|
|
323
|
+
`h264 High L5.2 + aac / MP4` (read with `ffprobe` — any modern browser plays it directly),
|
|
324
|
+
while `settings.json` has `fpsCap=0 / resMax=0` (the user enabled nothing) ⇒ the trigger was
|
|
325
|
+
the client's automatic fallback on `video.error` (`code 3/4`) to `/transcode?fps=24`.
|
|
326
|
+
`code 3/4` only means "this frame failed to decode", **not** "the browser cannot decode this codec".
|
|
327
|
+
|
|
328
|
+
**Fix**: a new **playability gate** (`/probe`, metadata-only, never spawns ffmpeg; verdict =
|
|
329
|
+
codec/container allow-list plus deterministic gaps such as h264+opus-in-MP4 and HEVC Main10;
|
|
330
|
+
"cannot tell" leaves behaviour unchanged) — directly-playable sources are served as-is and only
|
|
331
|
+
genuinely unsupported ones get transcoded. Three real bugs fixed along the way: the old
|
|
332
|
+
`direct-spec` read-through **ignored the codec** (HEVC was served directly → black screen), the
|
|
333
|
+
byte-cap eviction **started from the newest entry** (deleting the artifact it had just produced →
|
|
334
|
+
permanent cache miss), and **cancellation kept retrying other encoders** (a new ffmpeg spawned
|
|
335
|
+
while the user was switching wallpapers). Caps now live in one place: 12 artifacts / **512MB**,
|
|
336
|
+
concurrency **1**, 30s queue, 15min per job, **transcodes default to 1920 wide** (measured 4K
|
|
337
|
+
656MB → 1080p 275MB), **1GB available-memory admission** (refuse rather than swap the machine to
|
|
338
|
+
death), plus a startup prune with a log line. The three states (direct / transcoding / cached)
|
|
339
|
+
are logged and exposed as `window.__mpwWallpaperState` — no more silent 690MB.
|
|
340
|
+
|
|
341
|
+
> Details, verdict table, memory measurements and "measured-and-rejected options":
|
|
342
|
+
> [`docs/TRANSCODE-RESOURCE.md`](docs/TRANSCODE-RESOURCE.md); fallback switch
|
|
343
|
+
> `?mpwtranscode=legacy` (old behaviour) / `aggressive` (probe even user-set caps);
|
|
344
|
+
> regression: `node tools/transcode-limit-test.mjs` (43 assertions, wired into `tools/check.sh`).
|
|
345
|
+
> Debug switches (URL parameters, effective after a refresh, nothing written to settings):
|
|
346
|
+
> `?hdrfrost=legacy|off`, `?hdrblur=pseudo|element`, `?railink=off`, `?sbfill=wide`.
|
|
347
|
+
> After updating, refresh once and hit the diagnostic/report button — the `headerFrost` section of `diag-*.json`
|
|
348
|
+
> shows exactly which link of the chain is broken (`hostHasHeader` / `injected` / `px` / `computed.headerBg` /
|
|
349
|
+
> `computed.frostElBackdrop` / `reason`).
|
|
350
|
+
|
|
351
|
+
## Folder / file picker: behaviour contract & shortcuts (2026-09-17, item 13)
|
|
352
|
+
|
|
353
|
+
> User report: "inside the choose-folder feature, when I scroll the mouse up and down the view sometimes
|
|
354
|
+
> jumps back to the very top, and sometimes it locks at the top — you have never fixed this bug."
|
|
355
|
+
|
|
356
|
+
**Root cause (evidence-based; full write-up in [`docs/DIR-PICKER-SCROLL.md`](docs/DIR-PICKER-SCROLL.md))**:
|
|
357
|
+
the old "restore the scroll position afterwards" code was **dead code** — `dirScrollRef.current` was only ever
|
|
358
|
+
assigned `{anchorIdx, anchorOff}` and **never `ratio`**, so `if (ratio === void 0 || ratio === null) return;`
|
|
359
|
+
always returned early and neither the anchor compensation nor the ratio restore ever ran. On top of that the
|
|
360
|
+
scroller had no `overscroll-behavior: contain` (wheel chaining scrolled the host settings panel), and nothing
|
|
361
|
+
restored the user's `scrollTop` after React recreated the list node (a fresh node starts at `scrollTop = 0`).
|
|
362
|
+
Together these produce the reported "jump to / lock at the top".
|
|
363
|
+
|
|
364
|
+
**Fix (no new third-party dependency)**: the selector now **owns its scroll position** — the container remembers
|
|
365
|
+
the user's `scrollTop` per path (the scroll handler only writes a ref, never state) and writes it back
|
|
366
|
+
**synchronously before paint** in a `useLayoutEffect` (idempotent, so it can never fight the user's own
|
|
367
|
+
scrolling; all timing-window hacks removed). Scroll containers carry `overscroll-behavior: contain` +
|
|
368
|
+
`overflow-anchor: none`, row keys are `full path + directory name` (incremental updates instead of a full
|
|
369
|
+
rebuild), the dialog element has a stable key, and **there is no `focus()`/`autoFocus` anywhere**.
|
|
370
|
+
|
|
371
|
+
| Contract | Meaning |
|
|
372
|
+
|---|---|
|
|
373
|
+
| **Scroll position preserved** | Across refresh / filtering / shorter lists / 500-item directories / host re-renders / **the container node being recreated**, the position stays put (no single frame at the top) |
|
|
374
|
+
| **Never steals focus** | Opening the dialog focuses **the list container itself** with `focus({preventScroll:true})` (so it can never be scrolled into view); every row is `tabindex="-1"` with `mousedown` default prevented, so **no row ever becomes `document.activeElement`** (the active row only changes highlight + `aria-activedescendant`); re-renders never move focus |
|
|
375
|
+
| **Per-directory memory** | Directory A at 60 and directory B at 20 keep their own positions |
|
|
376
|
+
| **No wheel chaining** | Reaching the list boundary never scrolls the settings panel behind it |
|
|
377
|
+
| **Row-level incremental updates** | A refresh only adds/removes the diff (row key = full path; the test asserts row node uids stay the same) — never a full rebuild |
|
|
378
|
+
| **A missing anchor never means 0** | A remembered offset outside the new range is **clamped** (never reset to 0); a missing/invalid value (`null`/`""`/`NaN`) counts as "no anchor" and claims the current position |
|
|
379
|
+
|
|
380
|
+
**Shortcuts** (a visible hint is shown inside the dialog). Click the list (or `Tab` into it) first:
|
|
381
|
+
|
|
382
|
+
| Key | Action |
|
|
383
|
+
|---|---|
|
|
384
|
+
| `↑` / `↓` | Move the active row (no focus stealing; minimal `block:"nearest"` scroll only on key press) |
|
|
385
|
+
| `Home` / `End` | First / last directory |
|
|
386
|
+
| `Enter` | Open the active directory; **with no row selected it means "Choose this folder"** |
|
|
387
|
+
| `Backspace` / `Alt`+`↑` | Parent directory |
|
|
388
|
+
| `Esc` | Close the dialog |
|
|
389
|
+
|
|
390
|
+
**Regression gate**: `node tools/dir-picker-test.mjs` (**57 assertions**; group A is source-level and turns
|
|
391
|
+
**9/10 red** against the old `git show HEAD:lib/client.js`, proving the assertions have discriminating power;
|
|
392
|
+
group B runs a slice of the production implementation against a fake DOM + mini React).
|
|
393
|
+
The behaviour contract is aligned item-by-item with the test bench (8901/8902):
|
|
394
|
+
`docs/DIR-PICKER-SCROLL.md` §5 ↔ `vendor-ref/ww-pages/PATCH-NOTES.md` §10.5.
|
|
395
|
+
|
|
166
396
|
## Installation
|
|
167
397
|
|
|
168
398
|
Published on npm (`dsh-mpkg-wallpaper`). Pick one:
|
|
@@ -184,7 +414,7 @@ pnpm --dir $DSH_HOME/profiles/<profile> add dsh-mpkg-wallpaper
|
|
|
184
414
|
### Option 3: Git clone (developers / offline)
|
|
185
415
|
|
|
186
416
|
```bash
|
|
187
|
-
git clone https://github.com/XHR666/dsh-mpkg-wallpaper.git $DSH_HOME/profiles
|
|
417
|
+
git clone https://github.com/XHR666/dsh-mpkg-wallpaper.git $DSH_HOME/profiles/<profile>/node_modules/dsh-mpkg-wallpaper
|
|
188
418
|
# then register in the profile's cordis.patch.yml:
|
|
189
419
|
# - insert:
|
|
190
420
|
# - id: dsh-mpkg-wallpaper
|
|
@@ -196,24 +426,91 @@ git clone https://github.com/XHR666/dsh-mpkg-wallpaper.git $DSH_HOME/profiles/no
|
|
|
196
426
|
|
|
197
427
|
Uninstall: `dsh plugin --profile web remove dsh-mpkg-wallpaper`.
|
|
198
428
|
|
|
429
|
+
### Option 4: single-file bundle (offline / drop-in; **host half only**)
|
|
430
|
+
|
|
431
|
+
If you would rather not have DSH resolve a package (no npm/pnpm network), inline the host half into one
|
|
432
|
+
self-contained ESM file and register that file:
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
cd /path/to/dsh-mpkg-wallpaper
|
|
436
|
+
node tools/build-bundle.mjs # output: dist/dsh-mpkg-wallpaper.bundle.mjs (~342KB, sha256 printed)
|
|
437
|
+
node tools/build-bundle.mjs --check # parity vs. source: exports / 39 routes / ping JSON shape (20 assertions)
|
|
438
|
+
node tools/bundle-equivalence-test.mjs # full equivalence gate: same route assertions on source and bundle (38)
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Copy `dist/dsh-mpkg-wallpaper.bundle.mjs` anywhere (e.g. `~/.dsh/plugins/`), register it by **absolute path**
|
|
442
|
+
in the profile's `cordis.patch.yml`, then restart `dsh web`:
|
|
443
|
+
|
|
444
|
+
```yaml
|
|
445
|
+
# $DSH_HOME/profiles/<profile>/cordis.patch.yml
|
|
446
|
+
- insert:
|
|
447
|
+
- id: dsh-mpkg-wallpaper
|
|
448
|
+
name: /absolute/path/dsh-mpkg-wallpaper.bundle.mjs # ← the .mjs file itself
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**What this mode does and does not load** (code facts, not guesses):
|
|
452
|
+
|
|
453
|
+
| Item | Option 4 behaviour | Evidence |
|
|
454
|
+
| --- | --- | --- |
|
|
455
|
+
| Host half (upload/streaming + Range, scene extraction, audio listing, settings persistence, diagnostics — 39 routes) | **Complete** (`lib/index.js` + `pkg-extract.js` + `web-wallpaper.js` all inlined; external deps are node builtins only) | `node tools/build-bundle.mjs --check`: route table equal, 39/39 |
|
|
456
|
+
| `GET /api/mpkg-wallpaper/ping` | Same key set as source (`ok`, `version`, `betterSidebar`, `betterSidebarVersion`) | same `--check` run, section ③ |
|
|
457
|
+
| **Client half (settings panel / wallpaper layer / frost)** | **Not loaded.** The single file only exports the host surface (`apply`/`inject`/`__mpwTest`) | DSH discovers client halves **per package**: it scans host Loader entries for packages declaring `dsh.client` and resolves `exports["./client"]` (`@deepseek-ai/dsh-client-modules/lib/index.js:66-70,153-165,650-658`). A bare `.mjs` has no package.json ⇒ no `dsh.client` declaration |
|
|
458
|
+
| `GET /api/mpkg-wallpaper/lg/*` (legacy WebGL hosting route, no client caller) | **404** unless a `liquid-glass/` directory sits next to the bundle; `cp -r lib/liquid-glass <bundle dir>/` makes it byte-identical to source | that route locates `liquid-glass/` via `import.meta.url` (`lib/index.js:3304`); asserted in both layouts by `--check` |
|
|
459
|
+
| `ping.version` | `null` when the bundle's **parent** directory has no `package.json` (version display only) | same `new URL('../package.json', import.meta.url)` (`lib/index.js:1622`); equals source when a companion `package.json` is present |
|
|
460
|
+
| "Check for updates / one-click update" | `update-check` returns 500 without a companion `package.json`; `update-apply` writes files **next to/above the bundle** ⇒ **do not use one-click update in Option 4** | `lib/index.js:1801/1812/1847-1849` |
|
|
461
|
+
| Uninstall | delete the `.mjs` and its line in `cordis.patch.yml` | — |
|
|
462
|
+
|
|
463
|
+
> Bottom line: **Option 4 is a host-side-only, degraded install** (great for offline/emergency use or for reusing
|
|
464
|
+
> the routes from another host). Use Options 1–3 for the full UI. The artifact is **not committed**
|
|
465
|
+
> (`dist/` is gitignored: it is a pure derivative of `lib/*.js` and two builds are byte-identical, asserted in
|
|
466
|
+
> `tools/bundle-equivalence-test.mjs` §②; generate it at release time).
|
|
467
|
+
|
|
468
|
+
## Degraded behaviour without a Wallpaper Engine install (missing WE / non-Windows)
|
|
469
|
+
|
|
470
|
+
"WE installed" means the Steam build of Wallpaper Engine (appid **431960**). The host locates it with
|
|
471
|
+
`locateWallpaperEngine()` (`lib/index.js:303-327`): Windows registry `HKCU\Software\Valve\Steam\SteamPath` →
|
|
472
|
+
common Steam dirs (`C:\Program Files (x86)\Steam`, `D:\Steam`, …) → non-Windows Steam dirs
|
|
473
|
+
(macOS `~/Library/Application Support/Steam`, Linux/Android `~/.local/share/Steam`, WSL `/mnt/c/...`) →
|
|
474
|
+
any library listed in `steamapps/libraryfolders.vdf` containing 431960 → and it only accepts a library where
|
|
475
|
+
`<lib>/steamapps/common/wallpaper_engine/wallpaper32.exe` exists. **If nothing matches it returns `null`**, and
|
|
476
|
+
everything downstream follows the degraded paths below (this Linux box takes exactly that path):
|
|
477
|
+
|
|
478
|
+
| Situation | Actual behaviour (with code location) |
|
|
479
|
+
| --- | --- |
|
|
480
|
+
| WE not installed (or `wallpaper32.exe` not found) | `GET /api/mpkg-wallpaper/steam-inventory` returns **200 `{ok:true, installDir:null, wallpapers:[]}`** — not an error, no 500 (`lib/index.js:2967-2968`) |
|
|
481
|
+
| User clicks "scan local wallpaper library" | List stays empty plus an error line **"Wallpaper Engine install not found (requires Windows + Steam Wallpaper Engine)"** (`lib/client.js:8781` tests `!d.installDir`, string at `lib/client.js:11555`); an empty list also shows "No usable wallpapers found (or not a Windows environment)" (`lib/client.js:10419/11553`). **The scan itself does not fail** — it just returns nothing |
|
|
482
|
+
| WE installed but `projects/myprojects`, `projects/defaultprojects` and `steamapps/workshop/content/431960` are all absent | each root is checked separately (`scan()` starts with `if (!existsSync(root)) return`, `lib/index.js:2976-2977`) ⇒ empty list, and the "install not found" message is **not** shown (because `installDir` is non-null); the UI only shows "No usable wallpapers found (or not a Windows environment)" / an empty rotation group. **Not implemented**: there is no *dedicated* message for "WE installed but its asset dirs are missing" (the existing copy lumps it together with "not Windows"); to diagnose, inspect `installDir`/`wallpapers` from `steam-inventory` |
|
|
483
|
+
| Non-Windows / mobile | same as "WE not installed": `installDir=null` (the registry branch returns null when `process.platform !== 'win32'`, `lib/index.js:282-284`); all Steam probe paths are plain strings, so `existsSync` is simply false — no side effects |
|
|
484
|
+
| WE native playlists (`config.json` → `general.playlists`) | only when `installDir` exists and `config.json` parses; otherwise `playlists` is absent ⇒ after its first scan the client writes `rotSeeded:true` and **stops re-seeding** the rotation list (`lib/client.js:8782-8793`), so user-defined rotations are not overwritten on every scan |
|
|
485
|
+
| **Still works without WE** (the degradation is not "everything breaks") | (1) manual folder picking: `/list-dirs` + `/custom-dir` browse any drive/folder and use it as the wallpaper source; (2) importing `.mpkg` directly (hybrid mode streams from the host, no 600MB cap); (3) URL / local-file web and video wallpapers; (4) scene extraction, audio listing, settings persistence and diagnostics do not depend on WE at all |
|
|
486
|
+
| Host half entirely unavailable (Option 4 missing / port closed) | the client's `/ping` probe fails ⇒ falls back to **browser-only mode**: status line "Host unavailable (fell back to browser-only mode, 600MB cap)" (`lib/client.js:10203/11603`) and the library button reports "Host unavailable — cannot scan the local library" (`lib/client.js:8774/11554`); assets above 600MB cannot be handled in browser-only mode (see Limitations) |
|
|
487
|
+
| `ffmpeg` missing (video transcoding; unrelated to WE) | `GET /api/mpkg-wallpaper/ffmpeg-check` returns **200 `{ok:true, found:false, source:null, path:null, version:null}`** (`lib/index.js:3089-3090`); the client shows "not installed" and only starts the download chain when the user clicks; directly playable videos are never transcoded |
|
|
488
|
+
|
|
489
|
+
> In one line: **no WE install = you lose the "auto-discover the local library" convenience channel**; the plugin
|
|
490
|
+
> still works. Every degraded path returns an empty list with explicit copy and keeps the manual folder/upload
|
|
491
|
+
> channels — nothing fails silently and nothing returns 500.
|
|
492
|
+
|
|
199
493
|
## Limitations
|
|
200
494
|
|
|
201
495
|
- **Scene wallpapers cannot be fully dynamic on the web** (see [Scene wallpaper adaptation](#scene-wallpaper-adaptation)); mpkg adjustable options are read-only (apply changes in the WE app)
|
|
202
|
-
- **Web wallpapers are experimental**: heavy animation / external dependencies may freeze or fail (preflight tags + refresh recovery)
|
|
496
|
+
- **Web wallpapers are experimental**: heavy animation / external dependencies may freeze or fail (preflight tags + refresh recovery).
|
|
497
|
+
In sandbox mode (default) wallpaper scripts **cannot reach the DSH UI or local storage**, but the Live2D-style
|
|
498
|
+
"web wallpaper options" need compatibility mode; the audio-spectrum channel is wired but the plugin has no spectrum
|
|
499
|
+
source yet; **wallpapers with interactive features are not adapted yet** (the wallpaper layer receives no pointer events)
|
|
203
500
|
- **Huge assets** (pure-browser mode): standalone video >600MB, video textures >250MB, images >200MB unsupported; **hybrid mode** has no such limit
|
|
204
501
|
- Scene static-frame / layer-composite **first extraction takes a few seconds** (longer for 8K textures); afterwards served from cache
|
|
205
502
|
|
|
206
503
|
<!-- ## Screenshots
|
|
207
504
|
|
|
208
|
-
<!--
|
|
505
|
+
<!-- screenshot reference removed -->
|
|
209
506
|
|
|
210
507
|
<!-- *The dynamic wallpaper fills the whole UI. Sidebar collapsed, chat box centered with frosted blur; the sidebar is fully transparent so the wallpaper shows through cleanly.* -->
|
|
211
508
|
|
|
212
|
-
<!--
|
|
509
|
+
<!-- screenshot reference removed -->
|
|
213
510
|
|
|
214
511
|
<!-- *After adjusting panel opacity and unified blur: most UI areas are opacity-adjustable, the sidebar is semi-transparent with the wallpaper faintly visible behind.* -->
|
|
215
512
|
|
|
216
|
-
<!--
|
|
513
|
+
<!-- screenshot reference removed -->
|
|
217
514
|
|
|
218
515
|
<!-- *The wallpaper settings page. Beyond the screenshot, nearly everything is adjustable: unified blur, UI blur (dialogs/panels/popups/popovers/mask/sidebar frost), lens zoom & pan, wallpaper flip, theme color, sidebar/title-bar visibility, sharpen, and scene layer compositing with time-frame switching.* -->
|
|
219
516
|
|
|
@@ -229,10 +526,11 @@ Please include:
|
|
|
229
526
|
|
|
230
527
|
## Security
|
|
231
528
|
|
|
232
|
-
- **No passive outbound network requests by default**: the plugin never **actively** contacts external networks; everyday wallpaper playback only talks to the **local DSH host** (127.0.0.1). The only exceptions are **user-initiated** actions: Check update / Apply update accesses GitHub (`raw.githubusercontent.com`, `api.github.com`); downloading ffmpeg accesses GitHub Releases / the npm binary mirror (`registry.npmmirror.com`) —
|
|
529
|
+
- **No passive outbound network requests by default**: the plugin never **actively** contacts external networks; everyday wallpaper playback only talks to the **local DSH host** (127.0.0.1). The only exceptions are **user-initiated** actions: Check update / Apply update accesses GitHub (`raw.githubusercontent.com`, `api.github.com`); downloading ffmpeg accesses GitHub Releases / the npm binary mirror (`registry.npmmirror.com`) — "Apply update"/"download ffmpeg" fire only after the user clicks; **opening the settings panel silently checks the version once after 0.8s** (it only lights up the "update available" badge — no popup, no download, no local data uploaded). User-entered network image URLs and resources loaded by web wallpapers themselves are also external access.
|
|
233
530
|
- **No secrets**: no paths, keys, tokens or personal info in the source
|
|
234
531
|
- **Open-source deps only**: DSH's own react + official slots/locale APIs; the scene.pkg extractor is adopted from [elysia395/dsh-wallpaper-engine](https://github.com/elysia395/dsh-wallpaper-engine) (MIT, credited in the file header)
|
|
235
|
-
-
|
|
532
|
+
- **Web wallpaper sandbox**: in the default mode the wallpaper iframe is an **opaque origin** (`sandbox="allow-scripts"`, no `allow-same-origin`), so author scripts **cannot read the host DOM / `localStorage` / cookies** and cannot strip their own sandbox. Frame and parent only talk via `postMessage` (op whitelist + parent-window source check). Cross-origin reads of wallpaper assets are allowed **only for `Origin: null`** (the sandboxed frame); ordinary websites get no CORS header. Author script errors are caught in-frame and rate-limited (`console.warn` + `/diag`) without affecting the plugin.
|
|
533
|
+
- References: [dsh-bg-image](https://github.com/lyh9712/dsh-bg-image) (MIT, template), [unmpkg](https://github.com/aqnya/unmpkg) (GPL-3.0, mpkg format reference), [repkg](https://github.com/notscuffed/repkg) (**MIT**, .tex format research — this line previously said GPL by mistake; corrected 2026-09-17 from the upstream `LICENSE` text plus the project owner's confirmation, see `../docs/COPYING-RULES.md` §6/§9.10)
|
|
236
534
|
- Data boundary: all parsing happens locally; localStorage only stores the background and settings; settings are additionally stored at `~/.dsh-mpkg-wallpaper/settings.json`
|
|
237
535
|
|
|
238
536
|
## File Structure
|
|
@@ -244,21 +542,38 @@ dsh-mpkg-wallpaper/
|
|
|
244
542
|
├── LICENSE # MIT license
|
|
245
543
|
├── lib/
|
|
246
544
|
│ ├── index.js # host: upload/streaming + Steam discovery + custom folders + scene routes + settings persistence
|
|
545
|
+
│ ├── web-wallpaper.js # web wallpapers: content-first type detection + WE API shim source + entry-HTML injection + CORS policy (MIT, self-written)
|
|
247
546
|
│ ├── client.js # browser: mpkg parsing + settings page + bg DOM + blur suite + library + time-variation/web options + playback control/power saving
|
|
248
547
|
│ ├── pkg-extract.js# scene.pkg static-frame/layer extraction (PKG+LZ4+TEX, MIT, from elysia395)
|
|
249
548
|
│ ├── liquid-glass/ # WebGL liquid-glass library (**leftover, no runtime ref**; CSS version since v3.6.0)
|
|
250
|
-
│ └── liquid-glass-bundle.js # liquid-glass bundle (107KB, **
|
|
251
|
-
├── tools/ #
|
|
252
|
-
|
|
549
|
+
│ └── liquid-glass-bundle.js # liquid-glass bundle (107KB, **not referenced by the client**; byte-rebuildable from the kept sources, sha256 db50361c…)
|
|
550
|
+
├── tools/ # gates/tests/benches + lg build/inline scripts + liquid-demo page (for developers)
|
|
551
|
+
│ # audio scan: audio-scan-bench.mjs (timing table) / audio-scan-test.mjs (spec assertions · no full inflate · cache)
|
|
552
|
+
│ # scene-audio-route-test.mjs (/raw Range + probe route + security)
|
|
553
|
+
│ # web wallpapers: web-wallpaper-test.mjs (detection / sandbox / injection order / shim API diff / error boundary / no GPL)
|
|
554
|
+
│ # single-file install: build-bundle.mjs (inlines lib/index.js + relative deps into one ESM; `--check` for source parity)
|
|
555
|
+
│ # bundle-equivalence-test.mjs (gate step 11: same route assertions on source and bundle + mutation controls)
|
|
556
|
+
│ # style scope: style-scope-guard.mjs (gate step 12: every injected CSS rule must hit .mpw*/[data-mpw*];
|
|
557
|
+
│ # host/third-party scopes must be registered in the allow-list with a reason + docs pointer)
|
|
558
|
+
│ # note: the research-era Python tools (unmpkg/tex2png/mdl_explorer/xref) were
|
|
559
|
+
│ # **deleted (GPL lineage unresolved, 2026-09-16)** — see `../docs/COPYING-RULES.md` §6
|
|
560
|
+
├── dist/ # build output (**not committed**, gitignored): dsh-mpkg-wallpaper.bundle.mjs (Option 4, generated on demand)
|
|
561
|
+
├── docs/ # developer notes (not shipped): WEB-WALLPAPER.md (web-wallpaper spec / sandbox / API table / limits), RELEASE.md (release preconditions + commands) etc.
|
|
562
|
+
├── screenshots/ # (moved out of the repo)
|
|
253
563
|
├── README.md # Chinese
|
|
254
|
-
|
|
564
|
+
├── README.en.md # English
|
|
565
|
+
└── THIRD-PARTY.md # third-party provenance / clean-room record (shipped, MIT-side attribution)
|
|
255
566
|
```
|
|
256
|
-
> Note: `lib/liquid-glass/` and `lib/liquid-glass-bundle.js` still ship in the npm package because of `files: ["lib"]`, but the **client no longer references them** (WebGL was removed in v3.6.0 in favor of the CSS version).
|
|
567
|
+
> Note: `lib/liquid-glass/` and `lib/liquid-glass-bundle.js` still ship in the npm package because of `files: ["lib"]`, but the **client no longer references them** (WebGL was removed in v3.6.0 in favor of the CSS version); the host still serves the `/api/mpkg-wallpaper/lg` route (no callers). Local backups such as `lib/client.js.bak-*` are excluded by the negative `files` patterns (`!lib/**/*.bak*`) and are **not shipped** — enforced by section ⑨ of `tools/integrity-check.mjs`.
|
|
568
|
+
> Why `dist/` is **not committed**: it is a pure derivative of `lib/*.js` (an inlined artifact) that duplicates tracked sources byte for byte, and two builds are byte-identical (machine-asserted in `tools/bundle-equivalence-test.mjs` §②); committing it would only create drift ("edited lib, forgot to rebuild") . By contrast `lib/liquid-glass-bundle.js` **is** committed because it *used to be* a **runtime input** (base64 written into `lib/client.js` by `tools/inline-lg-bundle.mjs`) — ⚠ **measured 2026-09-18: that link is dead**: neither `LG_BUNDLE_B64` nor `LG_BUNDLE_SRC` exists in `lib/client.js` (the only `B64` hit is a comment), and both inline tools now fail with "missing placeholder". It is therefore currently **neither referenced by the client nor a runtime input**; it merely ships via `files: ["lib"]` and can be rebuilt **byte-identically** from the kept sources. `dist/` is also outside the `files` whitelist, so the npm package never carries it.
|
|
257
569
|
|
|
258
570
|
## Acknowledgements
|
|
259
571
|
|
|
572
|
+
> **Naming note (2026-09-18):** the renderer product this plugin integrates with is now called **WEwebLoader**; the upstream project is still **WebWallGL** (`oneincase/webwallgl`, MIT) — its attribution and licence are unchanged.
|
|
573
|
+
|
|
260
574
|
- [Bil812](https://github.com/Bil812) — proposed wallpaper tint, adaptive text and unified full-screen mask in [PR #2](https://github.com/XHR666/dsh-mpkg-wallpaper/pull/2) and maintains a fork; those ideas were absorbed as the "Aqua" experiment mode (toggles, off by default)
|
|
261
575
|
- [elysia395/dsh-wallpaper-engine](https://github.com/elysia395/dsh-wallpaper-engine) — the scene.pkg static-frame extractor (MIT); `lib/pkg-extract.js` is adopted from this project; its "settings persist to host file" and "Edge canvas-compat rendering" ideas are also borrowed
|
|
576
|
+
- [oneincase/webwallgl](https://github.com/oneincase/webwallgl) — sandboxed-iframe + WE API shim approach for web wallpapers (MIT): the **API list and semantics** of `lib/web-wallpaper.js` were studied against that project (**no code copied**; diff list in `docs/WEB-WALLPAPER.md` §10, ledger in `../docs/COPYING-RULES.md` §4)
|
|
262
577
|
- [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) community — listing & promotion
|
|
263
578
|
|
|
264
579
|
## Rendering Feasibility Research
|
|
@@ -266,3 +581,5 @@ dsh-mpkg-wallpaper/
|
|
|
266
581
|
- Full scenes (incl. Live2D puppets) can only be rendered by the proprietary engine: the WE app's native library (embedded Chromium + proprietary puppet renderer); the open-source [we-layerd](https://github.com/Aromatic05/we-layerd) (Rust) bundles the official renderer but is **Linux Wayland only**
|
|
267
582
|
- There is no mature WE scene renderer for browsers (pixeltris/wallpaper-engine-web is gone) — **independent of OS, no browser can render Live2D scenes directly**; the official renderer .so is closed-source, so it cannot be compiled to WASM
|
|
268
583
|
- This plugin's path: **static-frame extraction + layer compositing + (time-variation) mpkg-way slot switching** (see [Scene wallpaper adaptation](#scene-wallpaper-adaptation)); for full dynamics use "render externally to video → video wallpaper"
|
|
584
|
+
|
|
585
|
+
> Screenshots were moved out of the repository (they contain personal UI content): `../Delete/plugin-screenshots/`. Re-add sanitized copies under `docs/media/` if you want them shown.
|