wallpaper-engine-dsh 0.1.0 → 0.1.2
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 +40 -67
- package/README.zh.md +39 -68
- package/lib/client.js +571 -143
- package/lib/core.js +96 -4
- package/lib/index.js +32 -69
- package/package.json +17 -2
package/lib/client.js
CHANGED
|
@@ -14,6 +14,10 @@ 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
|
+
/** Battery at or below this level (and discharging) auto-pauses playback. */
|
|
18
|
+
const BATTERY_SAVER_LEVEL = 0.2;
|
|
19
|
+
/** Below this FPS the picker shows a performance hint. */
|
|
20
|
+
const LOW_FPS = 24;
|
|
17
21
|
|
|
18
22
|
// Respect the OS "reduce motion" preference: start paused rather than
|
|
19
23
|
// autoplaying a video loop at someone who asked for less motion.
|
|
@@ -21,6 +25,160 @@ window.__ModuleLoader__.load({
|
|
|
21
25
|
typeof window.matchMedia === "function" &&
|
|
22
26
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
23
27
|
|
|
28
|
+
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
29
|
+
const STRINGS = {
|
|
30
|
+
zh: {
|
|
31
|
+
settingsLabel: "壁纸背景 (Wallpaper Engine)",
|
|
32
|
+
scanning: "正在扫描 Wallpaper Engine…",
|
|
33
|
+
connError: "未能连接 Wallpaper Engine:",
|
|
34
|
+
retry: "重试",
|
|
35
|
+
refreshing: "刷新中…",
|
|
36
|
+
refresh: "刷新",
|
|
37
|
+
pause: "暂停",
|
|
38
|
+
play: "播放",
|
|
39
|
+
close: "关闭",
|
|
40
|
+
searchPlaceholder: "搜索壁纸(标题或 ID)…",
|
|
41
|
+
matched: (m, n) => "匹配 " + m + " / " + n + " 张",
|
|
42
|
+
emptyNoInstall: "未检测到 Wallpaper Engine — 请确认已通过 Steam 安装",
|
|
43
|
+
emptyNoUsable: (n) => "已发现 " + n + " 张壁纸,但都缺少可播放内容或预览图",
|
|
44
|
+
emptyNoProjects: "未在任何项目目录中发现壁纸(默认项目/我的项目/创意工坊)",
|
|
45
|
+
noMatch: (q) => "没有匹配「" + q + "」的壁纸",
|
|
46
|
+
closeCard: "✕ 关闭",
|
|
47
|
+
closeTitle: "关闭壁纸",
|
|
48
|
+
noPreview: "无预览",
|
|
49
|
+
badgeVideo: "视频",
|
|
50
|
+
badgeWeb: "网页",
|
|
51
|
+
badgeStill: "静态",
|
|
52
|
+
rotationList: "轮播列表",
|
|
53
|
+
selectList: "— 选择轮播列表 —",
|
|
54
|
+
noLists: "— 暂无轮播列表 —",
|
|
55
|
+
groupOption: (name, count, interval) => name + "(" + count + " 可用 · " + interval + " 分钟)",
|
|
56
|
+
newList: "新建",
|
|
57
|
+
editList: "编辑",
|
|
58
|
+
deleteList: "删除",
|
|
59
|
+
name: "名称",
|
|
60
|
+
interval: "间隔",
|
|
61
|
+
order: "顺序",
|
|
62
|
+
sequence: "顺序",
|
|
63
|
+
random: "随机",
|
|
64
|
+
minutes: (m) => m + " 分钟",
|
|
65
|
+
filterPlaceholder: "搜索以筛选…",
|
|
66
|
+
noUsable: "没有可用壁纸",
|
|
67
|
+
selectedCount: (n) => "已选 " + n + " 个",
|
|
68
|
+
importPlaylist: "从 WE 播放列表导入…",
|
|
69
|
+
playlistOption: (name, count) => name + "(" + count + " 可播放)",
|
|
70
|
+
save: "保存",
|
|
71
|
+
cancel: "取消",
|
|
72
|
+
autoRotate: "自动轮转",
|
|
73
|
+
intervalTitle: "当前列表的切换间隔",
|
|
74
|
+
needList: "请先选择或新建一个轮播列表",
|
|
75
|
+
needTwo: "当前列表至少需要 2 张可用壁纸",
|
|
76
|
+
wallpaperBlur: "壁纸模糊",
|
|
77
|
+
scrim: "暗化",
|
|
78
|
+
border: "边框",
|
|
79
|
+
glass: "玻璃",
|
|
80
|
+
pauseOnHidden: "页面隐藏时暂停",
|
|
81
|
+
pauseOnBattery: "低电量时暂停",
|
|
82
|
+
fpsTitle: "页面当前渲染帧率",
|
|
83
|
+
lowFpsHint: "帧率偏低,可尝试暂停视频、增大壁纸模糊或换用静态壁纸",
|
|
84
|
+
language: "语言",
|
|
85
|
+
langAuto: "自动",
|
|
86
|
+
statusGroup: (name, total, usable, interval, order) =>
|
|
87
|
+
"列表「" + name + "」:" + total + " 项 · " + usable + " 可用 · 每 " + interval + " 分钟 · " + order,
|
|
88
|
+
statusUsable: (n) => n + " 张可用壁纸",
|
|
89
|
+
autoRotating: "自动轮转中",
|
|
90
|
+
autoPaused: "已自动暂停",
|
|
91
|
+
confirmDelete: (name) => "删除轮播列表「" + name + "」?",
|
|
92
|
+
defaultGroupName: (n) => "轮播列表 " + n,
|
|
93
|
+
defaultGroupBase: "轮播列表",
|
|
94
|
+
},
|
|
95
|
+
en: {
|
|
96
|
+
settingsLabel: "Wallpaper Engine",
|
|
97
|
+
scanning: "Scanning Wallpaper Engine…",
|
|
98
|
+
connError: "Could not reach Wallpaper Engine: ",
|
|
99
|
+
retry: "Retry",
|
|
100
|
+
refreshing: "Refreshing…",
|
|
101
|
+
refresh: "Refresh",
|
|
102
|
+
pause: "Pause",
|
|
103
|
+
play: "Play",
|
|
104
|
+
close: "Close",
|
|
105
|
+
searchPlaceholder: "Search wallpapers (title or ID)…",
|
|
106
|
+
matched: (m, n) => m + " / " + n + " matched",
|
|
107
|
+
emptyNoInstall: "Wallpaper Engine not found — make sure it is installed via Steam",
|
|
108
|
+
emptyNoUsable: (n) => "Found " + n + " wallpapers, but none have playable media or a preview image",
|
|
109
|
+
emptyNoProjects: "No wallpapers found in any project folder (defaults / my projects / workshop)",
|
|
110
|
+
noMatch: (q) => "No wallpapers match \"" + q + "\"",
|
|
111
|
+
closeCard: "✕ Close",
|
|
112
|
+
closeTitle: "Clear wallpaper",
|
|
113
|
+
noPreview: "No preview",
|
|
114
|
+
badgeVideo: "Video",
|
|
115
|
+
badgeWeb: "Web",
|
|
116
|
+
badgeStill: "Still",
|
|
117
|
+
rotationList: "Rotation list",
|
|
118
|
+
selectList: "— Select a list —",
|
|
119
|
+
noLists: "— No lists yet —",
|
|
120
|
+
groupOption: (name, count, interval) => name + "(" + count + " usable · " + interval + " min)",
|
|
121
|
+
newList: "New",
|
|
122
|
+
editList: "Edit",
|
|
123
|
+
deleteList: "Delete",
|
|
124
|
+
name: "Name",
|
|
125
|
+
interval: "Interval",
|
|
126
|
+
order: "Order",
|
|
127
|
+
sequence: "Sequence",
|
|
128
|
+
random: "Random",
|
|
129
|
+
minutes: (m) => m + " min",
|
|
130
|
+
filterPlaceholder: "Filter…",
|
|
131
|
+
noUsable: "No usable wallpapers",
|
|
132
|
+
selectedCount: (n) => n + " selected",
|
|
133
|
+
importPlaylist: "Import from WE playlist…",
|
|
134
|
+
playlistOption: (name, count) => name + "(" + count + " playable)",
|
|
135
|
+
save: "Save",
|
|
136
|
+
cancel: "Cancel",
|
|
137
|
+
autoRotate: "Auto-rotate",
|
|
138
|
+
intervalTitle: "Switch interval for the current list",
|
|
139
|
+
needList: "Create or select a rotation list first",
|
|
140
|
+
needTwo: "The list needs at least 2 usable wallpapers",
|
|
141
|
+
wallpaperBlur: "Wallpaper blur",
|
|
142
|
+
scrim: "Scrim",
|
|
143
|
+
border: "Borders",
|
|
144
|
+
glass: "Glass",
|
|
145
|
+
pauseOnHidden: "Pause when tab hidden",
|
|
146
|
+
pauseOnBattery: "Pause on low battery",
|
|
147
|
+
fpsTitle: "Current page frame rate",
|
|
148
|
+
lowFpsHint: "Low frame rate — try pausing the video, raising wallpaper blur, or using a still wallpaper",
|
|
149
|
+
language: "Language",
|
|
150
|
+
langAuto: "Auto",
|
|
151
|
+
statusGroup: (name, total, usable, interval, order) =>
|
|
152
|
+
"List \"" + name + "\": " + total + " items · " + usable + " usable · every " + interval + " min · " + order,
|
|
153
|
+
statusUsable: (n) => n + " usable wallpapers",
|
|
154
|
+
autoRotating: "Auto-rotating",
|
|
155
|
+
autoPaused: "Auto-paused",
|
|
156
|
+
confirmDelete: (name) => "Delete rotation list \"" + name + "\"?",
|
|
157
|
+
defaultGroupName: (n) => "Rotation list " + n,
|
|
158
|
+
defaultGroupBase: "Rotation list",
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Language the DSH shell reports via its locale service (null = unknown). */
|
|
163
|
+
let shellLang = null;
|
|
164
|
+
|
|
165
|
+
function browserLang() {
|
|
166
|
+
if (typeof navigator !== "undefined" && navigator.language) {
|
|
167
|
+
return String(navigator.language).toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveLang() {
|
|
173
|
+
if (selection.lang === "zh" || selection.lang === "en") return selection.lang;
|
|
174
|
+
return shellLang || browserLang() || "zh";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Current string table. */
|
|
178
|
+
function S() {
|
|
179
|
+
return STRINGS[resolveLang()];
|
|
180
|
+
}
|
|
181
|
+
|
|
24
182
|
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
25
183
|
const DEFAULTS = {
|
|
26
184
|
scrim: 0.25,
|
|
@@ -32,6 +190,9 @@ window.__ModuleLoader__.load({
|
|
|
32
190
|
rotationGroupId: "",
|
|
33
191
|
rotationGroups: [],
|
|
34
192
|
rotationSeeded: false,
|
|
193
|
+
pauseOnHidden: true,
|
|
194
|
+
pauseOnBattery: true,
|
|
195
|
+
lang: "auto",
|
|
35
196
|
};
|
|
36
197
|
|
|
37
198
|
// ── Persisted selection ─────────────────────────────────────────────────────
|
|
@@ -39,11 +200,7 @@ window.__ModuleLoader__.load({
|
|
|
39
200
|
return typeof v === "number" && isFinite(v) && v >= lo && v <= hi ? v : fallback;
|
|
40
201
|
}
|
|
41
202
|
|
|
42
|
-
|
|
43
|
-
// picked from the inventory, its own switch interval (minutes) and order.
|
|
44
|
-
// Fully client-side (localStorage) — rotation never depends on Wallpaper
|
|
45
|
-
// Engine's own config.json playlist paths.
|
|
46
|
-
function readRotationGroups(raw) {
|
|
203
|
+
function readRotationGroups(raw, baseName) {
|
|
47
204
|
if (!Array.isArray(raw)) return [];
|
|
48
205
|
const groups = [];
|
|
49
206
|
for (const g of raw) {
|
|
@@ -52,7 +209,7 @@ window.__ModuleLoader__.load({
|
|
|
52
209
|
if (!id) continue;
|
|
53
210
|
groups.push({
|
|
54
211
|
id,
|
|
55
|
-
name: typeof g.name === "string" && g.name.trim() ? g.name.trim() :
|
|
212
|
+
name: typeof g.name === "string" && g.name.trim() ? g.name.trim() : baseName,
|
|
56
213
|
interval: clampNum(g.interval, 1, 1440, DEFAULTS.rotationInterval),
|
|
57
214
|
order: g.order === "random" ? "random" : "sequence",
|
|
58
215
|
wallpaperIds: Array.isArray(g.wallpaperIds)
|
|
@@ -76,8 +233,11 @@ window.__ModuleLoader__.load({
|
|
|
76
233
|
wallpaperBlur: clampNum(o.wallpaperBlur, 0, 60, DEFAULTS.wallpaperBlur),
|
|
77
234
|
rotationEnabled: o.rotationEnabled === true,
|
|
78
235
|
rotationGroupId: typeof o.rotationGroupId === "string" ? o.rotationGroupId : "",
|
|
79
|
-
rotationGroups: readRotationGroups(o.rotationGroups),
|
|
236
|
+
rotationGroups: readRotationGroups(o.rotationGroups, STRINGS.zh.defaultGroupBase),
|
|
80
237
|
rotationSeeded: o.rotationSeeded === true,
|
|
238
|
+
pauseOnHidden: o.pauseOnHidden !== false,
|
|
239
|
+
pauseOnBattery: o.pauseOnBattery !== false,
|
|
240
|
+
lang: o.lang === "zh" || o.lang === "en" ? o.lang : "auto",
|
|
81
241
|
};
|
|
82
242
|
} catch {
|
|
83
243
|
return { id: "", ...DEFAULTS };
|
|
@@ -92,6 +252,10 @@ window.__ModuleLoader__.load({
|
|
|
92
252
|
playing: !REDUCED_MOTION,
|
|
93
253
|
loading: false,
|
|
94
254
|
rotationTimer: null,
|
|
255
|
+
// Reasons playback is auto-paused (hidden tab / low battery). Distinct from
|
|
256
|
+
// the user's play/pause choice: effective playback = playing && no reasons.
|
|
257
|
+
autoPauseReasons: new Set(),
|
|
258
|
+
fps: null, // last sampled frames-per-second, null when unsampled
|
|
95
259
|
// Live draft of the group being created/edited (null when editor closed).
|
|
96
260
|
editing: null,
|
|
97
261
|
inventory: { installDir: null, wallpapers: [], total: 0, portableCount: 0, playlists: [], error: null },
|
|
@@ -120,10 +284,32 @@ window.__ModuleLoader__.load({
|
|
|
120
284
|
rotationGroupId: selection.rotationGroupId,
|
|
121
285
|
rotationGroups: selection.rotationGroups,
|
|
122
286
|
rotationSeeded: selection.rotationSeeded,
|
|
287
|
+
pauseOnHidden: selection.pauseOnHidden,
|
|
288
|
+
pauseOnBattery: selection.pauseOnBattery,
|
|
289
|
+
lang: selection.lang,
|
|
123
290
|
}));
|
|
124
291
|
} catch { /* storage full / blocked → session-only state, not fatal */ }
|
|
125
292
|
}
|
|
126
293
|
|
|
294
|
+
// ── Wallpaper predicates ─────────────────────────────────────────────────────
|
|
295
|
+
// playable: real media (video/web) renders live.
|
|
296
|
+
// static: scene/application with a preview image renders as a still.
|
|
297
|
+
function isPlayable(w) {
|
|
298
|
+
return Boolean(w && w.playable && (w.type === "video" || w.type === "web"));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isStatic(w) {
|
|
302
|
+
return Boolean(w && !isPlayable(w) && typeof w.preview === "string" && w.preview);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isSelectable(w) {
|
|
306
|
+
return isPlayable(w) || isStatic(w);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function selectableInventory() {
|
|
310
|
+
return selection.inventory.wallpapers.filter(isSelectable);
|
|
311
|
+
}
|
|
312
|
+
|
|
127
313
|
// ── Inventory ────────────────────────────────────────────────────────────────
|
|
128
314
|
async function loadInventory(forceRefresh) {
|
|
129
315
|
selection.loading = true;
|
|
@@ -173,8 +359,8 @@ window.__ModuleLoader__.load({
|
|
|
173
359
|
persistSelection();
|
|
174
360
|
}
|
|
175
361
|
|
|
176
|
-
// Drop a selection whose wallpaper vanished or
|
|
177
|
-
if (selection.id && !selection.inventory.wallpapers.some((w) => w.id === selection.id &&
|
|
362
|
+
// Drop a selection whose wallpaper vanished or became unusable.
|
|
363
|
+
if (selection.id && !selection.inventory.wallpapers.some((w) => w.id === selection.id && isSelectable(w))) {
|
|
178
364
|
selection.id = "";
|
|
179
365
|
persistSelection();
|
|
180
366
|
}
|
|
@@ -190,14 +376,6 @@ window.__ModuleLoader__.load({
|
|
|
190
376
|
emit();
|
|
191
377
|
}
|
|
192
378
|
|
|
193
|
-
function isPlayable(w) {
|
|
194
|
-
return Boolean(w && w.playable && (w.type === "video" || w.type === "web"));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function playableInventory() {
|
|
198
|
-
return selection.inventory.wallpapers.filter(isPlayable);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
379
|
// ── Rotation groups ─────────────────────────────────────────────────────────
|
|
202
380
|
function activeRotationGroup() {
|
|
203
381
|
return selection.rotationGroups.find((g) => g.id === selection.rotationGroupId) || null;
|
|
@@ -206,7 +384,7 @@ window.__ModuleLoader__.load({
|
|
|
206
384
|
function groupWallpapers(group) {
|
|
207
385
|
if (!group || !Array.isArray(group.wallpaperIds)) return [];
|
|
208
386
|
const byId = new Map(selection.inventory.wallpapers.map((w) => [w.id, w]));
|
|
209
|
-
return group.wallpaperIds.map((id) => byId.get(id)).filter(
|
|
387
|
+
return group.wallpaperIds.map((id) => byId.get(id)).filter(isSelectable);
|
|
210
388
|
}
|
|
211
389
|
|
|
212
390
|
function rotationCandidates() {
|
|
@@ -220,14 +398,14 @@ window.__ModuleLoader__.load({
|
|
|
220
398
|
function seedGroupsFromPlaylists() {
|
|
221
399
|
const source = selection.inventory.playlists.find((p) => (p.portableCount || 0) >= 2);
|
|
222
400
|
if (!source) return false;
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
401
|
+
const byId = new Map(selection.inventory.wallpapers.map((w) => [w.id, w]));
|
|
402
|
+
const ids = Array.isArray(source.wallpaperIds)
|
|
403
|
+
? source.wallpaperIds.filter((id) => isSelectable(byId.get(id)))
|
|
404
|
+
: [];
|
|
227
405
|
if (ids.length < 2) return false;
|
|
228
406
|
selection.rotationGroups.push({
|
|
229
407
|
id: nextGroupId(),
|
|
230
|
-
name: typeof source.name === "string" && source.name.trim() ? source.name.trim() :
|
|
408
|
+
name: typeof source.name === "string" && source.name.trim() ? source.name.trim() : S().defaultGroupBase,
|
|
231
409
|
interval: DEFAULTS.rotationInterval,
|
|
232
410
|
order: source.order === "random" ? "random" : "sequence",
|
|
233
411
|
wallpaperIds: ids,
|
|
@@ -286,7 +464,7 @@ window.__ModuleLoader__.load({
|
|
|
286
464
|
function startCreateGroup() {
|
|
287
465
|
selection.editing = {
|
|
288
466
|
id: nextGroupId(),
|
|
289
|
-
name:
|
|
467
|
+
name: S().defaultGroupName(selection.rotationGroups.length + 1),
|
|
290
468
|
interval: DEFAULTS.rotationInterval,
|
|
291
469
|
order: "sequence",
|
|
292
470
|
wallpaperIds: [],
|
|
@@ -300,7 +478,7 @@ window.__ModuleLoader__.load({
|
|
|
300
478
|
const idx = selection.rotationGroups.findIndex((g) => g.id === draft.id);
|
|
301
479
|
const cleaned = {
|
|
302
480
|
id: draft.id,
|
|
303
|
-
name: typeof draft.name === "string" && draft.name.trim() ? draft.name.trim() :
|
|
481
|
+
name: typeof draft.name === "string" && draft.name.trim() ? draft.name.trim() : S().defaultGroupBase,
|
|
304
482
|
interval: clampNum(draft.interval, 1, 1440, DEFAULTS.rotationInterval),
|
|
305
483
|
order: draft.order === "random" ? "random" : "sequence",
|
|
306
484
|
wallpaperIds: Array.isArray(draft.wallpaperIds)
|
|
@@ -346,8 +524,8 @@ window.__ModuleLoader__.load({
|
|
|
346
524
|
|
|
347
525
|
function importPlaylistIntoDraft(playlist) {
|
|
348
526
|
if (!selection.editing || !playlist || !Array.isArray(playlist.wallpaperIds)) return;
|
|
349
|
-
const
|
|
350
|
-
selection.editing.wallpaperIds = playlist.wallpaperIds.filter((id) =>
|
|
527
|
+
const usable = new Set(selectableInventory().map((w) => w.id));
|
|
528
|
+
selection.editing.wallpaperIds = playlist.wallpaperIds.filter((id) => usable.has(id));
|
|
351
529
|
emit();
|
|
352
530
|
}
|
|
353
531
|
|
|
@@ -357,32 +535,52 @@ window.__ModuleLoader__.load({
|
|
|
357
535
|
const w = selection.id
|
|
358
536
|
? selection.inventory.wallpapers.find((x) => x.id === selection.id)
|
|
359
537
|
: null;
|
|
360
|
-
if (
|
|
538
|
+
if (isPlayable(w)) {
|
|
539
|
+
selection.url = w.media;
|
|
540
|
+
selection.type = w.type; // "video" | "web"
|
|
541
|
+
} else if (w && isStatic(w)) {
|
|
542
|
+
selection.url = w.preview;
|
|
543
|
+
selection.type = "image";
|
|
544
|
+
} else {
|
|
361
545
|
selection.url = null;
|
|
362
546
|
selection.type = null;
|
|
363
|
-
} else {
|
|
364
|
-
selection.url = w.media;
|
|
365
|
-
selection.type = w.type;
|
|
366
547
|
}
|
|
367
548
|
syncRotationTimer();
|
|
368
549
|
emit();
|
|
369
550
|
}
|
|
370
551
|
|
|
552
|
+
// ── Auto-pause (resource monitor) ───────────────────────────────────────────
|
|
553
|
+
function setAutoPause(reason, on) {
|
|
554
|
+
const had = selection.autoPauseReasons.size > 0;
|
|
555
|
+
if (on) selection.autoPauseReasons.add(reason);
|
|
556
|
+
else selection.autoPauseReasons.delete(reason);
|
|
557
|
+
if ((selection.autoPauseReasons.size > 0) !== had) emit();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function effectivePlaying() {
|
|
561
|
+
return selection.playing && selection.autoPauseReasons.size === 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
371
564
|
// ── Behind-body layer: wallpaper + scrim, with crossfade ────────────────────
|
|
565
|
+
// Set by a media error handler to force a remount with fresh URLs; cleared
|
|
566
|
+
// once the remount happens.
|
|
567
|
+
let forceRemount = false;
|
|
568
|
+
let lastMediaRecoveryAt = 0;
|
|
569
|
+
|
|
372
570
|
function buildMedia(sel) {
|
|
373
|
-
|
|
374
|
-
? document.createElement("video")
|
|
375
|
-
: document.createElement("iframe");
|
|
571
|
+
let media;
|
|
376
572
|
if (sel.type === "video") {
|
|
573
|
+
media = document.createElement("video");
|
|
377
574
|
media.src = sel.url;
|
|
378
|
-
media.autoplay =
|
|
575
|
+
media.autoplay = effectivePlaying();
|
|
379
576
|
media.loop = true;
|
|
380
577
|
media.muted = true;
|
|
381
578
|
media.setAttribute("playsinline", "");
|
|
382
579
|
media.className = "webg-media";
|
|
383
|
-
} else {
|
|
384
|
-
//
|
|
385
|
-
//
|
|
580
|
+
} else if (sel.type === "web") {
|
|
581
|
+
// Sandboxed opaque origin: wallpaper JS runs but cannot touch DSH
|
|
582
|
+
// storage or APIs.
|
|
583
|
+
media = document.createElement("iframe");
|
|
386
584
|
media.src = sel.url;
|
|
387
585
|
media.setAttribute("sandbox", "allow-scripts");
|
|
388
586
|
media.setAttribute("referrerpolicy", "no-referrer");
|
|
@@ -390,24 +588,45 @@ window.__ModuleLoader__.load({
|
|
|
390
588
|
media.setAttribute("scrolling", "no");
|
|
391
589
|
media.setAttribute("title", "Wallpaper Engine web wallpaper");
|
|
392
590
|
media.className = "webg-media webg-iframe";
|
|
591
|
+
} else {
|
|
592
|
+
// Static wallpaper: the scene/application preview as a still image.
|
|
593
|
+
media = document.createElement("img");
|
|
594
|
+
media.src = sel.url;
|
|
595
|
+
media.alt = "";
|
|
596
|
+
media.draggable = false;
|
|
597
|
+
media.setAttribute("decoding", "async");
|
|
598
|
+
media.className = "webg-media";
|
|
393
599
|
}
|
|
600
|
+
// Self-heal: if the media fails to load (e.g. its token died on an
|
|
601
|
+
// inventory rebuild), refetch fresh URLs and remount — throttled to once
|
|
602
|
+
// per 10 s so a permanently-broken wallpaper cannot loop.
|
|
603
|
+
media.addEventListener("error", () => {
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
if (now - lastMediaRecoveryAt < 10000) return;
|
|
606
|
+
lastMediaRecoveryAt = now;
|
|
607
|
+
forceRemount = true;
|
|
608
|
+
loadInventory(true);
|
|
609
|
+
});
|
|
394
610
|
return media;
|
|
395
611
|
}
|
|
396
612
|
|
|
397
613
|
function syncLayers() {
|
|
398
614
|
const existing = document.getElementById(LAYER_ID);
|
|
615
|
+
const hasTimeout = typeof window !== "undefined" && typeof window.setTimeout === "function";
|
|
399
616
|
|
|
400
617
|
if (selection.url) {
|
|
401
|
-
|
|
618
|
+
// Key by wallpaper id, NOT by media URL: inventory rebuilds re-mint
|
|
619
|
+
// tokens, so a URL-keyed comparison would reload the playing video on
|
|
620
|
+
// every refresh. A genuinely dead URL is recovered via forceRemount.
|
|
621
|
+
const wantKey = selection.type + "|" + selection.id;
|
|
402
622
|
const gotKey = existing && existing.dataset.webgKey;
|
|
403
623
|
|
|
404
|
-
if (existing && gotKey === wantKey) {
|
|
624
|
+
if (existing && gotKey === wantKey && !forceRemount) {
|
|
405
625
|
// Same wallpaper — only play/pause may have changed.
|
|
406
626
|
} else {
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
// new layer for the play/pause step below.
|
|
627
|
+
forceRemount = false;
|
|
628
|
+
// Crossfade: the new layer fades in over the old one; the old layer
|
|
629
|
+
// surrenders the id FIRST so getElementById resolves to the new one.
|
|
411
630
|
if (existing) existing.removeAttribute("id");
|
|
412
631
|
const node = document.createElement("div");
|
|
413
632
|
node.id = LAYER_ID;
|
|
@@ -415,38 +634,43 @@ window.__ModuleLoader__.load({
|
|
|
415
634
|
node.dataset.webgKey = wantKey;
|
|
416
635
|
node.appendChild(buildMedia(selection));
|
|
417
636
|
document.body.appendChild(node);
|
|
418
|
-
|
|
419
|
-
void node.offsetHeight;
|
|
637
|
+
void node.offsetHeight; // style flush so the enter transition runs
|
|
420
638
|
node.classList.remove("webg-layer--enter");
|
|
421
639
|
if (existing) {
|
|
422
640
|
existing.classList.add("webg-layer--leave");
|
|
423
641
|
const stale = existing;
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
} else {
|
|
427
|
-
stale.remove();
|
|
428
|
-
}
|
|
642
|
+
if (hasTimeout) window.setTimeout(() => stale.remove(), CROSSFADE_MS + 120);
|
|
643
|
+
else stale.remove();
|
|
429
644
|
}
|
|
430
645
|
}
|
|
431
646
|
const node = document.getElementById(LAYER_ID);
|
|
432
647
|
const video = node && node.querySelector("video");
|
|
433
648
|
if (video) {
|
|
434
|
-
if (
|
|
649
|
+
if (effectivePlaying()) { try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch {} }
|
|
435
650
|
else video.pause();
|
|
436
651
|
}
|
|
437
652
|
} else if (existing) {
|
|
653
|
+
// Clearing: fade out, then tear down scrim + frame transparency when the
|
|
654
|
+
// fade completes — unless a new wallpaper was applied meanwhile.
|
|
655
|
+
existing.removeAttribute("id");
|
|
438
656
|
existing.classList.add("webg-layer--leave");
|
|
439
657
|
const stale = existing;
|
|
440
|
-
|
|
441
|
-
window.setTimeout(() => stale.remove(), CROSSFADE_MS + 120);
|
|
442
|
-
} else {
|
|
658
|
+
const finish = () => {
|
|
443
659
|
stale.remove();
|
|
444
|
-
|
|
660
|
+
if (!document.getElementById(LAYER_ID)) {
|
|
661
|
+
const s = document.getElementById(SCRIM_ID);
|
|
662
|
+
if (s) s.remove();
|
|
663
|
+
document.body.removeAttribute(ACTIVE_ATTR);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
if (hasTimeout) window.setTimeout(finish, CROSSFADE_MS + 120);
|
|
667
|
+
else finish();
|
|
445
668
|
}
|
|
446
669
|
|
|
447
|
-
// Scrim:
|
|
448
|
-
|
|
670
|
+
// Scrim: present while a wallpaper is active. On clear it outlives the
|
|
671
|
+
// layer until the fade-out finishes, so the veil fades WITH the wallpaper.
|
|
449
672
|
if (selection.url) {
|
|
673
|
+
const scrim = document.getElementById(SCRIM_ID);
|
|
450
674
|
if (!scrim) {
|
|
451
675
|
const s = document.createElement("div");
|
|
452
676
|
s.id = SCRIM_ID;
|
|
@@ -454,9 +678,6 @@ window.__ModuleLoader__.load({
|
|
|
454
678
|
document.body.appendChild(s);
|
|
455
679
|
}
|
|
456
680
|
document.body.setAttribute(ACTIVE_ATTR, "on");
|
|
457
|
-
} else {
|
|
458
|
-
if (scrim) scrim.remove();
|
|
459
|
-
document.body.removeAttribute(ACTIVE_ATTR);
|
|
460
681
|
}
|
|
461
682
|
}
|
|
462
683
|
|
|
@@ -512,30 +733,50 @@ window.__ModuleLoader__.load({
|
|
|
512
733
|
);
|
|
513
734
|
}
|
|
514
735
|
|
|
515
|
-
function
|
|
736
|
+
function typeBadge(w, t) {
|
|
737
|
+
if (w.type === "video") return t.badgeVideo;
|
|
738
|
+
if (w.type === "web") return t.badgeWeb;
|
|
739
|
+
return t.badgeStill;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function matchesQuery(w, q) {
|
|
743
|
+
if (!q) return true;
|
|
744
|
+
const needle = q.toLowerCase();
|
|
745
|
+
return (w.title || "").toLowerCase().includes(needle) || (w.id || "").toLowerCase().includes(needle);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function ThumbCard(w, selected, onClick, t) {
|
|
516
749
|
return React.createElement("button", {
|
|
517
750
|
key: w.id,
|
|
518
751
|
className: "webg-card" + (selected ? " webg-card--selected" : ""),
|
|
519
752
|
type: "button",
|
|
520
753
|
onClick: onClick,
|
|
521
|
-
title: w.title,
|
|
754
|
+
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
522
755
|
},
|
|
523
756
|
w.preview
|
|
524
757
|
? React.createElement("img", {
|
|
525
|
-
src: w.preview, alt: w.title, loading: "lazy",
|
|
758
|
+
src: w.preview, alt: w.title, loading: "lazy", decoding: "async",
|
|
526
759
|
onError: (e) => { e.target.style.display = "none"; },
|
|
527
760
|
})
|
|
528
|
-
: React.createElement("span", { className: "webg-card-placeholder" },
|
|
529
|
-
React.createElement("span", { className: "webg-card-type" }, w
|
|
761
|
+
: React.createElement("span", { className: "webg-card-placeholder" }, t.noPreview),
|
|
762
|
+
React.createElement("span", { className: "webg-card-type" }, typeBadge(w, t)),
|
|
530
763
|
React.createElement("span", { className: "webg-card-title" }, w.title),
|
|
531
764
|
);
|
|
532
765
|
}
|
|
533
766
|
|
|
534
767
|
function WallpaperPicker() {
|
|
535
768
|
const sel = useStore();
|
|
769
|
+
const [query, setQuery] = React.useState("");
|
|
770
|
+
const [editorQuery, setEditorQuery] = React.useState("");
|
|
771
|
+
const t = S();
|
|
536
772
|
const onTogglePlay = () => { selection.playing = !selection.playing; emit(); };
|
|
537
773
|
const onClear = () => applySelection("");
|
|
538
774
|
const onRefresh = () => loadInventory(true);
|
|
775
|
+
const onLangChange = (e) => {
|
|
776
|
+
selection.lang = e.target.value;
|
|
777
|
+
persistSelection();
|
|
778
|
+
emit();
|
|
779
|
+
};
|
|
539
780
|
const onGroupChange = (e) => {
|
|
540
781
|
selection.rotationGroupId = e.target.value;
|
|
541
782
|
if (selection.rotationEnabled) {
|
|
@@ -576,10 +817,22 @@ window.__ModuleLoader__.load({
|
|
|
576
817
|
const group = activeRotationGroup();
|
|
577
818
|
if (!group) return;
|
|
578
819
|
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
|
579
|
-
if (!window.confirm(
|
|
820
|
+
if (!window.confirm(t.confirmDelete(group.name))) return;
|
|
580
821
|
}
|
|
581
822
|
deleteGroup(group.id);
|
|
582
823
|
};
|
|
824
|
+
const onTogglePauseOnHidden = () => {
|
|
825
|
+
selection.pauseOnHidden = !selection.pauseOnHidden;
|
|
826
|
+
if (!selection.pauseOnHidden) setAutoPause("hidden", false);
|
|
827
|
+
persistSelection();
|
|
828
|
+
emit();
|
|
829
|
+
};
|
|
830
|
+
const onTogglePauseOnBattery = () => {
|
|
831
|
+
selection.pauseOnBattery = !selection.pauseOnBattery;
|
|
832
|
+
if (!selection.pauseOnBattery) setAutoPause("battery", false);
|
|
833
|
+
persistSelection();
|
|
834
|
+
emit();
|
|
835
|
+
};
|
|
583
836
|
|
|
584
837
|
const onScrim = (pct) => { selection.scrim = pct / 100; persistSelection(); applyEffects(); emit(); };
|
|
585
838
|
const onBorder = (pct) => { selection.border = pct / 100; persistSelection(); applyEffects(); emit(); };
|
|
@@ -588,87 +841,119 @@ window.__ModuleLoader__.load({
|
|
|
588
841
|
|
|
589
842
|
if (!sel.loaded) {
|
|
590
843
|
return React.createElement("div", { className: "webg-picker" },
|
|
591
|
-
React.createElement("span", { className: "webg-hint" },
|
|
844
|
+
React.createElement("span", { className: "webg-hint" }, t.scanning));
|
|
592
845
|
}
|
|
593
846
|
if (sel.inventory.error) {
|
|
594
847
|
return React.createElement("div", { className: "webg-picker" },
|
|
595
848
|
React.createElement("div", { className: "webg-error" },
|
|
596
|
-
|
|
849
|
+
t.connError + sel.inventory.error),
|
|
597
850
|
React.createElement("button", {
|
|
598
851
|
className: "webg-btn", type: "button", onClick: onRefresh, disabled: sel.loading,
|
|
599
|
-
}, sel.loading ?
|
|
852
|
+
}, sel.loading ? t.refreshing : t.retry));
|
|
600
853
|
}
|
|
601
854
|
|
|
602
|
-
const
|
|
855
|
+
const usableList = selectableInventory();
|
|
856
|
+
const q = query.trim();
|
|
857
|
+
const filteredList = usableList.filter((w) => matchesQuery(w, q));
|
|
603
858
|
const groups = sel.rotationGroups;
|
|
604
859
|
const group = activeRotationGroup();
|
|
605
860
|
const candidates = rotationCandidates();
|
|
606
|
-
const
|
|
861
|
+
const usableCount = candidates.length;
|
|
607
862
|
const editing = sel.editing;
|
|
863
|
+
const autoPaused = sel.autoPauseReasons.size > 0;
|
|
608
864
|
const INTERVALS = [1, 5, 10, 30, 60, 120];
|
|
609
865
|
|
|
610
866
|
return React.createElement("div", { className: "webg-picker" },
|
|
867
|
+
// ── Search + language ──
|
|
868
|
+
React.createElement("div", { className: "webg-row" },
|
|
869
|
+
React.createElement("input", {
|
|
870
|
+
className: "webg-text webg-search", type: "search",
|
|
871
|
+
placeholder: t.searchPlaceholder,
|
|
872
|
+
value: query,
|
|
873
|
+
onInput: (e) => setQuery(e.target.value),
|
|
874
|
+
}),
|
|
875
|
+
q && React.createElement("span", { className: "webg-hint" },
|
|
876
|
+
t.matched(filteredList.length, usableList.length)),
|
|
877
|
+
React.createElement("label", { className: "webg-lang" },
|
|
878
|
+
React.createElement("span", { className: "webg-hint" }, t.language),
|
|
879
|
+
React.createElement("select", {
|
|
880
|
+
className: "webg-select webg-lang-select",
|
|
881
|
+
value: sel.lang,
|
|
882
|
+
onChange: onLangChange,
|
|
883
|
+
},
|
|
884
|
+
React.createElement("option", { value: "auto" }, t.langAuto),
|
|
885
|
+
React.createElement("option", { value: "zh" }, "中文"),
|
|
886
|
+
React.createElement("option", { value: "en" }, "English"),
|
|
887
|
+
),
|
|
888
|
+
),
|
|
889
|
+
),
|
|
890
|
+
// ── Grid ──
|
|
611
891
|
React.createElement("div", { className: "webg-grid" },
|
|
612
892
|
React.createElement("button", {
|
|
613
893
|
className: "webg-card" + (sel.id ? "" : " webg-card--selected"),
|
|
614
894
|
type: "button",
|
|
615
895
|
onClick: onClear,
|
|
616
|
-
title:
|
|
896
|
+
title: t.closeTitle,
|
|
617
897
|
},
|
|
618
|
-
React.createElement("span", { className: "webg-card-close" },
|
|
898
|
+
React.createElement("span", { className: "webg-card-close" }, t.closeCard),
|
|
619
899
|
),
|
|
620
|
-
|
|
900
|
+
usableList.length === 0
|
|
621
901
|
? React.createElement("span", { className: "webg-hint" },
|
|
622
|
-
sel.inventory.
|
|
623
|
-
?
|
|
624
|
-
|
|
625
|
-
|
|
902
|
+
sel.inventory.installDir
|
|
903
|
+
? (sel.inventory.total > 0
|
|
904
|
+
? t.emptyNoUsable(sel.inventory.total)
|
|
905
|
+
: t.emptyNoProjects)
|
|
906
|
+
: t.emptyNoInstall)
|
|
907
|
+
: filteredList.length === 0
|
|
908
|
+
? React.createElement("span", { className: "webg-hint" }, t.noMatch(q))
|
|
909
|
+
: filteredList.map((w) => ThumbCard(w, w.id === sel.id, () => applySelection(w.id), t)),
|
|
626
910
|
),
|
|
911
|
+
// ── Actions ──
|
|
627
912
|
React.createElement("div", { className: "webg-row" },
|
|
628
913
|
React.createElement("button", {
|
|
629
914
|
className: "webg-btn", type: "button",
|
|
630
915
|
onClick: onTogglePlay, disabled: !sel.url || sel.type !== "video",
|
|
631
|
-
}, sel.playing ?
|
|
916
|
+
}, sel.playing ? t.pause : t.play),
|
|
632
917
|
React.createElement("button", {
|
|
633
918
|
className: "webg-btn", type: "button",
|
|
634
919
|
onClick: onClear, disabled: !sel.id,
|
|
635
|
-
},
|
|
920
|
+
}, t.close),
|
|
636
921
|
React.createElement("button", {
|
|
637
922
|
className: "webg-btn", type: "button",
|
|
638
923
|
onClick: onRefresh, disabled: sel.loading,
|
|
639
|
-
}, sel.loading ?
|
|
924
|
+
}, sel.loading ? t.refreshing : t.refresh),
|
|
640
925
|
),
|
|
641
926
|
// ── Rotation groups ──
|
|
642
927
|
React.createElement("div", { className: "webg-row webg-playlist-row" },
|
|
643
|
-
React.createElement("span", { className: "webg-hint webg-label" },
|
|
928
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.rotationList),
|
|
644
929
|
React.createElement("select", {
|
|
645
930
|
className: "webg-select webg-playlist-select",
|
|
646
931
|
value: sel.rotationGroupId,
|
|
647
932
|
onChange: onGroupChange,
|
|
648
933
|
disabled: groups.length === 0,
|
|
649
934
|
},
|
|
650
|
-
React.createElement("option", { value: "" }, groups.length ?
|
|
935
|
+
React.createElement("option", { value: "" }, groups.length ? t.selectList : t.noLists),
|
|
651
936
|
...groups.map((g) => React.createElement("option", {
|
|
652
937
|
key: g.id, value: g.id,
|
|
653
|
-
}, g.name
|
|
938
|
+
}, t.groupOption(g.name, groupWallpapers(g).length, g.interval))),
|
|
654
939
|
),
|
|
655
940
|
React.createElement("button", {
|
|
656
941
|
className: "webg-btn", type: "button", onClick: startCreateGroup,
|
|
657
|
-
},
|
|
942
|
+
}, t.newList),
|
|
658
943
|
React.createElement("button", {
|
|
659
944
|
className: "webg-btn", type: "button",
|
|
660
945
|
onClick: () => startEditGroup(sel.rotationGroupId),
|
|
661
946
|
disabled: !sel.rotationGroupId,
|
|
662
|
-
},
|
|
947
|
+
}, t.editList),
|
|
663
948
|
React.createElement("button", {
|
|
664
949
|
className: "webg-btn", type: "button",
|
|
665
950
|
onClick: onDeleteGroup,
|
|
666
951
|
disabled: !sel.rotationGroupId,
|
|
667
|
-
},
|
|
952
|
+
}, t.deleteList),
|
|
668
953
|
),
|
|
669
954
|
editing && React.createElement("div", { className: "webg-editor" },
|
|
670
955
|
React.createElement("div", { className: "webg-row" },
|
|
671
|
-
React.createElement("span", { className: "webg-hint webg-label" },
|
|
956
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.name),
|
|
672
957
|
React.createElement("input", {
|
|
673
958
|
className: "webg-text", type: "text",
|
|
674
959
|
value: editing.name,
|
|
@@ -676,54 +961,68 @@ window.__ModuleLoader__.load({
|
|
|
676
961
|
}),
|
|
677
962
|
),
|
|
678
963
|
React.createElement("div", { className: "webg-row" },
|
|
679
|
-
React.createElement("span", { className: "webg-hint webg-label" },
|
|
964
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.interval),
|
|
680
965
|
React.createElement("select", {
|
|
681
966
|
className: "webg-select",
|
|
682
967
|
value: String(editing.interval),
|
|
683
968
|
onChange: (e) => { editing.interval = clampNum(Number(e.target.value), 1, 1440, DEFAULTS.rotationInterval); emit(); },
|
|
684
969
|
},
|
|
685
970
|
...INTERVALS.map((minutes) =>
|
|
686
|
-
React.createElement("option", { key: minutes, value: String(minutes) }, minutes
|
|
971
|
+
React.createElement("option", { key: minutes, value: String(minutes) }, t.minutes(minutes)),
|
|
687
972
|
)),
|
|
688
|
-
React.createElement("span", { className: "webg-hint webg-label" },
|
|
973
|
+
React.createElement("span", { className: "webg-hint webg-label" }, t.order),
|
|
689
974
|
React.createElement("select", {
|
|
690
975
|
className: "webg-select webg-playlist-select",
|
|
691
976
|
value: editing.order,
|
|
692
977
|
onChange: (e) => { editing.order = e.target.value; emit(); },
|
|
693
978
|
},
|
|
694
|
-
React.createElement("option", { value: "sequence" },
|
|
695
|
-
React.createElement("option", { value: "random" },
|
|
979
|
+
React.createElement("option", { value: "sequence" }, t.sequence),
|
|
980
|
+
React.createElement("option", { value: "random" }, t.random),
|
|
696
981
|
),
|
|
697
982
|
),
|
|
983
|
+
React.createElement("input", {
|
|
984
|
+
className: "webg-text", type: "search",
|
|
985
|
+
placeholder: t.filterPlaceholder,
|
|
986
|
+
value: editorQuery,
|
|
987
|
+
onInput: (e) => setEditorQuery(e.target.value),
|
|
988
|
+
}),
|
|
698
989
|
React.createElement("div", { className: "webg-editor-grid" },
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
990
|
+
(() => {
|
|
991
|
+
const eq = editorQuery.trim();
|
|
992
|
+
const pool = usableList.filter((w) => matchesQuery(w, eq));
|
|
993
|
+
if (usableList.length === 0) {
|
|
994
|
+
return React.createElement("span", { className: "webg-hint" }, t.noUsable);
|
|
995
|
+
}
|
|
996
|
+
if (pool.length === 0) {
|
|
997
|
+
return React.createElement("span", { className: "webg-hint" }, t.noMatch(eq));
|
|
998
|
+
}
|
|
999
|
+
return pool.map((w) => {
|
|
1000
|
+
const checked = editing.wallpaperIds.indexOf(w.id) >= 0;
|
|
1001
|
+
return React.createElement("button", {
|
|
1002
|
+
key: w.id,
|
|
1003
|
+
className: "webg-editor-card" + (checked ? " webg-editor-card--checked" : ""),
|
|
1004
|
+
type: "button",
|
|
1005
|
+
title: w.title + "(" + typeBadge(w, t) + ")",
|
|
1006
|
+
onClick: () => {
|
|
1007
|
+
const i = editing.wallpaperIds.indexOf(w.id);
|
|
1008
|
+
if (i >= 0) editing.wallpaperIds.splice(i, 1);
|
|
1009
|
+
else editing.wallpaperIds.push(w.id);
|
|
1010
|
+
emit();
|
|
714
1011
|
},
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1012
|
+
},
|
|
1013
|
+
w.preview
|
|
1014
|
+
? React.createElement("img", {
|
|
1015
|
+
src: w.preview, alt: w.title, loading: "lazy", decoding: "async",
|
|
1016
|
+
onError: (e) => { e.target.style.display = "none"; },
|
|
1017
|
+
})
|
|
1018
|
+
: React.createElement("span", { className: "webg-card-placeholder" }, t.noPreview),
|
|
1019
|
+
checked && React.createElement("span", { className: "webg-editor-check" }, "✓"),
|
|
1020
|
+
);
|
|
1021
|
+
});
|
|
1022
|
+
})(),
|
|
724
1023
|
),
|
|
725
1024
|
React.createElement("div", { className: "webg-row" },
|
|
726
|
-
React.createElement("span", { className: "webg-hint" },
|
|
1025
|
+
React.createElement("span", { className: "webg-hint" }, t.selectedCount(editing.wallpaperIds.length)),
|
|
727
1026
|
sel.inventory.playlists.length > 0 && React.createElement("select", {
|
|
728
1027
|
className: "webg-select webg-playlist-select",
|
|
729
1028
|
value: "",
|
|
@@ -732,58 +1031,86 @@ window.__ModuleLoader__.load({
|
|
|
732
1031
|
if (p) importPlaylistIntoDraft(p);
|
|
733
1032
|
},
|
|
734
1033
|
},
|
|
735
|
-
React.createElement("option", { value: "" },
|
|
1034
|
+
React.createElement("option", { value: "" }, t.importPlaylist),
|
|
736
1035
|
...sel.inventory.playlists.map((p) => React.createElement("option", {
|
|
737
1036
|
key: p.id, value: p.id,
|
|
738
|
-
}, p.name
|
|
1037
|
+
}, t.playlistOption(p.name, p.portableCount || 0))),
|
|
739
1038
|
),
|
|
740
1039
|
),
|
|
741
1040
|
React.createElement("div", { className: "webg-row" },
|
|
742
1041
|
React.createElement("button", {
|
|
743
1042
|
className: "webg-btn webg-btn--primary", type: "button",
|
|
744
1043
|
onClick: saveEditingGroup,
|
|
745
|
-
},
|
|
1044
|
+
}, t.save),
|
|
746
1045
|
React.createElement("button", {
|
|
747
1046
|
className: "webg-btn", type: "button",
|
|
748
1047
|
onClick: cancelEditGroup,
|
|
749
|
-
},
|
|
1048
|
+
}, t.cancel),
|
|
750
1049
|
),
|
|
751
1050
|
),
|
|
752
1051
|
React.createElement("div", { className: "webg-row webg-rotation-row" },
|
|
753
|
-
React.createElement("label", { className: "webg-
|
|
1052
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
754
1053
|
React.createElement("input", {
|
|
755
1054
|
type: "checkbox",
|
|
756
1055
|
checked: sel.rotationEnabled,
|
|
757
1056
|
onChange: onToggleRotation,
|
|
758
|
-
disabled: !sel.rotationGroupId ||
|
|
1057
|
+
disabled: !sel.rotationGroupId || usableCount < 2,
|
|
759
1058
|
}),
|
|
760
|
-
|
|
1059
|
+
t.autoRotate,
|
|
761
1060
|
),
|
|
762
1061
|
React.createElement("select", {
|
|
763
1062
|
className: "webg-select webg-rotation-interval",
|
|
764
1063
|
value: String(group ? group.interval : DEFAULTS.rotationInterval),
|
|
765
1064
|
onChange: onGroupInterval,
|
|
766
|
-
disabled: !sel.rotationEnabled || !sel.rotationGroupId ||
|
|
767
|
-
title:
|
|
1065
|
+
disabled: !sel.rotationEnabled || !sel.rotationGroupId || usableCount < 2,
|
|
1066
|
+
title: t.intervalTitle,
|
|
768
1067
|
},
|
|
769
1068
|
...INTERVALS.map((minutes) =>
|
|
770
|
-
React.createElement("option", { key: minutes, value: String(minutes) }, minutes
|
|
1069
|
+
React.createElement("option", { key: minutes, value: String(minutes) }, t.minutes(minutes)),
|
|
771
1070
|
)),
|
|
772
|
-
!sel.rotationGroupId && React.createElement("span", { className: "webg-hint" },
|
|
773
|
-
sel.rotationGroupId &&
|
|
1071
|
+
!sel.rotationGroupId && React.createElement("span", { className: "webg-hint" }, t.needList),
|
|
1072
|
+
sel.rotationGroupId && usableCount < 2 && React.createElement("span", { className: "webg-hint" }, t.needTwo),
|
|
774
1073
|
),
|
|
1074
|
+
// ── Effects ──
|
|
775
1075
|
sel.id && React.createElement(React.Fragment, null,
|
|
776
|
-
SliderRow(
|
|
777
|
-
SliderRow(
|
|
778
|
-
SliderRow(
|
|
779
|
-
SliderRow(
|
|
1076
|
+
SliderRow(t.wallpaperBlur, 0, 60, 1, sel.wallpaperBlur, onWallpaperBlur, sel.wallpaperBlur + "px"),
|
|
1077
|
+
SliderRow(t.scrim, 0, 90, 5, Math.round(sel.scrim * 100), onScrim, Math.round(sel.scrim * 100) + "%"),
|
|
1078
|
+
SliderRow(t.border, 0, 90, 5, Math.round(sel.border * 100), onBorder, Math.round(sel.border * 100) + "%"),
|
|
1079
|
+
SliderRow(t.glass, 0, 40, 1, sel.blur, onBlur, sel.blur + "px"),
|
|
780
1080
|
),
|
|
1081
|
+
// ── Resource monitor ──
|
|
1082
|
+
sel.id && React.createElement("div", { className: "webg-row webg-monitor-row" },
|
|
1083
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
1084
|
+
React.createElement("input", {
|
|
1085
|
+
type: "checkbox", checked: sel.pauseOnHidden, onChange: onTogglePauseOnHidden,
|
|
1086
|
+
}),
|
|
1087
|
+
t.pauseOnHidden,
|
|
1088
|
+
),
|
|
1089
|
+
React.createElement("label", { className: "webg-toggle" },
|
|
1090
|
+
React.createElement("input", {
|
|
1091
|
+
type: "checkbox", checked: sel.pauseOnBattery, onChange: onTogglePauseOnBattery,
|
|
1092
|
+
}),
|
|
1093
|
+
t.pauseOnBattery,
|
|
1094
|
+
),
|
|
1095
|
+
sel.fps !== null && React.createElement("span", {
|
|
1096
|
+
className: "webg-hint webg-fps" + (sel.fps < LOW_FPS ? " webg-fps--low" : ""),
|
|
1097
|
+
title: t.fpsTitle,
|
|
1098
|
+
}, sel.fps + " fps"),
|
|
1099
|
+
),
|
|
1100
|
+
sel.id && sel.fps !== null && sel.fps < LOW_FPS && sel.type === "video" &&
|
|
1101
|
+
React.createElement("div", { className: "webg-row" },
|
|
1102
|
+
React.createElement("span", { className: "webg-hint" }, t.lowFpsHint),
|
|
1103
|
+
),
|
|
1104
|
+
// ── Status ──
|
|
781
1105
|
React.createElement("div", { className: "webg-row" },
|
|
782
1106
|
React.createElement("span", { className: "webg-hint" },
|
|
783
1107
|
(group
|
|
784
|
-
?
|
|
785
|
-
|
|
786
|
-
|
|
1108
|
+
? t.statusGroup(group.name, group.wallpaperIds.length, usableCount, group.interval,
|
|
1109
|
+
group.order === "random" ? t.random : t.sequence)
|
|
1110
|
+
: t.statusUsable(usableList.length)) +
|
|
1111
|
+
(sel.rotationEnabled ? " · " + t.autoRotating : "") +
|
|
1112
|
+
(autoPaused ? " · " + t.autoPaused : "") +
|
|
1113
|
+
(sel.inventory.installDir ? " · " + sel.inventory.installDir : "")),
|
|
787
1114
|
),
|
|
788
1115
|
);
|
|
789
1116
|
}
|
|
@@ -813,6 +1140,7 @@ window.__ModuleLoader__.load({
|
|
|
813
1140
|
position: fixed; inset: 0; z-index: -1;
|
|
814
1141
|
pointer-events: none;
|
|
815
1142
|
background: var(--webg-scrim-color, rgba(0, 0, 0, 0.25));
|
|
1143
|
+
transition: background 120ms linear;
|
|
816
1144
|
}
|
|
817
1145
|
|
|
818
1146
|
/* While active: transparent frame + sidebar, higher border contrast. */
|
|
@@ -904,13 +1232,23 @@ window.__ModuleLoader__.load({
|
|
|
904
1232
|
font-size: 0.85em;
|
|
905
1233
|
}
|
|
906
1234
|
.webg-text { flex: 1; min-width: 0; }
|
|
1235
|
+
.webg-search { max-width: 320px; }
|
|
1236
|
+
.webg-lang { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
|
|
907
1237
|
.webg-playlist-select { flex: 1; min-width: 0; }
|
|
908
1238
|
.webg-rotation-interval { margin-left: auto; }
|
|
909
|
-
.webg-
|
|
1239
|
+
.webg-toggle {
|
|
1240
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
1241
|
+
accent-color: var(--dsw-alias-brand-primary, #3964fe);
|
|
1242
|
+
font-size: 0.85em; cursor: pointer;
|
|
1243
|
+
}
|
|
910
1244
|
|
|
911
1245
|
.webg-slider { flex: 1; accent-color: var(--dsw-alias-brand-primary, #3964fe); }
|
|
912
1246
|
.webg-slider-row { display: flex; align-items: center; gap: 8px; }
|
|
913
1247
|
|
|
1248
|
+
.webg-monitor-row { gap: 14px; }
|
|
1249
|
+
.webg-fps { font-variant-numeric: tabular-nums; }
|
|
1250
|
+
.webg-fps--low { color: var(--dsw-alias-state-warn-primary, #b93); opacity: 1; }
|
|
1251
|
+
|
|
914
1252
|
/* Wallpaper thumbnail grid. */
|
|
915
1253
|
.webg-grid {
|
|
916
1254
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
@@ -994,8 +1332,26 @@ window.__ModuleLoader__.load({
|
|
|
994
1332
|
const inject = ["slots"];
|
|
995
1333
|
|
|
996
1334
|
function apply(ctx) {
|
|
997
|
-
//
|
|
998
|
-
//
|
|
1335
|
+
// 0. Language: follow the shell's locale service when present, and react
|
|
1336
|
+
// to its change events. Everything is optional — without the service
|
|
1337
|
+
// the browser language decides, and the manual override always wins.
|
|
1338
|
+
const localeSvc = typeof ctx.get === "function" ? ctx.get("locale") : undefined;
|
|
1339
|
+
if (localeSvc && typeof localeSvc.getLocale === "function") {
|
|
1340
|
+
try {
|
|
1341
|
+
const snap = localeSvc.getLocale();
|
|
1342
|
+
if (snap && snap.active) shellLang = snap.active === "en" ? "en" : "zh";
|
|
1343
|
+
} catch { /* keep browser fallback */ }
|
|
1344
|
+
}
|
|
1345
|
+
if (typeof ctx.on === "function") {
|
|
1346
|
+
try {
|
|
1347
|
+
ctx.on("locale/change", (snap) => {
|
|
1348
|
+
const next = snap && snap.active === "en" ? "en" : "zh";
|
|
1349
|
+
if (next !== shellLang) { shellLang = next; emit(); }
|
|
1350
|
+
});
|
|
1351
|
+
} catch { /* event bus unavailable → static language */ }
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// 1. Styles: owned by the fiber — injected on apply, REMOVED on dispose.
|
|
999
1355
|
if (ctx.effect && typeof document !== "undefined") {
|
|
1000
1356
|
ctx.effect(() => {
|
|
1001
1357
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(STYLE_TAG_ID) + "]");
|
|
@@ -1032,11 +1388,83 @@ window.__ModuleLoader__.load({
|
|
|
1032
1388
|
});
|
|
1033
1389
|
}
|
|
1034
1390
|
|
|
1035
|
-
// 3.
|
|
1391
|
+
// 3. Resource monitor: auto-pause on hidden tab / low battery, FPS meter.
|
|
1392
|
+
if (ctx.effect && typeof document !== "undefined" &&
|
|
1393
|
+
typeof document.addEventListener === "function") {
|
|
1394
|
+
ctx.effect(() => {
|
|
1395
|
+
const onVisibility = () => {
|
|
1396
|
+
if (!selection.pauseOnHidden) return;
|
|
1397
|
+
setAutoPause("hidden", document.visibilityState === "hidden");
|
|
1398
|
+
};
|
|
1399
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
1400
|
+
onVisibility();
|
|
1401
|
+
return () => {
|
|
1402
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
1403
|
+
setAutoPause("hidden", false);
|
|
1404
|
+
};
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
if (ctx.effect && typeof navigator !== "undefined" &&
|
|
1408
|
+
typeof navigator.getBattery === "function") {
|
|
1409
|
+
ctx.effect(() => {
|
|
1410
|
+
let battery = null;
|
|
1411
|
+
let cancelled = false;
|
|
1412
|
+
const update = () => {
|
|
1413
|
+
if (!battery || !selection.pauseOnBattery) return;
|
|
1414
|
+
setAutoPause("battery", !battery.charging && battery.level <= BATTERY_SAVER_LEVEL);
|
|
1415
|
+
};
|
|
1416
|
+
navigator.getBattery().then((b) => {
|
|
1417
|
+
if (cancelled) return;
|
|
1418
|
+
battery = b;
|
|
1419
|
+
b.addEventListener("chargingchange", update);
|
|
1420
|
+
b.addEventListener("levelchange", update);
|
|
1421
|
+
update();
|
|
1422
|
+
}).catch(() => {});
|
|
1423
|
+
return () => {
|
|
1424
|
+
cancelled = true;
|
|
1425
|
+
if (battery) {
|
|
1426
|
+
battery.removeEventListener("chargingchange", update);
|
|
1427
|
+
battery.removeEventListener("levelchange", update);
|
|
1428
|
+
}
|
|
1429
|
+
setAutoPause("battery", false);
|
|
1430
|
+
};
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
if (ctx.effect && typeof window !== "undefined" &&
|
|
1434
|
+
typeof window.requestAnimationFrame === "function") {
|
|
1435
|
+
ctx.effect(() => {
|
|
1436
|
+
// FPS sampler: counts rAF callbacks once per second. rAF stops by
|
|
1437
|
+
// itself in hidden tabs, so the meter idles exactly when the page does.
|
|
1438
|
+
let running = true;
|
|
1439
|
+
let rafId = 0;
|
|
1440
|
+
let frames = 0;
|
|
1441
|
+
let window0 = 0;
|
|
1442
|
+
const loop = (t) => {
|
|
1443
|
+
if (!running) return;
|
|
1444
|
+
frames++;
|
|
1445
|
+
if (window0 === 0) window0 = t;
|
|
1446
|
+
else if (t - window0 >= 1000) {
|
|
1447
|
+
selection.fps = Math.round((frames * 1000) / (t - window0));
|
|
1448
|
+
frames = 0;
|
|
1449
|
+
window0 = t;
|
|
1450
|
+
emit();
|
|
1451
|
+
}
|
|
1452
|
+
rafId = window.requestAnimationFrame(loop);
|
|
1453
|
+
};
|
|
1454
|
+
rafId = window.requestAnimationFrame(loop);
|
|
1455
|
+
return () => {
|
|
1456
|
+
running = false;
|
|
1457
|
+
if (typeof window.cancelAnimationFrame === "function") window.cancelAnimationFrame(rafId);
|
|
1458
|
+
selection.fps = null;
|
|
1459
|
+
};
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// 4. Settings row (this slot is NOT the overlay; safe to register into).
|
|
1036
1464
|
if (ctx.slots) {
|
|
1037
1465
|
ctx.slots.inject("settings.general.item", () =>
|
|
1038
1466
|
ctx.slots.register(
|
|
1039
|
-
{ name: "settings.general.item", id: "we-background", order: 500, label:
|
|
1467
|
+
{ name: "settings.general.item", id: "we-background", order: 500, label: S().settingsLabel },
|
|
1040
1468
|
() => React.createElement(WallpaperPicker),
|
|
1041
1469
|
),
|
|
1042
1470
|
);
|