wallpaper-engine-dsh 0.1.5 → 0.1.7
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/LICENSE +21 -0
- package/README.md +6 -5
- package/README.zh.md +5 -4
- package/lib/client.js +290 -53
- package/lib/core.js +68 -52
- package/lib/index.js +184 -110
- package/package.json +7 -3
package/lib/client.js
CHANGED
|
@@ -79,6 +79,8 @@ window.__ModuleLoader__.load({
|
|
|
79
79
|
glass: "玻璃",
|
|
80
80
|
pauseOnHidden: "页面隐藏时暂停",
|
|
81
81
|
pauseOnBattery: "低电量时暂停",
|
|
82
|
+
autoScrim: "智能可视",
|
|
83
|
+
autoScrimTitle: "采样壁纸亮度,过亮或过暗时自动加强遮罩,保证文字可读",
|
|
82
84
|
fpsTitle: "页面当前渲染帧率",
|
|
83
85
|
lowFpsHint: "帧率偏低,可尝试暂停视频、增大壁纸模糊或换用静态壁纸",
|
|
84
86
|
language: "语言",
|
|
@@ -102,6 +104,8 @@ window.__ModuleLoader__.load({
|
|
|
102
104
|
confirmDelete: (name) => "删除轮播列表「" + name + "」?",
|
|
103
105
|
defaultGroupName: (n) => "轮播列表 " + n,
|
|
104
106
|
defaultGroupBase: "轮播列表",
|
|
107
|
+
iframeTitle: "Wallpaper Engine 网页壁纸",
|
|
108
|
+
decodeFallback: "媒体无法解码,已改用预览图",
|
|
105
109
|
},
|
|
106
110
|
en: {
|
|
107
111
|
settingsLabel: "Wallpaper Engine",
|
|
@@ -155,6 +159,8 @@ window.__ModuleLoader__.load({
|
|
|
155
159
|
glass: "Glass",
|
|
156
160
|
pauseOnHidden: "Pause when tab hidden",
|
|
157
161
|
pauseOnBattery: "Pause on low battery",
|
|
162
|
+
autoScrim: "Smart veil",
|
|
163
|
+
autoScrimTitle: "Samples wallpaper brightness and strengthens the veil when needed so text stays readable",
|
|
158
164
|
fpsTitle: "Current page frame rate",
|
|
159
165
|
lowFpsHint: "Low frame rate — try pausing the video, raising wallpaper blur, or using a still wallpaper",
|
|
160
166
|
language: "Language",
|
|
@@ -178,6 +184,8 @@ window.__ModuleLoader__.load({
|
|
|
178
184
|
confirmDelete: (name) => "Delete rotation list \"" + name + "\"?",
|
|
179
185
|
defaultGroupName: (n) => "Rotation list " + n,
|
|
180
186
|
defaultGroupBase: "Rotation list",
|
|
187
|
+
iframeTitle: "Wallpaper Engine web wallpaper",
|
|
188
|
+
decodeFallback: "Media cannot be decoded here — showing the preview image",
|
|
181
189
|
},
|
|
182
190
|
};
|
|
183
191
|
|
|
@@ -214,6 +222,7 @@ window.__ModuleLoader__.load({
|
|
|
214
222
|
rotationSeeded: false,
|
|
215
223
|
pauseOnHidden: true,
|
|
216
224
|
pauseOnBattery: true,
|
|
225
|
+
autoScrim: true,
|
|
217
226
|
lang: "auto",
|
|
218
227
|
fit: "cover",
|
|
219
228
|
position: "center",
|
|
@@ -260,10 +269,11 @@ window.__ModuleLoader__.load({
|
|
|
260
269
|
wallpaperBlur: clampNum(o.wallpaperBlur, 0, 60, DEFAULTS.wallpaperBlur),
|
|
261
270
|
rotationEnabled: o.rotationEnabled === true,
|
|
262
271
|
rotationGroupId: typeof o.rotationGroupId === "string" ? o.rotationGroupId : "",
|
|
263
|
-
rotationGroups: readRotationGroups(o.rotationGroups, STRINGS
|
|
272
|
+
rotationGroups: readRotationGroups(o.rotationGroups, STRINGS[browserLang() || "zh"].defaultGroupBase),
|
|
264
273
|
rotationSeeded: o.rotationSeeded === true,
|
|
265
274
|
pauseOnHidden: o.pauseOnHidden !== false,
|
|
266
275
|
pauseOnBattery: o.pauseOnBattery !== false,
|
|
276
|
+
autoScrim: o.autoScrim !== false,
|
|
267
277
|
lang: o.lang === "zh" || o.lang === "en" ? o.lang : "auto",
|
|
268
278
|
fit: FIT_MODES.indexOf(o.fit) >= 0 ? o.fit : DEFAULTS.fit,
|
|
269
279
|
position: POSITIONS.indexOf(o.position) >= 0 ? o.position : DEFAULTS.position,
|
|
@@ -315,6 +325,7 @@ window.__ModuleLoader__.load({
|
|
|
315
325
|
rotationSeeded: selection.rotationSeeded,
|
|
316
326
|
pauseOnHidden: selection.pauseOnHidden,
|
|
317
327
|
pauseOnBattery: selection.pauseOnBattery,
|
|
328
|
+
autoScrim: selection.autoScrim,
|
|
318
329
|
lang: selection.lang,
|
|
319
330
|
fit: selection.fit,
|
|
320
331
|
position: selection.position,
|
|
@@ -493,9 +504,14 @@ window.__ModuleLoader__.load({
|
|
|
493
504
|
}
|
|
494
505
|
|
|
495
506
|
function startCreateGroup() {
|
|
507
|
+
// Default names must stay unique even after deletions: take the smallest
|
|
508
|
+
// free index instead of count+1.
|
|
509
|
+
const taken = new Set(selection.rotationGroups.map((g) => g.name));
|
|
510
|
+
let n = 1;
|
|
511
|
+
while (taken.has(S().defaultGroupName(n))) n++;
|
|
496
512
|
selection.editing = {
|
|
497
513
|
id: nextGroupId(),
|
|
498
|
-
name: S().defaultGroupName(
|
|
514
|
+
name: S().defaultGroupName(n),
|
|
499
515
|
interval: DEFAULTS.rotationInterval,
|
|
500
516
|
order: "sequence",
|
|
501
517
|
wallpaperIds: [],
|
|
@@ -513,7 +529,7 @@ window.__ModuleLoader__.load({
|
|
|
513
529
|
interval: clampNum(draft.interval, 1, 1440, DEFAULTS.rotationInterval),
|
|
514
530
|
order: draft.order === "random" ? "random" : "sequence",
|
|
515
531
|
wallpaperIds: Array.isArray(draft.wallpaperIds)
|
|
516
|
-
? draft.wallpaperIds.filter((x) => typeof x === "string" && x)
|
|
532
|
+
? [...new Set(draft.wallpaperIds.filter((x) => typeof x === "string" && x))]
|
|
517
533
|
: [],
|
|
518
534
|
};
|
|
519
535
|
if (idx >= 0) selection.rotationGroups[idx] = cleaned;
|
|
@@ -522,8 +538,13 @@ window.__ModuleLoader__.load({
|
|
|
522
538
|
selection.editing = null;
|
|
523
539
|
if (selection.rotationEnabled && !rotationCandidates().some((w) => w.id === selection.id)) {
|
|
524
540
|
const first = rotationCandidates()[0];
|
|
525
|
-
|
|
526
|
-
|
|
541
|
+
if (first) {
|
|
542
|
+
applySelection(first.id);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Saved list has nothing usable — keep the current wallpaper and simply
|
|
546
|
+
// stop rotation instead of clearing the screen.
|
|
547
|
+
selection.rotationEnabled = false;
|
|
527
548
|
}
|
|
528
549
|
persistSelection();
|
|
529
550
|
syncRotationTimer();
|
|
@@ -556,7 +577,7 @@ window.__ModuleLoader__.load({
|
|
|
556
577
|
function importPlaylistIntoDraft(playlist) {
|
|
557
578
|
if (!selection.editing || !playlist || !Array.isArray(playlist.wallpaperIds)) return;
|
|
558
579
|
const usable = new Set(selectableInventory().map((w) => w.id));
|
|
559
|
-
selection.editing.wallpaperIds = playlist.wallpaperIds.filter((id) => usable.has(id));
|
|
580
|
+
selection.editing.wallpaperIds = [...new Set(playlist.wallpaperIds.filter((id) => usable.has(id)))];
|
|
560
581
|
emit();
|
|
561
582
|
}
|
|
562
583
|
|
|
@@ -566,10 +587,12 @@ window.__ModuleLoader__.load({
|
|
|
566
587
|
const w = selection.id
|
|
567
588
|
? selection.inventory.wallpapers.find((x) => x.id === selection.id)
|
|
568
589
|
: null;
|
|
569
|
-
if (isPlayable(w)) {
|
|
590
|
+
if (isPlayable(w) && !demotedToPreview.has(selection.id)) {
|
|
570
591
|
selection.url = w.media;
|
|
571
592
|
selection.type = w.type; // "video" | "web"
|
|
572
|
-
} else if (w &&
|
|
593
|
+
} else if (w && w.preview) {
|
|
594
|
+
// Static path: scene/application wallpapers, and playable wallpapers
|
|
595
|
+
// demoted after repeated media failures (undecodable container…).
|
|
573
596
|
selection.url = w.preview;
|
|
574
597
|
selection.type = "image";
|
|
575
598
|
} else {
|
|
@@ -592,11 +615,27 @@ window.__ModuleLoader__.load({
|
|
|
592
615
|
return selection.playing && selection.autoPauseReasons.size === 0;
|
|
593
616
|
}
|
|
594
617
|
|
|
618
|
+
// Latest known battery state (null until getBattery resolves). Kept at module
|
|
619
|
+
// scope so re-enabling the toggle can re-evaluate immediately instead of
|
|
620
|
+
// waiting for the next battery event.
|
|
621
|
+
let batteryState = null;
|
|
622
|
+
function applyBatteryPause() {
|
|
623
|
+
if (!batteryState || !selection.pauseOnBattery) return;
|
|
624
|
+
setAutoPause("battery",
|
|
625
|
+
!batteryState.charging && batteryState.level <= BATTERY_SAVER_LEVEL);
|
|
626
|
+
}
|
|
627
|
+
|
|
595
628
|
// ── Behind-body layer: wallpaper + scrim, with crossfade ────────────────────
|
|
596
629
|
// Set by a media error handler to force a remount with fresh URLs; cleared
|
|
597
630
|
// once the remount happens.
|
|
598
631
|
let forceRemount = false;
|
|
599
632
|
let lastMediaRecoveryAt = 0;
|
|
633
|
+
// Media failure bookkeeping: the first error for a wallpaper may just be a
|
|
634
|
+
// stale token (fixed by a refresh+remount); a SECOND error for the same id
|
|
635
|
+
// means the bytes themselves are unusable (e.g. an mkv/avi Chromium cannot
|
|
636
|
+
// decode) — demote it to its preview image instead of looping forever.
|
|
637
|
+
const mediaFailures = new Map(); // wallpaperId → consecutive failure count
|
|
638
|
+
const demotedToPreview = new Set(); // wallpaperIds now rendered as stills
|
|
600
639
|
|
|
601
640
|
function buildMedia(sel) {
|
|
602
641
|
let media;
|
|
@@ -617,7 +656,7 @@ window.__ModuleLoader__.load({
|
|
|
617
656
|
media.setAttribute("referrerpolicy", "no-referrer");
|
|
618
657
|
media.setAttribute("frameborder", "0");
|
|
619
658
|
media.setAttribute("scrolling", "no");
|
|
620
|
-
media.setAttribute("title",
|
|
659
|
+
media.setAttribute("title", S().iframeTitle);
|
|
621
660
|
media.className = "webg-media webg-iframe";
|
|
622
661
|
} else {
|
|
623
662
|
// Static wallpaper: the scene/application preview as a still image.
|
|
@@ -628,22 +667,53 @@ window.__ModuleLoader__.load({
|
|
|
628
667
|
media.setAttribute("decoding", "async");
|
|
629
668
|
media.className = "webg-media";
|
|
630
669
|
}
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
|
|
670
|
+
// First luminance sample as soon as the frame is decodable (video/still
|
|
671
|
+
// only — iframe contents are opaque to us). Later samples ride the timer.
|
|
672
|
+
if (sel.type === "video" || sel.type === "image") {
|
|
673
|
+
media.addEventListener(sel.type === "video" ? "loadeddata" : "load", () => {
|
|
674
|
+
sampleWallpaperLuminance();
|
|
675
|
+
applyEffects();
|
|
676
|
+
});
|
|
677
|
+
}
|
|
634
678
|
media.addEventListener("error", () => {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
679
|
+
const id = sel.id;
|
|
680
|
+
const count = (mediaFailures.get(id) || 0) + 1;
|
|
681
|
+
mediaFailures.set(id, count);
|
|
682
|
+
const w = selection.inventory.wallpapers.find((x) => x.id === id);
|
|
683
|
+
if (count === 1) {
|
|
684
|
+
// Maybe just a dead token: refetch fresh URLs and remount (throttled).
|
|
685
|
+
const now = Date.now();
|
|
686
|
+
if (now - lastMediaRecoveryAt < 10000) return;
|
|
687
|
+
lastMediaRecoveryAt = now;
|
|
688
|
+
forceRemount = true;
|
|
689
|
+
loadInventory(true);
|
|
690
|
+
} else if (w && w.preview && !demotedToPreview.has(id)) {
|
|
691
|
+
// The media itself is unusable here — fall back to the still preview.
|
|
692
|
+
demotedToPreview.add(id);
|
|
693
|
+
applySelection(id);
|
|
694
|
+
}
|
|
640
695
|
});
|
|
641
696
|
return media;
|
|
642
697
|
}
|
|
643
698
|
|
|
699
|
+
// Crossfade/clear leave timers: tracked so dispose can cancel them instead of
|
|
700
|
+
// letting them fire on torn-down nodes afterwards.
|
|
701
|
+
const pendingTimers = new Set();
|
|
702
|
+
function schedule(fn, ms) {
|
|
703
|
+
if (typeof window === "undefined" || typeof window.setTimeout !== "function") {
|
|
704
|
+
fn();
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
const token = window.setTimeout(() => {
|
|
708
|
+
pendingTimers.delete(token);
|
|
709
|
+
fn();
|
|
710
|
+
}, ms);
|
|
711
|
+
pendingTimers.add(token);
|
|
712
|
+
return token;
|
|
713
|
+
}
|
|
714
|
+
|
|
644
715
|
function syncLayers() {
|
|
645
716
|
const existing = document.getElementById(LAYER_ID);
|
|
646
|
-
const hasTimeout = typeof window !== "undefined" && typeof window.setTimeout === "function";
|
|
647
717
|
|
|
648
718
|
if (selection.url) {
|
|
649
719
|
// Key by wallpaper id, NOT by media URL: inventory rebuilds re-mint
|
|
@@ -670,8 +740,7 @@ window.__ModuleLoader__.load({
|
|
|
670
740
|
if (existing) {
|
|
671
741
|
existing.classList.add("webg-layer--leave");
|
|
672
742
|
const stale = existing;
|
|
673
|
-
|
|
674
|
-
else stale.remove();
|
|
743
|
+
schedule(() => stale.remove(), CROSSFADE_MS + 120);
|
|
675
744
|
}
|
|
676
745
|
}
|
|
677
746
|
const node = document.getElementById(LAYER_ID);
|
|
@@ -686,16 +755,14 @@ window.__ModuleLoader__.load({
|
|
|
686
755
|
existing.removeAttribute("id");
|
|
687
756
|
existing.classList.add("webg-layer--leave");
|
|
688
757
|
const stale = existing;
|
|
689
|
-
|
|
758
|
+
schedule(() => {
|
|
690
759
|
stale.remove();
|
|
691
760
|
if (!document.getElementById(LAYER_ID)) {
|
|
692
761
|
const s = document.getElementById(SCRIM_ID);
|
|
693
762
|
if (s) s.remove();
|
|
694
763
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
695
764
|
}
|
|
696
|
-
};
|
|
697
|
-
if (hasTimeout) window.setTimeout(finish, CROSSFADE_MS + 120);
|
|
698
|
-
else finish();
|
|
765
|
+
}, CROSSFADE_MS + 120);
|
|
699
766
|
}
|
|
700
767
|
|
|
701
768
|
// Scrim: present while a wallpaper is active. On clear it outlives the
|
|
@@ -713,15 +780,61 @@ window.__ModuleLoader__.load({
|
|
|
713
780
|
}
|
|
714
781
|
|
|
715
782
|
// ── Effect application: push the knobs into CSS variables ───────────────────
|
|
783
|
+
// Wallpaper luminance sampled from the live media (null until the first
|
|
784
|
+
// sample; iframes of web wallpapers can never be sampled — opaque origin).
|
|
785
|
+
let sampledLuminance = null;
|
|
786
|
+
let sampleCanvas = null;
|
|
787
|
+
|
|
788
|
+
/** Average Rec.601 luminance (0..1) of the current video/img, or null. */
|
|
789
|
+
function sampleWallpaperLuminance() {
|
|
790
|
+
if (typeof document === "undefined") return;
|
|
791
|
+
const layer = document.getElementById(LAYER_ID);
|
|
792
|
+
const media = layer && (layer.querySelector("video") || layer.querySelector("img"));
|
|
793
|
+
if (!media) { sampledLuminance = null; return; }
|
|
794
|
+
try {
|
|
795
|
+
if (!sampleCanvas) {
|
|
796
|
+
const c = document.createElement("canvas");
|
|
797
|
+
if (typeof c.getContext !== "function") return; // no canvas → no sampling
|
|
798
|
+
c.width = 16; c.height = 16;
|
|
799
|
+
sampleCanvas = c;
|
|
800
|
+
}
|
|
801
|
+
const g = sampleCanvas.getContext("2d", { willReadFrequently: true });
|
|
802
|
+
g.drawImage(media, 0, 0, 16, 16);
|
|
803
|
+
const d = g.getImageData(0, 0, 16, 16).data;
|
|
804
|
+
let sum = 0;
|
|
805
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
806
|
+
sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];
|
|
807
|
+
}
|
|
808
|
+
sampledLuminance = sum / (d.length / 4) / 255;
|
|
809
|
+
} catch { /* frame not ready / unreadable → keep the previous sample */ }
|
|
810
|
+
}
|
|
811
|
+
|
|
716
812
|
function applyEffects() {
|
|
717
813
|
const s = document.body.style;
|
|
718
|
-
|
|
814
|
+
// The veil OPPOSES the text colour: dark theme (light text) darkens bright
|
|
815
|
+
// wallpapers, light theme (dark text) brightens dark ones — either way the
|
|
816
|
+
// text side wins. This is what makes ANY wallpaper survivable.
|
|
817
|
+
const dark = Boolean(document.body.hasAttribute && document.body.hasAttribute("data-ds-dark-theme"));
|
|
818
|
+
let alpha = selection.scrim;
|
|
819
|
+
if (selection.autoScrim && sampledLuminance !== null) {
|
|
820
|
+
// Distance of the wallpaper's brightness from the "comfortable middle"
|
|
821
|
+
// towards the text colour's weakness, mapped to a veil-alpha floor.
|
|
822
|
+
const need = dark
|
|
823
|
+
? Math.min(0.55, Math.max(0, sampledLuminance - 0.5))
|
|
824
|
+
: Math.min(0.55, Math.max(0, 0.5 - sampledLuminance));
|
|
825
|
+
alpha = Math.max(alpha, Math.round(need * 1000) / 1000);
|
|
826
|
+
}
|
|
827
|
+
const veilColor = "rgba(" + (dark ? "0,0,0" : "255,255,255") + "," + alpha + ")";
|
|
828
|
+
s.setProperty("--webg-scrim-color", veilColor);
|
|
719
829
|
s.setProperty("--webg-border-alpha", String(selection.border));
|
|
720
830
|
s.setProperty("--webg-blur", selection.blur + "px");
|
|
721
831
|
// The glass "colour melt" scales with blur radius: 0 blur → no melt.
|
|
722
832
|
s.setProperty("--webg-saturate", String(1.15 + selection.blur * 0.028));
|
|
723
833
|
s.setProperty("--webg-glass-brightness", "1.04");
|
|
724
834
|
s.setProperty("--webg-wallpaper-blur", selection.wallpaperBlur + "px");
|
|
835
|
+
// Dither-noise strength scales with blur: invisible at 0, ~4% at 60px.
|
|
836
|
+
s.setProperty("--webg-noise-opacity",
|
|
837
|
+
selection.wallpaperBlur > 0 ? (0.02 + selection.wallpaperBlur * 0.0004).toFixed(4) : "0");
|
|
725
838
|
// Canvas fit: object-fit/position for video and still wallpapers (iframes
|
|
726
839
|
// ignore object-fit — web wallpapers always fill their layer).
|
|
727
840
|
s.setProperty("--webg-fit", selection.fit);
|
|
@@ -730,7 +843,7 @@ window.__ModuleLoader__.load({
|
|
|
730
843
|
// Write the scrim colour directly too, then force a synchronous layout, so
|
|
731
844
|
// slider feedback lands on this frame even on stalled compositors.
|
|
732
845
|
const scrim = document.getElementById(SCRIM_ID);
|
|
733
|
-
if (scrim) scrim.style.background =
|
|
846
|
+
if (scrim) scrim.style.background = veilColor;
|
|
734
847
|
if (document.body && document.body.offsetHeight !== undefined) {
|
|
735
848
|
void document.body.offsetHeight;
|
|
736
849
|
}
|
|
@@ -744,6 +857,7 @@ window.__ModuleLoader__.load({
|
|
|
744
857
|
s.removeProperty("--webg-saturate");
|
|
745
858
|
s.removeProperty("--webg-glass-brightness");
|
|
746
859
|
s.removeProperty("--webg-wallpaper-blur");
|
|
860
|
+
s.removeProperty("--webg-noise-opacity");
|
|
747
861
|
s.removeProperty("--webg-fit");
|
|
748
862
|
s.removeProperty("--webg-position");
|
|
749
863
|
const scrim = document.getElementById(SCRIM_ID);
|
|
@@ -755,16 +869,25 @@ window.__ModuleLoader__.load({
|
|
|
755
869
|
// A Fragment of three cells — the surrounding .webg-effects grid keeps
|
|
756
870
|
// every slider starting and ending at the same x regardless of how wide
|
|
757
871
|
// the label text is (壁纸模糊 vs 暗化 vs Wallpaper blur…).
|
|
872
|
+
// --webg-p feeds the filled-track gradient; updated inline while dragging
|
|
873
|
+
// so the fill tracks the thumb even between re-renders.
|
|
874
|
+
const pct = ((value - min) / (max - min)) * 100;
|
|
875
|
+
const set = (e) => {
|
|
876
|
+
const v = Number(e.target.value);
|
|
877
|
+
if (e.target.style && typeof e.target.style.setProperty === "function") {
|
|
878
|
+
e.target.style.setProperty("--webg-p", String(((v - min) / (max - min)) * 100));
|
|
879
|
+
}
|
|
880
|
+
onInput(v);
|
|
881
|
+
};
|
|
758
882
|
return React.createElement(React.Fragment, null,
|
|
759
883
|
React.createElement("span", { className: "webg-hint webg-label" }, label),
|
|
760
884
|
React.createElement("input", {
|
|
761
885
|
className: "webg-slider", type: "range",
|
|
762
886
|
min: String(min), max: String(max), step: String(step),
|
|
763
887
|
value: String(value),
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
onChange: (e) => onInput(Number(e.target.value)),
|
|
888
|
+
style: { "--webg-p": String(pct) },
|
|
889
|
+
onInput: set,
|
|
890
|
+
onChange: set,
|
|
768
891
|
}),
|
|
769
892
|
React.createElement("span", { className: "webg-hint webg-value" }, suffix),
|
|
770
893
|
);
|
|
@@ -808,7 +931,13 @@ window.__ModuleLoader__.load({
|
|
|
808
931
|
const t = S();
|
|
809
932
|
const onTogglePlay = () => { selection.playing = !selection.playing; emit(); };
|
|
810
933
|
const onClear = () => applySelection("");
|
|
811
|
-
const onRefresh = () =>
|
|
934
|
+
const onRefresh = () => {
|
|
935
|
+
// A manual refresh also retries wallpapers previously demoted to their
|
|
936
|
+
// preview after decode failures.
|
|
937
|
+
demotedToPreview.clear();
|
|
938
|
+
mediaFailures.clear();
|
|
939
|
+
loadInventory(true);
|
|
940
|
+
};
|
|
812
941
|
const onLangChange = (e) => {
|
|
813
942
|
selection.lang = e.target.value;
|
|
814
943
|
persistSelection();
|
|
@@ -828,15 +957,12 @@ window.__ModuleLoader__.load({
|
|
|
828
957
|
};
|
|
829
958
|
const onToggleRotation = () => {
|
|
830
959
|
selection.rotationEnabled = !selection.rotationEnabled;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (
|
|
837
|
-
const first = rotationCandidates()[0];
|
|
838
|
-
if (first) { applySelection(first.id); return; }
|
|
839
|
-
}
|
|
960
|
+
// The checkbox is disabled without a usable group, so a group id always
|
|
961
|
+
// exists here when enabling.
|
|
962
|
+
if (selection.rotationEnabled &&
|
|
963
|
+
!rotationCandidates().some((w) => w.id === selection.id)) {
|
|
964
|
+
const first = rotationCandidates()[0];
|
|
965
|
+
if (first) { applySelection(first.id); return; }
|
|
840
966
|
}
|
|
841
967
|
persistSelection();
|
|
842
968
|
syncRotationTimer();
|
|
@@ -860,16 +986,31 @@ window.__ModuleLoader__.load({
|
|
|
860
986
|
};
|
|
861
987
|
const onTogglePauseOnHidden = () => {
|
|
862
988
|
selection.pauseOnHidden = !selection.pauseOnHidden;
|
|
863
|
-
if (
|
|
989
|
+
if (selection.pauseOnHidden) {
|
|
990
|
+
// Re-enabling while already hidden must re-apply the reason — the
|
|
991
|
+
// visibilitychange listener only fires on CHANGES.
|
|
992
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
993
|
+
setAutoPause("hidden", true);
|
|
994
|
+
}
|
|
995
|
+
} else {
|
|
996
|
+
setAutoPause("hidden", false);
|
|
997
|
+
}
|
|
864
998
|
persistSelection();
|
|
865
999
|
emit();
|
|
866
1000
|
};
|
|
867
1001
|
const onTogglePauseOnBattery = () => {
|
|
868
1002
|
selection.pauseOnBattery = !selection.pauseOnBattery;
|
|
869
|
-
if (
|
|
1003
|
+
if (selection.pauseOnBattery) applyBatteryPause(); // re-evaluate NOW
|
|
1004
|
+
else setAutoPause("battery", false);
|
|
870
1005
|
persistSelection();
|
|
871
1006
|
emit();
|
|
872
1007
|
};
|
|
1008
|
+
const onToggleAutoScrim = () => {
|
|
1009
|
+
selection.autoScrim = !selection.autoScrim;
|
|
1010
|
+
persistSelection();
|
|
1011
|
+
applyEffects();
|
|
1012
|
+
emit();
|
|
1013
|
+
};
|
|
873
1014
|
|
|
874
1015
|
const onScrim = (pct) => { selection.scrim = pct / 100; persistSelection(); applyEffects(); emit(); };
|
|
875
1016
|
const onBorder = (pct) => { selection.border = pct / 100; persistSelection(); applyEffects(); emit(); };
|
|
@@ -1165,6 +1306,12 @@ window.__ModuleLoader__.load({
|
|
|
1165
1306
|
}),
|
|
1166
1307
|
t.pauseOnBattery,
|
|
1167
1308
|
),
|
|
1309
|
+
React.createElement("label", { className: "webg-toggle", title: t.autoScrimTitle },
|
|
1310
|
+
React.createElement("input", {
|
|
1311
|
+
type: "checkbox", checked: sel.autoScrim, onChange: onToggleAutoScrim,
|
|
1312
|
+
}),
|
|
1313
|
+
t.autoScrim,
|
|
1314
|
+
),
|
|
1168
1315
|
sel.fps !== null && React.createElement("span", {
|
|
1169
1316
|
className: "webg-hint webg-fps" + (sel.fps < LOW_FPS ? " webg-fps--low" : ""),
|
|
1170
1317
|
title: t.fpsTitle,
|
|
@@ -1183,6 +1330,7 @@ window.__ModuleLoader__.load({
|
|
|
1183
1330
|
: t.statusUsable(usableList.length)) +
|
|
1184
1331
|
(sel.rotationEnabled ? " · " + t.autoRotating : "") +
|
|
1185
1332
|
(autoPaused ? " · " + t.autoPaused : "") +
|
|
1333
|
+
(sel.id && demotedToPreview.has(sel.id) ? " · " + t.decodeFallback : "") +
|
|
1186
1334
|
(sel.inventory.installDir ? " · " + sel.inventory.installDir : "")),
|
|
1187
1335
|
),
|
|
1188
1336
|
);
|
|
@@ -1200,21 +1348,35 @@ window.__ModuleLoader__.load({
|
|
|
1200
1348
|
}
|
|
1201
1349
|
.webg-layer--enter { opacity: 0; }
|
|
1202
1350
|
.webg-layer--leave { opacity: 0; }
|
|
1203
|
-
/* Overscan: the media is LARGER than the layer by
|
|
1351
|
+
/* Overscan: the media is LARGER than the layer by one blur radius on every
|
|
1204
1352
|
side, so the transparent fringe CSS blur produces is clipped by the
|
|
1205
1353
|
layer's overflow:hidden instead of compensated with a content-warping
|
|
1206
|
-
scale.
|
|
1354
|
+
scale. 1× is the fringe a Gaussian kernel actually needs; more would
|
|
1355
|
+
double GPU fill and mis-anchor non-center object-positions. A slight
|
|
1356
|
+
saturate keeps blurred colours from washing out. */
|
|
1207
1357
|
.webg-layer .webg-media {
|
|
1208
1358
|
position: absolute;
|
|
1209
|
-
inset: calc(-
|
|
1210
|
-
width: calc(100% +
|
|
1211
|
-
height: calc(100% +
|
|
1359
|
+
inset: calc(-1 * var(--webg-wallpaper-blur, 0px));
|
|
1360
|
+
width: calc(100% + 2 * var(--webg-wallpaper-blur, 0px));
|
|
1361
|
+
height: calc(100% + 2 * var(--webg-wallpaper-blur, 0px));
|
|
1212
1362
|
max-width: none; max-height: none;
|
|
1213
1363
|
object-fit: var(--webg-fit, cover);
|
|
1214
1364
|
object-position: var(--webg-position, center);
|
|
1215
1365
|
display: block;
|
|
1216
1366
|
background: transparent; border: 0;
|
|
1217
|
-
filter: blur(var(--webg-wallpaper-blur, 0px)) saturate(1.
|
|
1367
|
+
filter: blur(var(--webg-wallpaper-blur, 0px)) saturate(1.08);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/* Gaussian blur alone shows banding on smooth gradients; a whisper of
|
|
1371
|
+
fractal noise over the layer dithers it away (iOS does the same inside
|
|
1372
|
+
its frosted materials). Opacity rides the blur radius — zero blur, zero
|
|
1373
|
+
noise. */
|
|
1374
|
+
.webg-layer::after {
|
|
1375
|
+
content: ""; position: absolute; inset: 0; pointer-events: none;
|
|
1376
|
+
opacity: var(--webg-noise-opacity, 0);
|
|
1377
|
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch"/></filter><rect width="120" height="120" filter="url(%23n)"/></svg>');
|
|
1378
|
+
background-size: 120px 120px;
|
|
1379
|
+
mix-blend-mode: overlay;
|
|
1218
1380
|
}
|
|
1219
1381
|
|
|
1220
1382
|
/* Scrim: above the wallpaper (-1 > -2), below the UI. */
|
|
@@ -1314,13 +1476,33 @@ window.__ModuleLoader__.load({
|
|
|
1314
1476
|
}
|
|
1315
1477
|
|
|
1316
1478
|
.webg-select, .webg-text {
|
|
1317
|
-
padding:
|
|
1318
|
-
border-radius:
|
|
1479
|
+
padding: 5px 10px;
|
|
1480
|
+
border-radius: 8px;
|
|
1319
1481
|
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
1320
1482
|
background: var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.12));
|
|
1321
1483
|
color: var(--dsw-alias-label-primary, inherit);
|
|
1322
1484
|
font-size: 13px;
|
|
1485
|
+
transition: border-color 120ms ease, background-color 120ms ease;
|
|
1323
1486
|
}
|
|
1487
|
+
/* Custom select: drop the OS chrome, draw our own chevron (mid-gray reads
|
|
1488
|
+
on both light and dark themes), roomier hit area, shell-style focus. */
|
|
1489
|
+
.webg-select {
|
|
1490
|
+
-webkit-appearance: none;
|
|
1491
|
+
appearance: none;
|
|
1492
|
+
padding-right: 28px;
|
|
1493
|
+
cursor: pointer;
|
|
1494
|
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6"><path d="M1 1l4 4 4-4" fill="none" stroke="%23888" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>');
|
|
1495
|
+
background-repeat: no-repeat;
|
|
1496
|
+
background-position: right 10px center;
|
|
1497
|
+
}
|
|
1498
|
+
.webg-select:hover:not(:disabled), .webg-text:hover:not(:disabled) {
|
|
1499
|
+
border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.5));
|
|
1500
|
+
}
|
|
1501
|
+
.webg-select:focus-visible, .webg-text:focus-visible {
|
|
1502
|
+
outline: 2px solid var(--dsw-alias-brand-primary, rgb(15, 17, 21));
|
|
1503
|
+
outline-offset: 1px;
|
|
1504
|
+
}
|
|
1505
|
+
.webg-select:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
1324
1506
|
.webg-text { flex: 1; min-width: 0; }
|
|
1325
1507
|
.webg-search { max-width: 320px; }
|
|
1326
1508
|
.webg-lang { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
|
|
@@ -1332,7 +1514,33 @@ window.__ModuleLoader__.load({
|
|
|
1332
1514
|
font-size: 13px; cursor: pointer;
|
|
1333
1515
|
}
|
|
1334
1516
|
|
|
1335
|
-
|
|
1517
|
+
/* Custom range slider (Chromium): slim filled track + ringed thumb instead
|
|
1518
|
+
of the default chunky control. --webg-p (0–100) drives the fill. */
|
|
1519
|
+
.webg-slider {
|
|
1520
|
+
-webkit-appearance: none;
|
|
1521
|
+
appearance: none;
|
|
1522
|
+
width: 100%; height: 22px; margin: 0;
|
|
1523
|
+
background: transparent; cursor: pointer;
|
|
1524
|
+
}
|
|
1525
|
+
.webg-slider::-webkit-slider-runnable-track {
|
|
1526
|
+
height: 4px; border-radius: 2px;
|
|
1527
|
+
background: linear-gradient(to right,
|
|
1528
|
+
var(--dsw-alias-brand-primary, rgb(15, 17, 21)) 0%,
|
|
1529
|
+
var(--dsw-alias-brand-primary, rgb(15, 17, 21)) calc(var(--webg-p, 0) * 1%),
|
|
1530
|
+
var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)) calc(var(--webg-p, 0) * 1%));
|
|
1531
|
+
}
|
|
1532
|
+
.webg-slider::-webkit-slider-thumb {
|
|
1533
|
+
-webkit-appearance: none;
|
|
1534
|
+
width: 14px; height: 14px; border-radius: 50%;
|
|
1535
|
+
margin-top: -5px; /* centers the 14px thumb on the 4px track */
|
|
1536
|
+
background: var(--dsw-alias-brand-primary-invert, rgb(249, 250, 251));
|
|
1537
|
+
border: 2.5px solid var(--dsw-alias-brand-primary, rgb(15, 17, 21));
|
|
1538
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
|
|
1539
|
+
transition: transform 120ms ease;
|
|
1540
|
+
}
|
|
1541
|
+
.webg-slider:hover::-webkit-slider-thumb,
|
|
1542
|
+
.webg-slider:focus-visible::-webkit-slider-thumb { transform: scale(1.18); }
|
|
1543
|
+
.webg-slider:focus-visible { outline: none; }
|
|
1336
1544
|
/* Effects grid: one shared grid across all slider rows, so every slider
|
|
1337
1545
|
starts and ends at the same x no matter how wide its label is. */
|
|
1338
1546
|
.webg-effects {
|
|
@@ -1474,10 +1682,37 @@ window.__ModuleLoader__.load({
|
|
|
1474
1682
|
const unsubEffects = subscribe(applyEffects);
|
|
1475
1683
|
syncLayers();
|
|
1476
1684
|
applyEffects();
|
|
1685
|
+
// Luminance sampler: re-reads the wallpaper periodically so a bright
|
|
1686
|
+
// scene arriving mid-video re-raises the veil. (rAF-free: 2.5 s is
|
|
1687
|
+
// plenty for legibility, and an idle timer costs ~nothing.)
|
|
1688
|
+
let samplerId = null;
|
|
1689
|
+
if (typeof window !== "undefined" && typeof window.setInterval === "function") {
|
|
1690
|
+
samplerId = window.setInterval(() => {
|
|
1691
|
+
sampleWallpaperLuminance();
|
|
1692
|
+
applyEffects();
|
|
1693
|
+
}, 2500);
|
|
1694
|
+
}
|
|
1695
|
+
// A theme switch flips the veil colour immediately (black ↔ white).
|
|
1696
|
+
let observer = null;
|
|
1697
|
+
if (typeof MutationObserver === "function") {
|
|
1698
|
+
observer = new MutationObserver(() => applyEffects());
|
|
1699
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
|
|
1700
|
+
}
|
|
1477
1701
|
return () => {
|
|
1478
1702
|
unsubLayers();
|
|
1479
1703
|
unsubEffects();
|
|
1704
|
+
if (samplerId !== null) window.clearInterval(samplerId);
|
|
1705
|
+
if (observer) observer.disconnect();
|
|
1706
|
+
sampledLuminance = null;
|
|
1480
1707
|
clearRotationTimer();
|
|
1708
|
+
// Cancel pending crossfade/clear timers so they never fire on
|
|
1709
|
+
// torn-down nodes after dispose.
|
|
1710
|
+
for (const token of [...pendingTimers]) {
|
|
1711
|
+
if (typeof window !== "undefined" && typeof window.clearTimeout === "function") {
|
|
1712
|
+
window.clearTimeout(token);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
pendingTimers.clear();
|
|
1481
1716
|
// Remove EVERY layer instance (a crossfade-leaving layer has no id).
|
|
1482
1717
|
const doomed = document.querySelectorAll(".webg-layer");
|
|
1483
1718
|
for (const node of [...doomed]) node.remove();
|
|
@@ -1511,8 +1746,9 @@ window.__ModuleLoader__.load({
|
|
|
1511
1746
|
let battery = null;
|
|
1512
1747
|
let cancelled = false;
|
|
1513
1748
|
const update = () => {
|
|
1514
|
-
if (!battery
|
|
1515
|
-
|
|
1749
|
+
if (!battery) return;
|
|
1750
|
+
batteryState = { charging: battery.charging, level: battery.level };
|
|
1751
|
+
applyBatteryPause();
|
|
1516
1752
|
};
|
|
1517
1753
|
navigator.getBattery().then((b) => {
|
|
1518
1754
|
if (cancelled) return;
|
|
@@ -1527,6 +1763,7 @@ window.__ModuleLoader__.load({
|
|
|
1527
1763
|
battery.removeEventListener("chargingchange", update);
|
|
1528
1764
|
battery.removeEventListener("levelchange", update);
|
|
1529
1765
|
}
|
|
1766
|
+
batteryState = null;
|
|
1530
1767
|
setAutoPause("battery", false);
|
|
1531
1768
|
};
|
|
1532
1769
|
});
|