wallpaper-engine-dsh 1.0.2 → 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 +498 -420
- package/lib/core.js +27 -3
- package/lib/index.js +73 -36
- 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 ────────────────────────────────────────────────────────────────
|
|
@@ -417,7 +417,7 @@ window.__ModuleLoader__.load({
|
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
// Drop a selection whose wallpaper vanished or became unusable.
|
|
420
|
-
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))) {
|
|
421
421
|
selection.id = "";
|
|
422
422
|
persistSelection();
|
|
423
423
|
}
|
|
@@ -441,7 +441,7 @@ window.__ModuleLoader__.load({
|
|
|
441
441
|
function groupWallpapers(group) {
|
|
442
442
|
if (!group || !Array.isArray(group.wallpaperIds)) return [];
|
|
443
443
|
const byId = new Map(selection.inventory.wallpapers.map((w) => [w.id, w]));
|
|
444
|
-
return group.wallpaperIds.map((id) => byId.get(id)).filter(
|
|
444
|
+
return group.wallpaperIds.map((id) => byId.get(id)).filter(isPlayable);
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
function rotationCandidates() {
|
|
@@ -457,7 +457,7 @@ window.__ModuleLoader__.load({
|
|
|
457
457
|
if (!source) return false;
|
|
458
458
|
const byId = new Map(selection.inventory.wallpapers.map((w) => [w.id, w]));
|
|
459
459
|
const ids = Array.isArray(source.wallpaperIds)
|
|
460
|
-
? source.wallpaperIds.filter((id) =>
|
|
460
|
+
? source.wallpaperIds.filter((id) => isPlayable(byId.get(id)))
|
|
461
461
|
: [];
|
|
462
462
|
if (ids.length < 2) return false;
|
|
463
463
|
selection.rotationGroups.push({
|
|
@@ -672,7 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
672
672
|
media.setAttribute("frameborder", "0");
|
|
673
673
|
media.setAttribute("scrolling", "no");
|
|
674
674
|
media.setAttribute("title", S().iframeTitle);
|
|
675
|
-
media.className = "webg-media
|
|
675
|
+
media.className = "webg-media";
|
|
676
676
|
} else {
|
|
677
677
|
// Static fallback: a playable wallpaper demoted after repeated media
|
|
678
678
|
// failures — its preview still beats a black screen.
|
|
@@ -689,7 +689,10 @@ window.__ModuleLoader__.load({
|
|
|
689
689
|
}
|
|
690
690
|
|
|
691
691
|
function attachStillSampling(media, sel) {
|
|
692
|
-
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.
|
|
693
696
|
if (type !== "video" && type !== "image") return;
|
|
694
697
|
media.addEventListener(type === "video" ? "loadeddata" : "load", () => {
|
|
695
698
|
sampleWallpaperLuminance();
|
|
@@ -792,11 +795,10 @@ window.__ModuleLoader__.load({
|
|
|
792
795
|
};
|
|
793
796
|
const onPointerMove = (e) => {
|
|
794
797
|
if (!drag) return;
|
|
795
|
-
// translate % refers to the
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
const h = (overlay.clientHeight || 1) + 2 * selection.wallpaperBlur;
|
|
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;
|
|
800
802
|
selection.offsetX += ((e.clientX - drag.x) / w) * 100;
|
|
801
803
|
selection.offsetY += ((e.clientY - drag.y) / h) * 100;
|
|
802
804
|
clampCrop(); // gap-free guarantee: pan never outruns the zoom headroom
|
|
@@ -902,7 +904,7 @@ window.__ModuleLoader__.load({
|
|
|
902
904
|
if (existing) {
|
|
903
905
|
existing.classList.add("webg-layer--leave");
|
|
904
906
|
const stale = existing;
|
|
905
|
-
schedule(() => stale.remove(),
|
|
907
|
+
schedule(() => stale.remove(), LEAVE_MS);
|
|
906
908
|
}
|
|
907
909
|
}
|
|
908
910
|
const node = document.getElementById(LAYER_ID);
|
|
@@ -924,7 +926,7 @@ window.__ModuleLoader__.load({
|
|
|
924
926
|
if (s) s.remove();
|
|
925
927
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
926
928
|
}
|
|
927
|
-
},
|
|
929
|
+
}, LEAVE_MS);
|
|
928
930
|
}
|
|
929
931
|
|
|
930
932
|
// Scrim: present while a wallpaper is active. On clear it outlives the
|
|
@@ -946,6 +948,11 @@ window.__ModuleLoader__.load({
|
|
|
946
948
|
// sample; iframes of web wallpapers can never be sampled — opaque origin).
|
|
947
949
|
let sampledLuminance = null;
|
|
948
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;
|
|
949
956
|
|
|
950
957
|
/** Average Rec.601 luminance (0..1) of the current video/img, or null. */
|
|
951
958
|
function sampleWallpaperLuminance() {
|
|
@@ -973,11 +980,18 @@ window.__ModuleLoader__.load({
|
|
|
973
980
|
}
|
|
974
981
|
|
|
975
982
|
function applyEffects() {
|
|
976
|
-
const s = document.body.style;
|
|
977
983
|
// The veil OPPOSES the text colour: dark theme (light text) darkens bright
|
|
978
984
|
// wallpapers, light theme (dark text) brightens dark ones — either way the
|
|
979
985
|
// text side wins. This is what makes ANY wallpaper survivable.
|
|
980
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;
|
|
981
995
|
let alpha = selection.scrim;
|
|
982
996
|
if (selection.autoScrim && sampledLuminance !== null) {
|
|
983
997
|
// Distance of the wallpaper's brightness from the "comfortable middle"
|
|
@@ -994,7 +1008,6 @@ window.__ModuleLoader__.load({
|
|
|
994
1008
|
// The glass "colour melt" scales with blur radius: 0 blur → no melt.
|
|
995
1009
|
s.setProperty("--webg-saturate", String(1.15 + selection.blur * 0.028));
|
|
996
1010
|
s.setProperty("--webg-glass-brightness", "1.04");
|
|
997
|
-
s.setProperty("--webg-wallpaper-blur", selection.wallpaperBlur + "px");
|
|
998
1011
|
// At 0 blur, keep the media OUT of the filter pipeline entirely: even
|
|
999
1012
|
// blur(0px) forces an extra raster/compositing pass, and the noise overlay
|
|
1000
1013
|
// would still occupy a layer at opacity 0. filter:none + display:none keep
|
|
@@ -1025,13 +1038,13 @@ window.__ModuleLoader__.load({
|
|
|
1025
1038
|
}
|
|
1026
1039
|
|
|
1027
1040
|
function clearEffects() {
|
|
1041
|
+
lastEffectKey = null;
|
|
1028
1042
|
const s = document.body.style;
|
|
1029
1043
|
s.removeProperty("--webg-scrim-color");
|
|
1030
1044
|
s.removeProperty("--webg-border-alpha");
|
|
1031
1045
|
s.removeProperty("--webg-blur");
|
|
1032
1046
|
s.removeProperty("--webg-saturate");
|
|
1033
1047
|
s.removeProperty("--webg-glass-brightness");
|
|
1034
|
-
s.removeProperty("--webg-wallpaper-blur");
|
|
1035
1048
|
s.removeProperty("--webg-media-filter");
|
|
1036
1049
|
s.removeProperty("--webg-noise-display");
|
|
1037
1050
|
s.removeProperty("--webg-noise-opacity");
|
|
@@ -1085,6 +1098,16 @@ window.__ModuleLoader__.load({
|
|
|
1085
1098
|
return (w.title || "").toLowerCase().includes(needle) || (w.id || "").toLowerCase().includes(needle);
|
|
1086
1099
|
}
|
|
1087
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
|
+
|
|
1088
1111
|
function ThumbCard(w, selected, onClick, t) {
|
|
1089
1112
|
return React.createElement("button", {
|
|
1090
1113
|
key: w.id,
|
|
@@ -1093,426 +1116,480 @@ window.__ModuleLoader__.load({
|
|
|
1093
1116
|
onClick: onClick,
|
|
1094
1117
|
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
1095
1118
|
},
|
|
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),
|
|
1119
|
+
PreviewMedia(w, t),
|
|
1102
1120
|
React.createElement("span", { className: "webg-card-type" }, typeBadge(w, t)),
|
|
1103
1121
|
React.createElement("span", { className: "webg-card-title" }, w.title),
|
|
1104
1122
|
);
|
|
1105
1123
|
}
|
|
1106
1124
|
|
|
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
|
-
};
|
|
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
|
+
}
|
|
1195
1141
|
|
|
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
|
-
};
|
|
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.
|
|
1206
1147
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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;
|
|
1210
1174
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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; }
|
|
1218
1188
|
}
|
|
1189
|
+
persistSelection();
|
|
1190
|
+
syncRotationTimer();
|
|
1191
|
+
emit();
|
|
1192
|
+
}
|
|
1219
1193
|
|
|
1220
|
-
|
|
1221
|
-
const q = query.trim();
|
|
1222
|
-
const filteredList = usableList.filter((w) => matchesQuery(w, q));
|
|
1223
|
-
const groups = sel.rotationGroups;
|
|
1194
|
+
function onGroupInterval(e) {
|
|
1224
1195
|
const group = activeRotationGroup();
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1196
|
+
if (!group) return;
|
|
1197
|
+
group.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval);
|
|
1198
|
+
persistSelection();
|
|
1199
|
+
syncRotationTimer();
|
|
1200
|
+
emit();
|
|
1201
|
+
}
|
|
1230
1202
|
|
|
1231
|
-
|
|
1232
|
-
|
|
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" },
|
|
1233
1377
|
React.createElement("div", { className: "webg-row" },
|
|
1378
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.name),
|
|
1234
1379
|
React.createElement("input", {
|
|
1235
|
-
className: "webg-text
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
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(); },
|
|
1239
1383
|
}),
|
|
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
1384
|
),
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
React.createElement("
|
|
1258
|
-
className: "webg-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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(); },
|
|
1262
1398
|
},
|
|
1263
|
-
React.createElement("
|
|
1399
|
+
React.createElement("option", { value: "sequence" }, t.sequence),
|
|
1400
|
+
React.createElement("option", { value: "random" }, t.random),
|
|
1264
1401
|
),
|
|
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
1402
|
),
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
+
})(),
|
|
1290
1421
|
),
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
React.createElement("
|
|
1294
|
-
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", {
|
|
1295
1425
|
className: "webg-select webg-playlist-select",
|
|
1296
|
-
value:
|
|
1297
|
-
onChange:
|
|
1298
|
-
|
|
1426
|
+
value: "",
|
|
1427
|
+
onChange: (e) => {
|
|
1428
|
+
const p = sel.inventory.playlists.find((pl) => pl.id === e.target.value);
|
|
1429
|
+
if (p) importPlaylistIntoDraft(p);
|
|
1430
|
+
},
|
|
1299
1431
|
},
|
|
1300
|
-
React.createElement("option", { value: "" },
|
|
1301
|
-
...
|
|
1302
|
-
key:
|
|
1303
|
-
}, 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))),
|
|
1304
1436
|
),
|
|
1437
|
+
),
|
|
1438
|
+
React.createElement("div", { className: "webg-row" },
|
|
1305
1439
|
React.createElement("button", {
|
|
1306
|
-
className: "webg-btn", type: "button",
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
className: "webg-btn", type: "button",
|
|
1310
|
-
onClick: () => startEditGroup(sel.rotationGroupId),
|
|
1311
|
-
disabled: !sel.rotationGroupId,
|
|
1312
|
-
}, t.editList),
|
|
1440
|
+
className: "webg-btn webg-btn--primary", type: "button",
|
|
1441
|
+
onClick: saveEditingGroup,
|
|
1442
|
+
}, t.save),
|
|
1313
1443
|
React.createElement("button", {
|
|
1314
1444
|
className: "webg-btn", type: "button",
|
|
1315
|
-
onClick:
|
|
1316
|
-
|
|
1317
|
-
}, t.deleteList),
|
|
1445
|
+
onClick: cancelEditGroup,
|
|
1446
|
+
}, t.cancel),
|
|
1318
1447
|
),
|
|
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
|
-
),
|
|
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" },
|
|
1351
1454
|
React.createElement("input", {
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1455
|
+
type: "checkbox",
|
|
1456
|
+
checked: sel.rotationEnabled,
|
|
1457
|
+
onChange: onToggleRotation,
|
|
1458
|
+
disabled: !sel.rotationGroupId || usableCount < 2,
|
|
1356
1459
|
}),
|
|
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
|
-
),
|
|
1460
|
+
t.autoRotate,
|
|
1418
1461
|
),
|
|
1419
|
-
React.createElement("
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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,
|
|
1435
1488
|
},
|
|
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
|
-
)),
|
|
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) &&
|
|
1458
1497
|
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
|
-
),
|
|
1498
|
+
className: "webg-btn", type: "button", onClick: resetCrop,
|
|
1499
|
+
}, t.resetCrop),
|
|
1474
1500
|
),
|
|
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
|
-
|
|
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,
|
|
1499
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),
|
|
1500
1589
|
sel.id && sel.fps !== null && sel.fps < LOW_FPS && sel.type === "video" &&
|
|
1501
1590
|
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
|
-
),
|
|
1591
|
+
React.createElement("span", { className: "webg-hint" }, t.lowFpsHint)),
|
|
1592
|
+
StatusRow(t, sel, group, usableCount, usableList.length),
|
|
1516
1593
|
);
|
|
1517
1594
|
}
|
|
1518
1595
|
|
|
@@ -1528,17 +1605,18 @@ window.__ModuleLoader__.load({
|
|
|
1528
1605
|
}
|
|
1529
1606
|
.webg-layer--enter { opacity: 0; }
|
|
1530
1607
|
.webg-layer--leave { opacity: 0; }
|
|
1531
|
-
/*
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
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. */
|
|
1537
1615
|
.webg-layer .webg-media {
|
|
1538
1616
|
position: absolute;
|
|
1539
|
-
inset:
|
|
1540
|
-
width:
|
|
1541
|
-
height:
|
|
1617
|
+
inset: 0;
|
|
1618
|
+
width: 100%;
|
|
1619
|
+
height: 100%;
|
|
1542
1620
|
max-width: none; max-height: none;
|
|
1543
1621
|
object-fit: var(--webg-fit, cover);
|
|
1544
1622
|
object-position: center;
|
|
@@ -1631,10 +1709,10 @@ window.__ModuleLoader__.load({
|
|
|
1631
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);
|
|
1632
1710
|
backdrop-filter: blur(var(--webg-blur, 16px)) saturate(var(--webg-saturate, 1.8)) brightness(var(--webg-glass-brightness, 1.04)) contrast(1.01);
|
|
1633
1711
|
box-shadow:
|
|
1634
|
-
inset 0 1px 0 rgba(255, 255, 255,
|
|
1712
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
|
1635
1713
|
inset 0 -1px 0 rgba(255, 255, 255, 0.08),
|
|
1636
1714
|
inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
|
|
1637
|
-
0 12px 40px rgba(0, 0, 0,
|
|
1715
|
+
0 12px 40px rgba(0, 0, 0, 0.12);
|
|
1638
1716
|
}
|
|
1639
1717
|
|
|
1640
1718
|
/* ── Picker chrome (token-driven, theme-aware) ── */
|