wallpaper-engine-dsh 1.0.6 → 1.0.8
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 +3 -2
- package/README.zh.md +3 -2
- package/lib/client.js +120 -35
- package/lib/core.js +7 -1
- package/lib/index.js +1 -1
- 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,12 +58,13 @@ 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
|
|
|
65
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.
|
|
66
66
|
- Web wallpapers run isolated from the page's storage, so ones that persist state across loads won't keep it.
|
|
67
|
+
- Web wallpapers with sound may stay silent until you interact with the page — browsers gate audio in sandboxed documents on a user gesture, and wallpapers that rely on Wallpaper Engine's system-audio feed (visualizers) have no signal to draw from.
|
|
67
68
|
- The glass effect reads DSH design tokens; if a future shell redesign renames them, frosting degrades to plain transparency.
|
|
68
69
|
|
|
69
70
|
## Development
|
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,12 +58,13 @@ 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
|
|
|
65
65
|
- 场景与应用壁纸不出现在列表中:它们是 Wallpaper Engine 私有的 `.pkg` 场景包,动态渲染只能由桌面端的 Wallpaper Engine 完成。
|
|
66
66
|
- 网页壁纸与页面存储隔离,需要跨加载保存状态的壁纸无法保留状态。
|
|
67
|
+
- 带声音的网页壁纸可能在你与页面交互前保持静默——浏览器对沙箱文档的音频有自动播放限制;依赖 Wallpaper Engine 系统音频输入的壁纸(如音频可视化)在浏览器中没有信号可画。
|
|
67
68
|
- 玻璃效果读 DSH 设计令牌;若未来外壳重构重命名令牌,磨砂会退化为普通半透明。
|
|
68
69
|
|
|
69
70
|
## 开发
|
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.
|
|
@@ -684,6 +688,7 @@ window.__ModuleLoader__.load({
|
|
|
684
688
|
media = document.createElement("iframe");
|
|
685
689
|
media.src = sel.url;
|
|
686
690
|
media.setAttribute("sandbox", "allow-scripts");
|
|
691
|
+
media.setAttribute("allow", "autoplay");
|
|
687
692
|
media.setAttribute("referrerpolicy", "no-referrer");
|
|
688
693
|
media.setAttribute("frameborder", "0");
|
|
689
694
|
media.setAttribute("scrolling", "no");
|
|
@@ -1015,7 +1020,13 @@ window.__ModuleLoader__.load({
|
|
|
1015
1020
|
if (existing) {
|
|
1016
1021
|
existing.classList.add("webg-layer--leave");
|
|
1017
1022
|
const stale = existing;
|
|
1018
|
-
schedule(() =>
|
|
1023
|
+
schedule(() => {
|
|
1024
|
+
const v = stale.querySelector("video");
|
|
1025
|
+
if (v) { v.pause(); v.removeAttribute("src"); if (typeof v.load === "function") v.load(); }
|
|
1026
|
+
const f = stale.querySelector("iframe");
|
|
1027
|
+
if (f) { f.src = "about:blank"; }
|
|
1028
|
+
stale.remove();
|
|
1029
|
+
}, LEAVE_MS);
|
|
1019
1030
|
}
|
|
1020
1031
|
}
|
|
1021
1032
|
const node = document.getElementById(LAYER_ID);
|
|
@@ -1031,32 +1042,50 @@ window.__ModuleLoader__.load({
|
|
|
1031
1042
|
existing.classList.add("webg-layer--leave");
|
|
1032
1043
|
const stale = existing;
|
|
1033
1044
|
schedule(() => {
|
|
1045
|
+
const v = stale.querySelector("video");
|
|
1046
|
+
if (v) { v.pause(); v.removeAttribute("src"); if (typeof v.load === "function") v.load(); }
|
|
1047
|
+
const f = stale.querySelector("iframe");
|
|
1048
|
+
if (f) { f.src = "about:blank"; }
|
|
1034
1049
|
stale.remove();
|
|
1035
1050
|
if (!document.getElementById(LAYER_ID)) {
|
|
1036
1051
|
const s = document.getElementById(SCRIM_ID);
|
|
1037
1052
|
if (s) s.remove();
|
|
1038
1053
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
1054
|
+
if (previewProbe) { previewProbe.remove(); previewProbe = null; }
|
|
1055
|
+
sampledLuminance = null;
|
|
1056
|
+
clearEffects();
|
|
1039
1057
|
}
|
|
1040
1058
|
}, LEAVE_MS);
|
|
1041
1059
|
}
|
|
1042
1060
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1061
|
+
// Scrim: present while a wallpaper is active. On clear it outlives the
|
|
1062
|
+
// layer until the fade-out finishes, so the veil fades WITH the wallpaper.
|
|
1063
|
+
if (selection.url) {
|
|
1064
|
+
const scrim = document.getElementById(SCRIM_ID);
|
|
1065
|
+
if (!scrim) {
|
|
1066
|
+
const s = document.createElement("div");
|
|
1067
|
+
s.id = SCRIM_ID;
|
|
1068
|
+
s.className = "webg-scrim";
|
|
1069
|
+
document.body.appendChild(s);
|
|
1070
|
+
}
|
|
1071
|
+
document.body.setAttribute(ACTIVE_ATTR, "on");
|
|
1072
|
+
// Keep the smart veil's preview probe pointed at the showing web
|
|
1073
|
+
// wallpaper (mount and token re-issue alike).
|
|
1074
|
+
if (selection.type === "web") {
|
|
1075
|
+
const w = selection.inventory.wallpapers.find((x) => x.id === selection.id);
|
|
1076
|
+
if (w && w.preview) probePreviewLuminance(w.preview);
|
|
1077
|
+
} else if (previewProbe) {
|
|
1078
|
+
previewProbe.remove();
|
|
1079
|
+
previewProbe = null;
|
|
1080
|
+
}
|
|
1052
1081
|
}
|
|
1053
|
-
document.body.setAttribute(ACTIVE_ATTR, "on");
|
|
1054
|
-
}
|
|
1055
1082
|
}
|
|
1056
1083
|
|
|
1057
1084
|
// ── Effect application: push the knobs into CSS variables ───────────────────
|
|
1058
1085
|
// Wallpaper luminance sampled from the live media (null until the first
|
|
1059
|
-
// sample
|
|
1086
|
+
// sample). Web wallpapers render in an opaque-origin iframe whose pixels are
|
|
1087
|
+
// unreadable — for those, the inventory's PREVIEW still stands in as a
|
|
1088
|
+
// brightness proxy (see probePreviewLuminance).
|
|
1060
1089
|
let sampledLuminance = null;
|
|
1061
1090
|
let sampleCanvas = null;
|
|
1062
1091
|
// Snapshot of every input applyEffects reads, from its last full write. The
|
|
@@ -1065,29 +1094,69 @@ window.__ModuleLoader__.load({
|
|
|
1065
1094
|
// nothing. clearEffects() resets it so the next apply always writes.
|
|
1066
1095
|
let lastEffectKey = null;
|
|
1067
1096
|
|
|
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; }
|
|
1097
|
+
/** Average Rec.601 luminance (0..1) of a drawable media frame, or null. */
|
|
1098
|
+
function luminanceOf(media) {
|
|
1074
1099
|
try {
|
|
1075
1100
|
if (!sampleCanvas) {
|
|
1076
1101
|
const c = document.createElement("canvas");
|
|
1077
|
-
if (typeof c.getContext !== "function") return; // no canvas → no sampling
|
|
1102
|
+
if (typeof c.getContext !== "function") return null; // no canvas → no sampling
|
|
1078
1103
|
c.width = 16; c.height = 16;
|
|
1079
1104
|
sampleCanvas = c;
|
|
1080
1105
|
}
|
|
1081
1106
|
const g = sampleCanvas.getContext("2d", { willReadFrequently: true });
|
|
1082
1107
|
g.drawImage(media, 0, 0, 16, 16);
|
|
1083
1108
|
const d = g.getImageData(0, 0, 16, 16).data;
|
|
1084
|
-
if (!d || d.length < 4) return; // unreadable frame → keep previous sample
|
|
1109
|
+
if (!d || d.length < 4) return null; // unreadable frame → keep previous sample
|
|
1085
1110
|
let sum = 0;
|
|
1086
1111
|
for (let i = 0; i < d.length; i += 4) {
|
|
1087
1112
|
sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];
|
|
1088
1113
|
}
|
|
1089
|
-
|
|
1090
|
-
} catch { /* frame not ready / unreadable → keep
|
|
1114
|
+
return sum / (d.length / 4) / 255;
|
|
1115
|
+
} catch { return null; /* frame not ready / unreadable → keep previous */ }
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Hidden preview probe: whenever a web wallpaper is showing, its preview
|
|
1119
|
+
// still (same-origin through our route) is decoded here so the smart veil
|
|
1120
|
+
// can read the wallpaper's rough brightness even though the sandboxed
|
|
1121
|
+
// iframe's own pixels are forever unreadable.
|
|
1122
|
+
const PREVIEW_PROBE_ID = "wallpaper-engine-dsh-preview-probe";
|
|
1123
|
+
let previewProbe = null;
|
|
1124
|
+
function probePreviewLuminance(url) {
|
|
1125
|
+
if (!url) return;
|
|
1126
|
+
if (!previewProbe) {
|
|
1127
|
+
previewProbe = document.createElement("img");
|
|
1128
|
+
previewProbe.id = PREVIEW_PROBE_ID;
|
|
1129
|
+
previewProbe.alt = "";
|
|
1130
|
+
previewProbe.setAttribute("decoding", "async");
|
|
1131
|
+
// Offscreen, invisible, 1px: it exists to be decoded and drawn, nothing
|
|
1132
|
+
// else. Attached to <body> so tooling (and our verify harness) can see it.
|
|
1133
|
+
previewProbe.setAttribute("style",
|
|
1134
|
+
"position:fixed;left:-10px;top:-10px;width:1px;height:1px;opacity:0;pointer-events:none");
|
|
1135
|
+
previewProbe.addEventListener("load", () => {
|
|
1136
|
+
// A GIF preview draws its first frame — plenty for a brightness proxy.
|
|
1137
|
+
const lum = luminanceOf(previewProbe);
|
|
1138
|
+
if (lum !== null && selection.type === "web") {
|
|
1139
|
+
sampledLuminance = lum;
|
|
1140
|
+
applyEffects();
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
document.body.appendChild(previewProbe);
|
|
1144
|
+
}
|
|
1145
|
+
if (previewProbe.getAttribute("src") !== url) previewProbe.src = url;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function sampleWallpaperLuminance() {
|
|
1149
|
+
if (typeof document === "undefined") return;
|
|
1150
|
+
const layer = document.getElementById(LAYER_ID);
|
|
1151
|
+
const media = layer && (layer.querySelector("video") || layer.querySelector("img"));
|
|
1152
|
+
if (!media) {
|
|
1153
|
+
// Nothing live to sample. A web wallpaper keeps its preview-based
|
|
1154
|
+
// estimate; any other kind has no brightness evidence at all.
|
|
1155
|
+
if (selection.type !== "web") sampledLuminance = null;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const lum = luminanceOf(media);
|
|
1159
|
+
if (lum !== null) sampledLuminance = lum;
|
|
1091
1160
|
}
|
|
1092
1161
|
|
|
1093
1162
|
function applyEffects() {
|
|
@@ -1095,9 +1164,10 @@ window.__ModuleLoader__.load({
|
|
|
1095
1164
|
// wallpapers, light theme (dark text) brightens dark ones — either way the
|
|
1096
1165
|
// text side wins. This is what makes ANY wallpaper survivable.
|
|
1097
1166
|
const dark = Boolean(document.body.hasAttribute && document.body.hasAttribute("data-ds-dark-theme"));
|
|
1167
|
+
const roundedLum = sampledLuminance !== null ? Math.round(sampledLuminance * 100) / 100 : null;
|
|
1098
1168
|
const key = [
|
|
1099
1169
|
dark, selection.scrim, selection.border, selection.blur, selection.wallpaperBlur,
|
|
1100
|
-
selection.autoScrim,
|
|
1170
|
+
selection.autoScrim, roundedLum, selection.fit, selection.zoom,
|
|
1101
1171
|
selection.offsetX, selection.offsetY, selection.rotation, selection.mirrored,
|
|
1102
1172
|
].join("\u0000");
|
|
1103
1173
|
if (key === lastEffectKey) return; // nothing it reads changed → no writes, no reflow
|
|
@@ -1148,9 +1218,6 @@ window.__ModuleLoader__.load({
|
|
|
1148
1218
|
// slider feedback lands on this frame even on stalled compositors.
|
|
1149
1219
|
const scrim = document.getElementById(SCRIM_ID);
|
|
1150
1220
|
if (scrim) scrim.style.background = veilColor;
|
|
1151
|
-
if (document.body && document.body.offsetHeight !== undefined) {
|
|
1152
|
-
void document.body.offsetHeight;
|
|
1153
|
-
}
|
|
1154
1221
|
}
|
|
1155
1222
|
|
|
1156
1223
|
function clearEffects() {
|
|
@@ -1190,7 +1257,7 @@ window.__ModuleLoader__.load({
|
|
|
1190
1257
|
}
|
|
1191
1258
|
onInput(v);
|
|
1192
1259
|
};
|
|
1193
|
-
return React.createElement(React.Fragment,
|
|
1260
|
+
return React.createElement(React.Fragment, { key: label },
|
|
1194
1261
|
React.createElement("span", { className: "webg-hint webg-label" }, label),
|
|
1195
1262
|
React.createElement("input", {
|
|
1196
1263
|
className: "webg-slider", type: "range",
|
|
@@ -1394,6 +1461,7 @@ window.__ModuleLoader__.load({
|
|
|
1394
1461
|
className: "webg-text webg-search", type: "search",
|
|
1395
1462
|
placeholder: t.searchPlaceholder,
|
|
1396
1463
|
value: query,
|
|
1464
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1397
1465
|
onInput: (e) => setQuery(e.target.value),
|
|
1398
1466
|
}),
|
|
1399
1467
|
query.trim() && React.createElement("span", { className: "webg-hint" },
|
|
@@ -1424,14 +1492,14 @@ window.__ModuleLoader__.load({
|
|
|
1424
1492
|
React.createElement("span", { className: "webg-card-close" }, t.closeCard),
|
|
1425
1493
|
),
|
|
1426
1494
|
usableList.length === 0
|
|
1427
|
-
? React.createElement("span", { className: "webg-hint" },
|
|
1495
|
+
? React.createElement("span", { className: "webg-hint webg-grid-empty" },
|
|
1428
1496
|
sel.inventory.installDir
|
|
1429
1497
|
? (sel.inventory.total > 0
|
|
1430
1498
|
? t.emptyNoUsable(sel.inventory.total)
|
|
1431
1499
|
: t.emptyNoProjects)
|
|
1432
1500
|
: t.emptyNoInstall)
|
|
1433
1501
|
: filteredList.length === 0
|
|
1434
|
-
? React.createElement("span", { className: "webg-hint" }, t.noMatch(q))
|
|
1502
|
+
? React.createElement("span", { className: "webg-hint webg-grid-empty" }, t.noMatch(q))
|
|
1435
1503
|
: filteredList.map((w) => ThumbCard(w, w.id === sel.id, () => applySelection(w.id), t)),
|
|
1436
1504
|
);
|
|
1437
1505
|
}
|
|
@@ -1511,7 +1579,9 @@ window.__ModuleLoader__.load({
|
|
|
1511
1579
|
React.createElement("input", {
|
|
1512
1580
|
className: "webg-text", type: "text",
|
|
1513
1581
|
value: editing.name,
|
|
1514
|
-
|
|
1582
|
+
onChange: (e) => { editing.name = e.target.value; if (!e.nativeEvent || !e.nativeEvent.isComposing) emit(); },
|
|
1583
|
+
onInput: (e) => { editing.name = e.target.value; },
|
|
1584
|
+
onCompositionEnd: (e) => { editing.name = e.target.value; emit(); },
|
|
1515
1585
|
}),
|
|
1516
1586
|
),
|
|
1517
1587
|
React.createElement("div", { className: "webg-row" },
|
|
@@ -1536,6 +1606,7 @@ window.__ModuleLoader__.load({
|
|
|
1536
1606
|
className: "webg-text", type: "search",
|
|
1537
1607
|
placeholder: t.filterPlaceholder,
|
|
1538
1608
|
value: editorQuery,
|
|
1609
|
+
onChange: (e) => setEditorQuery(e.target.value),
|
|
1539
1610
|
onInput: (e) => setEditorQuery(e.target.value),
|
|
1540
1611
|
}),
|
|
1541
1612
|
React.createElement("div", { className: "webg-editor-grid" },
|
|
@@ -1543,10 +1614,10 @@ window.__ModuleLoader__.load({
|
|
|
1543
1614
|
const eq = editorQuery.trim();
|
|
1544
1615
|
const pool = usableList.filter((w) => matchesQuery(w, eq));
|
|
1545
1616
|
if (usableList.length === 0) {
|
|
1546
|
-
return React.createElement("span", { className: "webg-hint" }, t.noUsable);
|
|
1617
|
+
return React.createElement("span", { className: "webg-hint webg-grid-empty" }, t.noUsable);
|
|
1547
1618
|
}
|
|
1548
1619
|
if (pool.length === 0) {
|
|
1549
|
-
return React.createElement("span", { className: "webg-hint" }, t.noMatch(eq));
|
|
1620
|
+
return React.createElement("span", { className: "webg-hint webg-grid-empty" }, t.noMatch(eq));
|
|
1550
1621
|
}
|
|
1551
1622
|
return pool.map((w) => EditorCard(w, editing, t));
|
|
1552
1623
|
})(),
|
|
@@ -1633,7 +1704,7 @@ window.__ModuleLoader__.load({
|
|
|
1633
1704
|
className: "webg-btn", type: "button", onClick: enterAdjust,
|
|
1634
1705
|
disabled: sel.adjusting,
|
|
1635
1706
|
}, t.adjust),
|
|
1636
|
-
(sel.zoom !== 1 || sel.offsetX !== 0 || sel.offsetY !== 0) &&
|
|
1707
|
+
(sel.zoom !== 1 || sel.offsetX !== 0 || sel.offsetY !== 0 || sel.rotation !== 0) &&
|
|
1637
1708
|
React.createElement("button", {
|
|
1638
1709
|
className: "webg-btn", type: "button", onClick: resetCrop,
|
|
1639
1710
|
}, t.resetCrop),
|
|
@@ -1759,12 +1830,20 @@ window.__ModuleLoader__.load({
|
|
|
1759
1830
|
Gaussian's natural edge fade (≤ one blur radius, under the veil and the
|
|
1760
1831
|
UI chrome) is accepted. A slight saturate keeps blurred colours from
|
|
1761
1832
|
washing out. */
|
|
1833
|
+
/* color-scheme: normal stops the DSH theme from leaking INTO the sandboxed
|
|
1834
|
+
web-wallpaper iframe. The shell sets color-scheme: dark on <html> in the
|
|
1835
|
+
dark theme; that value propagates to embedded documents, and at least one
|
|
1836
|
+
official wallpaper (CORSAIR Collection, an Angular app) never paints
|
|
1837
|
+
under an inherited dark scheme — a solid white page instead. The value
|
|
1838
|
+
"normal" restores the neutral default; a wallpaper that opts into a
|
|
1839
|
+
scheme via its own styles still wins. */
|
|
1762
1840
|
.webg-layer .webg-media {
|
|
1763
1841
|
position: absolute;
|
|
1764
1842
|
inset: 0;
|
|
1765
1843
|
width: 100%;
|
|
1766
1844
|
height: 100%;
|
|
1767
1845
|
max-width: none; max-height: none;
|
|
1846
|
+
color-scheme: normal;
|
|
1768
1847
|
object-fit: var(--webg-fit, cover);
|
|
1769
1848
|
object-position: center;
|
|
1770
1849
|
display: block;
|
|
@@ -2010,6 +2089,11 @@ window.__ModuleLoader__.load({
|
|
|
2010
2089
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
2011
2090
|
gap: 8px; max-height: 300px; overflow-y: auto; padding: 2px;
|
|
2012
2091
|
}
|
|
2092
|
+
.webg-grid-empty {
|
|
2093
|
+
grid-column: 1 / -1;
|
|
2094
|
+
display: flex; align-items: center; justify-content: center;
|
|
2095
|
+
padding: 24px 12px; text-align: center;
|
|
2096
|
+
}
|
|
2013
2097
|
.webg-card {
|
|
2014
2098
|
position: relative; width: 100%; padding: 0; cursor: pointer;
|
|
2015
2099
|
aspect-ratio: 16 / 9; display: block; overflow: hidden;
|
|
@@ -2164,6 +2248,7 @@ window.__ModuleLoader__.load({
|
|
|
2164
2248
|
if (samplerId !== null) window.clearInterval(samplerId);
|
|
2165
2249
|
if (observer) observer.disconnect();
|
|
2166
2250
|
sampledLuminance = null;
|
|
2251
|
+
if (previewProbe) { previewProbe.remove(); previewProbe = null; }
|
|
2167
2252
|
clearRotationTimer();
|
|
2168
2253
|
// Cancel pending crossfade/clear timers so they never fire on
|
|
2169
2254
|
// torn-down nodes after dispose.
|
|
@@ -2262,7 +2347,7 @@ window.__ModuleLoader__.load({
|
|
|
2262
2347
|
if (ctx.slots) {
|
|
2263
2348
|
ctx.slots.inject("settings.general.item", () =>
|
|
2264
2349
|
ctx.slots.register(
|
|
2265
|
-
{ name: "settings.general.item", id: "we-background", order: 500, label: S().settingsLabel },
|
|
2350
|
+
{ name: "settings.general.item", id: "we-background", order: 500, label: () => S().settingsLabel },
|
|
2266
2351
|
() => React.createElement(WallpaperPicker),
|
|
2267
2352
|
),
|
|
2268
2353
|
);
|
package/lib/core.js
CHANGED
|
@@ -174,8 +174,10 @@ export function steamProbeDirs(platform = process.platform, home = homedir) {
|
|
|
174
174
|
return [
|
|
175
175
|
'C:\\Program Files (x86)\\Steam',
|
|
176
176
|
'C:\\Program Files\\Steam',
|
|
177
|
+
'C:\\SteamLibrary',
|
|
177
178
|
'D:\\Steam',
|
|
178
179
|
'D:\\SteamLibrary',
|
|
180
|
+
'E:\\Steam',
|
|
179
181
|
'E:\\SteamLibrary',
|
|
180
182
|
];
|
|
181
183
|
case 'darwin':
|
|
@@ -536,9 +538,13 @@ export function mimeFor(absPath) {
|
|
|
536
538
|
js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
|
|
537
539
|
css: 'text/css; charset=utf-8', json: 'application/json; charset=utf-8',
|
|
538
540
|
jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
539
|
-
png: 'image/png', webp: 'image/webp', svg: 'image/svg+xml',
|
|
541
|
+
png: 'image/png', webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon',
|
|
540
542
|
mp3: 'audio/mpeg', ogg: 'audio/ogg', wav: 'audio/wav',
|
|
543
|
+
flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4',
|
|
541
544
|
woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf',
|
|
545
|
+
otf: 'font/otf', eot: 'application/vnd.ms-fontobject',
|
|
546
|
+
wasm: 'application/wasm', txt: 'text/plain; charset=utf-8',
|
|
547
|
+
xml: 'application/xml; charset=utf-8',
|
|
542
548
|
}[ext] || 'application/octet-stream';
|
|
543
549
|
}
|
|
544
550
|
|
package/lib/index.js
CHANGED
|
@@ -265,7 +265,7 @@ export function createRouteRegistrar(webServer, opts = {}) {
|
|
|
265
265
|
// unguessable token remains the only capability — wildcard origin
|
|
266
266
|
// grants nothing beyond what a known URL already grants.
|
|
267
267
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
268
|
-
if (mime
|
|
268
|
+
if (mime.startsWith('text/html') || mime === 'image/svg+xml') {
|
|
269
269
|
// Script-capable documents (web wallpapers, SVG previews) get a
|
|
270
270
|
// sandbox CSP even on direct top-level navigation, so they can
|
|
271
271
|
// never execute against the DSH origin. `allow-scripts` keeps the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallpaper-engine-dsh",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|