wallpaper-engine-dsh 1.0.5 → 1.0.6

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
@@ -10,7 +10,7 @@ A DSH bundle that turns your local **Wallpaper Engine** library into the live ba
10
10
 
11
11
  **Legibility on any wallpaper.** Text stays readable by construction, not by luck. The veil always opposes the text colour — black in the dark theme, white in the light theme — so neither a pitch-black nor a pure-white wallpaper can swallow the UI. On top of that, a Smart veil samples the wallpaper's brightness every few seconds and automatically strengthens the veil when a scene drifts too bright (or too dark) for the current theme; it also strengthens the border contrast and the glass frosting on the same evidence. Dropdowns, menus, and dialogs sit on DSH's opaque surfaces and are never affected.
12
12
 
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 and zoomed from 0.1× to 4×, with just enough kept on screen that it can never be dragged out of sight. 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 framing — the transform applies to the iframe directly.
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
 
15
15
  **Four live sliders.** Wallpaper blur, Scrim, Borders, and Glass — every change lands instantly and persists. Blur renders in place: composition, layout, and crop stay identical at every blur level, and within one blur radius of the screen edge the picture fades softly into the veil beneath the UI chrome. A whisper of dither noise keeps smooth gradients band-free; at zero blur the media bypasses the filter pipeline entirely for bit-exact pixels.
16
16
 
package/README.zh.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  **任何壁纸上文字都可读。** 遮罩颜色始终与文字颜色相反:深色主题用黑色遮罩,浅色主题用白色遮罩,因此过亮或过暗的壁纸都不会盖住文字。在此基础上,「智能可视」每隔几秒采样一次壁纸亮度,亮度过高或过低时自动加强遮罩。下拉菜单、弹窗等浮层显示在 DSH 原生的不透明表面上,不受影响。
12
12
 
13
- **手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮缩放,拖动平移。前四种排布下裁剪范围经过限制,画面始终铺满屏幕,不露出边缘;「自由」把壁纸按原始大小放在黑色画布上,可拖动到任意位置,缩放范围 0.1–4×,并始终保留一小块在屏幕内,不会拖丢。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸使用相同的排布下拉与缩放/平移取景(变换直接作用于 iframe)。
13
+ **手动取景。** 先选基础排布(填充、适应、拉伸、原始、自由),再手动调整画面:点「调整画面」将壁纸全屏展开,滚轮缩放,拖动平移。前四种排布下裁剪范围经过限制,画面始终铺满屏幕,不露出边缘;「自由」把壁纸按原始大小放在黑色画布上,可拖动到任意位置,缩放范围 0.1–4×,并可旋转任意角度(±90° 按钮或 Alt+拖动),同时始终保留一小块在屏幕内,不会拖丢。「镜像」水平翻转壁纸,所有排布下都可用——翻转不会破坏铺满。按 Esc 或点「完成」退出,取景设置会保存,「重置裁剪」可一键恢复默认。网页壁纸使用相同的排布下拉与缩放/平移/旋转取景(变换直接作用于 iframe)。
14
14
 
15
15
  **四个实时滑杆。** 壁纸模糊、暗化、边框、玻璃,每一步调节立即生效并保存。模糊在原位渲染:任意模糊度下,画面的比例、排布与取景完全一致;屏幕边缘一个模糊半径内的画面向遮罩过渡,被界面边饰覆盖,通常不可见。模糊时叠加轻微噪声,消除渐变色带;模糊为零时媒体完全绕过滤镜管线,像素保持不变。
16
16
 
package/lib/client.js CHANGED
@@ -95,6 +95,12 @@ window.__ModuleLoader__.load({
95
95
  adjust: "调整画面",
96
96
  adjustDone: "完成",
97
97
  adjustHint: "拖动平移 · 滚轮缩放 · Esc 或点「完成」退出",
98
+ adjustHintFree: "拖动平移 · 滚轮缩放 · Alt+拖动旋转 · Esc 或点「完成」退出",
99
+ mirror: "镜像",
100
+ rotateCcw: "⟲",
101
+ rotateCw: "⟳",
102
+ rotateCcwTitle: "逆时针旋转 90°",
103
+ rotateCwTitle: "顺时针旋转 90°",
98
104
  resetCrop: "重置裁剪",
99
105
  statusGroup: (name, total, usable, interval, order) =>
100
106
  "列表「" + name + "」:" + total + " 项 · " + usable + " 可用 · 每 " + interval + " 分钟 · " + order,
@@ -173,6 +179,12 @@ window.__ModuleLoader__.load({
173
179
  adjust: "Adjust",
174
180
  adjustDone: "Done",
175
181
  adjustHint: "Drag to pan · wheel to zoom · Esc or Done to exit",
182
+ adjustHintFree: "Drag to pan · wheel to zoom · Alt+drag to rotate · Esc or Done to exit",
183
+ mirror: "Mirror",
184
+ rotateCcw: "⟲",
185
+ rotateCw: "⟳",
186
+ rotateCcwTitle: "Rotate 90° counterclockwise",
187
+ rotateCwTitle: "Rotate 90° clockwise",
176
188
  resetCrop: "Reset crop",
177
189
  statusGroup: (name, total, usable, interval, order) =>
178
190
  "List \"" + name + "\": " + total + " items · " + usable + " usable · every " + interval + " min · " + order,
@@ -226,6 +238,8 @@ window.__ModuleLoader__.load({
226
238
  zoom: 1,
227
239
  offsetX: 0,
228
240
  offsetY: 0,
241
+ rotation: 0, // free-fit only; degrees, positive = clockwise
242
+ mirrored: false, // horizontal flip, available in every fit mode
229
243
  };
230
244
 
231
245
  const FIT_MODES = ["cover", "contain", "fill", "none", "free"];
@@ -281,6 +295,8 @@ window.__ModuleLoader__.load({
281
295
  zoom: clampNum(o.zoom, 0.1, 4, DEFAULTS.zoom),
282
296
  offsetX: clampNum(o.offsetX, -300, 300, DEFAULTS.offsetX),
283
297
  offsetY: clampNum(o.offsetY, -300, 300, DEFAULTS.offsetY),
298
+ rotation: clampNum(o.rotation, -1080, 1080, DEFAULTS.rotation),
299
+ mirrored: o.mirrored === true,
284
300
  };
285
301
  } catch {
286
302
  return { id: "", ...DEFAULTS };
@@ -339,6 +355,8 @@ window.__ModuleLoader__.load({
339
355
  zoom: selection.zoom,
340
356
  offsetX: selection.offsetX,
341
357
  offsetY: selection.offsetY,
358
+ rotation: selection.rotation,
359
+ mirrored: selection.mirrored,
342
360
  }));
343
361
  } catch { /* storage full / blocked → session-only state, not fatal */ }
344
362
  }
@@ -734,18 +752,32 @@ window.__ModuleLoader__.load({
734
752
  //
735
753
  // Crop geometry is chosen by the fit mode. The four object-fit modes keep a
736
754
  // GAP-FREE invariant: the media box covers the viewport exactly at zoom 1,
737
- // zoom floors at 1, and pan is clamped to ±(zoom−1)/2·100 % — the image
738
- // always fills the screen. The "free" mode drops that invariant on purpose:
739
- // the media sits at natural size on a black canvas, zoom runs 0.1–4×, and
740
- // pan is only clamped to keep a sliver (≥10 % of the box) on screen so the
741
- // wallpaper can never be dragged entirely out of sight.
755
+ // zoom floors at 1, pan is clamped to ±(zoom−1)/2·100 %, and rotation is
756
+ // forced to 0 — a rotated rectangle cannot cover the screen, so turning
757
+ // belongs to free mode only. Mirroring is allowed everywhere: a flip is
758
+ // symmetric and never breaks coverage. The "free" mode drops the gap-free
759
+ // invariant on purpose: the media sits at natural size on a black canvas,
760
+ // zoom runs 0.1–4×, rotation is arbitrary, and pan is only clamped to keep
761
+ // a sliver (≥10 % of the box) on screen so the wallpaper can never be
762
+ // dragged entirely out of sight.
742
763
  function clampCrop() {
764
+ if (!isFreeFit()) selection.rotation = 0;
765
+ else {
766
+ // Keep the stored angle canonical (−180, 180]: Alt+drag can accumulate
767
+ // several full turns, and persisting raw degrees would come back from
768
+ // storage truncated at the ±1080 read clamp — a DIFFERENT visual angle.
769
+ // Modulo keeps every equivalent representation identical on disk.
770
+ selection.rotation = ((selection.rotation + 180) % 360 + 360) % 360 - 180;
771
+ }
743
772
  selection.zoom = Math.round(Math.min(4, Math.max(isFreeFit() ? 0.1 : 1, selection.zoom)) * 100) / 100;
744
773
  if (isFreeFit()) {
745
774
  // Content spans ±zoom·50 % of the box around its centre; the pan offset
746
- // shifts it by its own % of the box. |offset| ≤ (zoom+1)·50 − 10 keeps
747
- // at least 10 % of the media edge-to-edge visible in both axes.
748
- const reach = (selection.zoom + 1) * 50 - 10;
775
+ // shifts it by its own % of the box. Rotation expands the projected
776
+ // extent by k = |cos θ| + |sin θ| (up to √2 at 45°). |offset| ≤
777
+ // (zoom·k+1)·50 − 10 keeps ≥10 % of the media visible in both axes.
778
+ const rad = selection.rotation * Math.PI / 180;
779
+ const k = Math.abs(Math.cos(rad)) + Math.abs(Math.sin(rad));
780
+ const reach = (selection.zoom * k + 1) * 50 - 10;
749
781
  selection.offsetX = Math.min(reach, Math.max(-reach, selection.offsetX));
750
782
  selection.offsetY = Math.min(reach, Math.max(-reach, selection.offsetY));
751
783
  return;
@@ -786,12 +818,31 @@ window.__ModuleLoader__.load({
786
818
  bar.className = "webg-adjust-bar";
787
819
  const hint = document.createElement("span");
788
820
  hint.className = "webg-adjust-hint";
789
- hint.textContent = S().adjustHint;
821
+ hint.textContent = isFreeFit() ? S().adjustHintFree : S().adjustHint;
790
822
  const done = document.createElement("button");
791
823
  done.type = "button";
792
824
  done.className = "webg-btn webg-btn--primary";
793
825
  done.textContent = S().adjustDone;
794
826
  bar.appendChild(hint);
827
+ // Free fit only: a rotated rectangle cannot cover the screen, so turning
828
+ // exists solely where exposed edges are allowed. ±90° buttons cover the
829
+ // common cases; arbitrary angles come from Alt+drag below.
830
+ if (isFreeFit()) {
831
+ for (const dir of [-90, 90]) {
832
+ const rot = document.createElement("button");
833
+ rot.type = "button";
834
+ rot.className = "webg-btn";
835
+ rot.textContent = dir < 0 ? S().rotateCcw : S().rotateCw;
836
+ rot.title = dir < 0 ? S().rotateCcwTitle : S().rotateCwTitle;
837
+ rot.addEventListener("click", () => {
838
+ selection.rotation += dir;
839
+ clampCrop();
840
+ applyEffects();
841
+ persistSelection();
842
+ });
843
+ bar.appendChild(rot);
844
+ }
845
+ }
795
846
  bar.appendChild(done);
796
847
  overlay.appendChild(bar);
797
848
  document.body.appendChild(overlay);
@@ -801,14 +852,34 @@ window.__ModuleLoader__.load({
801
852
  // translate() in the transform list is applied AFTER scale() (right-to-
802
853
  // left), so pan deltas are independent of zoom: dx px = dx/width %.
803
854
  let drag = null;
855
+ // Alt+drag (free fit): swing the media around the overlay's centre —
856
+ // the angle delta between pointer positions becomes the rotation delta.
857
+ let rot = null; // { a0: starting pointer angle, base: starting rotation }
858
+ const centerAngle = (e) => {
859
+ const cx = (overlay.clientWidth || 1) / 2;
860
+ const cy = (overlay.clientHeight || 1) / 2;
861
+ return Math.atan2(e.clientY - cy, e.clientX - cx);
862
+ };
804
863
  const onPointerDown = (e) => {
805
864
  if (e.target === done || bar.contains(e.target)) return;
806
- drag = { x: e.clientX, y: e.clientY };
865
+ if (e.altKey && isFreeFit()) {
866
+ rot = { a0: centerAngle(e), base: selection.rotation };
867
+ } else {
868
+ drag = { x: e.clientX, y: e.clientY };
869
+ }
870
+ // Capture for EITHER gesture: pointer moves must keep arriving even
871
+ // when the cursor leaves the window mid-swing.
807
872
  if (overlay.setPointerCapture && e.pointerId !== undefined) {
808
873
  try { overlay.setPointerCapture(e.pointerId); } catch {}
809
874
  }
810
875
  };
811
876
  const onPointerMove = (e) => {
877
+ if (rot) {
878
+ selection.rotation = rot.base + (centerAngle(e) - rot.a0) * 180 / Math.PI;
879
+ clampCrop();
880
+ applyEffects();
881
+ return;
882
+ }
812
883
  if (!drag) return;
813
884
  // translate % refers to the media box, which is the viewport exactly —
814
885
  // divide the pixel delta by it so the content tracks the cursor 1:1.
@@ -820,18 +891,16 @@ window.__ModuleLoader__.load({
820
891
  drag = { x: e.clientX, y: e.clientY };
821
892
  applyEffects();
822
893
  };
823
- const onPointerUp = () => {
894
+ const endPointer = () => {
895
+ if (rot) { rot = null; persistSelection(); return; }
824
896
  if (!drag) return;
825
897
  drag = null;
826
898
  persistSelection();
827
899
  };
900
+ const onPointerUp = endPointer;
828
901
  // pointercancel (touch gesture takeover): end the drag without losing the
829
902
  // current offsets — otherwise drag stays "stuck" until the next pointerup.
830
- const onPointerCancel = () => {
831
- if (!drag) return;
832
- drag = null;
833
- persistSelection();
834
- };
903
+ const onPointerCancel = endPointer;
835
904
  const onWheel = (e) => {
836
905
  if (typeof e.preventDefault === "function") e.preventDefault();
837
906
  // clampCrop enforces the mode's zoom floor: the gap-free modes never
@@ -870,6 +939,7 @@ window.__ModuleLoader__.load({
870
939
  selection.zoom = 1;
871
940
  selection.offsetX = 0;
872
941
  selection.offsetY = 0;
942
+ selection.rotation = 0; // framing geometry home; mirroring keeps its own toggle
873
943
  persistSelection();
874
944
  applyEffects();
875
945
  emit();
@@ -1028,7 +1098,7 @@ window.__ModuleLoader__.load({
1028
1098
  const key = [
1029
1099
  dark, selection.scrim, selection.border, selection.blur, selection.wallpaperBlur,
1030
1100
  selection.autoScrim, sampledLuminance, selection.fit, selection.zoom,
1031
- selection.offsetX, selection.offsetY,
1101
+ selection.offsetX, selection.offsetY, selection.rotation, selection.mirrored,
1032
1102
  ].join("\u0000");
1033
1103
  if (key === lastEffectKey) return; // nothing it reads changed → no writes, no reflow
1034
1104
  lastEffectKey = key;
@@ -1071,6 +1141,8 @@ window.__ModuleLoader__.load({
1071
1141
  s.setProperty("--webg-zoom", String(selection.zoom));
1072
1142
  s.setProperty("--webg-offset-x", selection.offsetX + "%");
1073
1143
  s.setProperty("--webg-offset-y", selection.offsetY + "%");
1144
+ s.setProperty("--webg-rotate", selection.rotation + "deg");
1145
+ s.setProperty("--webg-mirror", selection.mirrored ? "-1" : "1");
1074
1146
 
1075
1147
  // Write the scrim colour directly too, then force a synchronous layout, so
1076
1148
  // slider feedback lands on this frame even on stalled compositors.
@@ -1097,6 +1169,8 @@ window.__ModuleLoader__.load({
1097
1169
  s.removeProperty("--webg-zoom");
1098
1170
  s.removeProperty("--webg-offset-x");
1099
1171
  s.removeProperty("--webg-offset-y");
1172
+ s.removeProperty("--webg-rotate");
1173
+ s.removeProperty("--webg-mirror");
1100
1174
  const scrim = document.getElementById(SCRIM_ID);
1101
1175
  if (scrim) scrim.style.background = "";
1102
1176
  }
@@ -1292,14 +1366,23 @@ window.__ModuleLoader__.load({
1292
1366
  function onFit(e) {
1293
1367
  selection.fit = FIT_MODES.indexOf(e.target.value) >= 0 ? e.target.value : "cover";
1294
1368
  // Re-seat the crop into the new mode's envelope: leaving free for a
1295
- // gap-free mode pulls zoom back up to 1 and reins the pan in; the other
1296
- // direction only ever relaxes bounds, so the framing carries over.
1369
+ // gap-free mode pulls zoom back up to 1, reins the pan in and zeroes the
1370
+ // rotation; the other direction only ever relaxes bounds, so the framing
1371
+ // carries over. Mirroring survives every switch — a flip never breaks
1372
+ // coverage.
1297
1373
  clampCrop();
1298
1374
  persistSelection();
1299
1375
  applyEffects();
1300
1376
  emit();
1301
1377
  }
1302
1378
 
1379
+ function onToggleMirror() {
1380
+ selection.mirrored = !selection.mirrored;
1381
+ persistSelection();
1382
+ applyEffects();
1383
+ emit();
1384
+ }
1385
+
1303
1386
  // ── Picker sections (presentational; every input arrives as an argument) ────
1304
1387
  // WallpaperPicker composes these in a fixed order. Each one owns exactly one
1305
1388
  // visual row/block of the settings panel and produces the same DOM the old
@@ -1539,6 +1622,13 @@ window.__ModuleLoader__.load({
1539
1622
  ...[t.fitCover, t.fitContain, t.fitFill, t.fitNone, t.fitFree].map((label, i) =>
1540
1623
  React.createElement("option", { key: FIT_MODES[i], value: FIT_MODES[i] }, label),
1541
1624
  )),
1625
+ // Mirror is a coverage-preserving flip: valid in EVERY fit mode and
1626
+ // for every wallpaper kind (the transform applies to iframes too).
1627
+ React.createElement("button", {
1628
+ className: "webg-btn" + (sel.mirrored ? " webg-btn--active" : ""),
1629
+ type: "button",
1630
+ onClick: onToggleMirror,
1631
+ }, t.mirror),
1542
1632
  React.createElement("button", {
1543
1633
  className: "webg-btn", type: "button", onClick: enterAdjust,
1544
1634
  disabled: sel.adjusting,
@@ -1680,9 +1770,13 @@ window.__ModuleLoader__.load({
1680
1770
  display: block;
1681
1771
  background: transparent; border: 0;
1682
1772
  filter: var(--webg-media-filter, none);
1683
- /* Manual crop: pan offsets are % of the media box (independent of zoom
1684
- because translate is listed before scale), zoom is centered. */
1685
- transform: translate(var(--webg-offset-x, 0%), var(--webg-offset-y, 0%)) scale(var(--webg-zoom, 1));
1773
+ /* Manual crop: the transform list is ordered pan → rotate → zoom →
1774
+ mirror. translate comes first so pan offsets stay screen-aligned no
1775
+ matter the rotation; mirror is innermost so the flip happens in the
1776
+ media's own axes. */
1777
+ transform: translate(var(--webg-offset-x, 0%), var(--webg-offset-y, 0%))
1778
+ rotate(var(--webg-rotate, 0deg)) scale(var(--webg-zoom, 1))
1779
+ scaleX(var(--webg-mirror, 1));
1686
1780
  transform-origin: center center;
1687
1781
  }
1688
1782
 
@@ -1803,17 +1897,32 @@ window.__ModuleLoader__.load({
1803
1897
  border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.5));
1804
1898
  }
1805
1899
  .webg-btn:disabled { opacity: 0.45; cursor: not-allowed; }
1806
- /* brand-primary is monochrome (near-black light / near-white dark), so
1807
- the label must use its inverse token — never a hardcoded white. */
1900
+ /* brand-primary is monochrome (near-black light / near-white dark). The
1901
+ shell's brand-primary-INVERT token aliases the same shade as brand-
1902
+ primary itself in both themes (no contrast), so anything sitting ON a
1903
+ brand-primary fill takes its contrast colour from the inverted label
1904
+ ramp instead — near-white on the dark light-theme fill, dark bluish-800
1905
+ on the near-white dark-theme fill. */
1906
+ body {
1907
+ --webg-on-brand: var(--dsw-alias-label-primary-inverted,
1908
+ var(--dsw-alias-label-primary-foreground, rgb(249, 250, 251)));
1909
+ }
1808
1910
  .webg-btn--primary {
1809
1911
  background: var(--dsw-alias-brand-primary, rgb(15, 17, 21));
1810
1912
  border-color: transparent;
1811
- color: var(--dsw-alias-brand-primary-invert, rgb(249, 250, 251));
1913
+ color: var(--webg-on-brand);
1812
1914
  }
1813
1915
  .webg-btn--primary:hover:not(:disabled) {
1814
1916
  background: var(--dsw-alias-brand-primary, rgb(15, 17, 21));
1815
1917
  opacity: 0.85;
1816
1918
  }
1919
+ /* Toggle-style button showing an engaged state (e.g. 镜像): brand fill
1920
+ with the contrasting on-brand label. */
1921
+ .webg-btn--active {
1922
+ background: var(--dsw-alias-brand-primary, rgb(15, 17, 21));
1923
+ border-color: transparent;
1924
+ color: var(--webg-on-brand);
1925
+ }
1817
1926
 
1818
1927
  .webg-select, .webg-text {
1819
1928
  padding: 5px 10px;
@@ -1873,7 +1982,7 @@ window.__ModuleLoader__.load({
1873
1982
  -webkit-appearance: none;
1874
1983
  width: 14px; height: 14px; border-radius: 50%;
1875
1984
  margin-top: -5px; /* centers the 14px thumb on the 4px track */
1876
- background: var(--dsw-alias-brand-primary-invert, rgb(249, 250, 251));
1985
+ background: var(--webg-on-brand);
1877
1986
  border: 2.5px solid var(--dsw-alias-brand-primary, rgb(15, 17, 21));
1878
1987
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
1879
1988
  transition: transform 120ms ease;
@@ -1971,7 +2080,7 @@ window.__ModuleLoader__.load({
1971
2080
  position: absolute; top: 4px; left: 4px; width: 18px; height: 18px;
1972
2081
  border-radius: 4px;
1973
2082
  background: var(--dsw-alias-brand-primary, rgb(15, 17, 21));
1974
- color: var(--dsw-alias-brand-primary-invert, rgb(249, 250, 251));
2083
+ color: var(--webg-on-brand);
1975
2084
  font-size: 12px; line-height: 18px; text-align: center;
1976
2085
  }
1977
2086
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallpaper-engine-dsh",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
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",