dsh-background-by-model 0.5.5 → 0.6.0
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/CHANGELOG.md +17 -1
- package/CHANGELOG.zh.md +17 -1
- package/README.md +29 -10
- package/README.zh.md +29 -10
- package/holiday/mid-autumn.webp +0 -0
- package/holiday/national-day.webp +0 -0
- package/lib/client.js +392 -46
- package/lib/client.js.map +1 -1
- package/lib/index.js +180 -4
- package/package.json +7 -3
package/lib/client.js
CHANGED
|
@@ -21,7 +21,6 @@ window.__ModuleLoader__.load({
|
|
|
21
21
|
const zh = {
|
|
22
22
|
nav: "模型背景",
|
|
23
23
|
brandTag: "按模型换背景",
|
|
24
|
-
close: "关闭",
|
|
25
24
|
pageInterface: "界面",
|
|
26
25
|
pageModelBg: "模型背景",
|
|
27
26
|
pageProfile: "配置",
|
|
@@ -58,7 +57,6 @@ window.__ModuleLoader__.load({
|
|
|
58
57
|
autoExtractHint: "只在规则还没有颜色时自动取色,绝不覆盖你手动选的颜色",
|
|
59
58
|
ruleAdd: "新增规则",
|
|
60
59
|
ruleFallbackBadge: "兜底",
|
|
61
|
-
ruleMatchLabel: "匹配串",
|
|
62
60
|
ruleMatchPlaceholder: "如 flash / glm(忽略大小写,留空则不参与匹配)",
|
|
63
61
|
ruleEnabled: "启用",
|
|
64
62
|
ruleUp: "上移",
|
|
@@ -83,15 +81,10 @@ window.__ModuleLoader__.load({
|
|
|
83
81
|
ruleColorPickHint: "优先从本图取色,取不到就用默认色",
|
|
84
82
|
ruleColorExtract: "从本图提取",
|
|
85
83
|
ruleLayout: "布局模式",
|
|
86
|
-
ruleFraming: "取景",
|
|
87
84
|
ruleFramingEdit: "编辑位置",
|
|
88
|
-
ruleFramingLocked: "仅「适应」模式可编辑位置",
|
|
89
85
|
ruleOpacity: "背景透明度",
|
|
90
86
|
ruleBlur: "背景模糊",
|
|
91
87
|
ruleBlurHint: "背景模糊作用于壁纸层;界面页里的模糊作用于界面各区域,两者互不影响",
|
|
92
|
-
colorHint: "在色轮外圈选择色相,在内部方形中调整饱和度和明度;双击滑块可恢复默认值",
|
|
93
|
-
swatchTitle: "灵感色板",
|
|
94
|
-
hexCaption: "当前颜色",
|
|
95
88
|
bgModeFit: "适应",
|
|
96
89
|
bgModeFill: "填充",
|
|
97
90
|
bgModeStretch: "拉伸",
|
|
@@ -102,9 +95,7 @@ window.__ModuleLoader__.load({
|
|
|
102
95
|
editorCommit: "确认",
|
|
103
96
|
editorCancel: "取消",
|
|
104
97
|
editorReset: "重置",
|
|
105
|
-
extractColor: "从图片提取主题色",
|
|
106
98
|
extracting: "取色中…",
|
|
107
|
-
extractNoWp: "请先为该规则选择图片",
|
|
108
99
|
extractDone: "已应用图片配色",
|
|
109
100
|
extractFail: "未在这张图里找到鲜明的颜色,请换一张",
|
|
110
101
|
eyedropper: "从图片取色",
|
|
@@ -123,12 +114,13 @@ window.__ModuleLoader__.load({
|
|
|
123
114
|
toastExportDone: "配置文件已开始下载",
|
|
124
115
|
importDone: "已导入配置",
|
|
125
116
|
importFail: "导入失败,文件格式不正确",
|
|
126
|
-
footerTag: "按模型换背景插件"
|
|
117
|
+
footerTag: "按模型换背景插件",
|
|
118
|
+
holidayTitle: "节日特殊背景",
|
|
119
|
+
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)"
|
|
127
120
|
};
|
|
128
121
|
const en = {
|
|
129
122
|
nav: "Model background",
|
|
130
123
|
brandTag: "Per-model wallpaper",
|
|
131
|
-
close: "Close",
|
|
132
124
|
pageInterface: "Interface",
|
|
133
125
|
pageModelBg: "Model background",
|
|
134
126
|
pageProfile: "Profile",
|
|
@@ -165,7 +157,6 @@ window.__ModuleLoader__.load({
|
|
|
165
157
|
autoExtractHint: "Only fills a rule that has no color yet — a color you picked is never overwritten",
|
|
166
158
|
ruleAdd: "Add rule",
|
|
167
159
|
ruleFallbackBadge: "fallback",
|
|
168
|
-
ruleMatchLabel: "Match",
|
|
169
160
|
ruleMatchPlaceholder: "e.g. flash / glm (case-insensitive, empty never matches)",
|
|
170
161
|
ruleEnabled: "Enabled",
|
|
171
162
|
ruleUp: "Move up",
|
|
@@ -190,15 +181,10 @@ window.__ModuleLoader__.load({
|
|
|
190
181
|
ruleColorPickHint: "Extracts from this image when it can, otherwise uses a default color",
|
|
191
182
|
ruleColorExtract: "Extract from image",
|
|
192
183
|
ruleLayout: "Layout mode",
|
|
193
|
-
ruleFraming: "Framing",
|
|
194
184
|
ruleFramingEdit: "Edit position",
|
|
195
|
-
ruleFramingLocked: "Position editing is only available in Fit mode",
|
|
196
185
|
ruleOpacity: "Background opacity",
|
|
197
186
|
ruleBlur: "Background blur",
|
|
198
187
|
ruleBlurHint: "Background blur affects the wallpaper layer; the blur on the Interface page affects interface surfaces — they are independent",
|
|
199
|
-
colorHint: "Pick hue on the outer ring, adjust saturation & lightness in the square; double-click a slider to reset",
|
|
200
|
-
swatchTitle: "Quick swatches",
|
|
201
|
-
hexCaption: "Current color",
|
|
202
188
|
bgModeFit: "Fit",
|
|
203
189
|
bgModeFill: "Fill",
|
|
204
190
|
bgModeStretch: "Stretch",
|
|
@@ -209,9 +195,7 @@ window.__ModuleLoader__.load({
|
|
|
209
195
|
editorCommit: "Confirm",
|
|
210
196
|
editorCancel: "Cancel",
|
|
211
197
|
editorReset: "Reset",
|
|
212
|
-
extractColor: "Extract from image",
|
|
213
198
|
extracting: "Extracting…",
|
|
214
|
-
extractNoWp: "Choose an image for this rule first",
|
|
215
199
|
extractDone: "Image colors applied",
|
|
216
200
|
extractFail: "No vivid color found in this image, try another",
|
|
217
201
|
eyedropper: "Eyedropper",
|
|
@@ -230,10 +214,168 @@ window.__ModuleLoader__.load({
|
|
|
230
214
|
toastExportDone: "Export started",
|
|
231
215
|
importDone: "Profile imported",
|
|
232
216
|
importFail: "Import failed — invalid file",
|
|
233
|
-
footerTag: "Per-model wallpaper plugin"
|
|
217
|
+
footerTag: "Per-model wallpaper plugin",
|
|
218
|
+
holidayTitle: "Holiday backgrounds",
|
|
219
|
+
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)"
|
|
234
220
|
};
|
|
235
221
|
//#endregion
|
|
222
|
+
//#region src/holiday.ts
|
|
223
|
+
const HOLIDAYS = [{
|
|
224
|
+
id: "mid-autumn",
|
|
225
|
+
slot: "h-midautumn",
|
|
226
|
+
asset: "mid-autumn.webp",
|
|
227
|
+
color: "#384A77",
|
|
228
|
+
kind: "lunar"
|
|
229
|
+
}, {
|
|
230
|
+
id: "national-day",
|
|
231
|
+
slot: "h-nationalday",
|
|
232
|
+
asset: "national-day.webp",
|
|
233
|
+
color: "#FFF6EB",
|
|
234
|
+
kind: "gregorian"
|
|
235
|
+
}];
|
|
236
|
+
const TZ = "Asia/Shanghai";
|
|
237
|
+
let civilFormatter;
|
|
238
|
+
function civilFmt() {
|
|
239
|
+
if (civilFormatter === void 0) try {
|
|
240
|
+
civilFormatter = new Intl.DateTimeFormat("en-US", {
|
|
241
|
+
timeZone: TZ,
|
|
242
|
+
year: "numeric",
|
|
243
|
+
month: "numeric",
|
|
244
|
+
day: "numeric"
|
|
245
|
+
});
|
|
246
|
+
} catch {
|
|
247
|
+
civilFormatter = null;
|
|
248
|
+
}
|
|
249
|
+
return civilFormatter;
|
|
250
|
+
}
|
|
251
|
+
let lunarFormatter;
|
|
252
|
+
function lunarFmt() {
|
|
253
|
+
if (lunarFormatter === void 0) try {
|
|
254
|
+
lunarFormatter = new Intl.DateTimeFormat("en-u-ca-chinese", {
|
|
255
|
+
timeZone: TZ,
|
|
256
|
+
year: "numeric",
|
|
257
|
+
month: "numeric",
|
|
258
|
+
day: "numeric"
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
lunarFormatter = null;
|
|
262
|
+
}
|
|
263
|
+
return lunarFormatter;
|
|
264
|
+
}
|
|
265
|
+
function partsOf(fmt, date) {
|
|
266
|
+
try {
|
|
267
|
+
const out = {};
|
|
268
|
+
for (const p of fmt.formatToParts(date)) if (p.type !== "literal") out[p.type] = p.value;
|
|
269
|
+
return out;
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** The civil (Gregorian) date in Asia/Shanghai, or null when ICU cannot answer. */
|
|
275
|
+
function shanghaiDate(date) {
|
|
276
|
+
const fmt = civilFmt();
|
|
277
|
+
if (fmt === null) return null;
|
|
278
|
+
const p = partsOf(fmt, date);
|
|
279
|
+
if (p === null) return null;
|
|
280
|
+
const y = Number(p.year);
|
|
281
|
+
const m = Number(p.month);
|
|
282
|
+
const d = Number(p.day);
|
|
283
|
+
if (!isFinite(y) || !isFinite(m) || !isFinite(d)) return null;
|
|
284
|
+
return {
|
|
285
|
+
y,
|
|
286
|
+
m,
|
|
287
|
+
d
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The Chinese-calendar month and day in Asia/Shanghai.
|
|
292
|
+
*
|
|
293
|
+
* ICU marks a **leap** month by suffixing `bis` — a leap fourth month reads
|
|
294
|
+
* `4bis`, a leap eighth month would read `8bis`. 中秋 is the FIFTEENTH DAY OF THE
|
|
295
|
+
* REGULAR EIGHTH MONTH, so the exact `month === '8'` test below is what keeps a
|
|
296
|
+
* leap eighth month (which does occur, roughly once every few decades) from
|
|
297
|
+
* dragging the festival a month late.
|
|
298
|
+
*/
|
|
299
|
+
function lunarMonthDay(date) {
|
|
300
|
+
const fmt = lunarFmt();
|
|
301
|
+
if (fmt === null) return null;
|
|
302
|
+
const p = partsOf(fmt, date);
|
|
303
|
+
if (p === null || p.month === void 0 || p.day === void 0) return null;
|
|
304
|
+
return {
|
|
305
|
+
month: p.month,
|
|
306
|
+
day: p.day
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/** 农历八月十五, in the regular eighth month — i.e. 中秋 itself. */
|
|
310
|
+
function isMidAutumn(date = /* @__PURE__ */ new Date()) {
|
|
311
|
+
const lunar = lunarMonthDay(date);
|
|
312
|
+
return lunar !== null && lunar.month === "8" && lunar.day === "15";
|
|
313
|
+
}
|
|
314
|
+
/** 公历 10 月 1–7 日 — the National Day holiday period. */
|
|
315
|
+
function isNationalDay(date = /* @__PURE__ */ new Date()) {
|
|
316
|
+
const civil = shanghaiDate(date);
|
|
317
|
+
return civil !== null && civil.m === 10 && civil.d >= 1 && civil.d <= 7;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The holiday whose window contains `date`, or null.
|
|
321
|
+
*
|
|
322
|
+
* 中秋 wins the overlap: it is a single precise day, while National Day is a
|
|
323
|
+
* seven-day window, and the two do collide (2025-10-06 was both). On a tie the
|
|
324
|
+
* rarer, more specific answer is the better one.
|
|
325
|
+
*/
|
|
326
|
+
function activeHoliday(date = /* @__PURE__ */ new Date()) {
|
|
327
|
+
if (isMidAutumn(date)) return "mid-autumn";
|
|
328
|
+
if (isNationalDay(date)) return "national-day";
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The holiday the background must follow right now, or null.
|
|
333
|
+
*
|
|
334
|
+
* Four gates, every one of them deliberate:
|
|
335
|
+
*
|
|
336
|
+
* 1. the master switch — an override that discards the user's rule list must
|
|
337
|
+
* never happen without being asked for;
|
|
338
|
+
* 2. a built-in holiday whose window contains today;
|
|
339
|
+
* 3. that holiday's own switch;
|
|
340
|
+
* 4. actual bytes in its slot.
|
|
341
|
+
*
|
|
342
|
+
* The last one matters most, and it is the reason this is not a one-liner at the
|
|
343
|
+
* call site: a switched-on holiday with no image at all has to fall through to
|
|
344
|
+
* the model rules, not blank the wallpaper.
|
|
345
|
+
*
|
|
346
|
+
* It lives here, as a pure function, so the decision is covered by
|
|
347
|
+
* `scripts/holiday-check.ts` instead of hiding inside the plugin's apply closure
|
|
348
|
+
* where nothing can reach it.
|
|
349
|
+
*/
|
|
350
|
+
function pickHoliday(master, items, todayId, hasImage) {
|
|
351
|
+
if (!master || todayId === null) return null;
|
|
352
|
+
const item = items.find((i) => i.id === todayId);
|
|
353
|
+
if (item === void 0 || !item.enabled) return null;
|
|
354
|
+
return hasImage(item.slot) ? item : null;
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
236
357
|
//#region src/schema.ts
|
|
358
|
+
/**
|
|
359
|
+
* The persisted configuration shape — declared exactly ONCE, for both halves.
|
|
360
|
+
*
|
|
361
|
+
* This shape used to be written out twice: the node half owns persistence and
|
|
362
|
+
* sanitizes every write, the browser half owns the UI and its own reading of the
|
|
363
|
+
* same file. A field added to only one side was silently dropped by the other
|
|
364
|
+
* side's sanitizer — the matching slider stayed live in memory, `writeConfig`
|
|
365
|
+
* wrote a config without it, and the next load fell back to the default.
|
|
366
|
+
* `blurs.rightbar` / `rightbarOpacity` in 0.3.2 was exactly that bug, and the key
|
|
367
|
+
* lists were duplicated 6 times across the two halves, so it was due to happen.
|
|
368
|
+
*
|
|
369
|
+
* The types, the key lists, the defaults and the pure normalizers therefore live
|
|
370
|
+
* here and both halves import them: adding a field means editing this file, and
|
|
371
|
+
* the two halves cannot disagree about the shape. The host additionally logs one
|
|
372
|
+
* warning per unknown key while sanitizing a write (see `warnUnknownConfigKeys`
|
|
373
|
+
* in `src/index.ts`), so a field that reaches the node half without being
|
|
374
|
+
* declared here shows up in the host log instead of quietly disappearing.
|
|
375
|
+
*
|
|
376
|
+
* Nothing in this module may touch the DOM, Node or the Cordis context: it is
|
|
377
|
+
* imported by the browser bundle and by the node half alike.
|
|
378
|
+
*/
|
|
237
379
|
const DEFAULT_BG_STATE = {
|
|
238
380
|
zoom: 1,
|
|
239
381
|
x: 0,
|
|
@@ -300,6 +442,67 @@ window.__ModuleLoader__.load({
|
|
|
300
442
|
rightbar: 0,
|
|
301
443
|
input: 0
|
|
302
444
|
};
|
|
445
|
+
/**
|
|
446
|
+
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
447
|
+
* null when the string is not a hex color.
|
|
448
|
+
*
|
|
449
|
+
* A holiday names its color as hex because that is how a color is quoted, and a
|
|
450
|
+
* hand-converted triple is a magic number nothing downstream can check. The
|
|
451
|
+
* conversion lives here rather than in ./holiday because `[h, s, l]` is this
|
|
452
|
+
* module's own rule shape.
|
|
453
|
+
*/
|
|
454
|
+
function hexToHsl(hex) {
|
|
455
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
456
|
+
if (m === null) return null;
|
|
457
|
+
const n = parseInt(m[1], 16);
|
|
458
|
+
const r = (n >> 16 & 255) / 255;
|
|
459
|
+
const g = (n >> 8 & 255) / 255;
|
|
460
|
+
const b = (n & 255) / 255;
|
|
461
|
+
const max = Math.max(r, g, b);
|
|
462
|
+
const min = Math.min(r, g, b);
|
|
463
|
+
const l = (max + min) / 2;
|
|
464
|
+
const d = max - min;
|
|
465
|
+
if (d === 0) return [
|
|
466
|
+
0,
|
|
467
|
+
0,
|
|
468
|
+
l
|
|
469
|
+
];
|
|
470
|
+
const s = d / (1 - Math.abs(2 * l - 1));
|
|
471
|
+
let h;
|
|
472
|
+
if (max === r) h = 60 * ((g - b) / d % 6);
|
|
473
|
+
else if (max === g) h = 60 * ((b - r) / d + 2);
|
|
474
|
+
else h = 60 * ((r - g) / d + 4);
|
|
475
|
+
if (h < 0) h += 360;
|
|
476
|
+
return [
|
|
477
|
+
clamp$1(h, 0, 360, 0),
|
|
478
|
+
clamp01(s, 0),
|
|
479
|
+
clamp01(l, .5)
|
|
480
|
+
];
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* One holiday's appearance, untouched.
|
|
484
|
+
*
|
|
485
|
+
* Holidays default to `fill` rather than a rule's `fit`: these are full-bleed
|
|
486
|
+
* festival wallpapers, and `fit` would letterbox them on every aspect ratio the
|
|
487
|
+
* art was not cut for.
|
|
488
|
+
*/
|
|
489
|
+
function defaultHolidayRule(def) {
|
|
490
|
+
return {
|
|
491
|
+
id: def.id,
|
|
492
|
+
slot: def.slot,
|
|
493
|
+
match: "",
|
|
494
|
+
enabled: true,
|
|
495
|
+
color: hexToHsl(def.color),
|
|
496
|
+
bgMode: "fill",
|
|
497
|
+
wallpaperOpacity: 1,
|
|
498
|
+
blur: 0,
|
|
499
|
+
bgState: { ...DEFAULT_BG_STATE }
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** Every built-in holiday at its default, in `HOLIDAYS` order. */
|
|
503
|
+
function defaultHolidayRules() {
|
|
504
|
+
return HOLIDAYS.map(defaultHolidayRule);
|
|
505
|
+
}
|
|
303
506
|
/** A default config with independent nested objects (never hand out the shared ones). */
|
|
304
507
|
function freshThemeConfig() {
|
|
305
508
|
return {
|
|
@@ -310,7 +513,11 @@ window.__ModuleLoader__.load({
|
|
|
310
513
|
chatTextOpacity: 0,
|
|
311
514
|
trajectoryOpacity: 1,
|
|
312
515
|
rightbarOpacity: null,
|
|
313
|
-
autoExtract: true
|
|
516
|
+
autoExtract: true,
|
|
517
|
+
holidays: {
|
|
518
|
+
enabled: true,
|
|
519
|
+
items: defaultHolidayRules()
|
|
520
|
+
}
|
|
314
521
|
};
|
|
315
522
|
}
|
|
316
523
|
/** Slot names reach the filesystem, so they are strictly whitelisted. */
|
|
@@ -358,6 +565,41 @@ window.__ModuleLoader__.load({
|
|
|
358
565
|
bgState: normalizeBgState(r.bgState)
|
|
359
566
|
};
|
|
360
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Coerce one persisted holiday entry.
|
|
570
|
+
*
|
|
571
|
+
* `id`, `slot` and `color` come from the DEFINITION, never from disk: a holiday's
|
|
572
|
+
* slot is the only thing tying it to its bytes, and its theme color is a fixed
|
|
573
|
+
* part of what that holiday looks like — so a stale or hand-edited value would
|
|
574
|
+
* either orphan the wallpaper, point the holiday at a rule's image, or paint a
|
|
575
|
+
* festival in a color it does not have. Everything else is sanitized exactly like
|
|
576
|
+
* a rule's field.
|
|
577
|
+
*/
|
|
578
|
+
function normalizeHolidayRule(def, raw) {
|
|
579
|
+
const r = raw ?? {};
|
|
580
|
+
const base = defaultHolidayRule(def);
|
|
581
|
+
return {
|
|
582
|
+
...base,
|
|
583
|
+
enabled: r.enabled !== false,
|
|
584
|
+
bgMode: BG_MODES$1.includes(r.bgMode) ? r.bgMode : base.bgMode,
|
|
585
|
+
wallpaperOpacity: clamp01(r.wallpaperOpacity, base.wallpaperOpacity),
|
|
586
|
+
blur: clamp$1(r.blur, 0, 60, base.blur),
|
|
587
|
+
bgState: normalizeBgState(r.bgState)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/** Coerce the whole holiday block; unknown ids are dropped, missing ones defaulted. */
|
|
591
|
+
function normalizeHolidays(raw) {
|
|
592
|
+
const r = raw ?? {};
|
|
593
|
+
const stored = /* @__PURE__ */ new Map();
|
|
594
|
+
if (Array.isArray(r.items)) for (const item of r.items) {
|
|
595
|
+
const id = item?.id;
|
|
596
|
+
if (typeof id === "string") stored.set(id, item);
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
enabled: r.enabled !== false,
|
|
600
|
+
items: HOLIDAYS.map((def) => normalizeHolidayRule(def, stored.get(def.id)))
|
|
601
|
+
};
|
|
602
|
+
}
|
|
361
603
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
362
604
|
function normalizeConfig(raw) {
|
|
363
605
|
const r = raw ?? {};
|
|
@@ -376,7 +618,8 @@ window.__ModuleLoader__.load({
|
|
|
376
618
|
chatTextOpacity: clamp01(r.chatTextOpacity, 0),
|
|
377
619
|
trajectoryOpacity: clamp01(r.trajectoryOpacity, 1),
|
|
378
620
|
rightbarOpacity: typeof r.rightbarOpacity === "number" && isFinite(r.rightbarOpacity) ? clamp01(r.rightbarOpacity, 1) : null,
|
|
379
|
-
autoExtract: r.autoExtract !== false
|
|
621
|
+
autoExtract: r.autoExtract !== false,
|
|
622
|
+
holidays: normalizeHolidays(r.holidays)
|
|
380
623
|
};
|
|
381
624
|
}
|
|
382
625
|
//#endregion
|
|
@@ -484,12 +727,24 @@ window.__ModuleLoader__.load({
|
|
|
484
727
|
function setModelLabel(label) {
|
|
485
728
|
modelLabel = label;
|
|
486
729
|
}
|
|
730
|
+
/**
|
|
731
|
+
* One rule by id — from the USER's list first, then from the holiday overrides.
|
|
732
|
+
*
|
|
733
|
+
* Every render accessor (`rWp` / `rColor` / `rBgMode` / `rBgState` …) resolves
|
|
734
|
+
* through here, so teaching this one function about holidays is what makes an
|
|
735
|
+
* active holiday paint through the entire existing pipeline instead of needing a
|
|
736
|
+
* parallel one.
|
|
737
|
+
*/
|
|
487
738
|
function ruleById(id) {
|
|
488
|
-
return cfg.rules.find((r) => r.id === id) ?? null;
|
|
739
|
+
return cfg.rules.find((r) => r.id === id) ?? cfg.holidays.items.find((r) => r.id === id) ?? null;
|
|
489
740
|
}
|
|
490
741
|
function activeRule() {
|
|
491
742
|
return activeRuleId === null ? null : ruleById(activeRuleId);
|
|
492
743
|
}
|
|
744
|
+
/** One holiday override entry, or null for an unknown id. */
|
|
745
|
+
function holidayById(id) {
|
|
746
|
+
return cfg.holidays.items.find((r) => r.id === id) ?? null;
|
|
747
|
+
}
|
|
493
748
|
function newRule(id, slot) {
|
|
494
749
|
return {
|
|
495
750
|
id,
|
|
@@ -577,7 +832,7 @@ window.__ModuleLoader__.load({
|
|
|
577
832
|
/** Move a possibly-absent partial config into the shape the UI reads. */
|
|
578
833
|
function adoptConfig(raw) {
|
|
579
834
|
cfg = normalizeConfig(raw);
|
|
580
|
-
if (activeRuleId !== null &&
|
|
835
|
+
if (activeRuleId !== null && ruleById(activeRuleId) === null) {
|
|
581
836
|
activeRuleId = null;
|
|
582
837
|
activeMatched = false;
|
|
583
838
|
}
|
|
@@ -1300,6 +1555,11 @@ window.__ModuleLoader__.load({
|
|
|
1300
1555
|
.dab-check-where code{font-family:var(--dab-mono);font-size:10.5px;overflow-wrap:anywhere}
|
|
1301
1556
|
.dab-check-note{margin:12px 0 0;font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);position:relative;z-index:1}
|
|
1302
1557
|
|
|
1558
|
+
/* ── holiday override (Profile page) ─────────────────────────────────────── */
|
|
1559
|
+
/* Only a label: the whole feature is one switch, and dab-toggle already exists.
|
|
1560
|
+
The per-holiday cards that used to live here are gone by design. */
|
|
1561
|
+
.dab-holiday-label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
1562
|
+
|
|
1303
1563
|
/* ── keyframes ───────────────────────────────────────────────────────────── */
|
|
1304
1564
|
@keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
|
|
1305
1565
|
@keyframes dab-page-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
|
@@ -4523,6 +4783,8 @@ window.__ModuleLoader__.load({
|
|
|
4523
4783
|
function ProfilePage({ p, notify }) {
|
|
4524
4784
|
const { t, exportTheme, importTheme } = p;
|
|
4525
4785
|
const importRef = (0, react.useRef)(null);
|
|
4786
|
+
p.useStore((s) => s.rev + s.rulesRev);
|
|
4787
|
+
const holidays = cfg.holidays;
|
|
4526
4788
|
const onImport = async (file) => {
|
|
4527
4789
|
try {
|
|
4528
4790
|
const ok = await importTheme(file);
|
|
@@ -4550,11 +4812,31 @@ window.__ModuleLoader__.load({
|
|
|
4550
4812
|
})
|
|
4551
4813
|
]
|
|
4552
4814
|
}),
|
|
4815
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
4816
|
+
className: "dab-card dab-rise",
|
|
4817
|
+
style: { "--d": 1 },
|
|
4818
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4819
|
+
className: "dab-chip-row",
|
|
4820
|
+
style: { alignItems: "center" },
|
|
4821
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4822
|
+
type: "button",
|
|
4823
|
+
className: `dab-toggle${holidays.enabled ? " is-on" : ""}`,
|
|
4824
|
+
role: "switch",
|
|
4825
|
+
"aria-checked": holidays.enabled,
|
|
4826
|
+
title: t("holidayEnable"),
|
|
4827
|
+
onClick: () => p.setHolidaysEnabled(!holidays.enabled),
|
|
4828
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
4829
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4830
|
+
className: "dab-holiday-label",
|
|
4831
|
+
children: t("holidayTitle")
|
|
4832
|
+
})]
|
|
4833
|
+
})
|
|
4834
|
+
}),
|
|
4553
4835
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4554
|
-
className: "dab-profile-grid",
|
|
4836
|
+
className: "dab-profile-grid dab-rise",
|
|
4837
|
+
style: { "--d": 2 },
|
|
4555
4838
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4556
|
-
className: "dab-card dab-card-hover
|
|
4557
|
-
style: { "--d": 1 },
|
|
4839
|
+
className: "dab-card dab-card-hover",
|
|
4558
4840
|
children: [
|
|
4559
4841
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4560
4842
|
className: "dab-profile-ico",
|
|
@@ -4579,8 +4861,7 @@ window.__ModuleLoader__.load({
|
|
|
4579
4861
|
})
|
|
4580
4862
|
]
|
|
4581
4863
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4582
|
-
className: "dab-card dab-card-hover
|
|
4583
|
-
style: { "--d": 2 },
|
|
4864
|
+
className: "dab-card dab-card-hover",
|
|
4584
4865
|
children: [
|
|
4585
4866
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4586
4867
|
className: "dab-profile-ico",
|
|
@@ -4829,7 +5110,11 @@ window.__ModuleLoader__.load({
|
|
|
4829
5110
|
};
|
|
4830
5111
|
/** Resolve the active rule for the current model and repaint everything. */
|
|
4831
5112
|
const applyActive = () => {
|
|
4832
|
-
const
|
|
5113
|
+
const holiday = activeHolidayRule();
|
|
5114
|
+
const { rule, matched } = holiday !== null ? {
|
|
5115
|
+
rule: holiday,
|
|
5116
|
+
matched: false
|
|
5117
|
+
} : matchRule(cfg.rules, modelText);
|
|
4833
5118
|
setActive(rule === null ? null : rule.id, matched);
|
|
4834
5119
|
const color = rule === null ? null : rule.color;
|
|
4835
5120
|
if (skinTimer !== null) window.clearTimeout(skinTimer);
|
|
@@ -4844,6 +5129,14 @@ window.__ModuleLoader__.load({
|
|
|
4844
5129
|
applyWp();
|
|
4845
5130
|
sync();
|
|
4846
5131
|
};
|
|
5132
|
+
/**
|
|
5133
|
+
* The holiday the background must follow right now, or null.
|
|
5134
|
+
*
|
|
5135
|
+
* The decision itself is `pickHoliday` (pure, and covered by
|
|
5136
|
+
* `scripts/holiday-check.ts`); this only supplies the two things it cannot
|
|
5137
|
+
* know — today's holiday from the calendar, and whether the slot has bytes.
|
|
5138
|
+
*/
|
|
5139
|
+
const activeHolidayRule = () => pickHoliday(cfg.holidays.enabled, cfg.holidays.items, activeHoliday(), (slot) => imageOf(slot) !== null);
|
|
4847
5140
|
syncHostScheme();
|
|
4848
5141
|
applyWp();
|
|
4849
5142
|
sync();
|
|
@@ -4873,18 +5166,63 @@ window.__ModuleLoader__.load({
|
|
|
4873
5166
|
applyActive();
|
|
4874
5167
|
}
|
|
4875
5168
|
})();
|
|
5169
|
+
let holidayKey = activeHoliday() ?? "";
|
|
5170
|
+
const holidayTimer = window.setInterval(() => {
|
|
5171
|
+
const next = activeHoliday() ?? "";
|
|
5172
|
+
if (next === holidayKey) return;
|
|
5173
|
+
holidayKey = next;
|
|
5174
|
+
const slot = todayHolidaySlot();
|
|
5175
|
+
if (slot === null) applyActive();
|
|
5176
|
+
else loadSlot(slot).then(() => applyActive());
|
|
5177
|
+
}, 3e4);
|
|
5178
|
+
ctx.effect(() => () => {
|
|
5179
|
+
window.clearInterval(holidayTimer);
|
|
5180
|
+
}, "dsh-background-by-model: holiday rollover");
|
|
5181
|
+
/** Read one slot's bytes into the cache unless they are already there. */
|
|
5182
|
+
const loadSlot = async (slot) => {
|
|
5183
|
+
if (imageOf(slot) !== null) return;
|
|
5184
|
+
const url = await readImage(slot);
|
|
5185
|
+
if (url !== null) setImage(slot, url);
|
|
5186
|
+
};
|
|
5187
|
+
/**
|
|
5188
|
+
* The slot of the holiday that could paint TODAY, or null.
|
|
5189
|
+
*
|
|
5190
|
+
* Gated on the CALENDAR, not merely on the switch. Now that the override is on
|
|
5191
|
+
* by default and invisible, a profile that simply is not on a holiday must not
|
|
5192
|
+
* pull ~730 KB of bundled art (≈970 KB of base64 over this channel) at every
|
|
5193
|
+
* boot just because the feature is armed.
|
|
5194
|
+
*/
|
|
5195
|
+
const todayHolidaySlot = () => {
|
|
5196
|
+
if (!cfg.holidays.enabled) return null;
|
|
5197
|
+
const id = activeHoliday();
|
|
5198
|
+
if (id === null) return null;
|
|
5199
|
+
const item = holidayById(id);
|
|
5200
|
+
return item !== null && item.enabled ? item.slot : null;
|
|
5201
|
+
};
|
|
5202
|
+
/** Every slot that can paint: the stored ones plus every slot the config names. */
|
|
5203
|
+
const allSlots = (stored) => {
|
|
5204
|
+
const holiday = todayHolidaySlot();
|
|
5205
|
+
return Array.from(/* @__PURE__ */ new Set([
|
|
5206
|
+
...stored,
|
|
5207
|
+
...cfg.rules.map((r) => r.slot),
|
|
5208
|
+
...holiday === null ? [] : [holiday]
|
|
5209
|
+
]));
|
|
5210
|
+
};
|
|
5211
|
+
/**
|
|
5212
|
+
* The slot that paints before anything else: the holiday in force when there
|
|
5213
|
+
* is one, otherwise the slot the current model's rule uses. A holiday outranks
|
|
5214
|
+
* the rule here for the same reason it outranks it when painting.
|
|
5215
|
+
*/
|
|
5216
|
+
const prioritySlot = () => todayHolidaySlot() ?? matchRule(cfg.rules, modelText).rule?.slot ?? null;
|
|
4876
5217
|
(async () => {
|
|
4877
5218
|
const persisted = await loadPersisted();
|
|
4878
5219
|
if (persisted !== null) {
|
|
4879
5220
|
adoptConfig(persisted.config);
|
|
4880
|
-
const
|
|
4881
|
-
const
|
|
4882
|
-
const first = priority === null ? null : priority.slot;
|
|
5221
|
+
const first = prioritySlot();
|
|
5222
|
+
const slots = allSlots(persisted.slots);
|
|
4883
5223
|
const order = first === null ? slots : [first, ...slots.filter((s) => s !== first)];
|
|
4884
5224
|
for (const slot of order) {
|
|
4885
|
-
|
|
4886
|
-
const url = await readImage(slot);
|
|
4887
|
-
if (url !== null) setImage(slot, url);
|
|
5225
|
+
await loadSlot(slot);
|
|
4888
5226
|
if (slot === first) applyActive();
|
|
4889
5227
|
}
|
|
4890
5228
|
}
|
|
@@ -5089,11 +5427,19 @@ window.__ModuleLoader__.load({
|
|
|
5089
5427
|
saveConfig();
|
|
5090
5428
|
sync();
|
|
5091
5429
|
},
|
|
5430
|
+
setHolidaysEnabled: (v) => {
|
|
5431
|
+
cfg.holidays.enabled = v;
|
|
5432
|
+
persistConfig();
|
|
5433
|
+
const slot = todayHolidaySlot();
|
|
5434
|
+
if (v && slot !== null) loadSlot(slot).then(() => applyActive());
|
|
5435
|
+
applyActive();
|
|
5436
|
+
},
|
|
5092
5437
|
exportTheme: () => {
|
|
5093
5438
|
const images = {};
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5439
|
+
const slots = cfg.rules.map((r) => r.slot);
|
|
5440
|
+
for (const slot of slots) {
|
|
5441
|
+
const url = imageOf(slot);
|
|
5442
|
+
if (url !== null) images[slot] = url;
|
|
5097
5443
|
}
|
|
5098
5444
|
const payload = {
|
|
5099
5445
|
version: 3,
|
|
@@ -5118,15 +5464,15 @@ window.__ModuleLoader__.load({
|
|
|
5118
5464
|
adoptConfig(d.config);
|
|
5119
5465
|
const incoming = d.images ?? {};
|
|
5120
5466
|
const keep = /* @__PURE__ */ new Set();
|
|
5121
|
-
for (const
|
|
5122
|
-
keep.add(
|
|
5123
|
-
const raw = incoming[
|
|
5467
|
+
for (const slot of cfg.rules.map((r) => r.slot)) {
|
|
5468
|
+
keep.add(slot);
|
|
5469
|
+
const raw = incoming[slot];
|
|
5124
5470
|
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
5125
|
-
setImage(
|
|
5126
|
-
writeImage(
|
|
5471
|
+
setImage(slot, raw);
|
|
5472
|
+
writeImage(slot, raw);
|
|
5127
5473
|
} else {
|
|
5128
|
-
setImage(
|
|
5129
|
-
deleteImage(
|
|
5474
|
+
setImage(slot, null);
|
|
5475
|
+
deleteImage(slot);
|
|
5130
5476
|
}
|
|
5131
5477
|
}
|
|
5132
5478
|
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && /^[A-Za-z0-9_-]{1,32}$/.test(slot)) deleteImage(slot);
|