wallpaper-engine-dsh 1.0.0 → 1.0.3
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 +4 -4
- package/README.zh.md +4 -4
- package/lib/client.js +527 -428
- package/lib/core.js +27 -3
- package/lib/index.js +78 -40
- package/lib/types/index.d.ts +23 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -14,6 +14,8 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
const SCRIM_ID = "wallpaper-engine-dsh-scrim";
|
|
15
15
|
const STYLE_TAG_ID = "wallpaper-engine-dsh/styles";
|
|
16
16
|
const CROSSFADE_MS = 520;
|
|
17
|
+
/** Crossfade settle time before a leaving layer is detached. */
|
|
18
|
+
const LEAVE_MS = CROSSFADE_MS + 120;
|
|
17
19
|
/** Battery at or below this level (and discharging) auto-pauses playback. */
|
|
18
20
|
const BATTERY_SAVER_LEVEL = 0.2;
|
|
19
21
|
/** Below this FPS the picker shows a performance hint. */
|
|
@@ -271,6 +273,8 @@ window.__ModuleLoader__.load({
|
|
|
271
273
|
autoScrim: o.autoScrim !== false,
|
|
272
274
|
lang: o.lang === "zh" || o.lang === "en" ? o.lang : "auto",
|
|
273
275
|
fit: FIT_MODES.indexOf(o.fit) >= 0 ? o.fit : DEFAULTS.fit,
|
|
276
|
+
// Older builds could persist zoom < 1 / unclamped pans — clampCrop()
|
|
277
|
+
// at store creation re-seats both into the gap-free envelope.
|
|
274
278
|
zoom: clampNum(o.zoom, 0.25, 4, DEFAULTS.zoom),
|
|
275
279
|
offsetX: clampNum(o.offsetX, -200, 200, DEFAULTS.offsetX),
|
|
276
280
|
offsetY: clampNum(o.offsetY, -200, 200, DEFAULTS.offsetY),
|
|
@@ -298,6 +302,9 @@ window.__ModuleLoader__.load({
|
|
|
298
302
|
inventory: { installDir: null, wallpapers: [], total: 0, portableCount: 0, playlists: [], error: null },
|
|
299
303
|
loaded: false,
|
|
300
304
|
};
|
|
305
|
+
// Older builds allowed zoom < 1 and unclamped pans — re-seat any persisted
|
|
306
|
+
// crop into the gap-free envelope before the first render.
|
|
307
|
+
clampCrop();
|
|
301
308
|
|
|
302
309
|
const listeners = new Set();
|
|
303
310
|
function emit() { for (const fn of [...listeners]) fn(); }
|
|
@@ -334,21 +341,19 @@ window.__ModuleLoader__.load({
|
|
|
334
341
|
}
|
|
335
342
|
|
|
336
343
|
// ── Wallpaper predicates ─────────────────────────────────────────────────────
|
|
337
|
-
// playable: real media (video/web) renders live.
|
|
338
344
|
// Renderable kinds ONLY — scene/application wallpapers are .pkg scene
|
|
339
345
|
// packages the browser cannot render (their live animation is Wallpaper
|
|
340
346
|
// Engine's desktop job), so they are filtered out of the grid entirely
|
|
341
|
-
// rather than offered as degraded preview stills.
|
|
347
|
+
// rather than offered as degraded preview stills. A wallpaper is selectable
|
|
348
|
+
// exactly when it is playable (real media renders live); preview-only
|
|
349
|
+
// entries exist in the inventory for the decode-fallback lookup but are
|
|
350
|
+
// never offered in the grid.
|
|
342
351
|
function isPlayable(w) {
|
|
343
352
|
return Boolean(w && w.playable && (w.type === "video" || w.type === "web"));
|
|
344
353
|
}
|
|
345
354
|
|
|
346
|
-
function isSelectable(w) {
|
|
347
|
-
return isPlayable(w);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
355
|
function selectableInventory() {
|
|
351
|
-
return selection.inventory.wallpapers.filter(
|
|
356
|
+
return selection.inventory.wallpapers.filter(isPlayable);
|
|
352
357
|
}
|
|
353
358
|
|
|
354
359
|
// ── Inventory ────────────────────────────────────────────────────────────────
|
|
@@ -412,7 +417,7 @@ window.__ModuleLoader__.load({
|
|
|
412
417
|
}
|
|
413
418
|
|
|
414
419
|
// Drop a selection whose wallpaper vanished or became unusable.
|
|
415
|
-
if (selection.id && !selection.inventory.wallpapers.some((w) => w.id === selection.id &&
|
|
420
|
+
if (selection.id && !selection.inventory.wallpapers.some((w) => w.id === selection.id && isPlayable(w))) {
|
|
416
421
|
selection.id = "";
|
|
417
422
|
persistSelection();
|
|
418
423
|
}
|
|
@@ -436,7 +441,7 @@ window.__ModuleLoader__.load({
|
|
|
436
441
|
function groupWallpapers(group) {
|
|
437
442
|
if (!group || !Array.isArray(group.wallpaperIds)) return [];
|
|
438
443
|
const byId = new Map(selection.inventory.wallpapers.map((w) => [w.id, w]));
|
|
439
|
-
return group.wallpaperIds.map((id) => byId.get(id)).filter(
|
|
444
|
+
return group.wallpaperIds.map((id) => byId.get(id)).filter(isPlayable);
|
|
440
445
|
}
|
|
441
446
|
|
|
442
447
|
function rotationCandidates() {
|
|
@@ -452,7 +457,7 @@ window.__ModuleLoader__.load({
|
|
|
452
457
|
if (!source) return false;
|
|
453
458
|
const byId = new Map(selection.inventory.wallpapers.map((w) => [w.id, w]));
|
|
454
459
|
const ids = Array.isArray(source.wallpaperIds)
|
|
455
|
-
? source.wallpaperIds.filter((id) =>
|
|
460
|
+
? source.wallpaperIds.filter((id) => isPlayable(byId.get(id)))
|
|
456
461
|
: [];
|
|
457
462
|
if (ids.length < 2) return false;
|
|
458
463
|
selection.rotationGroups.push({
|
|
@@ -667,7 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
667
672
|
media.setAttribute("frameborder", "0");
|
|
668
673
|
media.setAttribute("scrolling", "no");
|
|
669
674
|
media.setAttribute("title", S().iframeTitle);
|
|
670
|
-
media.className = "webg-media
|
|
675
|
+
media.className = "webg-media";
|
|
671
676
|
} else {
|
|
672
677
|
// Static fallback: a playable wallpaper demoted after repeated media
|
|
673
678
|
// failures — its preview still beats a black screen.
|
|
@@ -684,7 +689,10 @@ window.__ModuleLoader__.load({
|
|
|
684
689
|
}
|
|
685
690
|
|
|
686
691
|
function attachStillSampling(media, sel) {
|
|
687
|
-
const type =
|
|
692
|
+
const type = sel && sel.type;
|
|
693
|
+
// Web wallpapers cannot be sampled — the sandboxed iframe's origin is
|
|
694
|
+
// opaque, so its pixels are unreadable. Only video/img frames feed the
|
|
695
|
+
// smart veil.
|
|
688
696
|
if (type !== "video" && type !== "image") return;
|
|
689
697
|
media.addEventListener(type === "video" ? "loadeddata" : "load", () => {
|
|
690
698
|
sampleWallpaperLuminance();
|
|
@@ -724,6 +732,18 @@ window.__ModuleLoader__.load({
|
|
|
724
732
|
// The layer normally sits at z-index -2 behind the entire app, so it can
|
|
725
733
|
// never receive pointer events. Adjust mode moves the LIVE layer node into a
|
|
726
734
|
// topmost transparent overlay — the user edits the real thing, WYSIWYG.
|
|
735
|
+
//
|
|
736
|
+
// Crop geometry invariants: the media box covers the viewport exactly at
|
|
737
|
+
// zoom 1; zooming creates headroom, panning spends it. Zoom floors at 1
|
|
738
|
+
// and pan is clamped to ±(zoom-1)/2·100 %, so a gap-free fullscreen image
|
|
739
|
+
// is guaranteed at every setting.
|
|
740
|
+
function clampCrop() {
|
|
741
|
+
selection.zoom = Math.round(Math.min(4, Math.max(1, selection.zoom)) * 100) / 100;
|
|
742
|
+
const pan = (selection.zoom - 1) * 50;
|
|
743
|
+
selection.offsetX = Math.min(pan, Math.max(-pan, selection.offsetX));
|
|
744
|
+
selection.offsetY = Math.min(pan, Math.max(-pan, selection.offsetY));
|
|
745
|
+
}
|
|
746
|
+
|
|
727
747
|
let adjustOverlay = null;
|
|
728
748
|
let adjustCleanup = null;
|
|
729
749
|
|
|
@@ -775,16 +795,13 @@ window.__ModuleLoader__.load({
|
|
|
775
795
|
};
|
|
776
796
|
const onPointerMove = (e) => {
|
|
777
797
|
if (!drag) return;
|
|
778
|
-
// translate % refers to the
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
selection.offsetX + ((e.clientX - drag.x) / w) * 100));
|
|
786
|
-
selection.offsetY = Math.min(200, Math.max(-200,
|
|
787
|
-
selection.offsetY + ((e.clientY - drag.y) / h) * 100));
|
|
798
|
+
// translate % refers to the media box, which is the viewport exactly —
|
|
799
|
+
// divide the pixel delta by it so the content tracks the cursor 1:1.
|
|
800
|
+
const w = overlay.clientWidth || 1;
|
|
801
|
+
const h = overlay.clientHeight || 1;
|
|
802
|
+
selection.offsetX += ((e.clientX - drag.x) / w) * 100;
|
|
803
|
+
selection.offsetY += ((e.clientY - drag.y) / h) * 100;
|
|
804
|
+
clampCrop(); // gap-free guarantee: pan never outruns the zoom headroom
|
|
788
805
|
drag = { x: e.clientX, y: e.clientY };
|
|
789
806
|
applyEffects();
|
|
790
807
|
};
|
|
@@ -802,12 +819,12 @@ window.__ModuleLoader__.load({
|
|
|
802
819
|
};
|
|
803
820
|
const onWheel = (e) => {
|
|
804
821
|
if (typeof e.preventDefault === "function") e.preventDefault();
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
822
|
+
// Crop semantics: zoom IN only (1–4×). Zooming below cover would float a
|
|
823
|
+
// shrunken box in empty space — that is not a crop. Tightening zoom also
|
|
824
|
+
// re-clamps the pan into the new headroom, so zooming back to 1 pulls
|
|
825
|
+
// the framing home instead of leaving edge gaps.
|
|
826
|
+
selection.zoom *= Math.exp(-(e.deltaY || 0) * 0.0012);
|
|
827
|
+
clampCrop();
|
|
811
828
|
applyEffects();
|
|
812
829
|
persistSelection();
|
|
813
830
|
};
|
|
@@ -887,7 +904,7 @@ window.__ModuleLoader__.load({
|
|
|
887
904
|
if (existing) {
|
|
888
905
|
existing.classList.add("webg-layer--leave");
|
|
889
906
|
const stale = existing;
|
|
890
|
-
schedule(() => stale.remove(),
|
|
907
|
+
schedule(() => stale.remove(), LEAVE_MS);
|
|
891
908
|
}
|
|
892
909
|
}
|
|
893
910
|
const node = document.getElementById(LAYER_ID);
|
|
@@ -909,7 +926,7 @@ window.__ModuleLoader__.load({
|
|
|
909
926
|
if (s) s.remove();
|
|
910
927
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
911
928
|
}
|
|
912
|
-
},
|
|
929
|
+
}, LEAVE_MS);
|
|
913
930
|
}
|
|
914
931
|
|
|
915
932
|
// Scrim: present while a wallpaper is active. On clear it outlives the
|
|
@@ -931,6 +948,11 @@ window.__ModuleLoader__.load({
|
|
|
931
948
|
// sample; iframes of web wallpapers can never be sampled — opaque origin).
|
|
932
949
|
let sampledLuminance = null;
|
|
933
950
|
let sampleCanvas = null;
|
|
951
|
+
// Snapshot of every input applyEffects reads, from its last full write. The
|
|
952
|
+
// store emits once per second for the FPS readout — without this guard every
|
|
953
|
+
// tick would rewrite all CSS variables AND force a synchronous layout for
|
|
954
|
+
// nothing. clearEffects() resets it so the next apply always writes.
|
|
955
|
+
let lastEffectKey = null;
|
|
934
956
|
|
|
935
957
|
/** Average Rec.601 luminance (0..1) of the current video/img, or null. */
|
|
936
958
|
function sampleWallpaperLuminance() {
|
|
@@ -958,11 +980,18 @@ window.__ModuleLoader__.load({
|
|
|
958
980
|
}
|
|
959
981
|
|
|
960
982
|
function applyEffects() {
|
|
961
|
-
const s = document.body.style;
|
|
962
983
|
// The veil OPPOSES the text colour: dark theme (light text) darkens bright
|
|
963
984
|
// wallpapers, light theme (dark text) brightens dark ones — either way the
|
|
964
985
|
// text side wins. This is what makes ANY wallpaper survivable.
|
|
965
986
|
const dark = Boolean(document.body.hasAttribute && document.body.hasAttribute("data-ds-dark-theme"));
|
|
987
|
+
const key = [
|
|
988
|
+
dark, selection.scrim, selection.border, selection.blur, selection.wallpaperBlur,
|
|
989
|
+
selection.autoScrim, sampledLuminance, selection.fit, selection.zoom,
|
|
990
|
+
selection.offsetX, selection.offsetY,
|
|
991
|
+
].join("\u0000");
|
|
992
|
+
if (key === lastEffectKey) return; // nothing it reads changed → no writes, no reflow
|
|
993
|
+
lastEffectKey = key;
|
|
994
|
+
const s = document.body.style;
|
|
966
995
|
let alpha = selection.scrim;
|
|
967
996
|
if (selection.autoScrim && sampledLuminance !== null) {
|
|
968
997
|
// Distance of the wallpaper's brightness from the "comfortable middle"
|
|
@@ -979,7 +1008,6 @@ window.__ModuleLoader__.load({
|
|
|
979
1008
|
// The glass "colour melt" scales with blur radius: 0 blur → no melt.
|
|
980
1009
|
s.setProperty("--webg-saturate", String(1.15 + selection.blur * 0.028));
|
|
981
1010
|
s.setProperty("--webg-glass-brightness", "1.04");
|
|
982
|
-
s.setProperty("--webg-wallpaper-blur", selection.wallpaperBlur + "px");
|
|
983
1011
|
// At 0 blur, keep the media OUT of the filter pipeline entirely: even
|
|
984
1012
|
// blur(0px) forces an extra raster/compositing pass, and the noise overlay
|
|
985
1013
|
// would still occupy a layer at opacity 0. filter:none + display:none keep
|
|
@@ -1010,13 +1038,13 @@ window.__ModuleLoader__.load({
|
|
|
1010
1038
|
}
|
|
1011
1039
|
|
|
1012
1040
|
function clearEffects() {
|
|
1041
|
+
lastEffectKey = null;
|
|
1013
1042
|
const s = document.body.style;
|
|
1014
1043
|
s.removeProperty("--webg-scrim-color");
|
|
1015
1044
|
s.removeProperty("--webg-border-alpha");
|
|
1016
1045
|
s.removeProperty("--webg-blur");
|
|
1017
1046
|
s.removeProperty("--webg-saturate");
|
|
1018
1047
|
s.removeProperty("--webg-glass-brightness");
|
|
1019
|
-
s.removeProperty("--webg-wallpaper-blur");
|
|
1020
1048
|
s.removeProperty("--webg-media-filter");
|
|
1021
1049
|
s.removeProperty("--webg-noise-display");
|
|
1022
1050
|
s.removeProperty("--webg-noise-opacity");
|
|
@@ -1070,6 +1098,16 @@ window.__ModuleLoader__.load({
|
|
|
1070
1098
|
return (w.title || "").toLowerCase().includes(needle) || (w.id || "").toLowerCase().includes(needle);
|
|
1071
1099
|
}
|
|
1072
1100
|
|
|
1101
|
+
/** Preview <img>, or the "no preview" placeholder — shared by grid + editor. */
|
|
1102
|
+
function PreviewMedia(w, t) {
|
|
1103
|
+
return w.preview
|
|
1104
|
+
? React.createElement("img", {
|
|
1105
|
+
src: w.preview, alt: w.title, loading: "lazy", decoding: "async",
|
|
1106
|
+
onError: (e) => { e.target.style.display = "none"; },
|
|
1107
|
+
})
|
|
1108
|
+
: React.createElement("span", { className: "webg-card-placeholder" }, t.noPreview);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1073
1111
|
function ThumbCard(w, selected, onClick, t) {
|
|
1074
1112
|
return React.createElement("button", {
|
|
1075
1113
|
key: w.id,
|
|
@@ -1078,426 +1116,480 @@ window.__ModuleLoader__.load({
|
|
|
1078
1116
|
onClick: onClick,
|
|
1079
1117
|
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
1080
1118
|
},
|
|
1081
|
-
w
|
|
1082
|
-
? React.createElement("img", {
|
|
1083
|
-
src: w.preview, alt: w.title, loading: "lazy", decoding: "async",
|
|
1084
|
-
onError: (e) => { e.target.style.display = "none"; },
|
|
1085
|
-
})
|
|
1086
|
-
: React.createElement("span", { className: "webg-card-placeholder" }, t.noPreview),
|
|
1119
|
+
PreviewMedia(w, t),
|
|
1087
1120
|
React.createElement("span", { className: "webg-card-type" }, typeBadge(w, t)),
|
|
1088
1121
|
React.createElement("span", { className: "webg-card-title" }, w.title),
|
|
1089
1122
|
);
|
|
1090
1123
|
}
|
|
1091
1124
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
selection.lang = e.target.value;
|
|
1109
|
-
persistSelection();
|
|
1110
|
-
emit();
|
|
1111
|
-
};
|
|
1112
|
-
const onGroupChange = (e) => {
|
|
1113
|
-
selection.rotationGroupId = e.target.value;
|
|
1114
|
-
if (selection.rotationEnabled) {
|
|
1115
|
-
const first = rotationCandidates()[0];
|
|
1116
|
-
if (first) applySelection(first.id);
|
|
1117
|
-
else applySelection("");
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
persistSelection();
|
|
1121
|
-
syncRotationTimer();
|
|
1122
|
-
emit();
|
|
1123
|
-
};
|
|
1124
|
-
const onToggleRotation = () => {
|
|
1125
|
-
selection.rotationEnabled = !selection.rotationEnabled;
|
|
1126
|
-
// The checkbox is disabled without a usable group, so a group id always
|
|
1127
|
-
// exists here when enabling.
|
|
1128
|
-
if (selection.rotationEnabled &&
|
|
1129
|
-
!rotationCandidates().some((w) => w.id === selection.id)) {
|
|
1130
|
-
const first = rotationCandidates()[0];
|
|
1131
|
-
if (first) { applySelection(first.id); return; }
|
|
1132
|
-
}
|
|
1133
|
-
persistSelection();
|
|
1134
|
-
syncRotationTimer();
|
|
1135
|
-
emit();
|
|
1136
|
-
};
|
|
1137
|
-
const onGroupInterval = (e) => {
|
|
1138
|
-
const group = activeRotationGroup();
|
|
1139
|
-
if (!group) return;
|
|
1140
|
-
group.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval);
|
|
1141
|
-
persistSelection();
|
|
1142
|
-
syncRotationTimer();
|
|
1143
|
-
emit();
|
|
1144
|
-
};
|
|
1145
|
-
const onDeleteGroup = () => {
|
|
1146
|
-
const group = activeRotationGroup();
|
|
1147
|
-
if (!group) return;
|
|
1148
|
-
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
|
1149
|
-
if (!window.confirm(t.confirmDelete(group.name))) return;
|
|
1150
|
-
}
|
|
1151
|
-
deleteGroup(group.id);
|
|
1152
|
-
};
|
|
1153
|
-
const onTogglePauseOnHidden = () => {
|
|
1154
|
-
selection.pauseOnHidden = !selection.pauseOnHidden;
|
|
1155
|
-
if (selection.pauseOnHidden) {
|
|
1156
|
-
// Re-enabling while already hidden must re-apply the reason — the
|
|
1157
|
-
// visibilitychange listener only fires on CHANGES.
|
|
1158
|
-
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1159
|
-
setAutoPause("hidden", true);
|
|
1160
|
-
}
|
|
1161
|
-
} else {
|
|
1162
|
-
setAutoPause("hidden", false);
|
|
1163
|
-
}
|
|
1164
|
-
persistSelection();
|
|
1165
|
-
emit();
|
|
1166
|
-
};
|
|
1167
|
-
const onTogglePauseOnBattery = () => {
|
|
1168
|
-
selection.pauseOnBattery = !selection.pauseOnBattery;
|
|
1169
|
-
if (selection.pauseOnBattery) applyBatteryPause(); // re-evaluate NOW
|
|
1170
|
-
else setAutoPause("battery", false);
|
|
1171
|
-
persistSelection();
|
|
1172
|
-
emit();
|
|
1173
|
-
};
|
|
1174
|
-
const onToggleAutoScrim = () => {
|
|
1175
|
-
selection.autoScrim = !selection.autoScrim;
|
|
1176
|
-
persistSelection();
|
|
1177
|
-
applyEffects();
|
|
1178
|
-
emit();
|
|
1179
|
-
};
|
|
1125
|
+
/** Preset switch intervals offered by the rotation UI (minutes). */
|
|
1126
|
+
const ROTATION_INTERVALS = [1, 5, 10, 30, 60, 120];
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* <option> list for an interval select. A persisted interval outside the
|
|
1130
|
+
* preset steps (imported groups, hand-edited storage) still gets a matching
|
|
1131
|
+
* option — otherwise the select would silently display the first preset
|
|
1132
|
+
* instead of reality.
|
|
1133
|
+
*/
|
|
1134
|
+
function intervalOptions(current, t) {
|
|
1135
|
+
return [...new Set([...ROTATION_INTERVALS, current])]
|
|
1136
|
+
.sort((x, y) => x - y)
|
|
1137
|
+
.map((minutes) => React.createElement("option", {
|
|
1138
|
+
key: minutes, value: String(minutes),
|
|
1139
|
+
}, t.minutes(minutes)));
|
|
1140
|
+
}
|
|
1180
1141
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
selection.fit = FIT_MODES.indexOf(e.target.value) >= 0 ? e.target.value : "cover";
|
|
1187
|
-
persistSelection();
|
|
1188
|
-
applyEffects();
|
|
1189
|
-
emit();
|
|
1190
|
-
};
|
|
1142
|
+
// ── Picker actions (module scope) ────────────────────────────────────────────
|
|
1143
|
+
// Every control mutates the shared selection store and emits; none closes
|
|
1144
|
+
// over React state, so they live outside the component — stable across
|
|
1145
|
+
// re-renders (the picker re-renders once per second for the FPS readout)
|
|
1146
|
+
// and testable without mounting anything.
|
|
1191
1147
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1148
|
+
function onTogglePlay() { selection.playing = !selection.playing; emit(); }
|
|
1149
|
+
|
|
1150
|
+
function onClear() { applySelection(""); }
|
|
1151
|
+
|
|
1152
|
+
function onRefresh() {
|
|
1153
|
+
// A manual refresh also retries wallpapers previously demoted to their
|
|
1154
|
+
// preview after decode failures.
|
|
1155
|
+
demotedToPreview.clear();
|
|
1156
|
+
mediaFailures.clear();
|
|
1157
|
+
lastRecoveryAt.clear();
|
|
1158
|
+
loadInventory(true);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function onLangChange(e) {
|
|
1162
|
+
selection.lang = e.target.value;
|
|
1163
|
+
persistSelection();
|
|
1164
|
+
emit();
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function onGroupChange(e) {
|
|
1168
|
+
selection.rotationGroupId = e.target.value;
|
|
1169
|
+
if (selection.rotationEnabled) {
|
|
1170
|
+
const first = rotationCandidates()[0];
|
|
1171
|
+
if (first) applySelection(first.id);
|
|
1172
|
+
else applySelection("");
|
|
1173
|
+
return;
|
|
1195
1174
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1175
|
+
persistSelection();
|
|
1176
|
+
syncRotationTimer();
|
|
1177
|
+
emit();
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function onToggleRotation() {
|
|
1181
|
+
selection.rotationEnabled = !selection.rotationEnabled;
|
|
1182
|
+
// The checkbox is disabled without a usable group, so a group id always
|
|
1183
|
+
// exists here when enabling.
|
|
1184
|
+
if (selection.rotationEnabled &&
|
|
1185
|
+
!rotationCandidates().some((w) => w.id === selection.id)) {
|
|
1186
|
+
const first = rotationCandidates()[0];
|
|
1187
|
+
if (first) { applySelection(first.id); return; }
|
|
1203
1188
|
}
|
|
1189
|
+
persistSelection();
|
|
1190
|
+
syncRotationTimer();
|
|
1191
|
+
emit();
|
|
1192
|
+
}
|
|
1204
1193
|
|
|
1205
|
-
|
|
1206
|
-
const q = query.trim();
|
|
1207
|
-
const filteredList = usableList.filter((w) => matchesQuery(w, q));
|
|
1208
|
-
const groups = sel.rotationGroups;
|
|
1194
|
+
function onGroupInterval(e) {
|
|
1209
1195
|
const group = activeRotationGroup();
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1196
|
+
if (!group) return;
|
|
1197
|
+
group.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval);
|
|
1198
|
+
persistSelection();
|
|
1199
|
+
syncRotationTimer();
|
|
1200
|
+
emit();
|
|
1201
|
+
}
|
|
1215
1202
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1203
|
+
function onDeleteGroup() {
|
|
1204
|
+
const group = activeRotationGroup();
|
|
1205
|
+
if (!group) return;
|
|
1206
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
|
1207
|
+
if (!window.confirm(S().confirmDelete(group.name))) return;
|
|
1208
|
+
}
|
|
1209
|
+
deleteGroup(group.id);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function onTogglePauseOnHidden() {
|
|
1213
|
+
selection.pauseOnHidden = !selection.pauseOnHidden;
|
|
1214
|
+
if (selection.pauseOnHidden) {
|
|
1215
|
+
// Re-enabling while already hidden must re-apply the reason — the
|
|
1216
|
+
// visibilitychange listener only fires on CHANGES.
|
|
1217
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1218
|
+
setAutoPause("hidden", true);
|
|
1219
|
+
}
|
|
1220
|
+
} else {
|
|
1221
|
+
setAutoPause("hidden", false);
|
|
1222
|
+
}
|
|
1223
|
+
persistSelection();
|
|
1224
|
+
emit();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function onTogglePauseOnBattery() {
|
|
1228
|
+
selection.pauseOnBattery = !selection.pauseOnBattery;
|
|
1229
|
+
if (selection.pauseOnBattery) applyBatteryPause(); // re-evaluate NOW
|
|
1230
|
+
else setAutoPause("battery", false);
|
|
1231
|
+
persistSelection();
|
|
1232
|
+
emit();
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function onToggleAutoScrim() {
|
|
1236
|
+
selection.autoScrim = !selection.autoScrim;
|
|
1237
|
+
persistSelection();
|
|
1238
|
+
applyEffects();
|
|
1239
|
+
emit();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function onScrim(pct) { selection.scrim = pct / 100; persistSelection(); applyEffects(); emit(); }
|
|
1243
|
+
function onBorder(pct) { selection.border = pct / 100; persistSelection(); applyEffects(); emit(); }
|
|
1244
|
+
function onBlur(px) { selection.blur = px; persistSelection(); applyEffects(); emit(); }
|
|
1245
|
+
function onWallpaperBlur(px) { selection.wallpaperBlur = px; persistSelection(); applyEffects(); emit(); }
|
|
1246
|
+
|
|
1247
|
+
function onFit(e) {
|
|
1248
|
+
selection.fit = FIT_MODES.indexOf(e.target.value) >= 0 ? e.target.value : "cover";
|
|
1249
|
+
persistSelection();
|
|
1250
|
+
applyEffects();
|
|
1251
|
+
emit();
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// ── Picker sections (presentational; every input arrives as an argument) ────
|
|
1255
|
+
// WallpaperPicker composes these in a fixed order. Each one owns exactly one
|
|
1256
|
+
// visual row/block of the settings panel and produces the same DOM the old
|
|
1257
|
+
// monolithic render did.
|
|
1258
|
+
|
|
1259
|
+
function SearchRow(t, sel, query, setQuery, matched, total) {
|
|
1260
|
+
return React.createElement("div", { className: "webg-row" },
|
|
1261
|
+
React.createElement("input", {
|
|
1262
|
+
className: "webg-text webg-search", type: "search",
|
|
1263
|
+
placeholder: t.searchPlaceholder,
|
|
1264
|
+
value: query,
|
|
1265
|
+
onInput: (e) => setQuery(e.target.value),
|
|
1266
|
+
}),
|
|
1267
|
+
query.trim() && React.createElement("span", { className: "webg-hint" },
|
|
1268
|
+
t.matched(matched, total)),
|
|
1269
|
+
React.createElement("label", { className: "webg-lang" },
|
|
1270
|
+
React.createElement("span", { className: "webg-hint" }, t.language),
|
|
1271
|
+
React.createElement("select", {
|
|
1272
|
+
className: "webg-select webg-lang-select",
|
|
1273
|
+
value: sel.lang,
|
|
1274
|
+
onChange: onLangChange,
|
|
1275
|
+
},
|
|
1276
|
+
React.createElement("option", { value: "auto" }, t.langAuto),
|
|
1277
|
+
React.createElement("option", { value: "zh" }, "中文"),
|
|
1278
|
+
React.createElement("option", { value: "en" }, "English"),
|
|
1279
|
+
),
|
|
1280
|
+
),
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function WallpaperGrid(t, sel, usableList, filteredList, q) {
|
|
1285
|
+
return React.createElement("div", { className: "webg-grid" },
|
|
1286
|
+
React.createElement("button", {
|
|
1287
|
+
className: "webg-card" + (sel.id ? "" : " webg-card--selected"),
|
|
1288
|
+
type: "button",
|
|
1289
|
+
onClick: onClear,
|
|
1290
|
+
title: t.closeTitle,
|
|
1291
|
+
},
|
|
1292
|
+
React.createElement("span", { className: "webg-card-close" }, t.closeCard),
|
|
1293
|
+
),
|
|
1294
|
+
usableList.length === 0
|
|
1295
|
+
? React.createElement("span", { className: "webg-hint" },
|
|
1296
|
+
sel.inventory.installDir
|
|
1297
|
+
? (sel.inventory.total > 0
|
|
1298
|
+
? t.emptyNoUsable(sel.inventory.total)
|
|
1299
|
+
: t.emptyNoProjects)
|
|
1300
|
+
: t.emptyNoInstall)
|
|
1301
|
+
: filteredList.length === 0
|
|
1302
|
+
? React.createElement("span", { className: "webg-hint" }, t.noMatch(q))
|
|
1303
|
+
: filteredList.map((w) => ThumbCard(w, w.id === sel.id, () => applySelection(w.id), t)),
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function ActionRow(t, sel) {
|
|
1308
|
+
return React.createElement("div", { className: "webg-row" },
|
|
1309
|
+
React.createElement("button", {
|
|
1310
|
+
className: "webg-btn", type: "button",
|
|
1311
|
+
onClick: onTogglePlay, disabled: !sel.url || sel.type !== "video",
|
|
1312
|
+
}, sel.playing ? t.pause : t.play),
|
|
1313
|
+
React.createElement("button", {
|
|
1314
|
+
className: "webg-btn", type: "button",
|
|
1315
|
+
onClick: onClear, disabled: !sel.id,
|
|
1316
|
+
}, t.close),
|
|
1317
|
+
React.createElement("button", {
|
|
1318
|
+
className: "webg-btn", type: "button",
|
|
1319
|
+
onClick: onRefresh, disabled: sel.loading,
|
|
1320
|
+
}, sel.loading ? t.refreshing : t.refresh),
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function RotationListRow(t, sel, groups) {
|
|
1325
|
+
return React.createElement("div", { className: "webg-row webg-playlist-row" },
|
|
1326
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.rotationList),
|
|
1327
|
+
React.createElement("select", {
|
|
1328
|
+
className: "webg-select webg-playlist-select",
|
|
1329
|
+
value: sel.rotationGroupId,
|
|
1330
|
+
onChange: onGroupChange,
|
|
1331
|
+
disabled: groups.length === 0,
|
|
1332
|
+
},
|
|
1333
|
+
React.createElement("option", { value: "" }, groups.length ? t.selectList : t.noLists),
|
|
1334
|
+
...groups.map((g) => React.createElement("option", {
|
|
1335
|
+
key: g.id, value: g.id,
|
|
1336
|
+
}, t.groupOption(g.name, groupWallpapers(g).length, g.interval))),
|
|
1337
|
+
),
|
|
1338
|
+
React.createElement("button", {
|
|
1339
|
+
className: "webg-btn", type: "button", onClick: startCreateGroup,
|
|
1340
|
+
}, t.newList),
|
|
1341
|
+
React.createElement("button", {
|
|
1342
|
+
className: "webg-btn", type: "button",
|
|
1343
|
+
onClick: () => startEditGroup(sel.rotationGroupId),
|
|
1344
|
+
disabled: !sel.rotationGroupId,
|
|
1345
|
+
}, t.editList),
|
|
1346
|
+
React.createElement("button", {
|
|
1347
|
+
className: "webg-btn", type: "button",
|
|
1348
|
+
onClick: onDeleteGroup,
|
|
1349
|
+
disabled: !sel.rotationGroupId,
|
|
1350
|
+
}, t.deleteList),
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/** One toggleable wallpaper chip inside the group editor. */
|
|
1355
|
+
function EditorCard(w, editing, t) {
|
|
1356
|
+
const checked = editing.wallpaperIds.indexOf(w.id) >= 0;
|
|
1357
|
+
return React.createElement("button", {
|
|
1358
|
+
key: w.id,
|
|
1359
|
+
className: "webg-editor-card" + (checked ? " webg-editor-card--checked" : ""),
|
|
1360
|
+
type: "button",
|
|
1361
|
+
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
1362
|
+
onClick: () => {
|
|
1363
|
+
const i = editing.wallpaperIds.indexOf(w.id);
|
|
1364
|
+
if (i >= 0) editing.wallpaperIds.splice(i, 1);
|
|
1365
|
+
else editing.wallpaperIds.push(w.id);
|
|
1366
|
+
emit();
|
|
1367
|
+
},
|
|
1368
|
+
},
|
|
1369
|
+
PreviewMedia(w, t),
|
|
1370
|
+
checked && React.createElement("span", { className: "webg-editor-check" }, "✓"),
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function GroupEditorPanel(t, sel, usableList, editorQuery, setEditorQuery) {
|
|
1375
|
+
const editing = sel.editing;
|
|
1376
|
+
return React.createElement("div", { className: "webg-editor" },
|
|
1218
1377
|
React.createElement("div", { className: "webg-row" },
|
|
1378
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.name),
|
|
1219
1379
|
React.createElement("input", {
|
|
1220
|
-
className: "webg-text
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
onInput: (e) => setQuery(e.target.value),
|
|
1380
|
+
className: "webg-text", type: "text",
|
|
1381
|
+
value: editing.name,
|
|
1382
|
+
onInput: (e) => { editing.name = e.target.value; emit(); },
|
|
1224
1383
|
}),
|
|
1225
|
-
q && React.createElement("span", { className: "webg-hint" },
|
|
1226
|
-
t.matched(filteredList.length, usableList.length)),
|
|
1227
|
-
React.createElement("label", { className: "webg-lang" },
|
|
1228
|
-
React.createElement("span", { className: "webg-hint" }, t.language),
|
|
1229
|
-
React.createElement("select", {
|
|
1230
|
-
className: "webg-select webg-lang-select",
|
|
1231
|
-
value: sel.lang,
|
|
1232
|
-
onChange: onLangChange,
|
|
1233
|
-
},
|
|
1234
|
-
React.createElement("option", { value: "auto" }, t.langAuto),
|
|
1235
|
-
React.createElement("option", { value: "zh" }, "中文"),
|
|
1236
|
-
React.createElement("option", { value: "en" }, "English"),
|
|
1237
|
-
),
|
|
1238
|
-
),
|
|
1239
1384
|
),
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
React.createElement("
|
|
1243
|
-
className: "webg-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1385
|
+
React.createElement("div", { className: "webg-row" },
|
|
1386
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.interval),
|
|
1387
|
+
React.createElement("select", {
|
|
1388
|
+
className: "webg-select",
|
|
1389
|
+
value: String(editing.interval),
|
|
1390
|
+
onChange: (e) => { editing.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval); emit(); },
|
|
1391
|
+
},
|
|
1392
|
+
...intervalOptions(editing.interval, t)),
|
|
1393
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.order),
|
|
1394
|
+
React.createElement("select", {
|
|
1395
|
+
className: "webg-select webg-playlist-select",
|
|
1396
|
+
value: editing.order,
|
|
1397
|
+
onChange: (e) => { editing.order = e.target.value; emit(); },
|
|
1247
1398
|
},
|
|
1248
|
-
React.createElement("
|
|
1399
|
+
React.createElement("option", { value: "sequence" }, t.sequence),
|
|
1400
|
+
React.createElement("option", { value: "random" }, t.random),
|
|
1249
1401
|
),
|
|
1250
|
-
usableList.length === 0
|
|
1251
|
-
? React.createElement("span", { className: "webg-hint" },
|
|
1252
|
-
sel.inventory.installDir
|
|
1253
|
-
? (sel.inventory.total > 0
|
|
1254
|
-
? t.emptyNoUsable(sel.inventory.total)
|
|
1255
|
-
: t.emptyNoProjects)
|
|
1256
|
-
: t.emptyNoInstall)
|
|
1257
|
-
: filteredList.length === 0
|
|
1258
|
-
? React.createElement("span", { className: "webg-hint" }, t.noMatch(q))
|
|
1259
|
-
: filteredList.map((w) => ThumbCard(w, w.id === sel.id, () => applySelection(w.id), t)),
|
|
1260
1402
|
),
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1403
|
+
React.createElement("input", {
|
|
1404
|
+
className: "webg-text", type: "search",
|
|
1405
|
+
placeholder: t.filterPlaceholder,
|
|
1406
|
+
value: editorQuery,
|
|
1407
|
+
onInput: (e) => setEditorQuery(e.target.value),
|
|
1408
|
+
}),
|
|
1409
|
+
React.createElement("div", { className: "webg-editor-grid" },
|
|
1410
|
+
(() => {
|
|
1411
|
+
const eq = editorQuery.trim();
|
|
1412
|
+
const pool = usableList.filter((w) => matchesQuery(w, eq));
|
|
1413
|
+
if (usableList.length === 0) {
|
|
1414
|
+
return React.createElement("span", { className: "webg-hint" }, t.noUsable);
|
|
1415
|
+
}
|
|
1416
|
+
if (pool.length === 0) {
|
|
1417
|
+
return React.createElement("span", { className: "webg-hint" }, t.noMatch(eq));
|
|
1418
|
+
}
|
|
1419
|
+
return pool.map((w) => EditorCard(w, editing, t));
|
|
1420
|
+
})(),
|
|
1275
1421
|
),
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
React.createElement("
|
|
1279
|
-
React.createElement("select", {
|
|
1422
|
+
React.createElement("div", { className: "webg-row" },
|
|
1423
|
+
React.createElement("span", { className: "webg-hint" }, t.selectedCount(editing.wallpaperIds.length)),
|
|
1424
|
+
sel.inventory.playlists.length > 0 && React.createElement("select", {
|
|
1280
1425
|
className: "webg-select webg-playlist-select",
|
|
1281
|
-
value:
|
|
1282
|
-
onChange:
|
|
1283
|
-
|
|
1426
|
+
value: "",
|
|
1427
|
+
onChange: (e) => {
|
|
1428
|
+
const p = sel.inventory.playlists.find((pl) => pl.id === e.target.value);
|
|
1429
|
+
if (p) importPlaylistIntoDraft(p);
|
|
1430
|
+
},
|
|
1284
1431
|
},
|
|
1285
|
-
React.createElement("option", { value: "" },
|
|
1286
|
-
...
|
|
1287
|
-
key:
|
|
1288
|
-
}, t.
|
|
1432
|
+
React.createElement("option", { value: "" }, t.importPlaylist),
|
|
1433
|
+
...sel.inventory.playlists.map((p) => React.createElement("option", {
|
|
1434
|
+
key: p.id, value: p.id,
|
|
1435
|
+
}, t.playlistOption(p.name, p.portableCount || 0))),
|
|
1289
1436
|
),
|
|
1437
|
+
),
|
|
1438
|
+
React.createElement("div", { className: "webg-row" },
|
|
1290
1439
|
React.createElement("button", {
|
|
1291
|
-
className: "webg-btn", type: "button",
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
className: "webg-btn", type: "button",
|
|
1295
|
-
onClick: () => startEditGroup(sel.rotationGroupId),
|
|
1296
|
-
disabled: !sel.rotationGroupId,
|
|
1297
|
-
}, t.editList),
|
|
1440
|
+
className: "webg-btn webg-btn--primary", type: "button",
|
|
1441
|
+
onClick: saveEditingGroup,
|
|
1442
|
+
}, t.save),
|
|
1298
1443
|
React.createElement("button", {
|
|
1299
1444
|
className: "webg-btn", type: "button",
|
|
1300
|
-
onClick:
|
|
1301
|
-
|
|
1302
|
-
}, t.deleteList),
|
|
1445
|
+
onClick: cancelEditGroup,
|
|
1446
|
+
}, t.cancel),
|
|
1303
1447
|
),
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
onInput: (e) => { editing.name = e.target.value; emit(); },
|
|
1311
|
-
}),
|
|
1312
|
-
),
|
|
1313
|
-
React.createElement("div", { className: "webg-row" },
|
|
1314
|
-
React.createElement("span", { className: "webg-hint webg-label" }, t.interval),
|
|
1315
|
-
React.createElement("select", {
|
|
1316
|
-
className: "webg-select",
|
|
1317
|
-
value: String(editing.interval),
|
|
1318
|
-
onChange: (e) => { editing.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval); emit(); },
|
|
1319
|
-
},
|
|
1320
|
-
// A persisted interval outside the preset steps (imported groups,
|
|
1321
|
-
// hand-edited storage) still gets a matching option — otherwise the
|
|
1322
|
-
// select would silently display the first preset instead of reality.
|
|
1323
|
-
...[...new Set([...INTERVALS, editing.interval])].sort((x, y) => x - y).map((minutes) =>
|
|
1324
|
-
React.createElement("option", { key: minutes, value: String(minutes) }, t.minutes(minutes)),
|
|
1325
|
-
)),
|
|
1326
|
-
React.createElement("span", { className: "webg-hint webg-label" }, t.order),
|
|
1327
|
-
React.createElement("select", {
|
|
1328
|
-
className: "webg-select webg-playlist-select",
|
|
1329
|
-
value: editing.order,
|
|
1330
|
-
onChange: (e) => { editing.order = e.target.value; emit(); },
|
|
1331
|
-
},
|
|
1332
|
-
React.createElement("option", { value: "sequence" }, t.sequence),
|
|
1333
|
-
React.createElement("option", { value: "random" }, t.random),
|
|
1334
|
-
),
|
|
1335
|
-
),
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function RotationControlsRow(t, sel, group, usableCount) {
|
|
1452
|
+
return React.createElement("div", { className: "webg-row webg-rotation-row" },
|
|
1453
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
1336
1454
|
React.createElement("input", {
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1455
|
+
type: "checkbox",
|
|
1456
|
+
checked: sel.rotationEnabled,
|
|
1457
|
+
onChange: onToggleRotation,
|
|
1458
|
+
disabled: !sel.rotationGroupId || usableCount < 2,
|
|
1341
1459
|
}),
|
|
1342
|
-
|
|
1343
|
-
(() => {
|
|
1344
|
-
const eq = editorQuery.trim();
|
|
1345
|
-
const pool = usableList.filter((w) => matchesQuery(w, eq));
|
|
1346
|
-
if (usableList.length === 0) {
|
|
1347
|
-
return React.createElement("span", { className: "webg-hint" }, t.noUsable);
|
|
1348
|
-
}
|
|
1349
|
-
if (pool.length === 0) {
|
|
1350
|
-
return React.createElement("span", { className: "webg-hint" }, t.noMatch(eq));
|
|
1351
|
-
}
|
|
1352
|
-
return pool.map((w) => {
|
|
1353
|
-
const checked = editing.wallpaperIds.indexOf(w.id) >= 0;
|
|
1354
|
-
return React.createElement("button", {
|
|
1355
|
-
key: w.id,
|
|
1356
|
-
className: "webg-editor-card" + (checked ? " webg-editor-card--checked" : ""),
|
|
1357
|
-
type: "button",
|
|
1358
|
-
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
1359
|
-
onClick: () => {
|
|
1360
|
-
const i = editing.wallpaperIds.indexOf(w.id);
|
|
1361
|
-
if (i >= 0) editing.wallpaperIds.splice(i, 1);
|
|
1362
|
-
else editing.wallpaperIds.push(w.id);
|
|
1363
|
-
emit();
|
|
1364
|
-
},
|
|
1365
|
-
},
|
|
1366
|
-
w.preview
|
|
1367
|
-
? React.createElement("img", {
|
|
1368
|
-
src: w.preview, alt: w.title, loading: "lazy", decoding: "async",
|
|
1369
|
-
onError: (e) => { e.target.style.display = "none"; },
|
|
1370
|
-
})
|
|
1371
|
-
: React.createElement("span", { className: "webg-card-placeholder" }, t.noPreview),
|
|
1372
|
-
checked && React.createElement("span", { className: "webg-editor-check" }, "✓"),
|
|
1373
|
-
);
|
|
1374
|
-
});
|
|
1375
|
-
})(),
|
|
1376
|
-
),
|
|
1377
|
-
React.createElement("div", { className: "webg-row" },
|
|
1378
|
-
React.createElement("span", { className: "webg-hint" }, t.selectedCount(editing.wallpaperIds.length)),
|
|
1379
|
-
sel.inventory.playlists.length > 0 && React.createElement("select", {
|
|
1380
|
-
className: "webg-select webg-playlist-select",
|
|
1381
|
-
value: "",
|
|
1382
|
-
onChange: (e) => {
|
|
1383
|
-
const p = sel.inventory.playlists.find((pl) => pl.id === e.target.value);
|
|
1384
|
-
if (p) importPlaylistIntoDraft(p);
|
|
1385
|
-
},
|
|
1386
|
-
},
|
|
1387
|
-
React.createElement("option", { value: "" }, t.importPlaylist),
|
|
1388
|
-
...sel.inventory.playlists.map((p) => React.createElement("option", {
|
|
1389
|
-
key: p.id, value: p.id,
|
|
1390
|
-
}, t.playlistOption(p.name, p.portableCount || 0))),
|
|
1391
|
-
),
|
|
1392
|
-
),
|
|
1393
|
-
React.createElement("div", { className: "webg-row" },
|
|
1394
|
-
React.createElement("button", {
|
|
1395
|
-
className: "webg-btn webg-btn--primary", type: "button",
|
|
1396
|
-
onClick: saveEditingGroup,
|
|
1397
|
-
}, t.save),
|
|
1398
|
-
React.createElement("button", {
|
|
1399
|
-
className: "webg-btn", type: "button",
|
|
1400
|
-
onClick: cancelEditGroup,
|
|
1401
|
-
}, t.cancel),
|
|
1402
|
-
),
|
|
1460
|
+
t.autoRotate,
|
|
1403
1461
|
),
|
|
1404
|
-
React.createElement("
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1462
|
+
React.createElement("select", {
|
|
1463
|
+
className: "webg-select webg-rotation-interval",
|
|
1464
|
+
value: String(group ? group.interval : DEFAULTS.rotationInterval),
|
|
1465
|
+
onChange: onGroupInterval,
|
|
1466
|
+
disabled: !sel.rotationEnabled || !sel.rotationGroupId || usableCount < 2,
|
|
1467
|
+
title: t.intervalTitle,
|
|
1468
|
+
},
|
|
1469
|
+
...intervalOptions(group ? group.interval : DEFAULTS.rotationInterval, t)),
|
|
1470
|
+
!sel.rotationGroupId && React.createElement("span", { className: "webg-hint" }, t.needList),
|
|
1471
|
+
sel.rotationGroupId && usableCount < 2 && React.createElement("span", { className: "webg-hint" }, t.needTwo),
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function EffectsPanel(t, sel) {
|
|
1476
|
+
return React.createElement(React.Fragment, null,
|
|
1477
|
+
// object-fit only means something for video/still wallpapers — an
|
|
1478
|
+
// iframe ignores it and always fills its layer — so the base-mode
|
|
1479
|
+
// select is hidden for web. The manual crop (drag / wheel in the
|
|
1480
|
+
// adjust overlay) is a plain CSS transform and applies to EVERY kind,
|
|
1481
|
+
// so 调整画面 / 重置裁剪 stay available even for iframes.
|
|
1482
|
+
React.createElement("div", { className: "webg-row webg-fit-row" },
|
|
1483
|
+
sel.type !== "web" && React.createElement("span", { className: "webg-hint webg-label" }, t.fit),
|
|
1484
|
+
sel.type !== "web" && React.createElement("select", {
|
|
1485
|
+
className: "webg-select webg-fit-select",
|
|
1486
|
+
value: sel.fit,
|
|
1487
|
+
onChange: onFit,
|
|
1420
1488
|
},
|
|
1421
|
-
...[
|
|
1422
|
-
.
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
sel.id && React.createElement(React.Fragment, null,
|
|
1430
|
-
// Canvas fit only matters for video/still wallpapers — an iframe
|
|
1431
|
-
// ignores object-fit and always fills its layer. Manual crop (drag /
|
|
1432
|
-
// wheel in the adjust overlay) replaced the old fixed anchor select.
|
|
1433
|
-
sel.type !== "web" && React.createElement("div", { className: "webg-row webg-fit-row" },
|
|
1434
|
-
React.createElement("span", { className: "webg-hint webg-label" }, t.fit),
|
|
1435
|
-
React.createElement("select", {
|
|
1436
|
-
className: "webg-select webg-fit-select",
|
|
1437
|
-
value: sel.fit,
|
|
1438
|
-
onChange: onFit,
|
|
1439
|
-
},
|
|
1440
|
-
...[t.fitCover, t.fitContain, t.fitFill, t.fitNone].map((label, i) =>
|
|
1441
|
-
React.createElement("option", { key: FIT_MODES[i], value: FIT_MODES[i] }, label),
|
|
1442
|
-
)),
|
|
1489
|
+
...[t.fitCover, t.fitContain, t.fitFill, t.fitNone].map((label, i) =>
|
|
1490
|
+
React.createElement("option", { key: FIT_MODES[i], value: FIT_MODES[i] }, label),
|
|
1491
|
+
)),
|
|
1492
|
+
React.createElement("button", {
|
|
1493
|
+
className: "webg-btn", type: "button", onClick: enterAdjust,
|
|
1494
|
+
disabled: sel.adjusting,
|
|
1495
|
+
}, t.adjust),
|
|
1496
|
+
(sel.zoom !== 1 || sel.offsetX !== 0 || sel.offsetY !== 0) &&
|
|
1443
1497
|
React.createElement("button", {
|
|
1444
|
-
className: "webg-btn", type: "button", onClick:
|
|
1445
|
-
|
|
1446
|
-
}, t.adjust),
|
|
1447
|
-
(sel.zoom !== 1 || sel.offsetX !== 0 || sel.offsetY !== 0) &&
|
|
1448
|
-
React.createElement("button", {
|
|
1449
|
-
className: "webg-btn", type: "button", onClick: resetCrop,
|
|
1450
|
-
}, t.resetCrop),
|
|
1451
|
-
),
|
|
1452
|
-
React.createElement("div", { className: "webg-effects" },
|
|
1453
|
-
SliderRow(t.wallpaperBlur, 0, 60, 1, sel.wallpaperBlur, onWallpaperBlur, sel.wallpaperBlur + "px"),
|
|
1454
|
-
SliderRow(t.scrim, 0, 90, 5, Math.min(90, Math.round(sel.scrim * 100)), onScrim,
|
|
1455
|
-
Math.round(sel.scrim * 100) + "%"),
|
|
1456
|
-
SliderRow(t.border, 0, 90, 5, Math.round(sel.border * 100), onBorder, Math.round(sel.border * 100) + "%"),
|
|
1457
|
-
SliderRow(t.glass, 0, 40, 1, sel.blur, onBlur, sel.blur + "px"),
|
|
1458
|
-
),
|
|
1498
|
+
className: "webg-btn", type: "button", onClick: resetCrop,
|
|
1499
|
+
}, t.resetCrop),
|
|
1459
1500
|
),
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
React.createElement("
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1501
|
+
React.createElement("div", { className: "webg-effects" },
|
|
1502
|
+
SliderRow(t.wallpaperBlur, 0, 60, 1, sel.wallpaperBlur, onWallpaperBlur, sel.wallpaperBlur + "px"),
|
|
1503
|
+
SliderRow(t.scrim, 0, 90, 5, Math.min(90, Math.round(sel.scrim * 100)), onScrim,
|
|
1504
|
+
Math.round(sel.scrim * 100) + "%"),
|
|
1505
|
+
SliderRow(t.border, 0, 90, 5, Math.min(90, Math.round(sel.border * 100)), onBorder,
|
|
1506
|
+
Math.round(sel.border * 100) + "%"),
|
|
1507
|
+
SliderRow(t.glass, 0, 40, 1, sel.blur, onBlur, sel.blur + "px"),
|
|
1508
|
+
),
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function MonitorRow(t, sel) {
|
|
1513
|
+
return React.createElement("div", { className: "webg-row webg-monitor-row" },
|
|
1514
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
1515
|
+
React.createElement("input", {
|
|
1516
|
+
type: "checkbox", checked: sel.pauseOnHidden, onChange: onTogglePauseOnHidden,
|
|
1517
|
+
}),
|
|
1518
|
+
t.pauseOnHidden,
|
|
1519
|
+
),
|
|
1520
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
1521
|
+
React.createElement("input", {
|
|
1522
|
+
type: "checkbox", checked: sel.pauseOnBattery, onChange: onTogglePauseOnBattery,
|
|
1523
|
+
}),
|
|
1524
|
+
t.pauseOnBattery,
|
|
1484
1525
|
),
|
|
1526
|
+
React.createElement("label", { className: "webg-toggle", title: t.autoScrimTitle },
|
|
1527
|
+
React.createElement("input", {
|
|
1528
|
+
type: "checkbox", checked: sel.autoScrim, onChange: onToggleAutoScrim,
|
|
1529
|
+
}),
|
|
1530
|
+
t.autoScrim,
|
|
1531
|
+
),
|
|
1532
|
+
sel.fps !== null && React.createElement("span", {
|
|
1533
|
+
className: "webg-hint webg-fps" + (sel.fps < LOW_FPS ? " webg-fps--low" : ""),
|
|
1534
|
+
title: t.fpsTitle,
|
|
1535
|
+
}, sel.fps + " fps"),
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function StatusRow(t, sel, group, usableCount, usableTotal) {
|
|
1540
|
+
return React.createElement("div", { className: "webg-row" },
|
|
1541
|
+
React.createElement("span", { className: "webg-hint" },
|
|
1542
|
+
(group
|
|
1543
|
+
? t.statusGroup(group.name, group.wallpaperIds.length, usableCount, group.interval,
|
|
1544
|
+
group.order === "random" ? t.random : t.sequence)
|
|
1545
|
+
: t.statusUsable(usableTotal)) +
|
|
1546
|
+
(sel.rotationEnabled ? " · " + t.autoRotating : "") +
|
|
1547
|
+
(sel.autoPauseReasons.size > 0 ? " · " + t.autoPaused : "") +
|
|
1548
|
+
(sel.id && demotedToPreview.has(sel.id) ? " · " + t.decodeFallback : "") +
|
|
1549
|
+
(sel.inventory.installDir ? " · " + sel.inventory.installDir : "")),
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function WallpaperPicker() {
|
|
1554
|
+
const sel = useStore();
|
|
1555
|
+
const [query, setQuery] = React.useState("");
|
|
1556
|
+
const [editorQuery, setEditorQuery] = React.useState("");
|
|
1557
|
+
const t = S();
|
|
1558
|
+
|
|
1559
|
+
if (!sel.loaded) {
|
|
1560
|
+
return React.createElement("div", { className: "webg-picker" },
|
|
1561
|
+
React.createElement("span", { className: "webg-hint" }, t.scanning));
|
|
1562
|
+
}
|
|
1563
|
+
if (sel.inventory.error) {
|
|
1564
|
+
return React.createElement("div", { className: "webg-picker" },
|
|
1565
|
+
React.createElement("div", { className: "webg-error" },
|
|
1566
|
+
t.connError + sel.inventory.error),
|
|
1567
|
+
React.createElement("button", {
|
|
1568
|
+
className: "webg-btn", type: "button", onClick: onRefresh, disabled: sel.loading,
|
|
1569
|
+
}, sel.loading ? t.refreshing : t.retry));
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const usableList = selectableInventory();
|
|
1573
|
+
const q = query.trim();
|
|
1574
|
+
const filteredList = usableList.filter((w) => matchesQuery(w, q));
|
|
1575
|
+
const groups = sel.rotationGroups;
|
|
1576
|
+
const group = activeRotationGroup();
|
|
1577
|
+
const usableCount = rotationCandidates().length;
|
|
1578
|
+
const editing = sel.editing;
|
|
1579
|
+
|
|
1580
|
+
return React.createElement("div", { className: "webg-picker" },
|
|
1581
|
+
SearchRow(t, sel, query, setQuery, filteredList.length, usableList.length),
|
|
1582
|
+
WallpaperGrid(t, sel, usableList, filteredList, q),
|
|
1583
|
+
ActionRow(t, sel),
|
|
1584
|
+
RotationListRow(t, sel, groups),
|
|
1585
|
+
editing && GroupEditorPanel(t, sel, usableList, editorQuery, setEditorQuery),
|
|
1586
|
+
RotationControlsRow(t, sel, group, usableCount),
|
|
1587
|
+
sel.id && EffectsPanel(t, sel),
|
|
1588
|
+
sel.id && MonitorRow(t, sel),
|
|
1485
1589
|
sel.id && sel.fps !== null && sel.fps < LOW_FPS && sel.type === "video" &&
|
|
1486
1590
|
React.createElement("div", { className: "webg-row" },
|
|
1487
|
-
React.createElement("span", { className: "webg-hint" }, t.lowFpsHint),
|
|
1488
|
-
|
|
1489
|
-
// ── Status ──
|
|
1490
|
-
React.createElement("div", { className: "webg-row" },
|
|
1491
|
-
React.createElement("span", { className: "webg-hint" },
|
|
1492
|
-
(group
|
|
1493
|
-
? t.statusGroup(group.name, group.wallpaperIds.length, usableCount, group.interval,
|
|
1494
|
-
group.order === "random" ? t.random : t.sequence)
|
|
1495
|
-
: t.statusUsable(usableList.length)) +
|
|
1496
|
-
(sel.rotationEnabled ? " · " + t.autoRotating : "") +
|
|
1497
|
-
(autoPaused ? " · " + t.autoPaused : "") +
|
|
1498
|
-
(sel.id && demotedToPreview.has(sel.id) ? " · " + t.decodeFallback : "") +
|
|
1499
|
-
(sel.inventory.installDir ? " · " + sel.inventory.installDir : "")),
|
|
1500
|
-
),
|
|
1591
|
+
React.createElement("span", { className: "webg-hint" }, t.lowFpsHint)),
|
|
1592
|
+
StatusRow(t, sel, group, usableCount, usableList.length),
|
|
1501
1593
|
);
|
|
1502
1594
|
}
|
|
1503
1595
|
|
|
@@ -1513,17 +1605,18 @@ window.__ModuleLoader__.load({
|
|
|
1513
1605
|
}
|
|
1514
1606
|
.webg-layer--enter { opacity: 0; }
|
|
1515
1607
|
.webg-layer--leave { opacity: 0; }
|
|
1516
|
-
/*
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
saturate keeps blurred colours from
|
|
1608
|
+
/* The media box is ALWAYS exactly the layer. Blur must never re-frame the
|
|
1609
|
+
content: enlarging the box for overscan made object-fit:cover scale the
|
|
1610
|
+
picture up to fill the bigger box, silently cropping the wallpaper's
|
|
1611
|
+
four edges — and once cropped, no zoom could bring them back. Instead the
|
|
1612
|
+
Gaussian's natural edge fade (≤ one blur radius, under the veil and the
|
|
1613
|
+
UI chrome) is accepted. A slight saturate keeps blurred colours from
|
|
1614
|
+
washing out. */
|
|
1522
1615
|
.webg-layer .webg-media {
|
|
1523
1616
|
position: absolute;
|
|
1524
|
-
inset:
|
|
1525
|
-
width:
|
|
1526
|
-
height:
|
|
1617
|
+
inset: 0;
|
|
1618
|
+
width: 100%;
|
|
1619
|
+
height: 100%;
|
|
1527
1620
|
max-width: none; max-height: none;
|
|
1528
1621
|
object-fit: var(--webg-fit, cover);
|
|
1529
1622
|
object-position: center;
|
|
@@ -1616,10 +1709,10 @@ window.__ModuleLoader__.load({
|
|
|
1616
1709
|
-webkit-backdrop-filter: blur(var(--webg-blur, 16px)) saturate(var(--webg-saturate, 1.8)) brightness(var(--webg-glass-brightness, 1.04)) contrast(1.01);
|
|
1617
1710
|
backdrop-filter: blur(var(--webg-blur, 16px)) saturate(var(--webg-saturate, 1.8)) brightness(var(--webg-glass-brightness, 1.04)) contrast(1.01);
|
|
1618
1711
|
box-shadow:
|
|
1619
|
-
inset 0 1px 0 rgba(255, 255, 255,
|
|
1712
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
|
1620
1713
|
inset 0 -1px 0 rgba(255, 255, 255, 0.08),
|
|
1621
1714
|
inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
|
|
1622
|
-
0 12px 40px rgba(0, 0, 0,
|
|
1715
|
+
0 12px 40px rgba(0, 0, 0, 0.12);
|
|
1623
1716
|
}
|
|
1624
1717
|
|
|
1625
1718
|
/* ── Picker chrome (token-driven, theme-aware) ── */
|
|
@@ -1830,6 +1923,12 @@ window.__ModuleLoader__.load({
|
|
|
1830
1923
|
const inject = ["slots"];
|
|
1831
1924
|
|
|
1832
1925
|
function apply(ctx) {
|
|
1926
|
+
// A plugin UPDATE re-applies this module WITHOUT re-evaluating it: the old
|
|
1927
|
+
// fiber's cleanup has already set `disposed = true`, which would make the
|
|
1928
|
+
// new fiber's loadInventory return forever (blank picker, no wallpaper).
|
|
1929
|
+
// Every apply starts a fresh lifecycle.
|
|
1930
|
+
disposed = false;
|
|
1931
|
+
|
|
1833
1932
|
// 0. Language: follow the shell's locale service when present, and react
|
|
1834
1933
|
// to its change events. Everything is optional — without the service
|
|
1835
1934
|
// the browser language decides, and the manual override always wins.
|