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 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
- **任何壁纸上文字都可读。** 遮罩颜色始终与文字颜色相反:深色主题用黑色遮罩,浅色主题用白色遮罩,因此过亮或过暗的壁纸都不会盖住文字。在此基础上,「智能可视」每隔几秒采样一次壁纸亮度,亮度过高或过低时自动加强遮罩。下拉菜单、弹窗等浮层显示在 DSH 原生的不透明表面上,不受影响。
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 源的身份执行;媒体响应的 CORS 通配只允许持有令牌的一方读取其本来就能请求的文件,访问权限仍由令牌决定,且整个服务只监听本机回环地址;长度不足的响应会直接中断,而不是悄悄截断。清单由宿主缓存 30 秒;安装位置的发现过程在请求路径之外异步执行,并缓存 10 分钟。本插件不注册模型工具,不占用 prompt。
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(() => stale.remove(), LEAVE_MS);
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
- // Scrim: present while a wallpaper is active. On clear it outlives the
1044
- // layer until the fade-out finishes, so the veil fades WITH the wallpaper.
1045
- if (selection.url) {
1046
- const scrim = document.getElementById(SCRIM_ID);
1047
- if (!scrim) {
1048
- const s = document.createElement("div");
1049
- s.id = SCRIM_ID;
1050
- s.className = "webg-scrim";
1051
- document.body.appendChild(s);
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; iframes of web wallpapers can never be sampled — opaque origin).
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 the current video/img, or null. */
1069
- function sampleWallpaperLuminance() {
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
- sampledLuminance = sum / (d.length / 4) / 255;
1090
- } catch { /* frame not ready / unreadable → keep the previous sample */ }
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, sampledLuminance, selection.fit, selection.zoom,
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, null,
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
- onInput: (e) => { editing.name = e.target.value; emit(); },
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 === 'text/html' || mime === 'image/svg+xml') {
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.6",
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",