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/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: Math.max(1, clampNum(o.zoom, 0.25, 4, DEFAULTS.zoom)),
275
- // Pan may have been persisted with a larger headroom than the zoom
276
- // allows (older builds let zoom go below 1) — clampCrop re-seats it.
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(isSelectable);
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 && isSelectable(w))) {
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(isSelectable);
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) => isSelectable(byId.get(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 webg-iframe";
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 = (sel && sel.type) || (media.tagName === "IMG" ? "image" : null);
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 MEDIA BOX, which (blur overscan) is wider
796
- // than the viewport by 2× the wallpaper blur — divide by that, so the
797
- // content tracks the cursor 1:1 at any blur setting.
798
- const w = (overlay.clientWidth || 1) + 2 * selection.wallpaperBlur;
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(), CROSSFADE_MS + 120);
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
- }, CROSSFADE_MS + 120);
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.preview
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
- function WallpaperPicker() {
1108
- const sel = useStore();
1109
- const [query, setQuery] = React.useState("");
1110
- const [editorQuery, setEditorQuery] = React.useState("");
1111
- const t = S();
1112
- const onTogglePlay = () => { selection.playing = !selection.playing; emit(); };
1113
- const onClear = () => applySelection("");
1114
- const onRefresh = () => {
1115
- // A manual refresh also retries wallpapers previously demoted to their
1116
- // preview after decode failures.
1117
- demotedToPreview.clear();
1118
- mediaFailures.clear();
1119
- lastRecoveryAt.clear();
1120
- loadInventory(true);
1121
- };
1122
- const onLangChange = (e) => {
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
- const onScrim = (pct) => { selection.scrim = pct / 100; persistSelection(); applyEffects(); emit(); };
1197
- const onBorder = (pct) => { selection.border = pct / 100; persistSelection(); applyEffects(); emit(); };
1198
- const onBlur = (px) => { selection.blur = px; persistSelection(); applyEffects(); emit(); };
1199
- const onWallpaperBlur = (px) => { selection.wallpaperBlur = px; persistSelection(); applyEffects(); emit(); };
1200
- const onFit = (e) => {
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
- if (!sel.loaded) {
1208
- return React.createElement("div", { className: "webg-picker" },
1209
- React.createElement("span", { className: "webg-hint" }, t.scanning));
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
- if (sel.inventory.error) {
1212
- return React.createElement("div", { className: "webg-picker" },
1213
- React.createElement("div", { className: "webg-error" },
1214
- t.connError + sel.inventory.error),
1215
- React.createElement("button", {
1216
- className: "webg-btn", type: "button", onClick: onRefresh, disabled: sel.loading,
1217
- }, sel.loading ? t.refreshing : t.retry));
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
- const usableList = selectableInventory();
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
- const candidates = rotationCandidates();
1226
- const usableCount = candidates.length;
1227
- const editing = sel.editing;
1228
- const autoPaused = sel.autoPauseReasons.size > 0;
1229
- const INTERVALS = [1, 5, 10, 30, 60, 120];
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
- return React.createElement("div", { className: "webg-picker" },
1232
- // ── Search + language ──
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 webg-search", type: "search",
1236
- placeholder: t.searchPlaceholder,
1237
- value: query,
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
- // ── Grid ──
1256
- React.createElement("div", { className: "webg-grid" },
1257
- React.createElement("button", {
1258
- className: "webg-card" + (sel.id ? "" : " webg-card--selected"),
1259
- type: "button",
1260
- onClick: onClear,
1261
- title: t.closeTitle,
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("span", { className: "webg-card-close" }, t.closeCard),
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
- // ── Actions ──
1277
- React.createElement("div", { className: "webg-row" },
1278
- React.createElement("button", {
1279
- className: "webg-btn", type: "button",
1280
- onClick: onTogglePlay, disabled: !sel.url || sel.type !== "video",
1281
- }, sel.playing ? t.pause : t.play),
1282
- React.createElement("button", {
1283
- className: "webg-btn", type: "button",
1284
- onClick: onClear, disabled: !sel.id,
1285
- }, t.close),
1286
- React.createElement("button", {
1287
- className: "webg-btn", type: "button",
1288
- onClick: onRefresh, disabled: sel.loading,
1289
- }, sel.loading ? t.refreshing : t.refresh),
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
- // ── Rotation groups ──
1292
- React.createElement("div", { className: "webg-row webg-playlist-row" },
1293
- React.createElement("span", { className: "webg-hint webg-label" }, t.rotationList),
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: sel.rotationGroupId,
1297
- onChange: onGroupChange,
1298
- disabled: groups.length === 0,
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: "" }, groups.length ? t.selectList : t.noLists),
1301
- ...groups.map((g) => React.createElement("option", {
1302
- key: g.id, value: g.id,
1303
- }, t.groupOption(g.name, groupWallpapers(g).length, g.interval))),
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", onClick: startCreateGroup,
1307
- }, t.newList),
1308
- React.createElement("button", {
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: onDeleteGroup,
1316
- disabled: !sel.rotationGroupId,
1317
- }, t.deleteList),
1445
+ onClick: cancelEditGroup,
1446
+ }, t.cancel),
1318
1447
  ),
1319
- editing && React.createElement("div", { className: "webg-editor" },
1320
- React.createElement("div", { className: "webg-row" },
1321
- React.createElement("span", { className: "webg-hint webg-label" }, t.name),
1322
- React.createElement("input", {
1323
- className: "webg-text", type: "text",
1324
- value: editing.name,
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
- className: "webg-text", type: "search",
1353
- placeholder: t.filterPlaceholder,
1354
- value: editorQuery,
1355
- onInput: (e) => setEditorQuery(e.target.value),
1455
+ type: "checkbox",
1456
+ checked: sel.rotationEnabled,
1457
+ onChange: onToggleRotation,
1458
+ disabled: !sel.rotationGroupId || usableCount < 2,
1356
1459
  }),
1357
- React.createElement("div", { className: "webg-editor-grid" },
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("div", { className: "webg-row webg-rotation-row" },
1420
- React.createElement("label", { className: "webg-toggle" },
1421
- React.createElement("input", {
1422
- type: "checkbox",
1423
- checked: sel.rotationEnabled,
1424
- onChange: onToggleRotation,
1425
- disabled: !sel.rotationGroupId || usableCount < 2,
1426
- }),
1427
- t.autoRotate,
1428
- ),
1429
- React.createElement("select", {
1430
- className: "webg-select webg-rotation-interval",
1431
- value: String(group ? group.interval : DEFAULTS.rotationInterval),
1432
- onChange: onGroupInterval,
1433
- disabled: !sel.rotationEnabled || !sel.rotationGroupId || usableCount < 2,
1434
- title: t.intervalTitle,
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
- ...[...new Set([...INTERVALS, group ? group.interval : DEFAULTS.rotationInterval])]
1437
- .sort((x, y) => x - y).map((minutes) =>
1438
- React.createElement("option", { key: minutes, value: String(minutes) }, t.minutes(minutes)),
1439
- )),
1440
- !sel.rotationGroupId && React.createElement("span", { className: "webg-hint" }, t.needList),
1441
- sel.rotationGroupId && usableCount < 2 && React.createElement("span", { className: "webg-hint" }, t.needTwo),
1442
- ),
1443
- // ── Effects ──
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: () => enterAdjust(),
1460
- disabled: sel.adjusting,
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
- // ── Resource monitor ──
1476
- sel.id && React.createElement("div", { className: "webg-row webg-monitor-row" },
1477
- React.createElement("label", { className: "webg-toggle" },
1478
- React.createElement("input", {
1479
- type: "checkbox", checked: sel.pauseOnHidden, onChange: onTogglePauseOnHidden,
1480
- }),
1481
- t.pauseOnHidden,
1482
- ),
1483
- React.createElement("label", { className: "webg-toggle" },
1484
- React.createElement("input", {
1485
- type: "checkbox", checked: sel.pauseOnBattery, onChange: onTogglePauseOnBattery,
1486
- }),
1487
- t.pauseOnBattery,
1488
- ),
1489
- React.createElement("label", { className: "webg-toggle", title: t.autoScrimTitle },
1490
- React.createElement("input", {
1491
- type: "checkbox", checked: sel.autoScrim, onChange: onToggleAutoScrim,
1492
- }),
1493
- t.autoScrim,
1494
- ),
1495
- sel.fps !== null && React.createElement("span", {
1496
- className: "webg-hint webg-fps" + (sel.fps < LOW_FPS ? " webg-fps--low" : ""),
1497
- title: t.fpsTitle,
1498
- }, sel.fps + " fps"),
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
- /* Overscan: the media is LARGER than the layer by one blur radius on every
1532
- side, so the transparent fringe CSS blur produces is clipped by the
1533
- layer's overflow:hidden instead of compensated with a content-warping
1534
- scale. 1× is the fringe a Gaussian kernel actually needs; more would
1535
- double GPU fill and mis-anchor non-center object-positions. A slight
1536
- saturate keeps blurred colours from washing out. */
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: calc(-1 * var(--webg-wallpaper-blur, 0px));
1540
- width: calc(100% + 2 * var(--webg-wallpaper-blur, 0px));
1541
- height: calc(100% + 2 * var(--webg-wallpaper-blur, 0px));
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, var(--webg-glass-highlight, 0.32)),
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, var(--webg-glass-shadow, 0.12));
1715
+ 0 12px 40px rgba(0, 0, 0, 0.12);
1638
1716
  }
1639
1717
 
1640
1718
  /* ── Picker chrome (token-driven, theme-aware) ── */