wallpaper-engine-dsh 1.0.6 → 1.0.7
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 +2 -2
- package/README.zh.md +2 -2
- package/lib/client.js +83 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A DSH bundle that turns your local **Wallpaper Engine** library into the live ba
|
|
|
8
8
|
|
|
9
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
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.
|
|
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); 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
12
|
|
|
13
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.
|
|
14
14
|
|
|
@@ -58,7 +58,7 @@ The bundle has two halves:
|
|
|
58
58
|
- `GET /we-background/preview/<token>`
|
|
59
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.
|
|
60
60
|
|
|
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; the wildcard CORS on media only lets a caller who already knows a token read bytes it could already request — the token stays the capability, and the server is loopback-only; 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.
|
|
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; the wallpaper iframe additionally pins a neutral `color-scheme`, so the shell's dark-theme scheme never leaks into — and breaks — third-party wallpaper apps; the wildcard CORS on media only lets a caller who already knows a token read bytes it could already request — the token stays the capability, and the server is loopback-only; 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.
|
|
62
62
|
|
|
63
63
|
## Limitations
|
|
64
64
|
|
package/README.zh.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**只列出能真实播放的壁纸。** 网格中列出浏览器可以真实渲染的全部类型——视频与网页壁纸。场景/应用壁纸是 Wallpaper Engine 私有的 `.pkg` 场景包,浏览器无法执行,因此直接过滤,不以降级的静态图代替。列出的每张壁纸都真实播放,并按 视频/网页 标注类型;搜索框可按标题或创意工坊 ID 筛选较大的壁纸库。
|
|
10
10
|
|
|
11
|
-
**任何壁纸上文字都可读。**
|
|
11
|
+
**任何壁纸上文字都可读。** 遮罩颜色始终与文字颜色相反:深色主题用黑色遮罩,浅色主题用白色遮罩,因此过亮或过暗的壁纸都不会盖住文字。在此基础上,「智能可视」每隔几秒采样一次壁纸亮度,亮度过高或过低时自动加强遮罩(网页壁纸的像素在沙箱中读不到,改用其预览图作为采样依据)。下拉菜单、弹窗等浮层显示在 DSH 原生的不透明表面上,不受影响。
|
|
12
12
|
|
|
13
13
|
**手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮缩放,拖动平移。前四种排布下裁剪范围经过限制,画面始终铺满屏幕,不露出边缘;「自由」把壁纸按原始大小放在黑色画布上,可拖动到任意位置,缩放范围 0.1–4×,并可旋转任意角度(±90° 按钮或 Alt+拖动),同时始终保留一小块在屏幕内,不会拖丢。「镜像」水平翻转壁纸,所有排布下都可用——翻转不会破坏铺满。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸使用相同的排布下拉与缩放/平移/旋转取景(变换直接作用于 iframe)。
|
|
14
14
|
|
|
@@ -58,7 +58,7 @@ bundle 分为两半:
|
|
|
58
58
|
- `GET /we-background/preview/<token>`
|
|
59
59
|
- **浏览器插件** 把选中的壁纸渲染在应用框架之下的固定层,提供设置界面,所有视觉效果都读取 DSH 设计令牌,主题切换时自动适配。
|
|
60
60
|
|
|
61
|
-
媒体服务在多个层面做了防护:令牌是无法猜测的 72 位随机值,清单重建时保持不变(后台刷新不会中断正在播放的流),只在显式刷新时重新生成;子资源路径先做词法包含检查,再对照真实文件系统复核,符号链接无法指向壁纸目录之外;HTML 与 SVG 响应带 `sandbox` CSP,不能以 DSH
|
|
61
|
+
媒体服务在多个层面做了防护:令牌是无法猜测的 72 位随机值,清单重建时保持不变(后台刷新不会中断正在播放的流),只在显式刷新时重新生成;子资源路径先做词法包含检查,再对照真实文件系统复核,符号链接无法指向壁纸目录之外;HTML 与 SVG 响应带 `sandbox` CSP,不能以 DSH 源的身份执行;壁纸 iframe 固定使用中性 `color-scheme`,外壳的深色主题不会泄漏进(并破坏)第三方壁纸应用;媒体响应的 CORS 通配只允许持有令牌的一方读取其本来就能请求的文件,访问权限仍由令牌决定,且整个服务只监听本机回环地址;长度不足的响应会直接中断,而不是悄悄截断。清单由宿主缓存 30 秒;安装位置的发现过程在请求路径之外异步执行,并缓存 10 分钟。本插件不注册模型工具,不占用 prompt。
|
|
62
62
|
|
|
63
63
|
## 限制
|
|
64
64
|
|
package/lib/client.js
CHANGED
|
@@ -622,6 +622,10 @@ window.__ModuleLoader__.load({
|
|
|
622
622
|
if (isPlayable(w) && !demotedToPreview.has(selection.id)) {
|
|
623
623
|
selection.url = w.media;
|
|
624
624
|
selection.type = w.type; // "video" | "web"
|
|
625
|
+
// Brightness evidence belongs to the showing wallpaper: leaving a web
|
|
626
|
+
// wallpaper must drop its preview-based estimate instead of letting it
|
|
627
|
+
// mis-veil the next media until its own first frame arrives.
|
|
628
|
+
if (selection.type !== "web") sampledLuminance = null;
|
|
625
629
|
} else if (isPlayable(w) && w.preview) {
|
|
626
630
|
// Playable wallpaper demoted after repeated media failures (an
|
|
627
631
|
// undecodable container) — its preview still is the graceful fallback.
|
|
@@ -1040,23 +1044,31 @@ window.__ModuleLoader__.load({
|
|
|
1040
1044
|
}, LEAVE_MS);
|
|
1041
1045
|
}
|
|
1042
1046
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1047
|
+
// Scrim: present while a wallpaper is active. On clear it outlives the
|
|
1048
|
+
// layer until the fade-out finishes, so the veil fades WITH the wallpaper.
|
|
1049
|
+
if (selection.url) {
|
|
1050
|
+
const scrim = document.getElementById(SCRIM_ID);
|
|
1051
|
+
if (!scrim) {
|
|
1052
|
+
const s = document.createElement("div");
|
|
1053
|
+
s.id = SCRIM_ID;
|
|
1054
|
+
s.className = "webg-scrim";
|
|
1055
|
+
document.body.appendChild(s);
|
|
1056
|
+
}
|
|
1057
|
+
document.body.setAttribute(ACTIVE_ATTR, "on");
|
|
1058
|
+
// Keep the smart veil's preview probe pointed at the showing web
|
|
1059
|
+
// wallpaper (mount and token re-issue alike).
|
|
1060
|
+
if (selection.type === "web") {
|
|
1061
|
+
const w = selection.inventory.wallpapers.find((x) => x.id === selection.id);
|
|
1062
|
+
if (w && w.preview) probePreviewLuminance(w.preview);
|
|
1063
|
+
}
|
|
1052
1064
|
}
|
|
1053
|
-
document.body.setAttribute(ACTIVE_ATTR, "on");
|
|
1054
|
-
}
|
|
1055
1065
|
}
|
|
1056
1066
|
|
|
1057
1067
|
// ── Effect application: push the knobs into CSS variables ───────────────────
|
|
1058
1068
|
// Wallpaper luminance sampled from the live media (null until the first
|
|
1059
|
-
// sample
|
|
1069
|
+
// sample). Web wallpapers render in an opaque-origin iframe whose pixels are
|
|
1070
|
+
// unreadable — for those, the inventory's PREVIEW still stands in as a
|
|
1071
|
+
// brightness proxy (see probePreviewLuminance).
|
|
1060
1072
|
let sampledLuminance = null;
|
|
1061
1073
|
let sampleCanvas = null;
|
|
1062
1074
|
// Snapshot of every input applyEffects reads, from its last full write. The
|
|
@@ -1065,29 +1077,69 @@ window.__ModuleLoader__.load({
|
|
|
1065
1077
|
// nothing. clearEffects() resets it so the next apply always writes.
|
|
1066
1078
|
let lastEffectKey = null;
|
|
1067
1079
|
|
|
1068
|
-
/** Average Rec.601 luminance (0..1) of
|
|
1069
|
-
function
|
|
1070
|
-
if (typeof document === "undefined") return;
|
|
1071
|
-
const layer = document.getElementById(LAYER_ID);
|
|
1072
|
-
const media = layer && (layer.querySelector("video") || layer.querySelector("img"));
|
|
1073
|
-
if (!media) { sampledLuminance = null; return; }
|
|
1080
|
+
/** Average Rec.601 luminance (0..1) of a drawable media frame, or null. */
|
|
1081
|
+
function luminanceOf(media) {
|
|
1074
1082
|
try {
|
|
1075
1083
|
if (!sampleCanvas) {
|
|
1076
1084
|
const c = document.createElement("canvas");
|
|
1077
|
-
if (typeof c.getContext !== "function") return; // no canvas → no sampling
|
|
1085
|
+
if (typeof c.getContext !== "function") return null; // no canvas → no sampling
|
|
1078
1086
|
c.width = 16; c.height = 16;
|
|
1079
1087
|
sampleCanvas = c;
|
|
1080
1088
|
}
|
|
1081
1089
|
const g = sampleCanvas.getContext("2d", { willReadFrequently: true });
|
|
1082
1090
|
g.drawImage(media, 0, 0, 16, 16);
|
|
1083
1091
|
const d = g.getImageData(0, 0, 16, 16).data;
|
|
1084
|
-
if (!d || d.length < 4) return; // unreadable frame → keep previous sample
|
|
1092
|
+
if (!d || d.length < 4) return null; // unreadable frame → keep previous sample
|
|
1085
1093
|
let sum = 0;
|
|
1086
1094
|
for (let i = 0; i < d.length; i += 4) {
|
|
1087
1095
|
sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];
|
|
1088
1096
|
}
|
|
1089
|
-
|
|
1090
|
-
} catch { /* frame not ready / unreadable → keep
|
|
1097
|
+
return sum / (d.length / 4) / 255;
|
|
1098
|
+
} catch { return null; /* frame not ready / unreadable → keep previous */ }
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Hidden preview probe: whenever a web wallpaper is showing, its preview
|
|
1102
|
+
// still (same-origin through our route) is decoded here so the smart veil
|
|
1103
|
+
// can read the wallpaper's rough brightness even though the sandboxed
|
|
1104
|
+
// iframe's own pixels are forever unreadable.
|
|
1105
|
+
const PREVIEW_PROBE_ID = "wallpaper-engine-dsh-preview-probe";
|
|
1106
|
+
let previewProbe = null;
|
|
1107
|
+
function probePreviewLuminance(url) {
|
|
1108
|
+
if (!url) return;
|
|
1109
|
+
if (!previewProbe) {
|
|
1110
|
+
previewProbe = document.createElement("img");
|
|
1111
|
+
previewProbe.id = PREVIEW_PROBE_ID;
|
|
1112
|
+
previewProbe.alt = "";
|
|
1113
|
+
previewProbe.setAttribute("decoding", "async");
|
|
1114
|
+
// Offscreen, invisible, 1px: it exists to be decoded and drawn, nothing
|
|
1115
|
+
// else. Attached to <body> so tooling (and our verify harness) can see it.
|
|
1116
|
+
previewProbe.setAttribute("style",
|
|
1117
|
+
"position:fixed;left:-10px;top:-10px;width:1px;height:1px;opacity:0;pointer-events:none");
|
|
1118
|
+
previewProbe.addEventListener("load", () => {
|
|
1119
|
+
// A GIF preview draws its first frame — plenty for a brightness proxy.
|
|
1120
|
+
const lum = luminanceOf(previewProbe);
|
|
1121
|
+
if (lum !== null && selection.type === "web") {
|
|
1122
|
+
sampledLuminance = lum;
|
|
1123
|
+
applyEffects();
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
document.body.appendChild(previewProbe);
|
|
1127
|
+
}
|
|
1128
|
+
if (previewProbe.getAttribute("src") !== url) previewProbe.src = url;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function sampleWallpaperLuminance() {
|
|
1132
|
+
if (typeof document === "undefined") return;
|
|
1133
|
+
const layer = document.getElementById(LAYER_ID);
|
|
1134
|
+
const media = layer && (layer.querySelector("video") || layer.querySelector("img"));
|
|
1135
|
+
if (!media) {
|
|
1136
|
+
// Nothing live to sample. A web wallpaper keeps its preview-based
|
|
1137
|
+
// estimate; any other kind has no brightness evidence at all.
|
|
1138
|
+
if (selection.type !== "web") sampledLuminance = null;
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const lum = luminanceOf(media);
|
|
1142
|
+
if (lum !== null) sampledLuminance = lum;
|
|
1091
1143
|
}
|
|
1092
1144
|
|
|
1093
1145
|
function applyEffects() {
|
|
@@ -1759,12 +1811,20 @@ window.__ModuleLoader__.load({
|
|
|
1759
1811
|
Gaussian's natural edge fade (≤ one blur radius, under the veil and the
|
|
1760
1812
|
UI chrome) is accepted. A slight saturate keeps blurred colours from
|
|
1761
1813
|
washing out. */
|
|
1814
|
+
/* color-scheme: normal stops the DSH theme from leaking INTO the sandboxed
|
|
1815
|
+
web-wallpaper iframe. The shell sets color-scheme: dark on <html> in the
|
|
1816
|
+
dark theme; that value propagates to embedded documents, and at least one
|
|
1817
|
+
official wallpaper (CORSAIR Collection, an Angular app) never paints
|
|
1818
|
+
under an inherited dark scheme — a solid white page instead. The value
|
|
1819
|
+
"normal" restores the neutral default; a wallpaper that opts into a
|
|
1820
|
+
scheme via its own styles still wins. */
|
|
1762
1821
|
.webg-layer .webg-media {
|
|
1763
1822
|
position: absolute;
|
|
1764
1823
|
inset: 0;
|
|
1765
1824
|
width: 100%;
|
|
1766
1825
|
height: 100%;
|
|
1767
1826
|
max-width: none; max-height: none;
|
|
1827
|
+
color-scheme: normal;
|
|
1768
1828
|
object-fit: var(--webg-fit, cover);
|
|
1769
1829
|
object-position: center;
|
|
1770
1830
|
display: block;
|
|
@@ -2164,6 +2224,7 @@ window.__ModuleLoader__.load({
|
|
|
2164
2224
|
if (samplerId !== null) window.clearInterval(samplerId);
|
|
2165
2225
|
if (observer) observer.disconnect();
|
|
2166
2226
|
sampledLuminance = null;
|
|
2227
|
+
if (previewProbe) { previewProbe.remove(); previewProbe = null; }
|
|
2167
2228
|
clearRotationTimer();
|
|
2168
2229
|
// Cancel pending crossfade/clear timers so they never fire on
|
|
2169
2230
|
// torn-down nodes after dispose.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallpaper-engine-dsh",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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",
|