wallpaper-engine-dsh 1.0.2 → 1.0.4
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 +6 -6
- package/README.zh.md +13 -13
- package/lib/client.js +542 -438
- package/lib/core.js +27 -3
- package/lib/index.js +82 -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,9 +273,9 @@ 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,
|
|
274
|
-
zoom
|
|
275
|
-
//
|
|
276
|
-
|
|
276
|
+
// Older builds could persist zoom < 1 / unclamped pans — clampCrop()
|
|
277
|
+
// at store creation re-seats both into the gap-free envelope.
|
|
278
|
+
zoom: clampNum(o.zoom, 0.25, 4, DEFAULTS.zoom),
|
|
277
279
|
offsetX: clampNum(o.offsetX, -200, 200, DEFAULTS.offsetX),
|
|
278
280
|
offsetY: clampNum(o.offsetY, -200, 200, DEFAULTS.offsetY),
|
|
279
281
|
};
|
|
@@ -339,21 +341,19 @@ window.__ModuleLoader__.load({
|
|
|
339
341
|
}
|
|
340
342
|
|
|
341
343
|
// ── Wallpaper predicates ─────────────────────────────────────────────────────
|
|
342
|
-
// playable: real media (video/web) renders live.
|
|
343
344
|
// Renderable kinds ONLY — scene/application wallpapers are .pkg scene
|
|
344
345
|
// packages the browser cannot render (their live animation is Wallpaper
|
|
345
346
|
// Engine's desktop job), so they are filtered out of the grid entirely
|
|
346
|
-
// 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.
|
|
347
351
|
function isPlayable(w) {
|
|
348
352
|
return Boolean(w && w.playable && (w.type === "video" || w.type === "web"));
|
|
349
353
|
}
|
|
350
354
|
|
|
351
|
-
function isSelectable(w) {
|
|
352
|
-
return isPlayable(w);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
355
|
function selectableInventory() {
|
|
356
|
-
return selection.inventory.wallpapers.filter(
|
|
356
|
+
return selection.inventory.wallpapers.filter(isPlayable);
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
// ── Inventory ────────────────────────────────────────────────────────────────
|
|
@@ -403,25 +403,20 @@ window.__ModuleLoader__.load({
|
|
|
403
403
|
selection.rotationGroupId = "";
|
|
404
404
|
persistSelection();
|
|
405
405
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
406
|
+
// Rotation bookkeeping. A chosen list is never migrated away from — a
|
|
407
|
+
// list with fewer than 2 usable wallpapers simply keeps rotation
|
|
408
|
+
// disarmed (the timer needs a pair to alternate), and a hand-picked
|
|
409
|
+
// wallpaper outside the active list keeps playing until the next
|
|
410
|
+
// rotation tick brings the list back around.
|
|
411
|
+
if (selection.rotationEnabled && !selection.rotationGroupId) {
|
|
412
|
+
const usable = firstUsableGroup();
|
|
413
|
+
if (usable) selection.rotationGroupId = usable.id;
|
|
414
|
+
else selection.rotationEnabled = false;
|
|
416
415
|
persistSelection();
|
|
417
416
|
}
|
|
418
417
|
|
|
419
418
|
// Drop a selection whose wallpaper vanished or became unusable.
|
|
420
|
-
if (selection.id && !selection.inventory.wallpapers.some((w) => w.id === selection.id &&
|
|
421
|
-
selection.id = "";
|
|
422
|
-
persistSelection();
|
|
423
|
-
}
|
|
424
|
-
if (selection.rotationEnabled && selection.id && !rotationCandidates().some((w) => w.id === selection.id)) {
|
|
419
|
+
if (selection.id && !selection.inventory.wallpapers.some((w) => w.id === selection.id && isPlayable(w))) {
|
|
425
420
|
selection.id = "";
|
|
426
421
|
persistSelection();
|
|
427
422
|
}
|
|
@@ -441,7 +436,7 @@ window.__ModuleLoader__.load({
|
|
|
441
436
|
function groupWallpapers(group) {
|
|
442
437
|
if (!group || !Array.isArray(group.wallpaperIds)) return [];
|
|
443
438
|
const byId = new Map(selection.inventory.wallpapers.map((w) => [w.id, w]));
|
|
444
|
-
return group.wallpaperIds.map((id) => byId.get(id)).filter(
|
|
439
|
+
return group.wallpaperIds.map((id) => byId.get(id)).filter(isPlayable);
|
|
445
440
|
}
|
|
446
441
|
|
|
447
442
|
function rotationCandidates() {
|
|
@@ -457,7 +452,7 @@ window.__ModuleLoader__.load({
|
|
|
457
452
|
if (!source) return false;
|
|
458
453
|
const byId = new Map(selection.inventory.wallpapers.map((w) => [w.id, w]));
|
|
459
454
|
const ids = Array.isArray(source.wallpaperIds)
|
|
460
|
-
? source.wallpaperIds.filter((id) =>
|
|
455
|
+
? source.wallpaperIds.filter((id) => isPlayable(byId.get(id)))
|
|
461
456
|
: [];
|
|
462
457
|
if (ids.length < 2) return false;
|
|
463
458
|
selection.rotationGroups.push({
|
|
@@ -672,7 +667,7 @@ window.__ModuleLoader__.load({
|
|
|
672
667
|
media.setAttribute("frameborder", "0");
|
|
673
668
|
media.setAttribute("scrolling", "no");
|
|
674
669
|
media.setAttribute("title", S().iframeTitle);
|
|
675
|
-
media.className = "webg-media
|
|
670
|
+
media.className = "webg-media";
|
|
676
671
|
} else {
|
|
677
672
|
// Static fallback: a playable wallpaper demoted after repeated media
|
|
678
673
|
// failures — its preview still beats a black screen.
|
|
@@ -689,7 +684,10 @@ window.__ModuleLoader__.load({
|
|
|
689
684
|
}
|
|
690
685
|
|
|
691
686
|
function attachStillSampling(media, sel) {
|
|
692
|
-
const type =
|
|
687
|
+
const type = sel && sel.type;
|
|
688
|
+
// Web wallpapers cannot be sampled — the sandboxed iframe's origin is
|
|
689
|
+
// opaque, so its pixels are unreadable. Only video/img frames feed the
|
|
690
|
+
// smart veil.
|
|
693
691
|
if (type !== "video" && type !== "image") return;
|
|
694
692
|
media.addEventListener(type === "video" ? "loadeddata" : "load", () => {
|
|
695
693
|
sampleWallpaperLuminance();
|
|
@@ -717,9 +715,10 @@ window.__ModuleLoader__.load({
|
|
|
717
715
|
// The media itself is unusable here — fall back to the still preview.
|
|
718
716
|
demotedToPreview.add(id);
|
|
719
717
|
applySelection(id);
|
|
720
|
-
} else
|
|
721
|
-
//
|
|
722
|
-
// inventory): clear the selection
|
|
718
|
+
} else {
|
|
719
|
+
// Unrenderable here with no preview to fall back on (or the entry has
|
|
720
|
+
// vanished from the inventory entirely): clear the selection rather
|
|
721
|
+
// than leave a dead element on the layer.
|
|
723
722
|
applySelection("");
|
|
724
723
|
}
|
|
725
724
|
});
|
|
@@ -792,11 +791,10 @@ window.__ModuleLoader__.load({
|
|
|
792
791
|
};
|
|
793
792
|
const onPointerMove = (e) => {
|
|
794
793
|
if (!drag) return;
|
|
795
|
-
// translate % refers to the
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
const h = (overlay.clientHeight || 1) + 2 * selection.wallpaperBlur;
|
|
794
|
+
// translate % refers to the media box, which is the viewport exactly —
|
|
795
|
+
// divide the pixel delta by it so the content tracks the cursor 1:1.
|
|
796
|
+
const w = overlay.clientWidth || 1;
|
|
797
|
+
const h = overlay.clientHeight || 1;
|
|
800
798
|
selection.offsetX += ((e.clientX - drag.x) / w) * 100;
|
|
801
799
|
selection.offsetY += ((e.clientY - drag.y) / h) * 100;
|
|
802
800
|
clampCrop(); // gap-free guarantee: pan never outruns the zoom headroom
|
|
@@ -874,6 +872,30 @@ window.__ModuleLoader__.load({
|
|
|
874
872
|
return token;
|
|
875
873
|
}
|
|
876
874
|
|
|
875
|
+
/**
|
|
876
|
+
* Swap re-minted URLs into the LIVE media element instead of remounting.
|
|
877
|
+
* An explicit refresh re-mints every token, so the element's old URL is
|
|
878
|
+
* already dead — the next seek or buffer-miss would 404 on it and trigger
|
|
879
|
+
* a full recovery restart. Hot-swapping keeps the element identity (and
|
|
880
|
+
* for video, the playback position) intact across the token change.
|
|
881
|
+
*/
|
|
882
|
+
function refreshLiveMediaUrls(layer) {
|
|
883
|
+
const video = layer.querySelector("video");
|
|
884
|
+
if (video && video.getAttribute("src") !== selection.url) {
|
|
885
|
+
const at = video.currentTime;
|
|
886
|
+
video.src = selection.url;
|
|
887
|
+
if (at > 0) {
|
|
888
|
+
video.addEventListener("loadedmetadata", () => {
|
|
889
|
+
try { video.currentTime = at; } catch { /* not seekable yet */ }
|
|
890
|
+
}, { once: true });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const img = layer.querySelector("img");
|
|
894
|
+
if (img && img.getAttribute("src") !== selection.url) img.src = selection.url;
|
|
895
|
+
const frame = layer.querySelector("iframe");
|
|
896
|
+
if (frame && frame.getAttribute("src") !== selection.url) frame.src = selection.url;
|
|
897
|
+
}
|
|
898
|
+
|
|
877
899
|
function syncLayers() {
|
|
878
900
|
const existing = document.getElementById(LAYER_ID);
|
|
879
901
|
|
|
@@ -885,7 +907,9 @@ window.__ModuleLoader__.load({
|
|
|
885
907
|
const gotKey = existing && existing.dataset.webgKey;
|
|
886
908
|
|
|
887
909
|
if (existing && gotKey === wantKey && !forceRemount) {
|
|
888
|
-
// Same wallpaper — only play/pause may
|
|
910
|
+
// Same wallpaper — adopt any re-minted URL, then only play/pause may
|
|
911
|
+
// have changed.
|
|
912
|
+
refreshLiveMediaUrls(existing);
|
|
889
913
|
} else {
|
|
890
914
|
forceRemount = false;
|
|
891
915
|
// Crossfade: the new layer fades in over the old one; the old layer
|
|
@@ -902,7 +926,7 @@ window.__ModuleLoader__.load({
|
|
|
902
926
|
if (existing) {
|
|
903
927
|
existing.classList.add("webg-layer--leave");
|
|
904
928
|
const stale = existing;
|
|
905
|
-
schedule(() => stale.remove(),
|
|
929
|
+
schedule(() => stale.remove(), LEAVE_MS);
|
|
906
930
|
}
|
|
907
931
|
}
|
|
908
932
|
const node = document.getElementById(LAYER_ID);
|
|
@@ -924,7 +948,7 @@ window.__ModuleLoader__.load({
|
|
|
924
948
|
if (s) s.remove();
|
|
925
949
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
926
950
|
}
|
|
927
|
-
},
|
|
951
|
+
}, LEAVE_MS);
|
|
928
952
|
}
|
|
929
953
|
|
|
930
954
|
// Scrim: present while a wallpaper is active. On clear it outlives the
|
|
@@ -946,6 +970,11 @@ window.__ModuleLoader__.load({
|
|
|
946
970
|
// sample; iframes of web wallpapers can never be sampled — opaque origin).
|
|
947
971
|
let sampledLuminance = null;
|
|
948
972
|
let sampleCanvas = null;
|
|
973
|
+
// Snapshot of every input applyEffects reads, from its last full write. The
|
|
974
|
+
// store emits once per second for the FPS readout — without this guard every
|
|
975
|
+
// tick would rewrite all CSS variables AND force a synchronous layout for
|
|
976
|
+
// nothing. clearEffects() resets it so the next apply always writes.
|
|
977
|
+
let lastEffectKey = null;
|
|
949
978
|
|
|
950
979
|
/** Average Rec.601 luminance (0..1) of the current video/img, or null. */
|
|
951
980
|
function sampleWallpaperLuminance() {
|
|
@@ -973,11 +1002,18 @@ window.__ModuleLoader__.load({
|
|
|
973
1002
|
}
|
|
974
1003
|
|
|
975
1004
|
function applyEffects() {
|
|
976
|
-
const s = document.body.style;
|
|
977
1005
|
// The veil OPPOSES the text colour: dark theme (light text) darkens bright
|
|
978
1006
|
// wallpapers, light theme (dark text) brightens dark ones — either way the
|
|
979
1007
|
// text side wins. This is what makes ANY wallpaper survivable.
|
|
980
1008
|
const dark = Boolean(document.body.hasAttribute && document.body.hasAttribute("data-ds-dark-theme"));
|
|
1009
|
+
const key = [
|
|
1010
|
+
dark, selection.scrim, selection.border, selection.blur, selection.wallpaperBlur,
|
|
1011
|
+
selection.autoScrim, sampledLuminance, selection.fit, selection.zoom,
|
|
1012
|
+
selection.offsetX, selection.offsetY,
|
|
1013
|
+
].join("\u0000");
|
|
1014
|
+
if (key === lastEffectKey) return; // nothing it reads changed → no writes, no reflow
|
|
1015
|
+
lastEffectKey = key;
|
|
1016
|
+
const s = document.body.style;
|
|
981
1017
|
let alpha = selection.scrim;
|
|
982
1018
|
if (selection.autoScrim && sampledLuminance !== null) {
|
|
983
1019
|
// Distance of the wallpaper's brightness from the "comfortable middle"
|
|
@@ -994,7 +1030,6 @@ window.__ModuleLoader__.load({
|
|
|
994
1030
|
// The glass "colour melt" scales with blur radius: 0 blur → no melt.
|
|
995
1031
|
s.setProperty("--webg-saturate", String(1.15 + selection.blur * 0.028));
|
|
996
1032
|
s.setProperty("--webg-glass-brightness", "1.04");
|
|
997
|
-
s.setProperty("--webg-wallpaper-blur", selection.wallpaperBlur + "px");
|
|
998
1033
|
// At 0 blur, keep the media OUT of the filter pipeline entirely: even
|
|
999
1034
|
// blur(0px) forces an extra raster/compositing pass, and the noise overlay
|
|
1000
1035
|
// would still occupy a layer at opacity 0. filter:none + display:none keep
|
|
@@ -1025,13 +1060,13 @@ window.__ModuleLoader__.load({
|
|
|
1025
1060
|
}
|
|
1026
1061
|
|
|
1027
1062
|
function clearEffects() {
|
|
1063
|
+
lastEffectKey = null;
|
|
1028
1064
|
const s = document.body.style;
|
|
1029
1065
|
s.removeProperty("--webg-scrim-color");
|
|
1030
1066
|
s.removeProperty("--webg-border-alpha");
|
|
1031
1067
|
s.removeProperty("--webg-blur");
|
|
1032
1068
|
s.removeProperty("--webg-saturate");
|
|
1033
1069
|
s.removeProperty("--webg-glass-brightness");
|
|
1034
|
-
s.removeProperty("--webg-wallpaper-blur");
|
|
1035
1070
|
s.removeProperty("--webg-media-filter");
|
|
1036
1071
|
s.removeProperty("--webg-noise-display");
|
|
1037
1072
|
s.removeProperty("--webg-noise-opacity");
|
|
@@ -1085,6 +1120,16 @@ window.__ModuleLoader__.load({
|
|
|
1085
1120
|
return (w.title || "").toLowerCase().includes(needle) || (w.id || "").toLowerCase().includes(needle);
|
|
1086
1121
|
}
|
|
1087
1122
|
|
|
1123
|
+
/** Preview <img>, or the "no preview" placeholder — shared by grid + editor. */
|
|
1124
|
+
function PreviewMedia(w, t) {
|
|
1125
|
+
return w.preview
|
|
1126
|
+
? React.createElement("img", {
|
|
1127
|
+
src: w.preview, alt: w.title, loading: "lazy", decoding: "async",
|
|
1128
|
+
onError: (e) => { e.target.style.display = "none"; },
|
|
1129
|
+
})
|
|
1130
|
+
: React.createElement("span", { className: "webg-card-placeholder" }, t.noPreview);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1088
1133
|
function ThumbCard(w, selected, onClick, t) {
|
|
1089
1134
|
return React.createElement("button", {
|
|
1090
1135
|
key: w.id,
|
|
@@ -1093,426 +1138,484 @@ window.__ModuleLoader__.load({
|
|
|
1093
1138
|
onClick: onClick,
|
|
1094
1139
|
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
1095
1140
|
},
|
|
1096
|
-
w
|
|
1097
|
-
? React.createElement("img", {
|
|
1098
|
-
src: w.preview, alt: w.title, loading: "lazy", decoding: "async",
|
|
1099
|
-
onError: (e) => { e.target.style.display = "none"; },
|
|
1100
|
-
})
|
|
1101
|
-
: React.createElement("span", { className: "webg-card-placeholder" }, t.noPreview),
|
|
1141
|
+
PreviewMedia(w, t),
|
|
1102
1142
|
React.createElement("span", { className: "webg-card-type" }, typeBadge(w, t)),
|
|
1103
1143
|
React.createElement("span", { className: "webg-card-title" }, w.title),
|
|
1104
1144
|
);
|
|
1105
1145
|
}
|
|
1106
1146
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
selection.lang = e.target.value;
|
|
1124
|
-
persistSelection();
|
|
1125
|
-
emit();
|
|
1126
|
-
};
|
|
1127
|
-
const onGroupChange = (e) => {
|
|
1128
|
-
selection.rotationGroupId = e.target.value;
|
|
1129
|
-
if (selection.rotationEnabled) {
|
|
1130
|
-
const first = rotationCandidates()[0];
|
|
1131
|
-
if (first) applySelection(first.id);
|
|
1132
|
-
else applySelection("");
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
persistSelection();
|
|
1136
|
-
syncRotationTimer();
|
|
1137
|
-
emit();
|
|
1138
|
-
};
|
|
1139
|
-
const onToggleRotation = () => {
|
|
1140
|
-
selection.rotationEnabled = !selection.rotationEnabled;
|
|
1141
|
-
// The checkbox is disabled without a usable group, so a group id always
|
|
1142
|
-
// exists here when enabling.
|
|
1143
|
-
if (selection.rotationEnabled &&
|
|
1144
|
-
!rotationCandidates().some((w) => w.id === selection.id)) {
|
|
1145
|
-
const first = rotationCandidates()[0];
|
|
1146
|
-
if (first) { applySelection(first.id); return; }
|
|
1147
|
-
}
|
|
1148
|
-
persistSelection();
|
|
1149
|
-
syncRotationTimer();
|
|
1150
|
-
emit();
|
|
1151
|
-
};
|
|
1152
|
-
const onGroupInterval = (e) => {
|
|
1153
|
-
const group = activeRotationGroup();
|
|
1154
|
-
if (!group) return;
|
|
1155
|
-
group.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval);
|
|
1156
|
-
persistSelection();
|
|
1157
|
-
syncRotationTimer();
|
|
1158
|
-
emit();
|
|
1159
|
-
};
|
|
1160
|
-
const onDeleteGroup = () => {
|
|
1161
|
-
const group = activeRotationGroup();
|
|
1162
|
-
if (!group) return;
|
|
1163
|
-
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
|
1164
|
-
if (!window.confirm(t.confirmDelete(group.name))) return;
|
|
1165
|
-
}
|
|
1166
|
-
deleteGroup(group.id);
|
|
1167
|
-
};
|
|
1168
|
-
const onTogglePauseOnHidden = () => {
|
|
1169
|
-
selection.pauseOnHidden = !selection.pauseOnHidden;
|
|
1170
|
-
if (selection.pauseOnHidden) {
|
|
1171
|
-
// Re-enabling while already hidden must re-apply the reason — the
|
|
1172
|
-
// visibilitychange listener only fires on CHANGES.
|
|
1173
|
-
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1174
|
-
setAutoPause("hidden", true);
|
|
1175
|
-
}
|
|
1176
|
-
} else {
|
|
1177
|
-
setAutoPause("hidden", false);
|
|
1178
|
-
}
|
|
1179
|
-
persistSelection();
|
|
1180
|
-
emit();
|
|
1181
|
-
};
|
|
1182
|
-
const onTogglePauseOnBattery = () => {
|
|
1183
|
-
selection.pauseOnBattery = !selection.pauseOnBattery;
|
|
1184
|
-
if (selection.pauseOnBattery) applyBatteryPause(); // re-evaluate NOW
|
|
1185
|
-
else setAutoPause("battery", false);
|
|
1186
|
-
persistSelection();
|
|
1187
|
-
emit();
|
|
1188
|
-
};
|
|
1189
|
-
const onToggleAutoScrim = () => {
|
|
1190
|
-
selection.autoScrim = !selection.autoScrim;
|
|
1191
|
-
persistSelection();
|
|
1192
|
-
applyEffects();
|
|
1193
|
-
emit();
|
|
1194
|
-
};
|
|
1147
|
+
/** Preset switch intervals offered by the rotation UI (minutes). */
|
|
1148
|
+
const ROTATION_INTERVALS = [1, 5, 10, 30, 60, 120];
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* <option> list for an interval select. A persisted interval outside the
|
|
1152
|
+
* preset steps (imported groups, hand-edited storage) still gets a matching
|
|
1153
|
+
* option — otherwise the select would silently display the first preset
|
|
1154
|
+
* instead of reality.
|
|
1155
|
+
*/
|
|
1156
|
+
function intervalOptions(current, t) {
|
|
1157
|
+
return [...new Set([...ROTATION_INTERVALS, current])]
|
|
1158
|
+
.sort((x, y) => x - y)
|
|
1159
|
+
.map((minutes) => React.createElement("option", {
|
|
1160
|
+
key: minutes, value: String(minutes),
|
|
1161
|
+
}, t.minutes(minutes)));
|
|
1162
|
+
}
|
|
1195
1163
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
selection.fit = FIT_MODES.indexOf(e.target.value) >= 0 ? e.target.value : "cover";
|
|
1202
|
-
persistSelection();
|
|
1203
|
-
applyEffects();
|
|
1204
|
-
emit();
|
|
1205
|
-
};
|
|
1164
|
+
// ── Picker actions (module scope) ────────────────────────────────────────────
|
|
1165
|
+
// Every control mutates the shared selection store and emits; none closes
|
|
1166
|
+
// over React state, so they live outside the component — stable across
|
|
1167
|
+
// re-renders (the picker re-renders once per second for the FPS readout)
|
|
1168
|
+
// and testable without mounting anything.
|
|
1206
1169
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1170
|
+
function onTogglePlay() { selection.playing = !selection.playing; emit(); }
|
|
1171
|
+
|
|
1172
|
+
function onClear() { applySelection(""); }
|
|
1173
|
+
|
|
1174
|
+
function onRefresh() {
|
|
1175
|
+
// A manual refresh also retries wallpapers previously demoted to their
|
|
1176
|
+
// preview after decode failures.
|
|
1177
|
+
demotedToPreview.clear();
|
|
1178
|
+
mediaFailures.clear();
|
|
1179
|
+
lastRecoveryAt.clear();
|
|
1180
|
+
loadInventory(true);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function onLangChange(e) {
|
|
1184
|
+
selection.lang = e.target.value;
|
|
1185
|
+
persistSelection();
|
|
1186
|
+
emit();
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function onGroupChange(e) {
|
|
1190
|
+
selection.rotationGroupId = e.target.value;
|
|
1191
|
+
if (selection.rotationEnabled) {
|
|
1192
|
+
const first = rotationCandidates()[0];
|
|
1193
|
+
if (first) applySelection(first.id);
|
|
1194
|
+
else applySelection("");
|
|
1195
|
+
return;
|
|
1210
1196
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1197
|
+
persistSelection();
|
|
1198
|
+
syncRotationTimer();
|
|
1199
|
+
emit();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function onToggleRotation() {
|
|
1203
|
+
selection.rotationEnabled = !selection.rotationEnabled;
|
|
1204
|
+
// The checkbox is disabled without a usable group, so a group id always
|
|
1205
|
+
// exists here when enabling.
|
|
1206
|
+
if (selection.rotationEnabled &&
|
|
1207
|
+
!rotationCandidates().some((w) => w.id === selection.id)) {
|
|
1208
|
+
const first = rotationCandidates()[0];
|
|
1209
|
+
if (first) { applySelection(first.id); return; }
|
|
1218
1210
|
}
|
|
1211
|
+
persistSelection();
|
|
1212
|
+
syncRotationTimer();
|
|
1213
|
+
emit();
|
|
1214
|
+
}
|
|
1219
1215
|
|
|
1220
|
-
|
|
1221
|
-
const q = query.trim();
|
|
1222
|
-
const filteredList = usableList.filter((w) => matchesQuery(w, q));
|
|
1223
|
-
const groups = sel.rotationGroups;
|
|
1216
|
+
function onGroupInterval(e) {
|
|
1224
1217
|
const group = activeRotationGroup();
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1218
|
+
if (!group) return;
|
|
1219
|
+
group.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval);
|
|
1220
|
+
persistSelection();
|
|
1221
|
+
syncRotationTimer();
|
|
1222
|
+
emit();
|
|
1223
|
+
}
|
|
1230
1224
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1225
|
+
function onDeleteGroup() {
|
|
1226
|
+
const group = activeRotationGroup();
|
|
1227
|
+
if (!group) return;
|
|
1228
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
|
1229
|
+
if (!window.confirm(S().confirmDelete(group.name))) return;
|
|
1230
|
+
}
|
|
1231
|
+
deleteGroup(group.id);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function onTogglePauseOnHidden() {
|
|
1235
|
+
selection.pauseOnHidden = !selection.pauseOnHidden;
|
|
1236
|
+
if (selection.pauseOnHidden) {
|
|
1237
|
+
// Re-enabling while already hidden must re-apply the reason — the
|
|
1238
|
+
// visibilitychange listener only fires on CHANGES.
|
|
1239
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1240
|
+
setAutoPause("hidden", true);
|
|
1241
|
+
}
|
|
1242
|
+
} else {
|
|
1243
|
+
setAutoPause("hidden", false);
|
|
1244
|
+
}
|
|
1245
|
+
persistSelection();
|
|
1246
|
+
emit();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function onTogglePauseOnBattery() {
|
|
1250
|
+
selection.pauseOnBattery = !selection.pauseOnBattery;
|
|
1251
|
+
if (selection.pauseOnBattery) applyBatteryPause(); // re-evaluate NOW
|
|
1252
|
+
else setAutoPause("battery", false);
|
|
1253
|
+
persistSelection();
|
|
1254
|
+
emit();
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function onToggleAutoScrim() {
|
|
1258
|
+
selection.autoScrim = !selection.autoScrim;
|
|
1259
|
+
persistSelection();
|
|
1260
|
+
applyEffects();
|
|
1261
|
+
emit();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function onScrim(pct) { selection.scrim = pct / 100; persistSelection(); applyEffects(); emit(); }
|
|
1265
|
+
function onBorder(pct) { selection.border = pct / 100; persistSelection(); applyEffects(); emit(); }
|
|
1266
|
+
function onBlur(px) { selection.blur = px; persistSelection(); applyEffects(); emit(); }
|
|
1267
|
+
function onWallpaperBlur(px) { selection.wallpaperBlur = px; persistSelection(); applyEffects(); emit(); }
|
|
1268
|
+
|
|
1269
|
+
function onFit(e) {
|
|
1270
|
+
selection.fit = FIT_MODES.indexOf(e.target.value) >= 0 ? e.target.value : "cover";
|
|
1271
|
+
persistSelection();
|
|
1272
|
+
applyEffects();
|
|
1273
|
+
emit();
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// ── Picker sections (presentational; every input arrives as an argument) ────
|
|
1277
|
+
// WallpaperPicker composes these in a fixed order. Each one owns exactly one
|
|
1278
|
+
// visual row/block of the settings panel and produces the same DOM the old
|
|
1279
|
+
// monolithic render did.
|
|
1280
|
+
|
|
1281
|
+
function SearchRow(t, sel, query, setQuery, matched, total) {
|
|
1282
|
+
return React.createElement("div", { className: "webg-row" },
|
|
1283
|
+
React.createElement("input", {
|
|
1284
|
+
className: "webg-text webg-search", type: "search",
|
|
1285
|
+
placeholder: t.searchPlaceholder,
|
|
1286
|
+
value: query,
|
|
1287
|
+
onInput: (e) => setQuery(e.target.value),
|
|
1288
|
+
}),
|
|
1289
|
+
query.trim() && React.createElement("span", { className: "webg-hint" },
|
|
1290
|
+
t.matched(matched, total)),
|
|
1291
|
+
React.createElement("label", { className: "webg-lang" },
|
|
1292
|
+
React.createElement("span", { className: "webg-hint" }, t.language),
|
|
1293
|
+
React.createElement("select", {
|
|
1294
|
+
className: "webg-select webg-lang-select",
|
|
1295
|
+
value: sel.lang,
|
|
1296
|
+
onChange: onLangChange,
|
|
1297
|
+
},
|
|
1298
|
+
React.createElement("option", { value: "auto" }, t.langAuto),
|
|
1299
|
+
React.createElement("option", { value: "zh" }, "中文"),
|
|
1300
|
+
React.createElement("option", { value: "en" }, "English"),
|
|
1301
|
+
),
|
|
1302
|
+
),
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function WallpaperGrid(t, sel, usableList, filteredList, q) {
|
|
1307
|
+
return React.createElement("div", { className: "webg-grid" },
|
|
1308
|
+
React.createElement("button", {
|
|
1309
|
+
className: "webg-card" + (sel.id ? "" : " webg-card--selected"),
|
|
1310
|
+
type: "button",
|
|
1311
|
+
onClick: onClear,
|
|
1312
|
+
title: t.closeTitle,
|
|
1313
|
+
},
|
|
1314
|
+
React.createElement("span", { className: "webg-card-close" }, t.closeCard),
|
|
1315
|
+
),
|
|
1316
|
+
usableList.length === 0
|
|
1317
|
+
? React.createElement("span", { className: "webg-hint" },
|
|
1318
|
+
sel.inventory.installDir
|
|
1319
|
+
? (sel.inventory.total > 0
|
|
1320
|
+
? t.emptyNoUsable(sel.inventory.total)
|
|
1321
|
+
: t.emptyNoProjects)
|
|
1322
|
+
: t.emptyNoInstall)
|
|
1323
|
+
: filteredList.length === 0
|
|
1324
|
+
? React.createElement("span", { className: "webg-hint" }, t.noMatch(q))
|
|
1325
|
+
: filteredList.map((w) => ThumbCard(w, w.id === sel.id, () => applySelection(w.id), t)),
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function ActionRow(t, sel) {
|
|
1330
|
+
return React.createElement("div", { className: "webg-row" },
|
|
1331
|
+
React.createElement("button", {
|
|
1332
|
+
className: "webg-btn", type: "button",
|
|
1333
|
+
onClick: onTogglePlay, disabled: !sel.url || sel.type !== "video",
|
|
1334
|
+
}, sel.playing ? t.pause : t.play),
|
|
1335
|
+
React.createElement("button", {
|
|
1336
|
+
className: "webg-btn", type: "button",
|
|
1337
|
+
onClick: onClear, disabled: !sel.id,
|
|
1338
|
+
}, t.close),
|
|
1339
|
+
React.createElement("button", {
|
|
1340
|
+
className: "webg-btn", type: "button",
|
|
1341
|
+
onClick: onRefresh, disabled: sel.loading,
|
|
1342
|
+
}, sel.loading ? t.refreshing : t.refresh),
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function RotationListRow(t, sel, groups) {
|
|
1347
|
+
return React.createElement("div", { className: "webg-row webg-playlist-row" },
|
|
1348
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.rotationList),
|
|
1349
|
+
React.createElement("select", {
|
|
1350
|
+
className: "webg-select webg-playlist-select",
|
|
1351
|
+
value: sel.rotationGroupId,
|
|
1352
|
+
onChange: onGroupChange,
|
|
1353
|
+
disabled: groups.length === 0,
|
|
1354
|
+
},
|
|
1355
|
+
React.createElement("option", { value: "" }, groups.length ? t.selectList : t.noLists),
|
|
1356
|
+
...groups.map((g) => React.createElement("option", {
|
|
1357
|
+
key: g.id, value: g.id,
|
|
1358
|
+
}, t.groupOption(g.name, groupWallpapers(g).length, g.interval))),
|
|
1359
|
+
),
|
|
1360
|
+
React.createElement("button", {
|
|
1361
|
+
className: "webg-btn", type: "button", onClick: startCreateGroup,
|
|
1362
|
+
}, t.newList),
|
|
1363
|
+
React.createElement("button", {
|
|
1364
|
+
className: "webg-btn", type: "button",
|
|
1365
|
+
onClick: () => startEditGroup(sel.rotationGroupId),
|
|
1366
|
+
disabled: !sel.rotationGroupId,
|
|
1367
|
+
}, t.editList),
|
|
1368
|
+
React.createElement("button", {
|
|
1369
|
+
className: "webg-btn", type: "button",
|
|
1370
|
+
onClick: onDeleteGroup,
|
|
1371
|
+
disabled: !sel.rotationGroupId,
|
|
1372
|
+
}, t.deleteList),
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/** One toggleable wallpaper chip inside the group editor. */
|
|
1377
|
+
function EditorCard(w, editing, t) {
|
|
1378
|
+
const checked = editing.wallpaperIds.indexOf(w.id) >= 0;
|
|
1379
|
+
return React.createElement("button", {
|
|
1380
|
+
key: w.id,
|
|
1381
|
+
className: "webg-editor-card" + (checked ? " webg-editor-card--checked" : ""),
|
|
1382
|
+
type: "button",
|
|
1383
|
+
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
1384
|
+
onClick: () => {
|
|
1385
|
+
const i = editing.wallpaperIds.indexOf(w.id);
|
|
1386
|
+
if (i >= 0) editing.wallpaperIds.splice(i, 1);
|
|
1387
|
+
else editing.wallpaperIds.push(w.id);
|
|
1388
|
+
emit();
|
|
1389
|
+
},
|
|
1390
|
+
},
|
|
1391
|
+
PreviewMedia(w, t),
|
|
1392
|
+
checked && React.createElement("span", { className: "webg-editor-check" }, "✓"),
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function GroupEditorPanel(t, sel, usableList, editorQuery, setEditorQuery) {
|
|
1397
|
+
const editing = sel.editing;
|
|
1398
|
+
return React.createElement("div", { className: "webg-editor" },
|
|
1233
1399
|
React.createElement("div", { className: "webg-row" },
|
|
1400
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.name),
|
|
1234
1401
|
React.createElement("input", {
|
|
1235
|
-
className: "webg-text
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
onInput: (e) => setQuery(e.target.value),
|
|
1402
|
+
className: "webg-text", type: "text",
|
|
1403
|
+
value: editing.name,
|
|
1404
|
+
onInput: (e) => { editing.name = e.target.value; emit(); },
|
|
1239
1405
|
}),
|
|
1240
|
-
q && React.createElement("span", { className: "webg-hint" },
|
|
1241
|
-
t.matched(filteredList.length, usableList.length)),
|
|
1242
|
-
React.createElement("label", { className: "webg-lang" },
|
|
1243
|
-
React.createElement("span", { className: "webg-hint" }, t.language),
|
|
1244
|
-
React.createElement("select", {
|
|
1245
|
-
className: "webg-select webg-lang-select",
|
|
1246
|
-
value: sel.lang,
|
|
1247
|
-
onChange: onLangChange,
|
|
1248
|
-
},
|
|
1249
|
-
React.createElement("option", { value: "auto" }, t.langAuto),
|
|
1250
|
-
React.createElement("option", { value: "zh" }, "中文"),
|
|
1251
|
-
React.createElement("option", { value: "en" }, "English"),
|
|
1252
|
-
),
|
|
1253
|
-
),
|
|
1254
1406
|
),
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
React.createElement("
|
|
1258
|
-
className: "webg-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1407
|
+
React.createElement("div", { className: "webg-row" },
|
|
1408
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.interval),
|
|
1409
|
+
React.createElement("select", {
|
|
1410
|
+
className: "webg-select",
|
|
1411
|
+
value: String(editing.interval),
|
|
1412
|
+
onChange: (e) => { editing.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval); emit(); },
|
|
1413
|
+
},
|
|
1414
|
+
...intervalOptions(editing.interval, t)),
|
|
1415
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.order),
|
|
1416
|
+
React.createElement("select", {
|
|
1417
|
+
className: "webg-select webg-playlist-select",
|
|
1418
|
+
value: editing.order,
|
|
1419
|
+
onChange: (e) => { editing.order = e.target.value; emit(); },
|
|
1262
1420
|
},
|
|
1263
|
-
React.createElement("
|
|
1421
|
+
React.createElement("option", { value: "sequence" }, t.sequence),
|
|
1422
|
+
React.createElement("option", { value: "random" }, t.random),
|
|
1264
1423
|
),
|
|
1265
|
-
usableList.length === 0
|
|
1266
|
-
? React.createElement("span", { className: "webg-hint" },
|
|
1267
|
-
sel.inventory.installDir
|
|
1268
|
-
? (sel.inventory.total > 0
|
|
1269
|
-
? t.emptyNoUsable(sel.inventory.total)
|
|
1270
|
-
: t.emptyNoProjects)
|
|
1271
|
-
: t.emptyNoInstall)
|
|
1272
|
-
: filteredList.length === 0
|
|
1273
|
-
? React.createElement("span", { className: "webg-hint" }, t.noMatch(q))
|
|
1274
|
-
: filteredList.map((w) => ThumbCard(w, w.id === sel.id, () => applySelection(w.id), t)),
|
|
1275
1424
|
),
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1425
|
+
React.createElement("input", {
|
|
1426
|
+
className: "webg-text", type: "search",
|
|
1427
|
+
placeholder: t.filterPlaceholder,
|
|
1428
|
+
value: editorQuery,
|
|
1429
|
+
onInput: (e) => setEditorQuery(e.target.value),
|
|
1430
|
+
}),
|
|
1431
|
+
React.createElement("div", { className: "webg-editor-grid" },
|
|
1432
|
+
(() => {
|
|
1433
|
+
const eq = editorQuery.trim();
|
|
1434
|
+
const pool = usableList.filter((w) => matchesQuery(w, eq));
|
|
1435
|
+
if (usableList.length === 0) {
|
|
1436
|
+
return React.createElement("span", { className: "webg-hint" }, t.noUsable);
|
|
1437
|
+
}
|
|
1438
|
+
if (pool.length === 0) {
|
|
1439
|
+
return React.createElement("span", { className: "webg-hint" }, t.noMatch(eq));
|
|
1440
|
+
}
|
|
1441
|
+
return pool.map((w) => EditorCard(w, editing, t));
|
|
1442
|
+
})(),
|
|
1290
1443
|
),
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
React.createElement("
|
|
1294
|
-
React.createElement("select", {
|
|
1444
|
+
React.createElement("div", { className: "webg-row" },
|
|
1445
|
+
React.createElement("span", { className: "webg-hint" }, t.selectedCount(editing.wallpaperIds.length)),
|
|
1446
|
+
sel.inventory.playlists.length > 0 && React.createElement("select", {
|
|
1295
1447
|
className: "webg-select webg-playlist-select",
|
|
1296
|
-
value:
|
|
1297
|
-
onChange:
|
|
1298
|
-
|
|
1448
|
+
value: "",
|
|
1449
|
+
onChange: (e) => {
|
|
1450
|
+
const p = sel.inventory.playlists.find((pl) => pl.id === e.target.value);
|
|
1451
|
+
if (p) importPlaylistIntoDraft(p);
|
|
1452
|
+
},
|
|
1299
1453
|
},
|
|
1300
|
-
React.createElement("option", { value: "" },
|
|
1301
|
-
...
|
|
1302
|
-
key:
|
|
1303
|
-
}, t.
|
|
1454
|
+
React.createElement("option", { value: "" }, t.importPlaylist),
|
|
1455
|
+
...sel.inventory.playlists.map((p) => React.createElement("option", {
|
|
1456
|
+
key: p.id, value: p.id,
|
|
1457
|
+
}, t.playlistOption(p.name, p.portableCount || 0))),
|
|
1304
1458
|
),
|
|
1459
|
+
),
|
|
1460
|
+
React.createElement("div", { className: "webg-row" },
|
|
1305
1461
|
React.createElement("button", {
|
|
1306
|
-
className: "webg-btn", type: "button",
|
|
1307
|
-
|
|
1462
|
+
className: "webg-btn webg-btn--primary", type: "button",
|
|
1463
|
+
onClick: saveEditingGroup,
|
|
1464
|
+
}, t.save),
|
|
1308
1465
|
React.createElement("button", {
|
|
1309
1466
|
className: "webg-btn", type: "button",
|
|
1310
|
-
onClick:
|
|
1311
|
-
|
|
1312
|
-
}, t.editList),
|
|
1313
|
-
React.createElement("button", {
|
|
1314
|
-
className: "webg-btn", type: "button",
|
|
1315
|
-
onClick: onDeleteGroup,
|
|
1316
|
-
disabled: !sel.rotationGroupId,
|
|
1317
|
-
}, t.deleteList),
|
|
1467
|
+
onClick: cancelEditGroup,
|
|
1468
|
+
}, t.cancel),
|
|
1318
1469
|
),
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
onInput: (e) => { editing.name = e.target.value; emit(); },
|
|
1326
|
-
}),
|
|
1327
|
-
),
|
|
1328
|
-
React.createElement("div", { className: "webg-row" },
|
|
1329
|
-
React.createElement("span", { className: "webg-hint webg-label" }, t.interval),
|
|
1330
|
-
React.createElement("select", {
|
|
1331
|
-
className: "webg-select",
|
|
1332
|
-
value: String(editing.interval),
|
|
1333
|
-
onChange: (e) => { editing.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval); emit(); },
|
|
1334
|
-
},
|
|
1335
|
-
// A persisted interval outside the preset steps (imported groups,
|
|
1336
|
-
// hand-edited storage) still gets a matching option — otherwise the
|
|
1337
|
-
// select would silently display the first preset instead of reality.
|
|
1338
|
-
...[...new Set([...INTERVALS, editing.interval])].sort((x, y) => x - y).map((minutes) =>
|
|
1339
|
-
React.createElement("option", { key: minutes, value: String(minutes) }, t.minutes(minutes)),
|
|
1340
|
-
)),
|
|
1341
|
-
React.createElement("span", { className: "webg-hint webg-label" }, t.order),
|
|
1342
|
-
React.createElement("select", {
|
|
1343
|
-
className: "webg-select webg-playlist-select",
|
|
1344
|
-
value: editing.order,
|
|
1345
|
-
onChange: (e) => { editing.order = e.target.value; emit(); },
|
|
1346
|
-
},
|
|
1347
|
-
React.createElement("option", { value: "sequence" }, t.sequence),
|
|
1348
|
-
React.createElement("option", { value: "random" }, t.random),
|
|
1349
|
-
),
|
|
1350
|
-
),
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function RotationControlsRow(t, sel, group, usableCount) {
|
|
1474
|
+
return React.createElement("div", { className: "webg-row webg-rotation-row" },
|
|
1475
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
1351
1476
|
React.createElement("input", {
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1477
|
+
type: "checkbox",
|
|
1478
|
+
checked: sel.rotationEnabled,
|
|
1479
|
+
onChange: onToggleRotation,
|
|
1480
|
+
disabled: !sel.rotationGroupId || usableCount < 2,
|
|
1356
1481
|
}),
|
|
1357
|
-
|
|
1358
|
-
(() => {
|
|
1359
|
-
const eq = editorQuery.trim();
|
|
1360
|
-
const pool = usableList.filter((w) => matchesQuery(w, eq));
|
|
1361
|
-
if (usableList.length === 0) {
|
|
1362
|
-
return React.createElement("span", { className: "webg-hint" }, t.noUsable);
|
|
1363
|
-
}
|
|
1364
|
-
if (pool.length === 0) {
|
|
1365
|
-
return React.createElement("span", { className: "webg-hint" }, t.noMatch(eq));
|
|
1366
|
-
}
|
|
1367
|
-
return pool.map((w) => {
|
|
1368
|
-
const checked = editing.wallpaperIds.indexOf(w.id) >= 0;
|
|
1369
|
-
return React.createElement("button", {
|
|
1370
|
-
key: w.id,
|
|
1371
|
-
className: "webg-editor-card" + (checked ? " webg-editor-card--checked" : ""),
|
|
1372
|
-
type: "button",
|
|
1373
|
-
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
1374
|
-
onClick: () => {
|
|
1375
|
-
const i = editing.wallpaperIds.indexOf(w.id);
|
|
1376
|
-
if (i >= 0) editing.wallpaperIds.splice(i, 1);
|
|
1377
|
-
else editing.wallpaperIds.push(w.id);
|
|
1378
|
-
emit();
|
|
1379
|
-
},
|
|
1380
|
-
},
|
|
1381
|
-
w.preview
|
|
1382
|
-
? React.createElement("img", {
|
|
1383
|
-
src: w.preview, alt: w.title, loading: "lazy", decoding: "async",
|
|
1384
|
-
onError: (e) => { e.target.style.display = "none"; },
|
|
1385
|
-
})
|
|
1386
|
-
: React.createElement("span", { className: "webg-card-placeholder" }, t.noPreview),
|
|
1387
|
-
checked && React.createElement("span", { className: "webg-editor-check" }, "✓"),
|
|
1388
|
-
);
|
|
1389
|
-
});
|
|
1390
|
-
})(),
|
|
1391
|
-
),
|
|
1392
|
-
React.createElement("div", { className: "webg-row" },
|
|
1393
|
-
React.createElement("span", { className: "webg-hint" }, t.selectedCount(editing.wallpaperIds.length)),
|
|
1394
|
-
sel.inventory.playlists.length > 0 && React.createElement("select", {
|
|
1395
|
-
className: "webg-select webg-playlist-select",
|
|
1396
|
-
value: "",
|
|
1397
|
-
onChange: (e) => {
|
|
1398
|
-
const p = sel.inventory.playlists.find((pl) => pl.id === e.target.value);
|
|
1399
|
-
if (p) importPlaylistIntoDraft(p);
|
|
1400
|
-
},
|
|
1401
|
-
},
|
|
1402
|
-
React.createElement("option", { value: "" }, t.importPlaylist),
|
|
1403
|
-
...sel.inventory.playlists.map((p) => React.createElement("option", {
|
|
1404
|
-
key: p.id, value: p.id,
|
|
1405
|
-
}, t.playlistOption(p.name, p.portableCount || 0))),
|
|
1406
|
-
),
|
|
1407
|
-
),
|
|
1408
|
-
React.createElement("div", { className: "webg-row" },
|
|
1409
|
-
React.createElement("button", {
|
|
1410
|
-
className: "webg-btn webg-btn--primary", type: "button",
|
|
1411
|
-
onClick: saveEditingGroup,
|
|
1412
|
-
}, t.save),
|
|
1413
|
-
React.createElement("button", {
|
|
1414
|
-
className: "webg-btn", type: "button",
|
|
1415
|
-
onClick: cancelEditGroup,
|
|
1416
|
-
}, t.cancel),
|
|
1417
|
-
),
|
|
1482
|
+
t.autoRotate,
|
|
1418
1483
|
),
|
|
1419
|
-
React.createElement("
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1484
|
+
React.createElement("select", {
|
|
1485
|
+
className: "webg-select webg-rotation-interval",
|
|
1486
|
+
value: String(group ? group.interval : DEFAULTS.rotationInterval),
|
|
1487
|
+
onChange: onGroupInterval,
|
|
1488
|
+
disabled: !sel.rotationEnabled || !sel.rotationGroupId || usableCount < 2,
|
|
1489
|
+
title: t.intervalTitle,
|
|
1490
|
+
},
|
|
1491
|
+
...intervalOptions(group ? group.interval : DEFAULTS.rotationInterval, t)),
|
|
1492
|
+
!sel.rotationGroupId && React.createElement("span", { className: "webg-hint" }, t.needList),
|
|
1493
|
+
sel.rotationGroupId && usableCount < 2 && React.createElement("span", { className: "webg-hint" }, t.needTwo),
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function EffectsPanel(t, sel) {
|
|
1498
|
+
return React.createElement(React.Fragment, null,
|
|
1499
|
+
// object-fit only means something for video/still wallpapers — an
|
|
1500
|
+
// iframe ignores it and always fills its layer — so the base-mode
|
|
1501
|
+
// select is hidden for web. The manual crop (drag / wheel in the
|
|
1502
|
+
// adjust overlay) is a plain CSS transform and applies to EVERY kind,
|
|
1503
|
+
// so 调整画面 / 重置裁剪 stay available even for iframes.
|
|
1504
|
+
React.createElement("div", { className: "webg-row webg-fit-row" },
|
|
1505
|
+
sel.type !== "web" && React.createElement("span", { className: "webg-hint webg-label" }, t.fit),
|
|
1506
|
+
sel.type !== "web" && React.createElement("select", {
|
|
1507
|
+
className: "webg-select webg-fit-select",
|
|
1508
|
+
value: sel.fit,
|
|
1509
|
+
onChange: onFit,
|
|
1435
1510
|
},
|
|
1436
|
-
...[
|
|
1437
|
-
.
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
sel.id && React.createElement(React.Fragment, null,
|
|
1445
|
-
// Canvas fit only matters for video/still wallpapers — an iframe
|
|
1446
|
-
// ignores object-fit and always fills its layer. Manual crop (drag /
|
|
1447
|
-
// wheel in the adjust overlay) replaced the old fixed anchor select.
|
|
1448
|
-
sel.type !== "web" && React.createElement("div", { className: "webg-row webg-fit-row" },
|
|
1449
|
-
React.createElement("span", { className: "webg-hint webg-label" }, t.fit),
|
|
1450
|
-
React.createElement("select", {
|
|
1451
|
-
className: "webg-select webg-fit-select",
|
|
1452
|
-
value: sel.fit,
|
|
1453
|
-
onChange: onFit,
|
|
1454
|
-
},
|
|
1455
|
-
...[t.fitCover, t.fitContain, t.fitFill, t.fitNone].map((label, i) =>
|
|
1456
|
-
React.createElement("option", { key: FIT_MODES[i], value: FIT_MODES[i] }, label),
|
|
1457
|
-
)),
|
|
1511
|
+
...[t.fitCover, t.fitContain, t.fitFill, t.fitNone].map((label, i) =>
|
|
1512
|
+
React.createElement("option", { key: FIT_MODES[i], value: FIT_MODES[i] }, label),
|
|
1513
|
+
)),
|
|
1514
|
+
React.createElement("button", {
|
|
1515
|
+
className: "webg-btn", type: "button", onClick: enterAdjust,
|
|
1516
|
+
disabled: sel.adjusting,
|
|
1517
|
+
}, t.adjust),
|
|
1518
|
+
(sel.zoom !== 1 || sel.offsetX !== 0 || sel.offsetY !== 0) &&
|
|
1458
1519
|
React.createElement("button", {
|
|
1459
|
-
className: "webg-btn", type: "button", onClick:
|
|
1460
|
-
|
|
1461
|
-
}, t.adjust),
|
|
1462
|
-
(sel.zoom !== 1 || sel.offsetX !== 0 || sel.offsetY !== 0) &&
|
|
1463
|
-
React.createElement("button", {
|
|
1464
|
-
className: "webg-btn", type: "button", onClick: resetCrop,
|
|
1465
|
-
}, t.resetCrop),
|
|
1466
|
-
),
|
|
1467
|
-
React.createElement("div", { className: "webg-effects" },
|
|
1468
|
-
SliderRow(t.wallpaperBlur, 0, 60, 1, sel.wallpaperBlur, onWallpaperBlur, sel.wallpaperBlur + "px"),
|
|
1469
|
-
SliderRow(t.scrim, 0, 90, 5, Math.min(90, Math.round(sel.scrim * 100)), onScrim,
|
|
1470
|
-
Math.round(sel.scrim * 100) + "%"),
|
|
1471
|
-
SliderRow(t.border, 0, 90, 5, Math.round(sel.border * 100), onBorder, Math.round(sel.border * 100) + "%"),
|
|
1472
|
-
SliderRow(t.glass, 0, 40, 1, sel.blur, onBlur, sel.blur + "px"),
|
|
1473
|
-
),
|
|
1520
|
+
className: "webg-btn", type: "button", onClick: resetCrop,
|
|
1521
|
+
}, t.resetCrop),
|
|
1474
1522
|
),
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
React.createElement("
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1523
|
+
React.createElement("div", { className: "webg-effects" },
|
|
1524
|
+
SliderRow(t.wallpaperBlur, 0, 60, 1, sel.wallpaperBlur, onWallpaperBlur, sel.wallpaperBlur + "px"),
|
|
1525
|
+
SliderRow(t.scrim, 0, 90, 5, Math.min(90, Math.round(sel.scrim * 100)), onScrim,
|
|
1526
|
+
Math.round(sel.scrim * 100) + "%"),
|
|
1527
|
+
SliderRow(t.border, 0, 90, 5, Math.min(90, Math.round(sel.border * 100)), onBorder,
|
|
1528
|
+
Math.round(sel.border * 100) + "%"),
|
|
1529
|
+
SliderRow(t.glass, 0, 40, 1, sel.blur, onBlur, sel.blur + "px"),
|
|
1530
|
+
),
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function MonitorRow(t, sel) {
|
|
1535
|
+
return React.createElement("div", { className: "webg-row webg-monitor-row" },
|
|
1536
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
1537
|
+
React.createElement("input", {
|
|
1538
|
+
type: "checkbox", checked: sel.pauseOnHidden, onChange: onTogglePauseOnHidden,
|
|
1539
|
+
}),
|
|
1540
|
+
t.pauseOnHidden,
|
|
1541
|
+
),
|
|
1542
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
1543
|
+
React.createElement("input", {
|
|
1544
|
+
type: "checkbox", checked: sel.pauseOnBattery, onChange: onTogglePauseOnBattery,
|
|
1545
|
+
}),
|
|
1546
|
+
t.pauseOnBattery,
|
|
1547
|
+
),
|
|
1548
|
+
React.createElement("label", { className: "webg-toggle", title: t.autoScrimTitle },
|
|
1549
|
+
React.createElement("input", {
|
|
1550
|
+
type: "checkbox", checked: sel.autoScrim, onChange: onToggleAutoScrim,
|
|
1551
|
+
}),
|
|
1552
|
+
t.autoScrim,
|
|
1499
1553
|
),
|
|
1554
|
+
sel.fps !== null && React.createElement("span", {
|
|
1555
|
+
className: "webg-hint webg-fps" + (sel.fps < LOW_FPS ? " webg-fps--low" : ""),
|
|
1556
|
+
title: t.fpsTitle,
|
|
1557
|
+
}, sel.fps + " fps"),
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function StatusRow(t, sel, group, usableCount, usableTotal) {
|
|
1562
|
+
// "Auto-rotating" is claimed only when the timer is genuinely armed —
|
|
1563
|
+
// rotation may be enabled on a list with fewer than 2 usable wallpapers,
|
|
1564
|
+
// in which case nothing switches until the list grows.
|
|
1565
|
+
const rotating = sel.rotationEnabled && usableCount >= 2;
|
|
1566
|
+
return React.createElement("div", { className: "webg-row" },
|
|
1567
|
+
React.createElement("span", { className: "webg-hint" },
|
|
1568
|
+
(group
|
|
1569
|
+
? t.statusGroup(group.name, group.wallpaperIds.length, usableCount, group.interval,
|
|
1570
|
+
group.order === "random" ? t.random : t.sequence)
|
|
1571
|
+
: t.statusUsable(usableTotal)) +
|
|
1572
|
+
(rotating ? " · " + t.autoRotating : "") +
|
|
1573
|
+
(sel.autoPauseReasons.size > 0 ? " · " + t.autoPaused : "") +
|
|
1574
|
+
(sel.id && demotedToPreview.has(sel.id) ? " · " + t.decodeFallback : "") +
|
|
1575
|
+
(sel.inventory.installDir ? " · " + sel.inventory.installDir : "")),
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function WallpaperPicker() {
|
|
1580
|
+
const sel = useStore();
|
|
1581
|
+
const [query, setQuery] = React.useState("");
|
|
1582
|
+
const [editorQuery, setEditorQuery] = React.useState("");
|
|
1583
|
+
const t = S();
|
|
1584
|
+
|
|
1585
|
+
if (!sel.loaded) {
|
|
1586
|
+
return React.createElement("div", { className: "webg-picker" },
|
|
1587
|
+
React.createElement("span", { className: "webg-hint" }, t.scanning));
|
|
1588
|
+
}
|
|
1589
|
+
if (sel.inventory.error) {
|
|
1590
|
+
return React.createElement("div", { className: "webg-picker" },
|
|
1591
|
+
React.createElement("div", { className: "webg-error" },
|
|
1592
|
+
t.connError + sel.inventory.error),
|
|
1593
|
+
React.createElement("button", {
|
|
1594
|
+
className: "webg-btn", type: "button", onClick: onRefresh, disabled: sel.loading,
|
|
1595
|
+
}, sel.loading ? t.refreshing : t.retry));
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const usableList = selectableInventory();
|
|
1599
|
+
const q = query.trim();
|
|
1600
|
+
const filteredList = usableList.filter((w) => matchesQuery(w, q));
|
|
1601
|
+
const groups = sel.rotationGroups;
|
|
1602
|
+
const group = activeRotationGroup();
|
|
1603
|
+
const usableCount = rotationCandidates().length;
|
|
1604
|
+
const editing = sel.editing;
|
|
1605
|
+
|
|
1606
|
+
return React.createElement("div", { className: "webg-picker" },
|
|
1607
|
+
SearchRow(t, sel, query, setQuery, filteredList.length, usableList.length),
|
|
1608
|
+
WallpaperGrid(t, sel, usableList, filteredList, q),
|
|
1609
|
+
ActionRow(t, sel),
|
|
1610
|
+
RotationListRow(t, sel, groups),
|
|
1611
|
+
editing && GroupEditorPanel(t, sel, usableList, editorQuery, setEditorQuery),
|
|
1612
|
+
RotationControlsRow(t, sel, group, usableCount),
|
|
1613
|
+
sel.id && EffectsPanel(t, sel),
|
|
1614
|
+
sel.id && MonitorRow(t, sel),
|
|
1500
1615
|
sel.id && sel.fps !== null && sel.fps < LOW_FPS && sel.type === "video" &&
|
|
1501
1616
|
React.createElement("div", { className: "webg-row" },
|
|
1502
|
-
React.createElement("span", { className: "webg-hint" }, t.lowFpsHint),
|
|
1503
|
-
|
|
1504
|
-
// ── Status ──
|
|
1505
|
-
React.createElement("div", { className: "webg-row" },
|
|
1506
|
-
React.createElement("span", { className: "webg-hint" },
|
|
1507
|
-
(group
|
|
1508
|
-
? t.statusGroup(group.name, group.wallpaperIds.length, usableCount, group.interval,
|
|
1509
|
-
group.order === "random" ? t.random : t.sequence)
|
|
1510
|
-
: t.statusUsable(usableList.length)) +
|
|
1511
|
-
(sel.rotationEnabled ? " · " + t.autoRotating : "") +
|
|
1512
|
-
(autoPaused ? " · " + t.autoPaused : "") +
|
|
1513
|
-
(sel.id && demotedToPreview.has(sel.id) ? " · " + t.decodeFallback : "") +
|
|
1514
|
-
(sel.inventory.installDir ? " · " + sel.inventory.installDir : "")),
|
|
1515
|
-
),
|
|
1617
|
+
React.createElement("span", { className: "webg-hint" }, t.lowFpsHint)),
|
|
1618
|
+
StatusRow(t, sel, group, usableCount, usableList.length),
|
|
1516
1619
|
);
|
|
1517
1620
|
}
|
|
1518
1621
|
|
|
@@ -1528,17 +1631,18 @@ window.__ModuleLoader__.load({
|
|
|
1528
1631
|
}
|
|
1529
1632
|
.webg-layer--enter { opacity: 0; }
|
|
1530
1633
|
.webg-layer--leave { opacity: 0; }
|
|
1531
|
-
/*
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
saturate keeps blurred colours from
|
|
1634
|
+
/* The media box is ALWAYS exactly the layer. Blur must never re-frame the
|
|
1635
|
+
content: enlarging the box for overscan made object-fit:cover scale the
|
|
1636
|
+
picture up to fill the bigger box, silently cropping the wallpaper's
|
|
1637
|
+
four edges — and once cropped, no zoom could bring them back. Instead the
|
|
1638
|
+
Gaussian's natural edge fade (≤ one blur radius, under the veil and the
|
|
1639
|
+
UI chrome) is accepted. A slight saturate keeps blurred colours from
|
|
1640
|
+
washing out. */
|
|
1537
1641
|
.webg-layer .webg-media {
|
|
1538
1642
|
position: absolute;
|
|
1539
|
-
inset:
|
|
1540
|
-
width:
|
|
1541
|
-
height:
|
|
1643
|
+
inset: 0;
|
|
1644
|
+
width: 100%;
|
|
1645
|
+
height: 100%;
|
|
1542
1646
|
max-width: none; max-height: none;
|
|
1543
1647
|
object-fit: var(--webg-fit, cover);
|
|
1544
1648
|
object-position: center;
|
|
@@ -1631,10 +1735,10 @@ window.__ModuleLoader__.load({
|
|
|
1631
1735
|
-webkit-backdrop-filter: blur(var(--webg-blur, 16px)) saturate(var(--webg-saturate, 1.8)) brightness(var(--webg-glass-brightness, 1.04)) contrast(1.01);
|
|
1632
1736
|
backdrop-filter: blur(var(--webg-blur, 16px)) saturate(var(--webg-saturate, 1.8)) brightness(var(--webg-glass-brightness, 1.04)) contrast(1.01);
|
|
1633
1737
|
box-shadow:
|
|
1634
|
-
inset 0 1px 0 rgba(255, 255, 255,
|
|
1738
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
|
1635
1739
|
inset 0 -1px 0 rgba(255, 255, 255, 0.08),
|
|
1636
1740
|
inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
|
|
1637
|
-
0 12px 40px rgba(0, 0, 0,
|
|
1741
|
+
0 12px 40px rgba(0, 0, 0, 0.12);
|
|
1638
1742
|
}
|
|
1639
1743
|
|
|
1640
1744
|
/* ── Picker chrome (token-driven, theme-aware) ── */
|