dsh-background-by-model 0.5.5 → 0.7.1
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 +48 -1
- package/CHANGELOG.zh.md +47 -1
- package/README.md +91 -23
- package/README.zh.md +92 -24
- package/holiday/mid-autumn.webp +0 -0
- package/holiday/national-day.webp +0 -0
- package/lib/client.js +1697 -199
- package/lib/client.js.map +1 -1
- package/lib/index.js +320 -15
- package/package.json +10 -4
package/lib/client.js
CHANGED
|
@@ -21,12 +21,11 @@ window.__ModuleLoader__.load({
|
|
|
21
21
|
const zh = {
|
|
22
22
|
nav: "模型背景",
|
|
23
23
|
brandTag: "按模型换背景",
|
|
24
|
-
close: "关闭",
|
|
25
24
|
pageInterface: "界面",
|
|
26
25
|
pageModelBg: "模型背景",
|
|
27
26
|
pageProfile: "配置",
|
|
28
27
|
descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感。这里的设置对所有模型生效",
|
|
29
|
-
descModelBg: "每条规则 = 一个匹配串 +
|
|
28
|
+
descModelBg: "每条规则 = 一个匹配串 + 一组背景图与外观。切换模型时,从上往下找第一条匹配串出现在模型名里的规则;都没中就使用第 1 条。一条规则可以放多张图,按你设的节奏轮换",
|
|
30
29
|
descProfile: "把当前全部规则连同图片导出备份,或从文件一键恢复",
|
|
31
30
|
uiTitle: "主界面",
|
|
32
31
|
uiOpacity: "透明度",
|
|
@@ -47,26 +46,24 @@ window.__ModuleLoader__.load({
|
|
|
47
46
|
statusNoteEmptySelection: "本会话的模型选择投影还没有值(切一次模型即可写入)",
|
|
48
47
|
rulesTitle: "规则列表",
|
|
49
48
|
rulesHint: "从上到下即为优先级。规则 1 同时兼任兜底:所有匹配串都没命中时使用它",
|
|
49
|
+
hostStaleHint: "检测到 DSH 主进程仍在运行旧版插件:新版配置格式它还读不懂,为避免丢规则,本次只改界面不写入配置。重启 DSH 后多图与轮换即可正常保存",
|
|
50
50
|
tryoutTitle: "匹配试跑",
|
|
51
51
|
tryoutPlaceholder: "输入模型名,如 deepseek-flash",
|
|
52
52
|
tryoutUseCurrent: "填入当前模型",
|
|
53
53
|
tryoutHit: "命中规则",
|
|
54
54
|
tryoutFallback: "无匹配 · 走兜底规则",
|
|
55
|
-
tryoutNone: "
|
|
55
|
+
tryoutNone: "没有可用规则(规则需要启用,并且有图片或主题色)",
|
|
56
56
|
tryoutEmpty: "输入模型名即可试跑",
|
|
57
57
|
autoExtract: "选图后自动取主题色",
|
|
58
|
-
autoExtractHint: "
|
|
58
|
+
autoExtractHint: "只在图片还没有自己的颜色时自动取色,绝不覆盖你手动选的颜色",
|
|
59
59
|
ruleAdd: "新增规则",
|
|
60
60
|
ruleFallbackBadge: "兜底",
|
|
61
|
-
ruleMatchLabel: "匹配串",
|
|
62
61
|
ruleMatchPlaceholder: "如 flash / glm(忽略大小写,留空则不参与匹配)",
|
|
63
62
|
ruleEnabled: "启用",
|
|
64
63
|
ruleUp: "上移",
|
|
65
64
|
ruleDown: "下移",
|
|
66
65
|
ruleRemove: "删除规则",
|
|
67
66
|
ruleNoImage: "尚未选择图片",
|
|
68
|
-
rulePickImage: "选择图片",
|
|
69
|
-
ruleChangeImage: "更换图片",
|
|
70
67
|
ruleFromUrl: "从网址",
|
|
71
68
|
ruleUrlPlaceholder: "粘贴图片网址 https://…",
|
|
72
69
|
ruleUrlApply: "应用",
|
|
@@ -75,23 +72,49 @@ window.__ModuleLoader__.load({
|
|
|
75
72
|
ruleUrlBadHttp: "仅支持 http/https 图片网址",
|
|
76
73
|
ruleUrlFail: "获取图片失败",
|
|
77
74
|
ruleImageRemove: "移除图片",
|
|
78
|
-
ruleEmptyHint: "
|
|
75
|
+
ruleEmptyHint: "这条规则还没有图片,也没有自己的主题色:没有可显示的内容,匹配和兜底都会跳过它",
|
|
76
|
+
ruleColorOnlyHint: "这条规则还没有图片:不会铺壁纸,界面只用这个主题色",
|
|
77
|
+
ruleImagePendingHint: "这张图片还没有取到内容,稍后会自己出现(也可以直接替换它)",
|
|
79
78
|
ruleColor: "主题色",
|
|
80
79
|
ruleColorNone: "跟随系统主题",
|
|
81
80
|
ruleColorNoneHint: "未设颜色时跟随系统主题:界面用宿主的配色,壁纸与各部位的透明度、模糊照旧生效",
|
|
82
81
|
ruleColorPick: "自定义颜色",
|
|
83
82
|
ruleColorPickHint: "优先从本图取色,取不到就用默认色",
|
|
83
|
+
ruleColorNoImageHint: "这条规则还没有图片,取不到色:选一个颜色可以只铺主题色,或先添加图片",
|
|
84
|
+
ruleColorPendingImageHint: "这张图片还没有取到内容,暂时取不了色:可以直接选一个颜色,或等它加载完",
|
|
84
85
|
ruleColorExtract: "从本图提取",
|
|
86
|
+
ruleColorTargetImage: "作用于选中的图片:",
|
|
87
|
+
ruleColorTargetRule: "作用于本规则(它还没有图片)",
|
|
85
88
|
ruleLayout: "布局模式",
|
|
86
|
-
ruleFraming: "取景",
|
|
87
89
|
ruleFramingEdit: "编辑位置",
|
|
88
|
-
ruleFramingLocked: "仅「适应」模式可编辑位置",
|
|
89
90
|
ruleOpacity: "背景透明度",
|
|
90
91
|
ruleBlur: "背景模糊",
|
|
91
92
|
ruleBlurHint: "背景模糊作用于壁纸层;界面页里的模糊作用于界面各区域,两者互不影响",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
rotTitle: "多图轮换",
|
|
94
|
+
rotEnable: "轮换这组图",
|
|
95
|
+
rotNeedTwo: "至少两张图才能轮换",
|
|
96
|
+
rotEvery: "每张停留",
|
|
97
|
+
rotCustom: "自定义秒数",
|
|
98
|
+
rotSeconds: "秒",
|
|
99
|
+
rotOrder: "换图顺序",
|
|
100
|
+
rotOrderSeq: "按顺序",
|
|
101
|
+
rotOrderShuffle: "随机",
|
|
102
|
+
rotOnSwitch: "切换模型时也换一张",
|
|
103
|
+
rotNext: "下一张",
|
|
104
|
+
rotNextInactive: "只有正在生效的规则能换画面上的图",
|
|
105
|
+
rotPos: "当前第",
|
|
106
|
+
rotShowing: "正在显示",
|
|
107
|
+
rotAddImage: "添加图片",
|
|
108
|
+
rotReplaceImage: "替换这张",
|
|
109
|
+
rotImageEarlier: "前移一张",
|
|
110
|
+
rotImageLater: "后移一张",
|
|
111
|
+
rotMakeFirst: "设为第一张",
|
|
112
|
+
rotEvery10s: "10 秒",
|
|
113
|
+
rotEvery30s: "30 秒",
|
|
114
|
+
rotEvery1m: "1 分钟",
|
|
115
|
+
rotEvery5m: "5 分钟",
|
|
116
|
+
rotEvery30m: "30 分钟",
|
|
117
|
+
rotEvery1h: "1 小时",
|
|
95
118
|
bgModeFit: "适应",
|
|
96
119
|
bgModeFill: "填充",
|
|
97
120
|
bgModeStretch: "拉伸",
|
|
@@ -102,9 +125,7 @@ window.__ModuleLoader__.load({
|
|
|
102
125
|
editorCommit: "确认",
|
|
103
126
|
editorCancel: "取消",
|
|
104
127
|
editorReset: "重置",
|
|
105
|
-
extractColor: "从图片提取主题色",
|
|
106
128
|
extracting: "取色中…",
|
|
107
|
-
extractNoWp: "请先为该规则选择图片",
|
|
108
129
|
extractDone: "已应用图片配色",
|
|
109
130
|
extractFail: "未在这张图里找到鲜明的颜色,请换一张",
|
|
110
131
|
eyedropper: "从图片取色",
|
|
@@ -123,17 +144,18 @@ window.__ModuleLoader__.load({
|
|
|
123
144
|
toastExportDone: "配置文件已开始下载",
|
|
124
145
|
importDone: "已导入配置",
|
|
125
146
|
importFail: "导入失败,文件格式不正确",
|
|
126
|
-
footerTag: "按模型换背景插件"
|
|
147
|
+
footerTag: "按模型换背景插件",
|
|
148
|
+
holidayTitle: "节日特殊背景",
|
|
149
|
+
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)"
|
|
127
150
|
};
|
|
128
151
|
const en = {
|
|
129
152
|
nav: "Model background",
|
|
130
153
|
brandTag: "Per-model wallpaper",
|
|
131
|
-
close: "Close",
|
|
132
154
|
pageInterface: "Interface",
|
|
133
155
|
pageModelBg: "Model background",
|
|
134
156
|
pageProfile: "Profile",
|
|
135
157
|
descInterface: "Tune opacity and blur per surface to build depth. These settings are global and apply to every model",
|
|
136
|
-
descModelBg: "Each rule is a match string plus one look. On a model switch the list is scanned top-down for the first match string contained in the model name; if nothing hits, rule 1 is used",
|
|
158
|
+
descModelBg: "Each rule is a match string plus a set of wallpapers and one look. On a model switch the list is scanned top-down for the first match string contained in the model name; if nothing hits, rule 1 is used. A rule can hold several images and rotate through them on a rhythm you set",
|
|
137
159
|
descProfile: "Back up every rule together with its image, or restore one from a file",
|
|
138
160
|
uiTitle: "Interface",
|
|
139
161
|
uiOpacity: "Opacity",
|
|
@@ -154,26 +176,24 @@ window.__ModuleLoader__.load({
|
|
|
154
176
|
statusNoteEmptySelection: "The model-selection projection exists but carries no value yet (selecting a model once writes it)",
|
|
155
177
|
rulesTitle: "Rules",
|
|
156
178
|
rulesHint: "Top to bottom is the priority. Rule 1 doubles as the fallback — it is used when no match string hits",
|
|
179
|
+
hostStaleHint: "The DSH host process is still running the older plugin, which cannot read the new config shape. To avoid dropping rules, this session changes the interface but holds every config write — restart DSH and multi-image plus rotation will save normally",
|
|
157
180
|
tryoutTitle: "Match test",
|
|
158
181
|
tryoutPlaceholder: "Type a model name, e.g. deepseek-flash",
|
|
159
182
|
tryoutUseCurrent: "Use current model",
|
|
160
183
|
tryoutHit: "matches rule",
|
|
161
184
|
tryoutFallback: "no match · fallback rule",
|
|
162
|
-
tryoutNone: "No usable rule (a rule needs to be enabled and
|
|
185
|
+
tryoutNone: "No usable rule (a rule needs to be enabled and carry an image or a theme color)",
|
|
163
186
|
tryoutEmpty: "Type a model name to test the list",
|
|
164
187
|
autoExtract: "Extract the theme color when an image is chosen",
|
|
165
|
-
autoExtractHint: "Only fills
|
|
188
|
+
autoExtractHint: "Only fills an image that has no color of its own yet — a color you picked is never overwritten",
|
|
166
189
|
ruleAdd: "Add rule",
|
|
167
190
|
ruleFallbackBadge: "fallback",
|
|
168
|
-
ruleMatchLabel: "Match",
|
|
169
191
|
ruleMatchPlaceholder: "e.g. flash / glm (case-insensitive, empty never matches)",
|
|
170
192
|
ruleEnabled: "Enabled",
|
|
171
193
|
ruleUp: "Move up",
|
|
172
194
|
ruleDown: "Move down",
|
|
173
195
|
ruleRemove: "Remove rule",
|
|
174
196
|
ruleNoImage: "No image yet",
|
|
175
|
-
rulePickImage: "Choose image",
|
|
176
|
-
ruleChangeImage: "Replace image",
|
|
177
197
|
ruleFromUrl: "From URL",
|
|
178
198
|
ruleUrlPlaceholder: "Paste an image URL https://…",
|
|
179
199
|
ruleUrlApply: "Apply",
|
|
@@ -182,23 +202,49 @@ window.__ModuleLoader__.load({
|
|
|
182
202
|
ruleUrlBadHttp: "Only http/https image URLs are supported",
|
|
183
203
|
ruleUrlFail: "Could not fetch the image",
|
|
184
204
|
ruleImageRemove: "Remove image",
|
|
185
|
-
ruleEmptyHint: "This rule has no image
|
|
205
|
+
ruleEmptyHint: "This rule has no image and no theme color of its own: it has nothing to show, so matching and the fallback both skip it",
|
|
206
|
+
ruleColorOnlyHint: "No image in this rule yet: no wallpaper is shown, and the interface is painted from this theme color alone",
|
|
207
|
+
ruleImagePendingHint: "This image has no content yet — it appears once it loads; you can also replace it",
|
|
186
208
|
ruleColor: "Theme color",
|
|
187
209
|
ruleColorNone: "Follow system theme",
|
|
188
210
|
ruleColorNoneHint: "Without a color this rule follows the system theme: the host palette paints the interface while the wallpaper and every opacity/blur slider keep working",
|
|
189
211
|
ruleColorPick: "Custom color",
|
|
190
212
|
ruleColorPickHint: "Extracts from this image when it can, otherwise uses a default color",
|
|
213
|
+
ruleColorNoImageHint: "This rule has no image to take a color from: pick a color to tint the interface alone, or add an image first",
|
|
214
|
+
ruleColorPendingImageHint: "This image has no content yet, so there is nothing to extract from: pick a color directly, or wait for it to load",
|
|
215
|
+
ruleColorTargetImage: "Applies to the selected image:",
|
|
216
|
+
ruleColorTargetRule: "Applies to this rule (it has no image yet)",
|
|
191
217
|
ruleColorExtract: "Extract from image",
|
|
192
218
|
ruleLayout: "Layout mode",
|
|
193
|
-
ruleFraming: "Framing",
|
|
194
219
|
ruleFramingEdit: "Edit position",
|
|
195
|
-
ruleFramingLocked: "Position editing is only available in Fit mode",
|
|
196
220
|
ruleOpacity: "Background opacity",
|
|
197
221
|
ruleBlur: "Background blur",
|
|
198
222
|
ruleBlurHint: "Background blur affects the wallpaper layer; the blur on the Interface page affects interface surfaces — they are independent",
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
223
|
+
rotTitle: "Image rotation",
|
|
224
|
+
rotEnable: "Rotate these images",
|
|
225
|
+
rotNeedTwo: "At least two images are needed to rotate",
|
|
226
|
+
rotEvery: "Dwell time per image",
|
|
227
|
+
rotCustom: "Custom seconds",
|
|
228
|
+
rotSeconds: "s",
|
|
229
|
+
rotOrder: "Order",
|
|
230
|
+
rotOrderSeq: "In order",
|
|
231
|
+
rotOrderShuffle: "Shuffle",
|
|
232
|
+
rotOnSwitch: "Also step on a model switch",
|
|
233
|
+
rotNext: "Next image",
|
|
234
|
+
rotNextInactive: "Only the active rule can step the background",
|
|
235
|
+
rotPos: "Image",
|
|
236
|
+
rotShowing: "On screen",
|
|
237
|
+
rotAddImage: "Add images",
|
|
238
|
+
rotReplaceImage: "Replace this image",
|
|
239
|
+
rotImageEarlier: "Move earlier",
|
|
240
|
+
rotImageLater: "Move later",
|
|
241
|
+
rotMakeFirst: "Make first",
|
|
242
|
+
rotEvery10s: "10 s",
|
|
243
|
+
rotEvery30s: "30 s",
|
|
244
|
+
rotEvery1m: "1 min",
|
|
245
|
+
rotEvery5m: "5 min",
|
|
246
|
+
rotEvery30m: "30 min",
|
|
247
|
+
rotEvery1h: "1 h",
|
|
202
248
|
bgModeFit: "Fit",
|
|
203
249
|
bgModeFill: "Fill",
|
|
204
250
|
bgModeStretch: "Stretch",
|
|
@@ -209,9 +255,7 @@ window.__ModuleLoader__.load({
|
|
|
209
255
|
editorCommit: "Confirm",
|
|
210
256
|
editorCancel: "Cancel",
|
|
211
257
|
editorReset: "Reset",
|
|
212
|
-
extractColor: "Extract from image",
|
|
213
258
|
extracting: "Extracting…",
|
|
214
|
-
extractNoWp: "Choose an image for this rule first",
|
|
215
259
|
extractDone: "Image colors applied",
|
|
216
260
|
extractFail: "No vivid color found in this image, try another",
|
|
217
261
|
eyedropper: "Eyedropper",
|
|
@@ -230,10 +274,168 @@ window.__ModuleLoader__.load({
|
|
|
230
274
|
toastExportDone: "Export started",
|
|
231
275
|
importDone: "Profile imported",
|
|
232
276
|
importFail: "Import failed — invalid file",
|
|
233
|
-
footerTag: "Per-model wallpaper plugin"
|
|
277
|
+
footerTag: "Per-model wallpaper plugin",
|
|
278
|
+
holidayTitle: "Holiday backgrounds",
|
|
279
|
+
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)"
|
|
234
280
|
};
|
|
235
281
|
//#endregion
|
|
282
|
+
//#region src/holiday.ts
|
|
283
|
+
const HOLIDAYS = [{
|
|
284
|
+
id: "mid-autumn",
|
|
285
|
+
slot: "h-midautumn",
|
|
286
|
+
asset: "mid-autumn.webp",
|
|
287
|
+
color: "#384A77",
|
|
288
|
+
kind: "lunar"
|
|
289
|
+
}, {
|
|
290
|
+
id: "national-day",
|
|
291
|
+
slot: "h-nationalday",
|
|
292
|
+
asset: "national-day.webp",
|
|
293
|
+
color: "#FFF6EB",
|
|
294
|
+
kind: "gregorian"
|
|
295
|
+
}];
|
|
296
|
+
const TZ = "Asia/Shanghai";
|
|
297
|
+
let civilFormatter;
|
|
298
|
+
function civilFmt() {
|
|
299
|
+
if (civilFormatter === void 0) try {
|
|
300
|
+
civilFormatter = new Intl.DateTimeFormat("en-US", {
|
|
301
|
+
timeZone: TZ,
|
|
302
|
+
year: "numeric",
|
|
303
|
+
month: "numeric",
|
|
304
|
+
day: "numeric"
|
|
305
|
+
});
|
|
306
|
+
} catch {
|
|
307
|
+
civilFormatter = null;
|
|
308
|
+
}
|
|
309
|
+
return civilFormatter;
|
|
310
|
+
}
|
|
311
|
+
let lunarFormatter;
|
|
312
|
+
function lunarFmt() {
|
|
313
|
+
if (lunarFormatter === void 0) try {
|
|
314
|
+
lunarFormatter = new Intl.DateTimeFormat("en-u-ca-chinese", {
|
|
315
|
+
timeZone: TZ,
|
|
316
|
+
year: "numeric",
|
|
317
|
+
month: "numeric",
|
|
318
|
+
day: "numeric"
|
|
319
|
+
});
|
|
320
|
+
} catch {
|
|
321
|
+
lunarFormatter = null;
|
|
322
|
+
}
|
|
323
|
+
return lunarFormatter;
|
|
324
|
+
}
|
|
325
|
+
function partsOf(fmt, date) {
|
|
326
|
+
try {
|
|
327
|
+
const out = {};
|
|
328
|
+
for (const p of fmt.formatToParts(date)) if (p.type !== "literal") out[p.type] = p.value;
|
|
329
|
+
return out;
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** The civil (Gregorian) date in Asia/Shanghai, or null when ICU cannot answer. */
|
|
335
|
+
function shanghaiDate(date) {
|
|
336
|
+
const fmt = civilFmt();
|
|
337
|
+
if (fmt === null) return null;
|
|
338
|
+
const p = partsOf(fmt, date);
|
|
339
|
+
if (p === null) return null;
|
|
340
|
+
const y = Number(p.year);
|
|
341
|
+
const m = Number(p.month);
|
|
342
|
+
const d = Number(p.day);
|
|
343
|
+
if (!isFinite(y) || !isFinite(m) || !isFinite(d)) return null;
|
|
344
|
+
return {
|
|
345
|
+
y,
|
|
346
|
+
m,
|
|
347
|
+
d
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The Chinese-calendar month and day in Asia/Shanghai.
|
|
352
|
+
*
|
|
353
|
+
* ICU marks a **leap** month by suffixing `bis` — a leap fourth month reads
|
|
354
|
+
* `4bis`, a leap eighth month would read `8bis`. 中秋 is the FIFTEENTH DAY OF THE
|
|
355
|
+
* REGULAR EIGHTH MONTH, so the exact `month === '8'` test below is what keeps a
|
|
356
|
+
* leap eighth month (which does occur, roughly once every few decades) from
|
|
357
|
+
* dragging the festival a month late.
|
|
358
|
+
*/
|
|
359
|
+
function lunarMonthDay(date) {
|
|
360
|
+
const fmt = lunarFmt();
|
|
361
|
+
if (fmt === null) return null;
|
|
362
|
+
const p = partsOf(fmt, date);
|
|
363
|
+
if (p === null || p.month === void 0 || p.day === void 0) return null;
|
|
364
|
+
return {
|
|
365
|
+
month: p.month,
|
|
366
|
+
day: p.day
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** 农历八月十五, in the regular eighth month — i.e. 中秋 itself. */
|
|
370
|
+
function isMidAutumn(date = /* @__PURE__ */ new Date()) {
|
|
371
|
+
const lunar = lunarMonthDay(date);
|
|
372
|
+
return lunar !== null && lunar.month === "8" && lunar.day === "15";
|
|
373
|
+
}
|
|
374
|
+
/** 公历 10 月 1–7 日 — the National Day holiday period. */
|
|
375
|
+
function isNationalDay(date = /* @__PURE__ */ new Date()) {
|
|
376
|
+
const civil = shanghaiDate(date);
|
|
377
|
+
return civil !== null && civil.m === 10 && civil.d >= 1 && civil.d <= 7;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The holiday whose window contains `date`, or null.
|
|
381
|
+
*
|
|
382
|
+
* 中秋 wins the overlap: it is a single precise day, while National Day is a
|
|
383
|
+
* seven-day window, and the two do collide (2025-10-06 was both). On a tie the
|
|
384
|
+
* rarer, more specific answer is the better one.
|
|
385
|
+
*/
|
|
386
|
+
function activeHoliday(date = /* @__PURE__ */ new Date()) {
|
|
387
|
+
if (isMidAutumn(date)) return "mid-autumn";
|
|
388
|
+
if (isNationalDay(date)) return "national-day";
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The holiday the background must follow right now, or null.
|
|
393
|
+
*
|
|
394
|
+
* Four gates, every one of them deliberate:
|
|
395
|
+
*
|
|
396
|
+
* 1. the master switch — an override that discards the user's rule list must
|
|
397
|
+
* never happen without being asked for;
|
|
398
|
+
* 2. a built-in holiday whose window contains today;
|
|
399
|
+
* 3. that holiday's own switch;
|
|
400
|
+
* 4. actual bytes in its slot.
|
|
401
|
+
*
|
|
402
|
+
* The last one matters most, and it is the reason this is not a one-liner at the
|
|
403
|
+
* call site: a switched-on holiday with no image at all has to fall through to
|
|
404
|
+
* the model rules, not blank the wallpaper.
|
|
405
|
+
*
|
|
406
|
+
* It lives here, as a pure function, so the decision is covered by
|
|
407
|
+
* `scripts/holiday-check.ts` instead of hiding inside the plugin's apply closure
|
|
408
|
+
* where nothing can reach it.
|
|
409
|
+
*/
|
|
410
|
+
function pickHoliday(master, items, todayId, hasImage) {
|
|
411
|
+
if (!master || todayId === null) return null;
|
|
412
|
+
const item = items.find((i) => i.id === todayId);
|
|
413
|
+
if (item === void 0 || !item.enabled) return null;
|
|
414
|
+
return item.images.some((image) => hasImage(image.slot)) ? item : null;
|
|
415
|
+
}
|
|
416
|
+
//#endregion
|
|
236
417
|
//#region src/schema.ts
|
|
418
|
+
/**
|
|
419
|
+
* The persisted configuration shape — declared exactly ONCE, for both halves.
|
|
420
|
+
*
|
|
421
|
+
* This shape used to be written out twice: the node half owns persistence and
|
|
422
|
+
* sanitizes every write, the browser half owns the UI and its own reading of the
|
|
423
|
+
* same file. A field added to only one side was silently dropped by the other
|
|
424
|
+
* side's sanitizer — the matching slider stayed live in memory, `writeConfig`
|
|
425
|
+
* wrote a config without it, and the next load fell back to the default.
|
|
426
|
+
* `blurs.rightbar` / `rightbarOpacity` in 0.3.2 was exactly that bug, and the key
|
|
427
|
+
* lists were duplicated 6 times across the two halves, so it was due to happen.
|
|
428
|
+
*
|
|
429
|
+
* The types, the key lists, the defaults and the pure normalizers therefore live
|
|
430
|
+
* here and both halves import them: adding a field means editing this file, and
|
|
431
|
+
* the two halves cannot disagree about the shape. The host additionally logs one
|
|
432
|
+
* warning per unknown key while sanitizing a write (see `warnUnknownConfigKeys`
|
|
433
|
+
* in `src/index.ts`), so a field that reaches the node half without being
|
|
434
|
+
* declared here shows up in the host log instead of quietly disappearing.
|
|
435
|
+
*
|
|
436
|
+
* Nothing in this module may touch the DOM, Node or the Cordis context: it is
|
|
437
|
+
* imported by the browser bundle and by the node half alike.
|
|
438
|
+
*/
|
|
237
439
|
const DEFAULT_BG_STATE = {
|
|
238
440
|
zoom: 1,
|
|
239
441
|
x: 0,
|
|
@@ -249,6 +451,26 @@ window.__ModuleLoader__.load({
|
|
|
249
451
|
"tile",
|
|
250
452
|
"center"
|
|
251
453
|
];
|
|
454
|
+
/** Every accepted `rotate.order`, in UI order. */
|
|
455
|
+
const ROTATE_ORDERS = ["order", "shuffle"];
|
|
456
|
+
/** Fastest allowed dwell time — below this the cross-fade never settles. */
|
|
457
|
+
const ROTATE_MIN_MS = 5e3;
|
|
458
|
+
/** Slowest allowed dwell time (24 h): one image per day is still "rotation". */
|
|
459
|
+
const ROTATE_MAX_MS = 864e5;
|
|
460
|
+
/** Longest allowed cross-fade; 0 is a hard cut. */
|
|
461
|
+
const ROTATE_FADE_MAX_MS = 3e3;
|
|
462
|
+
/** Everything off: the shipped default, and what a config predating this feature gets. */
|
|
463
|
+
const DEFAULT_ROTATION = {
|
|
464
|
+
enabled: false,
|
|
465
|
+
intervalMs: 6e4,
|
|
466
|
+
order: "order",
|
|
467
|
+
advanceOnSwitch: false,
|
|
468
|
+
fadeMs: 320
|
|
469
|
+
};
|
|
470
|
+
/** A fresh rotation block (never hand out the shared default object). */
|
|
471
|
+
function defaultRotation() {
|
|
472
|
+
return { ...DEFAULT_ROTATION };
|
|
473
|
+
}
|
|
252
474
|
/**
|
|
253
475
|
* Main interface opacities (0..1), global (Interface page):
|
|
254
476
|
* `bg` main background (`--dsw-alias-bg-base`)
|
|
@@ -300,6 +522,103 @@ window.__ModuleLoader__.load({
|
|
|
300
522
|
rightbar: 0,
|
|
301
523
|
input: 0
|
|
302
524
|
};
|
|
525
|
+
/** Every slot a rule owns, in order. The one thing the render and store layers
|
|
526
|
+
* ask of the image list, so a rule's slots are never enumerated ad hoc. */
|
|
527
|
+
function ruleSlots(rule) {
|
|
528
|
+
return rule.images.map((i) => i.slot);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Whether a rule has anything to show at all: an image, or a theme color of its
|
|
532
|
+
* own. Used by matching (`usable` in ./client/modelbg) and by the panel's copy.
|
|
533
|
+
*
|
|
534
|
+
* An image is the obvious way a rule paints, and until 0.7 it was the ONLY one —
|
|
535
|
+
* which stopped being true the moment the last image became removable. A
|
|
536
|
+
* rule-level theme color is the second: it recolors the interface through the
|
|
537
|
+
* token palette with no wallpaper involved, so a rule whose pictures were all
|
|
538
|
+
* removed still paints, and treating it as unusable made every control left on
|
|
539
|
+
* its card (the wheel, the swatches, the clear-to-system-theme button) dead — the
|
|
540
|
+
* one state where the panel was confidently showing a lie. It also silently
|
|
541
|
+
* handed the model to the NEXT rule, so clearing the default rule's images put
|
|
542
|
+
* another model's wallpaper on the screen.
|
|
543
|
+
*
|
|
544
|
+
* Note that a per-image color (0.7.1) does not enter this question at all: an
|
|
545
|
+
* image is already enough, so the answer here is about the IMAGE LIST being
|
|
546
|
+
* non-empty, never about what those images are painted in.
|
|
547
|
+
*
|
|
548
|
+
* A rule with no image and no color of its own paints nothing: the system theme
|
|
549
|
+
* is not a paint of this rule's own, so such a rule is skipped by matching, which
|
|
550
|
+
* is what makes a freshly added rule (empty, colorless) invisible until the user
|
|
551
|
+
* gives it something.
|
|
552
|
+
*/
|
|
553
|
+
function ruleCanPaint(rule) {
|
|
554
|
+
return rule.enabled && (ruleSlots(rule).length > 0 || rule.color !== null);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
558
|
+
* null when the string is not a hex color.
|
|
559
|
+
*
|
|
560
|
+
* A holiday names its color as hex because that is how a color is quoted, and a
|
|
561
|
+
* hand-converted triple is a magic number nothing downstream can check. The
|
|
562
|
+
* conversion lives here rather than in ./holiday because `[h, s, l]` is this
|
|
563
|
+
* module's own rule shape.
|
|
564
|
+
*/
|
|
565
|
+
function hexToHsl(hex) {
|
|
566
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
567
|
+
if (m === null) return null;
|
|
568
|
+
const n = parseInt(m[1], 16);
|
|
569
|
+
const r = (n >> 16 & 255) / 255;
|
|
570
|
+
const g = (n >> 8 & 255) / 255;
|
|
571
|
+
const b = (n & 255) / 255;
|
|
572
|
+
const max = Math.max(r, g, b);
|
|
573
|
+
const min = Math.min(r, g, b);
|
|
574
|
+
const l = (max + min) / 2;
|
|
575
|
+
const d = max - min;
|
|
576
|
+
if (d === 0) return [
|
|
577
|
+
0,
|
|
578
|
+
0,
|
|
579
|
+
l
|
|
580
|
+
];
|
|
581
|
+
const s = d / (1 - Math.abs(2 * l - 1));
|
|
582
|
+
let h;
|
|
583
|
+
if (max === r) h = 60 * ((g - b) / d % 6);
|
|
584
|
+
else if (max === g) h = 60 * ((b - r) / d + 2);
|
|
585
|
+
else h = 60 * ((r - g) / d + 4);
|
|
586
|
+
if (h < 0) h += 360;
|
|
587
|
+
return [
|
|
588
|
+
clamp$1(h, 0, 360, 0),
|
|
589
|
+
clamp01(s, 0),
|
|
590
|
+
clamp01(l, .5)
|
|
591
|
+
];
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* One holiday's appearance, untouched.
|
|
595
|
+
*
|
|
596
|
+
* Holidays default to `fill` rather than a rule's `fit`: these are full-bleed
|
|
597
|
+
* festival wallpapers, and `fit` would letterbox them on every aspect ratio the
|
|
598
|
+
* art was not cut for.
|
|
599
|
+
*/
|
|
600
|
+
function defaultHolidayRule(def) {
|
|
601
|
+
const color = hexToHsl(def.color);
|
|
602
|
+
return {
|
|
603
|
+
id: def.id,
|
|
604
|
+
images: [{
|
|
605
|
+
slot: def.slot,
|
|
606
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
607
|
+
color
|
|
608
|
+
}],
|
|
609
|
+
match: "",
|
|
610
|
+
enabled: true,
|
|
611
|
+
color,
|
|
612
|
+
bgMode: "fill",
|
|
613
|
+
wallpaperOpacity: 1,
|
|
614
|
+
blur: 0,
|
|
615
|
+
rotate: defaultRotation()
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
/** Every built-in holiday at its default, in `HOLIDAYS` order. */
|
|
619
|
+
function defaultHolidayRules() {
|
|
620
|
+
return HOLIDAYS.map(defaultHolidayRule);
|
|
621
|
+
}
|
|
303
622
|
/** A default config with independent nested objects (never hand out the shared ones). */
|
|
304
623
|
function freshThemeConfig() {
|
|
305
624
|
return {
|
|
@@ -310,7 +629,11 @@ window.__ModuleLoader__.load({
|
|
|
310
629
|
chatTextOpacity: 0,
|
|
311
630
|
trajectoryOpacity: 1,
|
|
312
631
|
rightbarOpacity: null,
|
|
313
|
-
autoExtract: true
|
|
632
|
+
autoExtract: true,
|
|
633
|
+
holidays: {
|
|
634
|
+
enabled: true,
|
|
635
|
+
items: defaultHolidayRules()
|
|
636
|
+
}
|
|
314
637
|
};
|
|
315
638
|
}
|
|
316
639
|
/** Slot names reach the filesystem, so they are strictly whitelisted. */
|
|
@@ -339,23 +662,143 @@ window.__ModuleLoader__.load({
|
|
|
339
662
|
clamp$1(raw[2], 0, 1, .25)
|
|
340
663
|
];
|
|
341
664
|
}
|
|
342
|
-
/** Coerce one persisted
|
|
665
|
+
/** Coerce one persisted rotation block; a config without one gets the shipped default. */
|
|
666
|
+
function normalizeRotation(raw) {
|
|
667
|
+
const r = raw ?? {};
|
|
668
|
+
const d = DEFAULT_ROTATION;
|
|
669
|
+
return {
|
|
670
|
+
enabled: r.enabled === true,
|
|
671
|
+
intervalMs: clamp$1(r.intervalMs, ROTATE_MIN_MS, ROTATE_MAX_MS, d.intervalMs),
|
|
672
|
+
order: ROTATE_ORDERS.includes(r.order) ? r.order : d.order,
|
|
673
|
+
advanceOnSwitch: r.advanceOnSwitch === true,
|
|
674
|
+
fadeMs: clamp$1(r.fadeMs, 0, ROTATE_FADE_MAX_MS, d.fadeMs)
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/** Own-property test for values that may be anything at all (a raw JSON entry). */
|
|
678
|
+
function hasOwn(o, key) {
|
|
679
|
+
return Object.prototype.hasOwnProperty.call(o, key);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Coerce one persisted image entry, or null when it lacks a usable slot.
|
|
683
|
+
*
|
|
684
|
+
* `inherit` is the RULE's own color, and it is applied only to an entry that has
|
|
685
|
+
* no `color` key at all — the shape every release before 0.7.1 wrote, where a
|
|
686
|
+
* rule's color was the only color there was. Lifting it here, once, is what makes
|
|
687
|
+
* an upgrade a no-op on screen: a themed wallpaper keeps its theme instead of
|
|
688
|
+
* silently dropping to the system palette, and it happens in the shared sanitizer
|
|
689
|
+
* so both halves lift identically.
|
|
690
|
+
*
|
|
691
|
+
* The KEY's presence is the whole distinction, which is why `null` is not the
|
|
692
|
+
* same as absent: an entry with `color: null` is an image the user explicitly set
|
|
693
|
+
* to "follow the system theme", and re-inheriting the rule's color over that
|
|
694
|
+
* would undo a deliberate choice on every load.
|
|
695
|
+
*/
|
|
696
|
+
function normalizeImage(raw, inherit = null) {
|
|
697
|
+
const i = raw ?? {};
|
|
698
|
+
if (typeof i.slot !== "string" || !SLOT_RE.test(i.slot)) return null;
|
|
699
|
+
return {
|
|
700
|
+
slot: i.slot,
|
|
701
|
+
bgState: normalizeBgState(i.bgState),
|
|
702
|
+
color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Coerce one persisted rule, or null when it names no image AT ALL.
|
|
707
|
+
*
|
|
708
|
+
* Accepts BOTH image shapes: the 0.7 list (`images`), and the single
|
|
709
|
+
* `slot`/`bgState` pair every earlier version wrote — the latter is lifted to a
|
|
710
|
+
* one-entry list here rather than at a dozen call sites, which is what keeps the
|
|
711
|
+
* rest of the codebase free of "old config" branches. Duplicate slots inside one
|
|
712
|
+
* rule are dropped: two entries pointing at the same bytes would only make the
|
|
713
|
+
* rotation look stuck.
|
|
714
|
+
*
|
|
715
|
+
* The rule's color is resolved FIRST and handed to every image entry, because it
|
|
716
|
+
* is what an entry written before 0.7.1 inherits (`normalizeImage`). The rule keeps
|
|
717
|
+
* it as well: it is not a default for its images at paint time any more, but it
|
|
718
|
+
* is still what the rule paints once its last picture is gone.
|
|
719
|
+
*
|
|
720
|
+
* An EMPTY list is a legitimate rule, not a broken one: removing a rule's last
|
|
721
|
+
* picture has to be expressible, and forcing an entry back in is what produced a
|
|
722
|
+
* phantom blank image that took slot 1 and could never be deleted again (the next
|
|
723
|
+
* upload landed at position 2 and the rule kept painting nothing). What such a
|
|
724
|
+
* rule then does is `ruleCanPaint`'s business: with a color of its own it paints
|
|
725
|
+
* the interface, with neither it is skipped by matching — which the card's own
|
|
726
|
+
* hint says in so many words. Only a rule that names no image anywhere — no list,
|
|
727
|
+
* no legacy slot, or a slot that could never be a filename — is dropped.
|
|
728
|
+
*/
|
|
343
729
|
function normalizeRule(raw) {
|
|
344
730
|
const r = raw ?? {};
|
|
345
731
|
const id = typeof r.id === "string" && r.id !== "" ? r.id : null;
|
|
346
|
-
|
|
347
|
-
|
|
732
|
+
if (id === null) return null;
|
|
733
|
+
const color = normalizeHsl(r.color);
|
|
734
|
+
const images = [];
|
|
735
|
+
const seen = /* @__PURE__ */ new Set();
|
|
736
|
+
const hadList = Array.isArray(r.images);
|
|
737
|
+
if (hadList) for (const entry of r.images) {
|
|
738
|
+
const image = normalizeImage(entry, color);
|
|
739
|
+
if (image === null || seen.has(image.slot)) continue;
|
|
740
|
+
seen.add(image.slot);
|
|
741
|
+
images.push(image);
|
|
742
|
+
}
|
|
743
|
+
const legacySlot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
|
|
744
|
+
if (images.length === 0 && legacySlot !== null) images.push({
|
|
745
|
+
slot: legacySlot,
|
|
746
|
+
bgState: normalizeBgState(r.bgState),
|
|
747
|
+
color
|
|
748
|
+
});
|
|
749
|
+
if (images.length === 0 && !hadList) return null;
|
|
348
750
|
const mode = BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit";
|
|
349
751
|
return {
|
|
350
752
|
id,
|
|
351
|
-
|
|
753
|
+
images,
|
|
352
754
|
match: typeof r.match === "string" ? r.match : "",
|
|
353
755
|
enabled: r.enabled !== false,
|
|
354
|
-
color
|
|
756
|
+
color,
|
|
355
757
|
bgMode: mode,
|
|
356
758
|
wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
|
|
357
759
|
blur: clamp$1(r.blur, 0, 60, 0),
|
|
358
|
-
|
|
760
|
+
rotate: normalizeRotation(r.rotate)
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Coerce one persisted holiday entry.
|
|
765
|
+
*
|
|
766
|
+
* `id`, `images` and every theme color (the rule's and each image's) come from
|
|
767
|
+
* the DEFINITION, never from disk: a holiday's slot is the only thing tying it to
|
|
768
|
+
* its bytes, and its theme color is a fixed part of what that holiday looks like —
|
|
769
|
+
* so a stale or hand-edited value would either orphan the wallpaper, point the
|
|
770
|
+
* holiday at a rule's image, or paint a festival in a color it does not have.
|
|
771
|
+
* Everything else is sanitized exactly like a rule's field — except the image
|
|
772
|
+
* list and the rotation, which stay the definition's own single image and stay
|
|
773
|
+
* off, so a festival can never be swapped or cycled.
|
|
774
|
+
*/
|
|
775
|
+
function normalizeHolidayRule(def, raw) {
|
|
776
|
+
const r = raw ?? {};
|
|
777
|
+
const base = defaultHolidayRule(def);
|
|
778
|
+
return {
|
|
779
|
+
...base,
|
|
780
|
+
enabled: r.enabled !== false,
|
|
781
|
+
bgMode: BG_MODES$1.includes(r.bgMode) ? r.bgMode : base.bgMode,
|
|
782
|
+
wallpaperOpacity: clamp01(r.wallpaperOpacity, base.wallpaperOpacity),
|
|
783
|
+
blur: clamp$1(r.blur, 0, 60, base.blur),
|
|
784
|
+
images: base.images.map((image) => ({
|
|
785
|
+
slot: image.slot,
|
|
786
|
+
bgState: normalizeBgState(r.images?.[0]?.bgState ?? r.bgState),
|
|
787
|
+
color: image.color
|
|
788
|
+
}))
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/** Coerce the whole holiday block; unknown ids are dropped, missing ones defaulted. */
|
|
792
|
+
function normalizeHolidays(raw) {
|
|
793
|
+
const r = raw ?? {};
|
|
794
|
+
const stored = /* @__PURE__ */ new Map();
|
|
795
|
+
if (Array.isArray(r.items)) for (const item of r.items) {
|
|
796
|
+
const id = item?.id;
|
|
797
|
+
if (typeof id === "string") stored.set(id, item);
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
enabled: r.enabled !== false,
|
|
801
|
+
items: HOLIDAYS.map((def) => normalizeHolidayRule(def, stored.get(def.id)))
|
|
359
802
|
};
|
|
360
803
|
}
|
|
361
804
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
@@ -376,7 +819,8 @@ window.__ModuleLoader__.load({
|
|
|
376
819
|
chatTextOpacity: clamp01(r.chatTextOpacity, 0),
|
|
377
820
|
trajectoryOpacity: clamp01(r.trajectoryOpacity, 1),
|
|
378
821
|
rightbarOpacity: typeof r.rightbarOpacity === "number" && isFinite(r.rightbarOpacity) ? clamp01(r.rightbarOpacity, 1) : null,
|
|
379
|
-
autoExtract: r.autoExtract !== false
|
|
822
|
+
autoExtract: r.autoExtract !== false,
|
|
823
|
+
holidays: normalizeHolidays(r.holidays)
|
|
380
824
|
};
|
|
381
825
|
}
|
|
382
826
|
//#endregion
|
|
@@ -477,35 +921,126 @@ window.__ModuleLoader__.load({
|
|
|
477
921
|
let activeRuleId = null;
|
|
478
922
|
let activeMatched = false;
|
|
479
923
|
let modelLabel = "";
|
|
924
|
+
/**
|
|
925
|
+
* Which image of the ACTIVE rule is painted right now.
|
|
926
|
+
*
|
|
927
|
+
* Deliberately NOT persisted: a background that comes back on image 3 of 7 after
|
|
928
|
+
* a reload, with no way to say why, is worse than one that starts at the first
|
|
929
|
+
* image every time. Clamped to the rule's own length on every read, so an index
|
|
930
|
+
* left over from a longer list can never point at nothing.
|
|
931
|
+
*/
|
|
932
|
+
let rotIndex = 0;
|
|
480
933
|
function setActive(ruleId, matched) {
|
|
934
|
+
if (ruleId !== activeRuleId) rotIndex = 0;
|
|
481
935
|
activeRuleId = ruleId;
|
|
482
936
|
activeMatched = matched;
|
|
483
937
|
}
|
|
484
938
|
function setModelLabel(label) {
|
|
485
939
|
modelLabel = label;
|
|
486
940
|
}
|
|
941
|
+
/** The image index the render layer is on, clamped into `rule`'s own range. */
|
|
942
|
+
function imageIndexOf(rule) {
|
|
943
|
+
if (rule.images.length === 0) return 0;
|
|
944
|
+
const i = Math.floor(rotIndex);
|
|
945
|
+
return i >= 0 && i < rule.images.length ? i : 0;
|
|
946
|
+
}
|
|
947
|
+
/** Point the active rule at another of its images (the rotation's one mutator). */
|
|
948
|
+
function setRotIndex(i) {
|
|
949
|
+
rotIndex = Math.floor(i);
|
|
950
|
+
}
|
|
951
|
+
/** The image the active rule is painting now, or null when it has none. */
|
|
952
|
+
function activeImage() {
|
|
953
|
+
const rule = activeRule();
|
|
954
|
+
if (rule === null) return null;
|
|
955
|
+
return rule.images[imageIndexOf(rule)] ?? null;
|
|
956
|
+
}
|
|
957
|
+
/** Slot of the active rule's current image; '' when there is nothing to paint. */
|
|
958
|
+
function activeSlot() {
|
|
959
|
+
return activeImage()?.slot ?? "";
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* The theme color in force right now, or null for "follow the system theme".
|
|
963
|
+
*
|
|
964
|
+
* Two rungs, in this order and no others:
|
|
965
|
+
* 1. the color of the IMAGE that is on screen, when the rule has one;
|
|
966
|
+
* 2. the rule's own color, which is all an image-less rule has to paint with.
|
|
967
|
+
*
|
|
968
|
+
* There is deliberately no third rung where an image INHERITS its rule's color:
|
|
969
|
+
* a rule's color is not a default for its pictures (see `BgRule.color`). If it
|
|
970
|
+
* were, a rotation through a green picture, a red one and a system-themed one
|
|
971
|
+
* could not be expressed at all — clearing a picture's color would only hand it
|
|
972
|
+
* back to the rule's, and the clear button would look broken. Images that predate
|
|
973
|
+
* per-image colors get the rule's color LIFTED onto them at read time instead
|
|
974
|
+
* (`normalizeImage`), so nothing loses its theme on upgrade.
|
|
975
|
+
*
|
|
976
|
+
* The image is the one being PAINTED (`imageIndexOf`), not the one the panel has
|
|
977
|
+
* selected — the panel's selection is an editing cursor and must not repaint the
|
|
978
|
+
* interface while the user is fixing another picture's framing.
|
|
979
|
+
*/
|
|
980
|
+
function activeColor() {
|
|
981
|
+
const rule = activeRule();
|
|
982
|
+
if (rule === null) return null;
|
|
983
|
+
if (rule.images.length === 0) return rule.color;
|
|
984
|
+
return activeImage()?.color ?? null;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* One rule by id — from the USER's list first, then from the holiday overrides.
|
|
988
|
+
*
|
|
989
|
+
* Every render accessor (`rWp` / `rColor` / `rBgMode` / `rBgState` …) resolves
|
|
990
|
+
* through here, so teaching this one function about holidays is what makes an
|
|
991
|
+
* active holiday paint through the entire existing pipeline instead of needing a
|
|
992
|
+
* parallel one.
|
|
993
|
+
*/
|
|
487
994
|
function ruleById(id) {
|
|
488
|
-
return cfg.rules.find((r) => r.id === id) ?? null;
|
|
995
|
+
return cfg.rules.find((r) => r.id === id) ?? cfg.holidays.items.find((r) => r.id === id) ?? null;
|
|
489
996
|
}
|
|
490
997
|
function activeRule() {
|
|
491
998
|
return activeRuleId === null ? null : ruleById(activeRuleId);
|
|
492
999
|
}
|
|
493
|
-
|
|
1000
|
+
/** One holiday override entry, or null for an unknown id. */
|
|
1001
|
+
function holidayById(id) {
|
|
1002
|
+
return cfg.holidays.items.find((r) => r.id === id) ?? null;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* A fresh rule, holding NO image yet.
|
|
1006
|
+
*
|
|
1007
|
+
* It used to be born with an allocated slot and an empty entry, which is what put
|
|
1008
|
+
* a blank tile at position 1 of a new rule's strip — and, worse, stopped the
|
|
1009
|
+
* picture the user added next from being the first one (a rule paints its first
|
|
1010
|
+
* image). A rule with no pictures is a valid state: `ruleCanPaint` (./schema) skips
|
|
1011
|
+
* it — it has no image and no color yet — until either arrives, and the slot is
|
|
1012
|
+
* allocated when the first picture does.
|
|
1013
|
+
*/
|
|
1014
|
+
function newRule(id) {
|
|
494
1015
|
return {
|
|
495
1016
|
id,
|
|
496
|
-
|
|
1017
|
+
images: [],
|
|
497
1018
|
match: "",
|
|
498
1019
|
enabled: true,
|
|
499
1020
|
color: null,
|
|
500
1021
|
bgMode: "fit",
|
|
501
1022
|
wallpaperOpacity: 1,
|
|
502
1023
|
blur: 0,
|
|
503
|
-
|
|
1024
|
+
rotate: defaultRotation()
|
|
504
1025
|
};
|
|
505
1026
|
}
|
|
506
|
-
/**
|
|
1027
|
+
/** Every slot any rule owns — the user's rules and the holiday entries alike. */
|
|
1028
|
+
function takenSlots() {
|
|
1029
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1030
|
+
for (const rule of cfg.rules) for (const slot of ruleSlots(rule)) taken.add(slot);
|
|
1031
|
+
for (const rule of cfg.holidays.items) for (const slot of ruleSlots(rule)) taken.add(slot);
|
|
1032
|
+
return taken;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* First free image slot (`m1`, `m2`, …).
|
|
1036
|
+
*
|
|
1037
|
+
* Scans EVERY slot in the config, not just the primary one of each rule: with
|
|
1038
|
+
* several images per rule, "the slots this file already uses" is the only set
|
|
1039
|
+
* that can be safely handed out, and a collision here would silently point two
|
|
1040
|
+
* cards at the same bytes (and let one card's delete orphan the other).
|
|
1041
|
+
*/
|
|
507
1042
|
function nextSlot() {
|
|
508
|
-
const taken =
|
|
1043
|
+
const taken = takenSlots();
|
|
509
1044
|
for (let i = 1; i < 1e3; i++) {
|
|
510
1045
|
const slot = `m${i}`;
|
|
511
1046
|
if (!taken.has(slot)) return slot;
|
|
@@ -521,11 +1056,24 @@ window.__ModuleLoader__.load({
|
|
|
521
1056
|
}
|
|
522
1057
|
return `r${Date.now()}`;
|
|
523
1058
|
}
|
|
1059
|
+
/** Patch one rule's rotation block, through the shared sanitizer so a live edit
|
|
1060
|
+
* can never hold a value the next load would clamp differently. */
|
|
1061
|
+
function patchRotation(rule, patch) {
|
|
1062
|
+
rule.rotate = normalizeRotation({
|
|
1063
|
+
...rule.rotate,
|
|
1064
|
+
...patch
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
/** Re-sanitize one rule in place, so what the UI holds equals what would persist. */
|
|
1068
|
+
function normalizeRuleInPlace(rule) {
|
|
1069
|
+
const normalized = normalizeRule(rule);
|
|
1070
|
+
if (normalized !== null) Object.assign(rule, normalized);
|
|
1071
|
+
}
|
|
524
1072
|
function rHasColor() {
|
|
525
|
-
return
|
|
1073
|
+
return activeColor() !== null;
|
|
526
1074
|
}
|
|
527
1075
|
function rColor() {
|
|
528
|
-
return
|
|
1076
|
+
return activeColor() ?? [
|
|
529
1077
|
220,
|
|
530
1078
|
.55,
|
|
531
1079
|
.25
|
|
@@ -540,13 +1088,22 @@ window.__ModuleLoader__.load({
|
|
|
540
1088
|
function rBl() {
|
|
541
1089
|
return clamp$1(activeRule()?.blur, 0, 60, 0);
|
|
542
1090
|
}
|
|
1091
|
+
/** Framing of the CURRENT image (per image, see BgImage). */
|
|
543
1092
|
function rBgState() {
|
|
544
|
-
return
|
|
1093
|
+
return activeImage()?.bgState ?? DEFAULT_BG_STATE;
|
|
545
1094
|
}
|
|
546
|
-
/**
|
|
1095
|
+
/** Rotation of the active rule; the shipped default when nothing is active. */
|
|
1096
|
+
function rRotation() {
|
|
1097
|
+
return activeRule()?.rotate ?? defaultRotation();
|
|
1098
|
+
}
|
|
1099
|
+
/** Cross-fade duration the active rule asks for, in ms (0 = hard cut). */
|
|
1100
|
+
function rFadeMs() {
|
|
1101
|
+
return rRotation().fadeMs;
|
|
1102
|
+
}
|
|
1103
|
+
/** Paintable URL of the active rule's CURRENT image, or null when it has none. */
|
|
547
1104
|
function rWp() {
|
|
548
|
-
const
|
|
549
|
-
return
|
|
1105
|
+
const slot = activeSlot();
|
|
1106
|
+
return slot === "" ? null : displayImageOf(slot);
|
|
550
1107
|
}
|
|
551
1108
|
function rOps() {
|
|
552
1109
|
const o = cfg.opacities ?? {};
|
|
@@ -577,16 +1134,130 @@ window.__ModuleLoader__.load({
|
|
|
577
1134
|
/** Move a possibly-absent partial config into the shape the UI reads. */
|
|
578
1135
|
function adoptConfig(raw) {
|
|
579
1136
|
cfg = normalizeConfig(raw);
|
|
580
|
-
if (activeRuleId !== null &&
|
|
1137
|
+
if (activeRuleId !== null && ruleById(activeRuleId) === null) {
|
|
581
1138
|
activeRuleId = null;
|
|
582
1139
|
activeMatched = false;
|
|
583
1140
|
}
|
|
1141
|
+
rotIndex = 0;
|
|
1142
|
+
}
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/client/rotation.ts
|
|
1145
|
+
/** Dwell-time presets the settings UI offers, in ms. */
|
|
1146
|
+
const ROTATE_PRESETS = [
|
|
1147
|
+
{
|
|
1148
|
+
ms: 1e4,
|
|
1149
|
+
key: "rotEvery10s"
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
ms: 3e4,
|
|
1153
|
+
key: "rotEvery30s"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
ms: 6e4,
|
|
1157
|
+
key: "rotEvery1m"
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
ms: 3e5,
|
|
1161
|
+
key: "rotEvery5m"
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
ms: 18e5,
|
|
1165
|
+
key: "rotEvery30m"
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
ms: 36e5,
|
|
1169
|
+
key: "rotEvery1h"
|
|
1170
|
+
}
|
|
1171
|
+
];
|
|
1172
|
+
/**
|
|
1173
|
+
* The index to paint after the current one, or null when there is nowhere to go
|
|
1174
|
+
* (fewer than two images — the caller must not start a timer at all then).
|
|
1175
|
+
*
|
|
1176
|
+
* `order` walks the list and wraps; `shuffle` picks uniformly among every index
|
|
1177
|
+
* EXCEPT the current one, because landing on the same image reads as a missed
|
|
1178
|
+
* tick. `current` outside the list is treated as 0 rather than trusted: it comes
|
|
1179
|
+
* from the UI as often as from the timer, and a stale index must not be able to
|
|
1180
|
+
* index past the end.
|
|
1181
|
+
*
|
|
1182
|
+
* `rand` is injectable so the shuffle branch is testable without stubbing a
|
|
1183
|
+
* global.
|
|
1184
|
+
*/
|
|
1185
|
+
function nextIndex(len, order, current, rand = Math.random) {
|
|
1186
|
+
if (!Number.isFinite(len) || len < 2) return null;
|
|
1187
|
+
const count = Math.floor(len);
|
|
1188
|
+
const cur = Number.isFinite(current) && current >= 0 && current < count ? Math.floor(current) : 0;
|
|
1189
|
+
if (order === "shuffle") {
|
|
1190
|
+
const roll = Math.min(Math.floor(Math.max(rand(), 0) * (count - 1)), count - 2);
|
|
1191
|
+
return roll >= cur ? roll + 1 : roll;
|
|
1192
|
+
}
|
|
1193
|
+
return (cur + 1) % count;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Whether a rotation is worth running: two images, the switch on, and a positive
|
|
1197
|
+
* dwell time. Kept here (rather than at the call site) so the UI's "rotating"
|
|
1198
|
+
* badge and the timer agree by construction — a badge that says "rotating" while
|
|
1199
|
+
* no timer exists is the one failure this feature can produce silently.
|
|
1200
|
+
*/
|
|
1201
|
+
function isRotating(len, enabled, intervalMs) {
|
|
1202
|
+
return enabled && len >= 2 && Number.isFinite(intervalMs) && intervalMs > 0;
|
|
1203
|
+
}
|
|
1204
|
+
//#endregion
|
|
1205
|
+
//#region src/client/repaint.ts
|
|
1206
|
+
/**
|
|
1207
|
+
* The repaint decision, as a pure function.
|
|
1208
|
+
*
|
|
1209
|
+
* Every edit to a rule ends with the same question: does the interface have to be
|
|
1210
|
+
* repainted, or is refreshing the panel's readout enough? Repainting is the
|
|
1211
|
+
* expensive half (the theme skin is re-registered, the wallpaper layer is
|
|
1212
|
+
* rewritten, the host's theme is switched), so it must not be done for a rule
|
|
1213
|
+
* that is not the one on screen — but it MUST be done the moment the edit
|
|
1214
|
+
* changes what should be on screen.
|
|
1215
|
+
*
|
|
1216
|
+
* The naive test — "is the edited rule the active one?" — is wrong in exactly the
|
|
1217
|
+
* case that kept being reported as "更新的不太及时,只有我动调色盘或者切模型的时候
|
|
1218
|
+
* 才出现": an edit that CREATES the winner. Uploading a rule's first picture, or
|
|
1219
|
+
* an auto-extracted color landing on a rule that had none, makes that rule
|
|
1220
|
+
* paintable *through that very edit* (see `ruleCanPaint`), so the rule is not the
|
|
1221
|
+
* active one at the moment of the check; the wallpaper and the skin then only
|
|
1222
|
+
* showed up later, when an unrelated drag or model switch happened to re-run the
|
|
1223
|
+
* resolution.
|
|
1224
|
+
*
|
|
1225
|
+
* Sampling the winner on BOTH sides of the edit is what makes such a change land
|
|
1226
|
+
* at once, and `editedId === winnerAfter` additionally covers the edit that does
|
|
1227
|
+
* not move the resolution but changes the look of the rule that IS painting (a
|
|
1228
|
+
* color, a slider, a framing).
|
|
1229
|
+
*
|
|
1230
|
+
* `null` is "nothing of ours is painting" (the interface falls back to the host's
|
|
1231
|
+
* own look), which is a winner like any other id.
|
|
1232
|
+
*
|
|
1233
|
+
* Kept free of imports on purpose: `scripts/repaint-check.ts` exercises it on
|
|
1234
|
+
* plain node, the same way `rotation.ts` is exercised. A rule's `color`/`match`
|
|
1235
|
+
* are the inputs that make the resolution move, which is why they are the fields
|
|
1236
|
+
* whose write paths all end here.
|
|
1237
|
+
*/
|
|
1238
|
+
function shouldRepaint(editedId, winnerBefore, winnerAfter) {
|
|
1239
|
+
return winnerAfter === editedId || winnerAfter !== winnerBefore;
|
|
584
1240
|
}
|
|
585
1241
|
//#endregion
|
|
586
1242
|
//#region src/client/rpc.ts
|
|
587
1243
|
const RPC_CHANNEL = "/dsh-background-by-model";
|
|
588
1244
|
const RPC_NS = "dshBackgroundByModel";
|
|
589
1245
|
const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
|
|
1246
|
+
let hostShape = "unknown";
|
|
1247
|
+
let warnedLegacy = false;
|
|
1248
|
+
/** Whether this bundle may write its config to the host it is talking to. */
|
|
1249
|
+
function canWriteConfig() {
|
|
1250
|
+
return hostShape !== "legacy";
|
|
1251
|
+
}
|
|
1252
|
+
/** Whether the running host process is older than this client bundle. */
|
|
1253
|
+
function hostIsLegacy() {
|
|
1254
|
+
return hostShape === "legacy";
|
|
1255
|
+
}
|
|
1256
|
+
function holdWrite() {
|
|
1257
|
+
if (warnedLegacy) return;
|
|
1258
|
+
warnedLegacy = true;
|
|
1259
|
+
console.warn("dsh-background-by-model: the running host is an older build that cannot read the multi-image config shape — holding config writes so its sanitizer cannot drop this profile's rules. Restart DSH to load the new node half.");
|
|
1260
|
+
}
|
|
590
1261
|
let rpcCallFn = null;
|
|
591
1262
|
function initRpc(call) {
|
|
592
1263
|
rpcCallFn = call;
|
|
@@ -615,6 +1286,10 @@ window.__ModuleLoader__.load({
|
|
|
615
1286
|
const SAVE_DEBOUNCE_MS = 250;
|
|
616
1287
|
let saveTimer;
|
|
617
1288
|
function saveConfig() {
|
|
1289
|
+
if (!canWriteConfig()) {
|
|
1290
|
+
holdWrite();
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
618
1293
|
if (saveTimer !== void 0) window.clearTimeout(saveTimer);
|
|
619
1294
|
saveTimer = window.setTimeout(() => {
|
|
620
1295
|
saveTimer = void 0;
|
|
@@ -625,10 +1300,18 @@ window.__ModuleLoader__.load({
|
|
|
625
1300
|
if (saveTimer === void 0) return;
|
|
626
1301
|
window.clearTimeout(saveTimer);
|
|
627
1302
|
saveTimer = void 0;
|
|
1303
|
+
if (!canWriteConfig()) {
|
|
1304
|
+
holdWrite();
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
628
1307
|
rpcCall("writeConfig", { config: cfg });
|
|
629
1308
|
}
|
|
630
1309
|
/** Persist the current config immediately (no debounce). */
|
|
631
1310
|
function persistConfig() {
|
|
1311
|
+
if (!canWriteConfig()) {
|
|
1312
|
+
holdWrite();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
632
1315
|
rpcCall("writeConfig", { config: cfg });
|
|
633
1316
|
}
|
|
634
1317
|
/** Load the persisted config plus the list of stored image slots. */
|
|
@@ -637,9 +1320,13 @@ window.__ModuleLoader__.load({
|
|
|
637
1320
|
if (data === null || typeof data !== "object") return null;
|
|
638
1321
|
const d = data;
|
|
639
1322
|
const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
|
|
1323
|
+
const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
|
|
1324
|
+
hostShape = schema === null ? "legacy" : schema >= 4 ? "current" : "legacy";
|
|
1325
|
+
if (hostShape === "legacy") holdWrite();
|
|
640
1326
|
return {
|
|
641
1327
|
config: d.config,
|
|
642
|
-
slots
|
|
1328
|
+
slots,
|
|
1329
|
+
schema
|
|
643
1330
|
};
|
|
644
1331
|
}
|
|
645
1332
|
/** Read one rule image as a data URL (null when the slot is empty). */
|
|
@@ -1116,6 +1803,9 @@ window.__ModuleLoader__.load({
|
|
|
1116
1803
|
.dab-wheel-glow{position:absolute;width:230px;height:230px;border-radius:50%;filter:blur(48px);opacity:.2;background:var(--c,#888);pointer-events:none;transition:background .4s ease}
|
|
1117
1804
|
.dab-wheel{position:relative;cursor:crosshair;border-radius:50%;box-shadow:0 12px 32px -14px rgba(0,0,0,.4)}
|
|
1118
1805
|
.dab-hint{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);padding:0 4px}
|
|
1806
|
+
/* The "your edits are being held" notice: it reports a state the user has to act
|
|
1807
|
+
on, so it is tinted like a warning rather than left as grey hint text. */
|
|
1808
|
+
.dab-stale{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,transparent);border-radius:9px;padding:8px 10px}
|
|
1119
1809
|
|
|
1120
1810
|
/* ── precise color inputs ────────────────────────────────────────────────── */
|
|
1121
1811
|
.dab-inputs{display:flex;flex-direction:column;gap:10px;min-width:172px}
|
|
@@ -1250,12 +1940,48 @@ window.__ModuleLoader__.load({
|
|
|
1250
1940
|
.dab-icon-btn:disabled{opacity:.35;cursor:not-allowed}
|
|
1251
1941
|
.dab-icon-btn-danger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
|
|
1252
1942
|
.dab-rule-body{margin-top:13px;display:flex;flex-direction:column;gap:13px}
|
|
1253
|
-
.dab-rule-thumb{position:relative;border-radius:12px;overflow:hidden;aspect-ratio:16/9;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)
|
|
1943
|
+
.dab-rule-thumb{position:relative;border-radius:12px;overflow:hidden;aspect-ratio:16/9;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}
|
|
1254
1944
|
.dab-rule-thumb img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1255
1945
|
.dab-rule-thumb-empty{position:absolute;inset:5px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;border:1.5px dashed var(--dsw-alias-border-l2);border-radius:9px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center;padding:0 10px}
|
|
1256
|
-
|
|
1946
|
+
/* The empty tile is the upload button while the rule has no picture at all, so
|
|
1947
|
+
it is a real <button> (focusable, keyboard-reachable) reset down to the box the
|
|
1948
|
+
plain preview occupies — a button brings its own padding, font and border. The
|
|
1949
|
+
dashed frame it looks like is .dab-rule-thumb-empty, which is why the hover and
|
|
1950
|
+
the drag highlight have to reach it through a descendant selector too. */
|
|
1951
|
+
.dab-rule-thumb-add{display:block;width:100%;padding:0;margin:0;font:inherit;color:inherit;cursor:pointer;-webkit-appearance:none;appearance:none;transition:border-color .2s,background .2s}
|
|
1952
|
+
.dab-rule-thumb-add:hover,.dab-rule-thumb-add.is-over{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 6%,var(--dsw-alias-bg-layer-2))}
|
|
1953
|
+
.dab-rule-thumb-add:hover .dab-rule-thumb-empty,.dab-rule-thumb-add.is-over .dab-rule-thumb-empty{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
|
|
1954
|
+
.dab-rule-thumb-add:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
1257
1955
|
.dab-rule-cols{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:18px;align-items:start}
|
|
1258
1956
|
.dab-rule-section-title{font-size:12px;font-weight:600;margin-bottom:9px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
1957
|
+
|
|
1958
|
+
/* ── image strip (multi-image rules) ─────────────────────────────────────── */
|
|
1959
|
+
/* A horizontally scrolling row of thumbnails: the edited one is ringed, the one
|
|
1960
|
+
the wallpaper is actually painting carries a live dot. Drops land here, which
|
|
1961
|
+
is why the border reacts to a drag rather than to hover. */
|
|
1962
|
+
.dab-strip{display:flex;gap:7px;margin-top:9px;padding:7px;border-radius:12px;border:1.5px dashed var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);overflow-x:auto;scrollbar-width:thin}
|
|
1963
|
+
.dab-strip.is-over{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
|
|
1964
|
+
.dab-strip-item{position:relative;flex:none;width:62px;height:44px;padding:0;border-radius:8px;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);cursor:pointer;transition:border-color .2s,box-shadow .2s,transform .18s}
|
|
1965
|
+
.dab-strip-item img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1966
|
+
.dab-strip-item:hover{transform:translateY(-1px);border-color:var(--dsw-alias-brand-primary)}
|
|
1967
|
+
.dab-strip-item.is-sel{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 40%,transparent)}
|
|
1968
|
+
.dab-strip-item.is-live{box-shadow:0 0 0 2px var(--dsw-alias-brand-primary)}
|
|
1969
|
+
.dab-strip-wait{position:absolute;inset:0;background:repeating-linear-gradient(45deg,transparent 0 4px,color-mix(in srgb,var(--dsw-alias-label-tertiary) 14%,transparent) 4px 8px)}
|
|
1970
|
+
.dab-strip-num{position:absolute;left:3px;bottom:2px;font-family:var(--dab-mono);font-size:9.5px;line-height:1;padding:1px 4px;border-radius:5px;background:rgba(0,0,0,.45);color:#fff}
|
|
1971
|
+
.dab-strip-live{position:absolute;right:3px;top:3px;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px rgba(0,0,0,.35);animation:dab-pulse 1.8s ease-in-out infinite}
|
|
1972
|
+
/* Each image's OWN theme color, when it has one. It shares the right edge with
|
|
1973
|
+
the live dot but sits at the bottom, where nothing else is: the item number is
|
|
1974
|
+
bottom-LEFT. Two rings (dark outside, light inside) keep the swatch readable
|
|
1975
|
+
over a dark and a light thumbnail alike. */
|
|
1976
|
+
.dab-strip-dot{position:absolute;right:3px;bottom:3px;width:8px;height:8px;border-radius:50%;box-shadow:0 0 0 1.5px rgba(0,0,0,.45),inset 0 0 0 1px rgba(255,255,255,.35)}
|
|
1977
|
+
.dab-strip-add{flex:none;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;width:62px;height:44px;border-radius:8px;border:1.5px dashed var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);font-size:9.5px;line-height:1.15;cursor:pointer;transition:border-color .2s,color .2s}
|
|
1978
|
+
.dab-strip-add:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
|
|
1979
|
+
.dab-strip-pos{display:inline-flex;align-items:center;font-family:var(--dab-mono);font-size:11px;color:var(--dsw-alias-label-tertiary);padding:0 4px}
|
|
1980
|
+
|
|
1981
|
+
/* ── rotation panel ──────────────────────────────────────────────────────── */
|
|
1982
|
+
.dab-rot-panel{margin-top:10px;padding:12px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2)}
|
|
1983
|
+
.dab-num-sec{flex:none;width:88px;min-width:0}
|
|
1984
|
+
|
|
1259
1985
|
@container (max-width:760px){.dab-rule-cols{grid-template-columns:1fr}}
|
|
1260
1986
|
|
|
1261
1987
|
/* ── host self-check ─────────────────────────────────────────────────────── */
|
|
@@ -1300,6 +2026,11 @@ window.__ModuleLoader__.load({
|
|
|
1300
2026
|
.dab-check-where code{font-family:var(--dab-mono);font-size:10.5px;overflow-wrap:anywhere}
|
|
1301
2027
|
.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
2028
|
|
|
2029
|
+
/* ── holiday override (Profile page) ─────────────────────────────────────── */
|
|
2030
|
+
/* Only a label: the whole feature is one switch, and dab-toggle already exists.
|
|
2031
|
+
The per-holiday cards that used to live here are gone by design. */
|
|
2032
|
+
.dab-holiday-label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
2033
|
+
|
|
1303
2034
|
/* ── keyframes ───────────────────────────────────────────────────────────── */
|
|
1304
2035
|
@keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
|
|
1305
2036
|
@keyframes dab-page-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
|
@@ -1310,6 +2041,9 @@ window.__ModuleLoader__.load({
|
|
|
1310
2041
|
@keyframes dab-toast-in{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
1311
2042
|
@keyframes dab-zoom-in{from{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:none}}
|
|
1312
2043
|
@keyframes dab-flow{to{background-position:300% 50%}}
|
|
2044
|
+
/* The strip's "this is the image on screen" dot. It is the only marker that says
|
|
2045
|
+
the rotation is alive, so it breathes rather than merely sitting there. */
|
|
2046
|
+
@keyframes dab-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.72)}}
|
|
1313
2047
|
|
|
1314
2048
|
/* ── responsive & motion preferences ─────────────────────────────────────── */
|
|
1315
2049
|
@container (max-width:620px){
|
|
@@ -2055,7 +2789,6 @@ window.__ModuleLoader__.load({
|
|
|
2055
2789
|
themeObserver = null;
|
|
2056
2790
|
};
|
|
2057
2791
|
}
|
|
2058
|
-
const FADE_MS = 320;
|
|
2059
2792
|
let layers = null;
|
|
2060
2793
|
/** Index of the layer the user is looking at. */
|
|
2061
2794
|
let front = 0;
|
|
@@ -2261,7 +2994,8 @@ window.__ModuleLoader__.load({
|
|
|
2261
2994
|
from.el.style.zIndex = "0";
|
|
2262
2995
|
paintLayer(target, url);
|
|
2263
2996
|
applyWpEffects();
|
|
2264
|
-
|
|
2997
|
+
const fadeMs = rFadeMs();
|
|
2998
|
+
if (!canFade(from) || fadeMs <= 0) {
|
|
2265
2999
|
el.style.opacity = "1";
|
|
2266
3000
|
front = idx;
|
|
2267
3001
|
clearLayer(from);
|
|
@@ -2271,10 +3005,10 @@ window.__ModuleLoader__.load({
|
|
|
2271
3005
|
pending = idx;
|
|
2272
3006
|
requestAnimationFrame(() => {
|
|
2273
3007
|
if (pending !== idx || layers === null) return;
|
|
2274
|
-
el.style.transition = `opacity ${
|
|
3008
|
+
el.style.transition = `opacity ${fadeMs}ms ease`;
|
|
2275
3009
|
el.style.opacity = "1";
|
|
2276
3010
|
});
|
|
2277
|
-
fadeTimer = window.setTimeout(() => finishFade(idx),
|
|
3011
|
+
fadeTimer = window.setTimeout(() => finishFade(idx), fadeMs + 160);
|
|
2278
3012
|
}
|
|
2279
3013
|
function applyWpEffects() {
|
|
2280
3014
|
if (wpEl === null || layers === null) return;
|
|
@@ -2339,9 +3073,9 @@ window.__ModuleLoader__.load({
|
|
|
2339
3073
|
}
|
|
2340
3074
|
//#endregion
|
|
2341
3075
|
//#region src/client/modelbg.ts
|
|
2342
|
-
/** Rules that can actually paint:
|
|
3076
|
+
/** Rules that can actually paint: see `ruleCanPaint` (./schema). */
|
|
2343
3077
|
function usable(rules) {
|
|
2344
|
-
return rules.filter(
|
|
3078
|
+
return rules.filter(ruleCanPaint);
|
|
2345
3079
|
}
|
|
2346
3080
|
/**
|
|
2347
3081
|
* Resolve the rule for one model text.
|
|
@@ -2349,6 +3083,10 @@ window.__ModuleLoader__.load({
|
|
|
2349
3083
|
* Top→bottom, first case-insensitive substring hit wins; an empty `match` never
|
|
2350
3084
|
* hits on its own. When nothing hits, the FIRST usable rule is the fallback
|
|
2351
3085
|
* (rule 1 deliberately doubles as both a matcher and the fallback).
|
|
3086
|
+
*
|
|
3087
|
+
* "Usable" is `ruleCanPaint` — an image, or a theme color of its own — so a rule
|
|
3088
|
+
* whose pictures were all removed still holds the models it matches (and paints
|
|
3089
|
+
* its color alone) instead of quietly handing them to the next rule.
|
|
2352
3090
|
*/
|
|
2353
3091
|
function matchRule(rules, modelText) {
|
|
2354
3092
|
const list = usable(rules);
|
|
@@ -3972,6 +4710,11 @@ window.__ModuleLoader__.load({
|
|
|
3972
4710
|
className: "dab-card dab-rise",
|
|
3973
4711
|
style: { "--d": 1 },
|
|
3974
4712
|
children: [
|
|
4713
|
+
store.hostStale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4714
|
+
className: "dab-hint dab-stale",
|
|
4715
|
+
style: { marginBottom: 10 },
|
|
4716
|
+
children: t("hostStaleHint")
|
|
4717
|
+
}) : null,
|
|
3975
4718
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3976
4719
|
className: "dab-swatch-title",
|
|
3977
4720
|
children: t("tryoutTitle")
|
|
@@ -4069,6 +4812,7 @@ window.__ModuleLoader__.load({
|
|
|
4069
4812
|
rule,
|
|
4070
4813
|
index: i,
|
|
4071
4814
|
total: rules.length,
|
|
4815
|
+
liveIndex: rule.id === store.activeRuleId ? store.rotIndex : -1,
|
|
4072
4816
|
active: rule.id === store.activeRuleId,
|
|
4073
4817
|
notify
|
|
4074
4818
|
}, rule.id))
|
|
@@ -4086,7 +4830,7 @@ window.__ModuleLoader__.load({
|
|
|
4086
4830
|
})
|
|
4087
4831
|
] });
|
|
4088
4832
|
}
|
|
4089
|
-
function RuleCard({ p, rule, index, total, active, notify }) {
|
|
4833
|
+
function RuleCard({ p, rule, index, total, active, liveIndex, notify }) {
|
|
4090
4834
|
const { t } = p;
|
|
4091
4835
|
const [open, setOpen] = (0, react.useState)(active);
|
|
4092
4836
|
const [dragOver, setDragOver] = (0, react.useState)(false);
|
|
@@ -4097,49 +4841,98 @@ window.__ModuleLoader__.load({
|
|
|
4097
4841
|
const [editorOpen, setEditorOpen] = (0, react.useState)(false);
|
|
4098
4842
|
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
4099
4843
|
const [extracting, setExtracting] = (0, react.useState)(false);
|
|
4844
|
+
const [secDraft, setSecDraft] = (0, react.useState)(null);
|
|
4845
|
+
/** Whether the next file picked replaces the selected image or is added. */
|
|
4846
|
+
const [replace, setReplace] = (0, react.useState)(false);
|
|
4100
4847
|
const fileRef = (0, react.useRef)(null);
|
|
4101
|
-
const
|
|
4102
|
-
const
|
|
4848
|
+
const [sel, setSel] = (0, react.useState)(0);
|
|
4849
|
+
const selIdx = rule.images.length === 0 ? 0 : Math.min(sel, rule.images.length - 1);
|
|
4850
|
+
const image = rule.images[selIdx];
|
|
4851
|
+
const url = image === void 0 ? null : p.imageOf(image.slot);
|
|
4852
|
+
const ownColor = image === void 0 ? rule.color : image.color;
|
|
4853
|
+
const setColor = (color) => {
|
|
4854
|
+
if (image === void 0) p.setRule(rule.id, { color });
|
|
4855
|
+
else p.setImageColor(rule.id, image.slot, color);
|
|
4856
|
+
};
|
|
4857
|
+
const [h, s, l] = ownColor ?? SEED_COLOR;
|
|
4103
4858
|
const wheel = hslToHsv(h, s, l);
|
|
4859
|
+
(0, react.useEffect)(() => {
|
|
4860
|
+
if (open) p.loadRuleImages(rule.id);
|
|
4861
|
+
}, [
|
|
4862
|
+
open,
|
|
4863
|
+
rule.id,
|
|
4864
|
+
rule.images.length,
|
|
4865
|
+
p
|
|
4866
|
+
]);
|
|
4104
4867
|
const onColor = (nh, ns, nl) => {
|
|
4105
4868
|
const [sh, ss, sl] = hsvToHsl(nh, ns, nl);
|
|
4106
|
-
|
|
4869
|
+
setColor([
|
|
4107
4870
|
sh,
|
|
4108
4871
|
ss,
|
|
4109
4872
|
sl
|
|
4110
|
-
]
|
|
4873
|
+
]);
|
|
4111
4874
|
};
|
|
4112
|
-
const
|
|
4113
|
-
|
|
4114
|
-
|
|
4875
|
+
const onFiles = (files) => {
|
|
4876
|
+
const list = files.filter((f) => f.type.startsWith("image/"));
|
|
4877
|
+
if (list.length === 0) return;
|
|
4878
|
+
if (replace) {
|
|
4879
|
+
const slot = rule.images[selIdx]?.slot;
|
|
4880
|
+
if (slot !== void 0) readImg(list[0], (d) => {
|
|
4881
|
+
if (d !== null) p.setRuleImage(rule.id, slot, d);
|
|
4882
|
+
});
|
|
4883
|
+
return;
|
|
4884
|
+
}
|
|
4885
|
+
const out = [];
|
|
4886
|
+
const first = rule.images.length;
|
|
4887
|
+
let pending = list.length;
|
|
4888
|
+
list.forEach((f, i) => {
|
|
4889
|
+
readImg(f, (d) => {
|
|
4890
|
+
if (d !== null) out[i] = d;
|
|
4891
|
+
pending--;
|
|
4892
|
+
if (pending === 0) {
|
|
4893
|
+
p.addRuleImages(rule.id, out.filter((x) => typeof x === "string"));
|
|
4894
|
+
setSel(first);
|
|
4895
|
+
}
|
|
4896
|
+
});
|
|
4115
4897
|
});
|
|
4116
4898
|
};
|
|
4117
4899
|
const onDrop = (e) => {
|
|
4118
4900
|
e.preventDefault();
|
|
4119
4901
|
setDragOver(false);
|
|
4120
|
-
const
|
|
4121
|
-
if (
|
|
4902
|
+
const files = Array.from(e.dataTransfer.files ?? []);
|
|
4903
|
+
if (files.length > 0) onFiles(files);
|
|
4122
4904
|
};
|
|
4123
4905
|
const applyUrl = async () => {
|
|
4124
|
-
const
|
|
4125
|
-
if (
|
|
4906
|
+
const urls = urlVal.split(/\s+/).map((u) => u.trim()).filter((u) => u !== "");
|
|
4907
|
+
if (urls.length === 0) {
|
|
4908
|
+
setUrlErr(t("ruleUrlBadHttp"));
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
if (urls.some((u) => !/^https?:\/\//i.test(u))) {
|
|
4126
4912
|
setUrlErr(t("ruleUrlBadHttp"));
|
|
4127
4913
|
return;
|
|
4128
4914
|
}
|
|
4129
4915
|
setUrlBusy(true);
|
|
4130
4916
|
setUrlErr(null);
|
|
4131
|
-
const
|
|
4917
|
+
const first = rule.images.length;
|
|
4918
|
+
for (const u of urls) {
|
|
4919
|
+
const res = await p.addRuleImageFromUrl(rule.id, u);
|
|
4920
|
+
if (!res.ok) {
|
|
4921
|
+
setUrlBusy(false);
|
|
4922
|
+
setUrlErr(res.error === "invalid url" || res.error === "unsupported scheme" ? t("ruleUrlBadHttp") : res.error ?? t("ruleUrlFail"));
|
|
4923
|
+
return;
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4132
4926
|
setUrlBusy(false);
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
} else setUrlErr(res.error === "invalid url" || res.error === "unsupported scheme" ? t("ruleUrlBadHttp") : res.error ?? t("ruleUrlFail"));
|
|
4927
|
+
setUrlOpen(false);
|
|
4928
|
+
setUrlVal("");
|
|
4929
|
+
setSel(first);
|
|
4137
4930
|
};
|
|
4138
4931
|
const onExtract = async () => {
|
|
4139
|
-
if (url === null || extracting) return;
|
|
4932
|
+
if (url === null || extracting || image === void 0) return;
|
|
4140
4933
|
setExtracting(true);
|
|
4141
4934
|
try {
|
|
4142
|
-
const ok = await p.extractColor(rule.id);
|
|
4935
|
+
const ok = await p.extractColor(rule.id, image.slot);
|
|
4143
4936
|
notify(ok ? t("extractDone") : t("extractFail"), ok);
|
|
4144
4937
|
} catch {
|
|
4145
4938
|
notify(t("extractFail"), false);
|
|
@@ -4154,17 +4947,37 @@ window.__ModuleLoader__.load({
|
|
|
4154
4947
|
* extract button runs and returns false when nothing vivid was found.)
|
|
4155
4948
|
*/
|
|
4156
4949
|
const onPickColor = async () => {
|
|
4157
|
-
if (url !== null) {
|
|
4950
|
+
if (url !== null && image !== void 0) {
|
|
4158
4951
|
setExtracting(true);
|
|
4159
4952
|
try {
|
|
4160
|
-
if (await p.extractColor(rule.id)) return;
|
|
4953
|
+
if (await p.extractColor(rule.id, image.slot)) return;
|
|
4161
4954
|
} catch {} finally {
|
|
4162
4955
|
setExtracting(false);
|
|
4163
4956
|
}
|
|
4164
4957
|
}
|
|
4165
|
-
|
|
4958
|
+
setColor(SEED_COLOR);
|
|
4166
4959
|
};
|
|
4167
4960
|
const cls = `dab-rule${active ? " is-active" : ""}${rule.enabled ? "" : " is-off"}`;
|
|
4961
|
+
const rotatable = rule.images.length >= 2;
|
|
4962
|
+
const preset = ROTATE_PRESETS.find((x) => x.ms === rule.rotate.intervalMs);
|
|
4963
|
+
const selSlot = image?.slot ?? null;
|
|
4964
|
+
const moveSel = (dir) => {
|
|
4965
|
+
if (selSlot !== null) p.moveRuleImage(rule.id, selSlot, dir);
|
|
4966
|
+
};
|
|
4967
|
+
const removeSel = () => {
|
|
4968
|
+
if (selSlot === null) return;
|
|
4969
|
+
const at = selIdx;
|
|
4970
|
+
p.removeRuleImage(rule.id, selSlot);
|
|
4971
|
+
setSel(Math.max(0, Math.min(at, rule.images.length - 2)));
|
|
4972
|
+
};
|
|
4973
|
+
/** Commit the custom dwell time the moment it is typed (seconds → ms). */
|
|
4974
|
+
const commitSeconds = () => {
|
|
4975
|
+
if (secDraft === null) return;
|
|
4976
|
+
const secs = Number(secDraft.trim());
|
|
4977
|
+
setSecDraft(null);
|
|
4978
|
+
if (!Number.isFinite(secs) || secs <= 0) return;
|
|
4979
|
+
p.setRuleRotation(rule.id, { intervalMs: Math.round(secs * 1e3) });
|
|
4980
|
+
};
|
|
4168
4981
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4169
4982
|
className: cls,
|
|
4170
4983
|
children: [
|
|
@@ -4232,33 +5045,107 @@ window.__ModuleLoader__.load({
|
|
|
4232
5045
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4233
5046
|
className: "dab-rule-cols",
|
|
4234
5047
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
4235
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4236
|
-
className:
|
|
4237
|
-
|
|
5048
|
+
url !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5049
|
+
className: "dab-rule-thumb",
|
|
5050
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
5051
|
+
src: url,
|
|
5052
|
+
alt: "",
|
|
5053
|
+
draggable: false
|
|
5054
|
+
})
|
|
5055
|
+
}) : rule.images.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5056
|
+
type: "button",
|
|
5057
|
+
className: `dab-rule-thumb dab-rule-thumb-add${dragOver ? " is-over" : ""}`,
|
|
5058
|
+
title: t("rotAddImage"),
|
|
4238
5059
|
onDragOver: (e) => {
|
|
4239
5060
|
e.preventDefault();
|
|
4240
5061
|
setDragOver(true);
|
|
4241
5062
|
},
|
|
4242
5063
|
onDragLeave: () => setDragOver(false),
|
|
4243
5064
|
onDrop,
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
5065
|
+
onClick: () => {
|
|
5066
|
+
setReplace(false);
|
|
5067
|
+
fileRef.current?.click();
|
|
5068
|
+
},
|
|
5069
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5070
|
+
className: "dab-rule-thumb-empty",
|
|
5071
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rotAddImage") })]
|
|
5072
|
+
})
|
|
5073
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5074
|
+
className: "dab-rule-thumb",
|
|
5075
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4249
5076
|
className: "dab-rule-thumb-empty",
|
|
4250
5077
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("ruleNoImage") })]
|
|
4251
5078
|
})
|
|
4252
5079
|
}),
|
|
5080
|
+
rule.images.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5081
|
+
className: `dab-strip${dragOver ? " is-over" : ""}`,
|
|
5082
|
+
onDragOver: (e) => {
|
|
5083
|
+
e.preventDefault();
|
|
5084
|
+
setDragOver(true);
|
|
5085
|
+
},
|
|
5086
|
+
onDragLeave: () => setDragOver(false),
|
|
5087
|
+
onDrop,
|
|
5088
|
+
children: [rule.images.map((img, i) => {
|
|
5089
|
+
const thumb = p.imageOf(img.slot);
|
|
5090
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5091
|
+
type: "button",
|
|
5092
|
+
className: `dab-strip-item${i === selIdx ? " is-sel" : ""}${liveIndex === i ? " is-live" : ""}`,
|
|
5093
|
+
title: `${i + 1} / ${rule.images.length}`,
|
|
5094
|
+
onClick: () => setSel(i),
|
|
5095
|
+
children: [
|
|
5096
|
+
thumb !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
5097
|
+
src: thumb,
|
|
5098
|
+
alt: "",
|
|
5099
|
+
draggable: false
|
|
5100
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-strip-wait" }),
|
|
5101
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5102
|
+
className: "dab-strip-num",
|
|
5103
|
+
children: i + 1
|
|
5104
|
+
}),
|
|
5105
|
+
img.color !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5106
|
+
className: "dab-strip-dot",
|
|
5107
|
+
title: toHex(hslToRgb(img.color[0], img.color[1], img.color[2])).toUpperCase(),
|
|
5108
|
+
style: { background: `hsl(${img.color[0]} ${Math.round(img.color[1] * 100)}% ${Math.round(img.color[2] * 100)}%)` }
|
|
5109
|
+
}) : null,
|
|
5110
|
+
liveIndex === i ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5111
|
+
className: "dab-strip-live",
|
|
5112
|
+
title: t("rotShowing")
|
|
5113
|
+
}) : null
|
|
5114
|
+
]
|
|
5115
|
+
}, img.slot);
|
|
5116
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5117
|
+
type: "button",
|
|
5118
|
+
className: "dab-strip-add",
|
|
5119
|
+
title: t("rotAddImage"),
|
|
5120
|
+
onClick: () => {
|
|
5121
|
+
setReplace(false);
|
|
5122
|
+
fileRef.current?.click();
|
|
5123
|
+
},
|
|
5124
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 16 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rotAddImage") })]
|
|
5125
|
+
})]
|
|
5126
|
+
}) : null,
|
|
4253
5127
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4254
5128
|
className: "dab-chip-row",
|
|
4255
5129
|
style: { marginTop: 10 },
|
|
4256
5130
|
children: [
|
|
5131
|
+
rule.images.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5132
|
+
className: "dab-strip-pos",
|
|
5133
|
+
children: [
|
|
5134
|
+
t("rotPos"),
|
|
5135
|
+
" ",
|
|
5136
|
+
selIdx + 1,
|
|
5137
|
+
" / ",
|
|
5138
|
+
rule.images.length
|
|
5139
|
+
]
|
|
5140
|
+
}) : null,
|
|
4257
5141
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4258
5142
|
type: "button",
|
|
4259
5143
|
className: "dab-btn",
|
|
4260
|
-
onClick: () =>
|
|
4261
|
-
|
|
5144
|
+
onClick: () => {
|
|
5145
|
+
setReplace(false);
|
|
5146
|
+
fileRef.current?.click();
|
|
5147
|
+
},
|
|
5148
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 13 }), t("rotAddImage")]
|
|
4262
5149
|
}),
|
|
4263
5150
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4264
5151
|
type: "button",
|
|
@@ -4266,16 +5153,51 @@ window.__ModuleLoader__.load({
|
|
|
4266
5153
|
onClick: () => setUrlOpen((o) => !o),
|
|
4267
5154
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkIcon, { size: 13 }), t("ruleFromUrl")]
|
|
4268
5155
|
}),
|
|
5156
|
+
selSlot !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5157
|
+
type: "button",
|
|
5158
|
+
className: "dab-btn dab-btn-ghost",
|
|
5159
|
+
onClick: () => {
|
|
5160
|
+
setReplace(true);
|
|
5161
|
+
fileRef.current?.click();
|
|
5162
|
+
},
|
|
5163
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 13 }), t("rotReplaceImage")]
|
|
5164
|
+
}) : null,
|
|
4269
5165
|
url !== null && rule.bgMode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4270
5166
|
type: "button",
|
|
4271
5167
|
className: "dab-btn",
|
|
4272
5168
|
onClick: () => setEditorOpen(true),
|
|
4273
5169
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 13 }), t("ruleFramingEdit")]
|
|
4274
5170
|
}) : null,
|
|
4275
|
-
|
|
5171
|
+
rule.images.length >= 2 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5173
|
+
type: "button",
|
|
5174
|
+
className: "dab-icon-btn",
|
|
5175
|
+
disabled: selIdx === 0,
|
|
5176
|
+
title: t("rotImageEarlier"),
|
|
5177
|
+
onClick: () => moveSel(-1),
|
|
5178
|
+
children: "↑"
|
|
5179
|
+
}),
|
|
5180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5181
|
+
type: "button",
|
|
5182
|
+
className: "dab-icon-btn",
|
|
5183
|
+
disabled: selIdx === rule.images.length - 1,
|
|
5184
|
+
title: t("rotImageLater"),
|
|
5185
|
+
onClick: () => moveSel(1),
|
|
5186
|
+
children: "↓"
|
|
5187
|
+
}),
|
|
5188
|
+
selIdx !== 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5189
|
+
type: "button",
|
|
5190
|
+
className: "dab-btn dab-btn-ghost",
|
|
5191
|
+
onClick: () => {
|
|
5192
|
+
if (selSlot !== null) p.setCurrentImage(rule.id, selSlot);
|
|
5193
|
+
},
|
|
5194
|
+
children: t("rotMakeFirst")
|
|
5195
|
+
}) : null
|
|
5196
|
+
] }) : null,
|
|
5197
|
+
selSlot !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4276
5198
|
type: "button",
|
|
4277
5199
|
className: "dab-btn dab-btn-ghost dab-btn-danger",
|
|
4278
|
-
onClick:
|
|
5200
|
+
onClick: removeSel,
|
|
4279
5201
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 }), t("ruleImageRemove")]
|
|
4280
5202
|
}) : null
|
|
4281
5203
|
]
|
|
@@ -4330,11 +5252,150 @@ window.__ModuleLoader__.load({
|
|
|
4330
5252
|
},
|
|
4331
5253
|
children: urlErr
|
|
4332
5254
|
}) : null,
|
|
4333
|
-
|
|
5255
|
+
rule.images.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5256
|
+
className: "dab-hint",
|
|
5257
|
+
style: { marginTop: 8 },
|
|
5258
|
+
children: rule.color === null ? t("ruleEmptyHint") : t("ruleColorOnlyHint")
|
|
5259
|
+
}) : null,
|
|
5260
|
+
rule.images.length > 0 && url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4334
5261
|
className: "dab-hint",
|
|
4335
5262
|
style: { marginTop: 8 },
|
|
4336
|
-
children: t("
|
|
5263
|
+
children: t("ruleImagePendingHint")
|
|
4337
5264
|
}) : null,
|
|
5265
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5266
|
+
style: { marginTop: 14 },
|
|
5267
|
+
children: [
|
|
5268
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5269
|
+
className: "dab-rule-section-title",
|
|
5270
|
+
children: t("rotTitle")
|
|
5271
|
+
}),
|
|
5272
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5273
|
+
className: "dab-chip-row",
|
|
5274
|
+
style: { alignItems: "center" },
|
|
5275
|
+
children: [
|
|
5276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5277
|
+
type: "button",
|
|
5278
|
+
className: `dab-toggle${rule.rotate.enabled ? " is-on" : ""}`,
|
|
5279
|
+
role: "switch",
|
|
5280
|
+
"aria-checked": rule.rotate.enabled,
|
|
5281
|
+
title: t("rotEnable"),
|
|
5282
|
+
disabled: !rotatable,
|
|
5283
|
+
onClick: () => p.setRuleRotation(rule.id, { enabled: !rule.rotate.enabled }),
|
|
5284
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5285
|
+
}),
|
|
5286
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5287
|
+
className: "dab-hint",
|
|
5288
|
+
style: { padding: 0 },
|
|
5289
|
+
children: t("rotEnable")
|
|
5290
|
+
}),
|
|
5291
|
+
!rotatable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5292
|
+
className: "dab-hint",
|
|
5293
|
+
style: { padding: 0 },
|
|
5294
|
+
children: t("rotNeedTwo")
|
|
5295
|
+
}) : null
|
|
5296
|
+
]
|
|
5297
|
+
}),
|
|
5298
|
+
rotatable && rule.rotate.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5299
|
+
className: "dab-rot-panel",
|
|
5300
|
+
children: [
|
|
5301
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5302
|
+
className: "dab-rule-section-title",
|
|
5303
|
+
children: t("rotEvery")
|
|
5304
|
+
}),
|
|
5305
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5306
|
+
className: "dab-chip-row",
|
|
5307
|
+
children: [
|
|
5308
|
+
ROTATE_PRESETS.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5309
|
+
type: "button",
|
|
5310
|
+
className: `dab-chip${item.ms === rule.rotate.intervalMs ? " is-active" : ""}`,
|
|
5311
|
+
onClick: () => p.setRuleRotation(rule.id, { intervalMs: item.ms }),
|
|
5312
|
+
children: t(item.key)
|
|
5313
|
+
}, item.ms)),
|
|
5314
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5315
|
+
type: "number",
|
|
5316
|
+
className: "dab-num dab-num-sec",
|
|
5317
|
+
min: 5,
|
|
5318
|
+
max: 86400,
|
|
5319
|
+
step: 5,
|
|
5320
|
+
value: secDraft ?? (preset === void 0 ? Math.round(rule.rotate.intervalMs / 1e3) : ""),
|
|
5321
|
+
placeholder: t("rotCustom"),
|
|
5322
|
+
onChange: (e) => setSecDraft(e.target.value),
|
|
5323
|
+
onBlur: commitSeconds,
|
|
5324
|
+
onKeyDown: (e) => {
|
|
5325
|
+
if (e.key === "Enter") {
|
|
5326
|
+
e.preventDefault();
|
|
5327
|
+
commitSeconds();
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
}),
|
|
5331
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5332
|
+
className: "dab-hint",
|
|
5333
|
+
style: { padding: 0 },
|
|
5334
|
+
children: t("rotSeconds")
|
|
5335
|
+
})
|
|
5336
|
+
]
|
|
5337
|
+
}),
|
|
5338
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5339
|
+
className: "dab-rule-section-title",
|
|
5340
|
+
style: { marginTop: 12 },
|
|
5341
|
+
children: t("rotOrder")
|
|
5342
|
+
}),
|
|
5343
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5344
|
+
className: "dab-chip-row",
|
|
5345
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5346
|
+
type: "button",
|
|
5347
|
+
className: `dab-chip${rule.rotate.order === "order" ? " is-active" : ""}`,
|
|
5348
|
+
onClick: () => p.setRuleRotation(rule.id, { order: "order" }),
|
|
5349
|
+
children: t("rotOrderSeq")
|
|
5350
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5351
|
+
type: "button",
|
|
5352
|
+
className: `dab-chip${rule.rotate.order === "shuffle" ? " is-active" : ""}`,
|
|
5353
|
+
onClick: () => p.setRuleRotation(rule.id, { order: "shuffle" }),
|
|
5354
|
+
children: t("rotOrderShuffle")
|
|
5355
|
+
})]
|
|
5356
|
+
}),
|
|
5357
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5358
|
+
className: "dab-chip-row",
|
|
5359
|
+
style: {
|
|
5360
|
+
marginTop: 12,
|
|
5361
|
+
alignItems: "center"
|
|
5362
|
+
},
|
|
5363
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5364
|
+
type: "button",
|
|
5365
|
+
className: `dab-toggle${rule.rotate.advanceOnSwitch ? " is-on" : ""}`,
|
|
5366
|
+
role: "switch",
|
|
5367
|
+
"aria-checked": rule.rotate.advanceOnSwitch,
|
|
5368
|
+
title: t("rotOnSwitch"),
|
|
5369
|
+
onClick: () => p.setRuleRotation(rule.id, { advanceOnSwitch: !rule.rotate.advanceOnSwitch }),
|
|
5370
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5371
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5372
|
+
className: "dab-hint",
|
|
5373
|
+
style: { padding: 0 },
|
|
5374
|
+
children: t("rotOnSwitch")
|
|
5375
|
+
})]
|
|
5376
|
+
}),
|
|
5377
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5378
|
+
className: "dab-chip-row",
|
|
5379
|
+
style: {
|
|
5380
|
+
marginTop: 12,
|
|
5381
|
+
alignItems: "center"
|
|
5382
|
+
},
|
|
5383
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5384
|
+
type: "button",
|
|
5385
|
+
className: "dab-btn",
|
|
5386
|
+
disabled: !active,
|
|
5387
|
+
onClick: () => p.rotateNow(rule.id),
|
|
5388
|
+
children: t("rotNext")
|
|
5389
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5390
|
+
className: "dab-hint",
|
|
5391
|
+
style: { padding: 0 },
|
|
5392
|
+
children: active ? `${t("rotShowing")} ${rule.images.length === 0 ? 0 : Math.min(liveIndex + 1, rule.images.length)} / ${rule.images.length}` : t("rotNextInactive")
|
|
5393
|
+
})]
|
|
5394
|
+
})
|
|
5395
|
+
]
|
|
5396
|
+
}) : null
|
|
5397
|
+
]
|
|
5398
|
+
}),
|
|
4338
5399
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4339
5400
|
style: { marginTop: 14 },
|
|
4340
5401
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -4355,7 +5416,12 @@ window.__ModuleLoader__.load({
|
|
|
4355
5416
|
className: "dab-rule-section-title",
|
|
4356
5417
|
children: t("ruleColor")
|
|
4357
5418
|
}),
|
|
4358
|
-
|
|
5419
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5420
|
+
className: "dab-hint",
|
|
5421
|
+
style: { marginTop: 6 },
|
|
5422
|
+
children: image === void 0 ? t("ruleColorTargetRule") : `${t("ruleColorTargetImage")} ${selIdx + 1} / ${rule.images.length}`
|
|
5423
|
+
}),
|
|
5424
|
+
ownColor === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4359
5425
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4360
5426
|
className: "dab-chip-row",
|
|
4361
5427
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -4377,10 +5443,18 @@ window.__ModuleLoader__.load({
|
|
|
4377
5443
|
style: { marginTop: 8 },
|
|
4378
5444
|
children: t("ruleColorNoneHint")
|
|
4379
5445
|
}),
|
|
4380
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5446
|
+
url !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4381
5447
|
className: "dab-hint",
|
|
4382
5448
|
style: { marginTop: 6 },
|
|
4383
5449
|
children: t("ruleColorPickHint")
|
|
5450
|
+
}) : image === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5451
|
+
className: "dab-hint",
|
|
5452
|
+
style: { marginTop: 6 },
|
|
5453
|
+
children: t("ruleColorNoImageHint")
|
|
5454
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5455
|
+
className: "dab-hint",
|
|
5456
|
+
style: { marginTop: 6 },
|
|
5457
|
+
children: t("ruleColorPendingImageHint")
|
|
4384
5458
|
})
|
|
4385
5459
|
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4386
5460
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4426,7 +5500,7 @@ window.__ModuleLoader__.load({
|
|
|
4426
5500
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4427
5501
|
type: "button",
|
|
4428
5502
|
className: "dab-btn dab-btn-ghost",
|
|
4429
|
-
onClick: () =>
|
|
5503
|
+
onClick: () => setColor(null),
|
|
4430
5504
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }), t("ruleColorNone")]
|
|
4431
5505
|
})
|
|
4432
5506
|
]
|
|
@@ -4439,11 +5513,11 @@ window.__ModuleLoader__.load({
|
|
|
4439
5513
|
className: "dab-swatch",
|
|
4440
5514
|
style: { background: `hsl(${sh} ${Math.round(ss * 100)}% ${Math.round(sl * 100)}%)` },
|
|
4441
5515
|
title: toHex(hslToRgb(sh, ss, sl)).toUpperCase(),
|
|
4442
|
-
onClick: () =>
|
|
5516
|
+
onClick: () => setColor([
|
|
4443
5517
|
sh,
|
|
4444
5518
|
ss,
|
|
4445
5519
|
sl
|
|
4446
|
-
]
|
|
5520
|
+
])
|
|
4447
5521
|
}, i))
|
|
4448
5522
|
})
|
|
4449
5523
|
] }),
|
|
@@ -4482,27 +5556,29 @@ window.__ModuleLoader__.load({
|
|
|
4482
5556
|
ref: fileRef,
|
|
4483
5557
|
type: "file",
|
|
4484
5558
|
accept: "image/*",
|
|
5559
|
+
multiple: true,
|
|
4485
5560
|
style: { display: "none" },
|
|
4486
5561
|
onChange: (e) => {
|
|
4487
|
-
const
|
|
4488
|
-
if (
|
|
4489
|
-
|
|
5562
|
+
const files = Array.from(e.target.files ?? []);
|
|
5563
|
+
if (files.length === 0) return;
|
|
5564
|
+
onFiles(files);
|
|
4490
5565
|
e.target.value = "";
|
|
5566
|
+
setReplace(false);
|
|
4491
5567
|
}
|
|
4492
5568
|
}),
|
|
4493
|
-
editorOpen && url !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
|
|
5569
|
+
editorOpen && url !== null && image !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
|
|
4494
5570
|
url,
|
|
4495
|
-
state:
|
|
5571
|
+
state: image.bgState,
|
|
4496
5572
|
t,
|
|
4497
5573
|
onClose: () => setEditorOpen(false),
|
|
4498
5574
|
onCommit: (z, x, y, iw, ih) => {
|
|
4499
|
-
p.
|
|
5575
|
+
p.setImageFraming(rule.id, image.slot, {
|
|
4500
5576
|
zoom: z,
|
|
4501
5577
|
x,
|
|
4502
5578
|
y,
|
|
4503
5579
|
iw,
|
|
4504
5580
|
ih
|
|
4505
|
-
}
|
|
5581
|
+
});
|
|
4506
5582
|
setEditorOpen(false);
|
|
4507
5583
|
}
|
|
4508
5584
|
}) : null,
|
|
@@ -4523,6 +5599,8 @@ window.__ModuleLoader__.load({
|
|
|
4523
5599
|
function ProfilePage({ p, notify }) {
|
|
4524
5600
|
const { t, exportTheme, importTheme } = p;
|
|
4525
5601
|
const importRef = (0, react.useRef)(null);
|
|
5602
|
+
p.useStore((s) => s.rev + s.rulesRev);
|
|
5603
|
+
const holidays = cfg.holidays;
|
|
4526
5604
|
const onImport = async (file) => {
|
|
4527
5605
|
try {
|
|
4528
5606
|
const ok = await importTheme(file);
|
|
@@ -4550,11 +5628,31 @@ window.__ModuleLoader__.load({
|
|
|
4550
5628
|
})
|
|
4551
5629
|
]
|
|
4552
5630
|
}),
|
|
5631
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
5632
|
+
className: "dab-card dab-rise",
|
|
5633
|
+
style: { "--d": 1 },
|
|
5634
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5635
|
+
className: "dab-chip-row",
|
|
5636
|
+
style: { alignItems: "center" },
|
|
5637
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5638
|
+
type: "button",
|
|
5639
|
+
className: `dab-toggle${holidays.enabled ? " is-on" : ""}`,
|
|
5640
|
+
role: "switch",
|
|
5641
|
+
"aria-checked": holidays.enabled,
|
|
5642
|
+
title: t("holidayEnable"),
|
|
5643
|
+
onClick: () => p.setHolidaysEnabled(!holidays.enabled),
|
|
5644
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5645
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5646
|
+
className: "dab-holiday-label",
|
|
5647
|
+
children: t("holidayTitle")
|
|
5648
|
+
})]
|
|
5649
|
+
})
|
|
5650
|
+
}),
|
|
4553
5651
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4554
|
-
className: "dab-profile-grid",
|
|
5652
|
+
className: "dab-profile-grid dab-rise",
|
|
5653
|
+
style: { "--d": 2 },
|
|
4555
5654
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4556
|
-
className: "dab-card dab-card-hover
|
|
4557
|
-
style: { "--d": 1 },
|
|
5655
|
+
className: "dab-card dab-card-hover",
|
|
4558
5656
|
children: [
|
|
4559
5657
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4560
5658
|
className: "dab-profile-ico",
|
|
@@ -4579,8 +5677,7 @@ window.__ModuleLoader__.load({
|
|
|
4579
5677
|
})
|
|
4580
5678
|
]
|
|
4581
5679
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4582
|
-
className: "dab-card dab-card-hover
|
|
4583
|
-
style: { "--d": 2 },
|
|
5680
|
+
className: "dab-card dab-card-hover",
|
|
4584
5681
|
children: [
|
|
4585
5682
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4586
5683
|
className: "dab-profile-ico",
|
|
@@ -4766,6 +5863,35 @@ window.__ModuleLoader__.load({
|
|
|
4766
5863
|
if (ctx.theme.getTheme().preference === CUSTOM_ID) ctx.theme.setTheme("system");
|
|
4767
5864
|
} catch {}
|
|
4768
5865
|
};
|
|
5866
|
+
/**
|
|
5867
|
+
* Re-emit the interface palette for whatever is painted RIGHT NOW.
|
|
5868
|
+
*
|
|
5869
|
+
* Called from two places, and the second one is why this is a function: a
|
|
5870
|
+
* model switch (`applyActive`) and a ROTATION STEP (`paintImage`). A theme color
|
|
5871
|
+
* belongs to an image since 0.7.1, so stepping to the next picture changes the
|
|
5872
|
+
* palette as much as switching models does — and a step that only repainted the
|
|
5873
|
+
* wallpaper would leave the previous picture's color on the interface until
|
|
5874
|
+
* something else happened to re-run an apply (dragging the wheel, switching
|
|
5875
|
+
* models), which is precisely the "it only updates when I touch the color wheel"
|
|
5876
|
+
* report this plugin already had once.
|
|
5877
|
+
*
|
|
5878
|
+
* The debounce is for the OTHER caller: a color drag runs a full apply dozens of
|
|
5879
|
+
* times per second, and the host-visible skin switch (dispose + register +
|
|
5880
|
+
* activate) must not happen on every pointer move. A rotation step pays the same
|
|
5881
|
+
* 60 ms, which is invisible next to a dwell time of 5 s or more.
|
|
5882
|
+
*/
|
|
5883
|
+
const applyPalette = () => {
|
|
5884
|
+
const color = activeColor();
|
|
5885
|
+
if (skinTimer !== null) window.clearTimeout(skinTimer);
|
|
5886
|
+
if (color === null) {
|
|
5887
|
+
skinTimer = null;
|
|
5888
|
+
dropCustom();
|
|
5889
|
+
} else skinTimer = window.setTimeout(() => {
|
|
5890
|
+
skinTimer = null;
|
|
5891
|
+
registerCustom(color[0], color[1], color[2]);
|
|
5892
|
+
}, 60);
|
|
5893
|
+
syncHostScheme();
|
|
5894
|
+
};
|
|
4769
5895
|
ctx.effect(() => () => {
|
|
4770
5896
|
if (skinTimer !== null) window.clearTimeout(skinTimer);
|
|
4771
5897
|
customDispose?.();
|
|
@@ -4793,9 +5919,13 @@ window.__ModuleLoader__.load({
|
|
|
4793
5919
|
modelSource: "session",
|
|
4794
5920
|
modelNote: "waiting",
|
|
4795
5921
|
activeRuleId: null,
|
|
4796
|
-
matched: false
|
|
5922
|
+
matched: false,
|
|
5923
|
+
rotIndex: 0,
|
|
5924
|
+
rotTotal: 0,
|
|
5925
|
+
rotating: false,
|
|
5926
|
+
hostStale: false
|
|
4797
5927
|
}),
|
|
4798
|
-
actions: { sync: (d, url, r, rr, model, source, note, id, matched) => {
|
|
5928
|
+
actions: { sync: (d, url, r, rr, model, source, note, id, matched, rotIndex, rotTotal, rotating, hostStale) => {
|
|
4799
5929
|
if (r > d.rev) {
|
|
4800
5930
|
d.url = url;
|
|
4801
5931
|
d.rev = r;
|
|
@@ -4806,12 +5936,31 @@ window.__ModuleLoader__.load({
|
|
|
4806
5936
|
d.modelNote = note;
|
|
4807
5937
|
d.activeRuleId = id;
|
|
4808
5938
|
d.matched = matched;
|
|
5939
|
+
d.rotIndex = rotIndex;
|
|
5940
|
+
d.rotTotal = rotTotal;
|
|
5941
|
+
d.rotating = rotating;
|
|
5942
|
+
d.hostStale = hostStale;
|
|
4809
5943
|
} }
|
|
4810
5944
|
});
|
|
4811
5945
|
let bound = null;
|
|
5946
|
+
/** Rotation facts of whatever is painted now, for the section's readout. */
|
|
5947
|
+
const rotationFacts = () => {
|
|
5948
|
+
const rule = activeRule();
|
|
5949
|
+
if (rule === null) return {
|
|
5950
|
+
index: 0,
|
|
5951
|
+
total: 0,
|
|
5952
|
+
rotating: false
|
|
5953
|
+
};
|
|
5954
|
+
return {
|
|
5955
|
+
index: imageIndexOf(rule),
|
|
5956
|
+
total: rule.images.length,
|
|
5957
|
+
rotating: isRotating(rule.images.length, rule.rotate.enabled, rule.rotate.intervalMs) && !document.hidden
|
|
5958
|
+
};
|
|
5959
|
+
};
|
|
4812
5960
|
const sync = () => {
|
|
4813
5961
|
rev++;
|
|
4814
|
-
|
|
5962
|
+
const rot = rotationFacts();
|
|
5963
|
+
bound?.sync(rWp(), rev, rulesRev, modelLabel !== "" ? modelLabel : modelText, modelSource, modelNote, activeRuleId, activeMatched, rot.index, rot.total, rot.rotating, hostIsLegacy());
|
|
4815
5964
|
};
|
|
4816
5965
|
/**
|
|
4817
5966
|
* Publish the scheme the host's own preference resolves to. The theme service
|
|
@@ -4827,23 +5976,149 @@ window.__ModuleLoader__.load({
|
|
|
4827
5976
|
setHostScheme(null);
|
|
4828
5977
|
}
|
|
4829
5978
|
};
|
|
4830
|
-
/**
|
|
4831
|
-
|
|
4832
|
-
|
|
5979
|
+
/**
|
|
5980
|
+
* Which rule wins for the current model RIGHT NOW, and whether it was a
|
|
5981
|
+
* substring hit (a holiday override is never a "match" of the model).
|
|
5982
|
+
*
|
|
5983
|
+
* Read by the repaint AND by the panel's write path, which has to know whether
|
|
5984
|
+
* an edit changed the answer: `color`, `enabled` and `match` all feed the
|
|
5985
|
+
* resolution (a rule with no image still paints from its own color — see
|
|
5986
|
+
* `ruleCanPaint`). Without this, editing a rule that was not the active one —
|
|
5987
|
+
* giving an emptied rule its first color, typing a match string that should
|
|
5988
|
+
* promote it — changed nothing on screen until some unrelated event happened to
|
|
5989
|
+
* re-run the resolution.
|
|
5990
|
+
*/
|
|
5991
|
+
const resolveRule = () => {
|
|
5992
|
+
const holiday = activeHolidayRule();
|
|
5993
|
+
return holiday !== null ? {
|
|
5994
|
+
rule: holiday,
|
|
5995
|
+
matched: false
|
|
5996
|
+
} : matchRule(cfg.rules, modelText);
|
|
5997
|
+
};
|
|
5998
|
+
/** Id of the rule that would paint right now; `null` = the host's own look. */
|
|
5999
|
+
const winnerId = () => resolveRule().rule?.id ?? null;
|
|
6000
|
+
/**
|
|
6001
|
+
* Close a rule edit: repaint when it could have moved what is on screen, and
|
|
6002
|
+
* merely refresh the panel's readout when it could not.
|
|
6003
|
+
*
|
|
6004
|
+
* `winnerBefore` is sampled by the caller BEFORE its mutation (`winnerId()`),
|
|
6005
|
+
* and the comparison itself lives in ./repaint, where it is checked on plain
|
|
6006
|
+
* node. Every write path below ends here — the old per-path test
|
|
6007
|
+
* (`id === activeRuleId`) missed the edit that CREATES the winner, which is the
|
|
6008
|
+
* "上传图片 / 提取主题色之后界面不动,动一下调色盘或切模型才出现" report.
|
|
6009
|
+
*/
|
|
6010
|
+
const repaintIfMoved = (id, winnerBefore) => {
|
|
6011
|
+
if (shouldRepaint(id, winnerBefore, winnerId())) applyActive();
|
|
6012
|
+
else sync();
|
|
6013
|
+
};
|
|
6014
|
+
/**
|
|
6015
|
+
* Resolve the active rule for the current model and repaint everything.
|
|
6016
|
+
*
|
|
6017
|
+
* @param advance - true only on a MODEL switch: the rotation of the rule that
|
|
6018
|
+
* took over may then step once (`rotate.advanceOnSwitch`), which is how a
|
|
6019
|
+
* model gets "a different picture every time" without any timer at all.
|
|
6020
|
+
*/
|
|
6021
|
+
const applyActive = (advance = false) => {
|
|
6022
|
+
const { rule, matched } = resolveRule();
|
|
4833
6023
|
setActive(rule === null ? null : rule.id, matched);
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
skinTimer = null;
|
|
4841
|
-
registerCustom(color[0], color[1], color[2]);
|
|
4842
|
-
}, 60);
|
|
4843
|
-
syncHostScheme();
|
|
6024
|
+
if (advance && rule !== null && rule.rotate.advanceOnSwitch) {
|
|
6025
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
6026
|
+
if (next !== null) setRotIndex(next);
|
|
6027
|
+
}
|
|
6028
|
+
scheduleRotation();
|
|
6029
|
+
applyPalette();
|
|
4844
6030
|
applyWp();
|
|
4845
6031
|
sync();
|
|
4846
6032
|
};
|
|
6033
|
+
let rotTimer = null;
|
|
6034
|
+
/** Identity of the schedule currently armed; a change means "re-arm". */
|
|
6035
|
+
let rotSig = "";
|
|
6036
|
+
/** Warm one image so a later switch fades toward bytes that are already here. */
|
|
6037
|
+
const warmImage = (rule, idx) => {
|
|
6038
|
+
const slot = rule.images[idx]?.slot;
|
|
6039
|
+
if (slot !== void 0) loadSlot(slot);
|
|
6040
|
+
};
|
|
6041
|
+
/** Show the image at `idx` at once, loading it first when it is not in memory. */
|
|
6042
|
+
const paintImage = (rule, idx) => {
|
|
6043
|
+
const slot = rule.images[idx]?.slot;
|
|
6044
|
+
if (slot === void 0) return;
|
|
6045
|
+
applyPalette();
|
|
6046
|
+
if (imageOf(slot) !== null) {
|
|
6047
|
+
applyWp();
|
|
6048
|
+
sync();
|
|
6049
|
+
return;
|
|
6050
|
+
}
|
|
6051
|
+
loadSlot(slot).then(() => {
|
|
6052
|
+
applyWp();
|
|
6053
|
+
sync();
|
|
6054
|
+
});
|
|
6055
|
+
};
|
|
6056
|
+
const rotateTick = () => {
|
|
6057
|
+
const rule = activeRule();
|
|
6058
|
+
if (rule === null) return;
|
|
6059
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
6060
|
+
if (next === null) {
|
|
6061
|
+
scheduleRotation(true);
|
|
6062
|
+
return;
|
|
6063
|
+
}
|
|
6064
|
+
setRotIndex(next);
|
|
6065
|
+
paintImage(rule, next);
|
|
6066
|
+
const after = nextIndex(rule.images.length, rule.rotate.order, next);
|
|
6067
|
+
if (after !== null) warmImage(rule, after);
|
|
6068
|
+
};
|
|
6069
|
+
/**
|
|
6070
|
+
* Arm (or re-arm) the rotation for the active rule.
|
|
6071
|
+
*
|
|
6072
|
+
* Idempotent by SIGNATURE rather than by "already armed": a slider drag on the
|
|
6073
|
+
* active rule re-runs the whole apply dozens of times per second, and restarting
|
|
6074
|
+
* the interval on each of those would mean the dwell timer never elapses while
|
|
6075
|
+
* the user is dragging (and a 10 s rotation would never advance). The signature
|
|
6076
|
+
* carries exactly the inputs the schedule depends on, so a real change — another
|
|
6077
|
+
* rule, another interval, the tab becoming visible again — re-arms while a
|
|
6078
|
+
* repaint does not.
|
|
6079
|
+
*/
|
|
6080
|
+
const scheduleRotation = (force = false) => {
|
|
6081
|
+
const rule = activeRule();
|
|
6082
|
+
const sig = rule === null ? "" : [
|
|
6083
|
+
rule.id,
|
|
6084
|
+
rule.rotate.enabled,
|
|
6085
|
+
rule.rotate.intervalMs,
|
|
6086
|
+
rule.rotate.order,
|
|
6087
|
+
rule.images.length,
|
|
6088
|
+
document.hidden
|
|
6089
|
+
].join("|");
|
|
6090
|
+
if (!force && sig === rotSig) return;
|
|
6091
|
+
rotSig = sig;
|
|
6092
|
+
if (rotTimer !== null) {
|
|
6093
|
+
window.clearInterval(rotTimer);
|
|
6094
|
+
rotTimer = null;
|
|
6095
|
+
}
|
|
6096
|
+
if (rule === null || document.hidden) return;
|
|
6097
|
+
if (!isRotating(rule.images.length, rule.rotate.enabled, rule.rotate.intervalMs)) return;
|
|
6098
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
6099
|
+
if (next !== null) warmImage(rule, next);
|
|
6100
|
+
rotTimer = window.setInterval(rotateTick, rule.rotate.intervalMs);
|
|
6101
|
+
};
|
|
6102
|
+
ctx.effect(() => () => {
|
|
6103
|
+
if (rotTimer !== null) {
|
|
6104
|
+
window.clearInterval(rotTimer);
|
|
6105
|
+
rotTimer = null;
|
|
6106
|
+
}
|
|
6107
|
+
}, "dsh-background-by-model: rotation timer");
|
|
6108
|
+
const onVisibility = () => {
|
|
6109
|
+
scheduleRotation();
|
|
6110
|
+
sync();
|
|
6111
|
+
};
|
|
6112
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
6113
|
+
ctx.effect(() => () => document.removeEventListener("visibilitychange", onVisibility), "dsh-background-by-model: rotation visibility");
|
|
6114
|
+
/**
|
|
6115
|
+
* The holiday the background must follow right now, or null.
|
|
6116
|
+
*
|
|
6117
|
+
* The decision itself is `pickHoliday` (pure, and covered by
|
|
6118
|
+
* `scripts/holiday-check.ts`); this only supplies the two things it cannot
|
|
6119
|
+
* know — today's holiday from the calendar, and whether the slot has bytes.
|
|
6120
|
+
*/
|
|
6121
|
+
const activeHolidayRule = () => pickHoliday(cfg.holidays.enabled, cfg.holidays.items, activeHoliday(), (slot) => imageOf(slot) !== null);
|
|
4847
6122
|
syncHostScheme();
|
|
4848
6123
|
applyWp();
|
|
4849
6124
|
sync();
|
|
@@ -4857,7 +6132,7 @@ window.__ModuleLoader__.load({
|
|
|
4857
6132
|
modelSource = source;
|
|
4858
6133
|
modelNote = note;
|
|
4859
6134
|
setModelLabel(label !== "" ? label : text);
|
|
4860
|
-
if (textChanged) applyActive();
|
|
6135
|
+
if (textChanged) applyActive(true);
|
|
4861
6136
|
else sync();
|
|
4862
6137
|
});
|
|
4863
6138
|
ctx.effect(() => () => offModel(), "dsh-background-by-model: model watch");
|
|
@@ -4873,18 +6148,89 @@ window.__ModuleLoader__.load({
|
|
|
4873
6148
|
applyActive();
|
|
4874
6149
|
}
|
|
4875
6150
|
})();
|
|
6151
|
+
let holidayKey = activeHoliday() ?? "";
|
|
6152
|
+
const holidayTimer = window.setInterval(() => {
|
|
6153
|
+
const next = activeHoliday() ?? "";
|
|
6154
|
+
if (next === holidayKey) return;
|
|
6155
|
+
holidayKey = next;
|
|
6156
|
+
const slot = todayHolidaySlot();
|
|
6157
|
+
if (slot === null) applyActive();
|
|
6158
|
+
else loadSlot(slot).then(() => applyActive());
|
|
6159
|
+
}, 3e4);
|
|
6160
|
+
ctx.effect(() => () => {
|
|
6161
|
+
window.clearInterval(holidayTimer);
|
|
6162
|
+
}, "dsh-background-by-model: holiday rollover");
|
|
6163
|
+
/** Read one slot's bytes into the cache unless they are already there. */
|
|
6164
|
+
const loadSlot = async (slot) => {
|
|
6165
|
+
if (imageOf(slot) !== null) return;
|
|
6166
|
+
const url = await readImage(slot);
|
|
6167
|
+
if (url !== null) setImage(slot, url);
|
|
6168
|
+
};
|
|
6169
|
+
/**
|
|
6170
|
+
* The slot of the holiday that could paint TODAY, or null.
|
|
6171
|
+
*
|
|
6172
|
+
* Gated on the CALENDAR, not merely on the switch. Now that the override is on
|
|
6173
|
+
* by default and invisible, a profile that simply is not on a holiday must not
|
|
6174
|
+
* pull ~730 KB of bundled art (≈970 KB of base64 over this channel) at every
|
|
6175
|
+
* boot just because the feature is armed.
|
|
6176
|
+
*/
|
|
6177
|
+
const todayHolidaySlot = () => {
|
|
6178
|
+
if (!cfg.holidays.enabled) return null;
|
|
6179
|
+
const id = activeHoliday();
|
|
6180
|
+
if (id === null) return null;
|
|
6181
|
+
const item = holidayById(id);
|
|
6182
|
+
if (item === null || !item.enabled) return null;
|
|
6183
|
+
return ruleSlots(item)[0] ?? null;
|
|
6184
|
+
};
|
|
6185
|
+
/**
|
|
6186
|
+
* The slots boot hydrates, in no particular order: the FIRST image of every
|
|
6187
|
+
* rule, today's holiday, and the image the current model paints.
|
|
6188
|
+
*
|
|
6189
|
+
* That first-image set is exactly what this plugin loaded per rule before
|
|
6190
|
+
* multi-image existed — a rule card draws its first image as its thumbnail, so
|
|
6191
|
+
* the settings page looks the same as it always did. Everything past the first
|
|
6192
|
+
* image of a rule is read ON DEMAND: by the rotation (which warms the image it
|
|
6193
|
+
* is about to need as soon as its schedule is armed) and by the settings page
|
|
6194
|
+
* when a card is expanded. Hydrating every image of every rule here would mean
|
|
6195
|
+
* twenty full-size data URLs over this channel before the first frame on a
|
|
6196
|
+
* five-rule setup with four pictures each, which is the one cost this feature
|
|
6197
|
+
* must not pay.
|
|
6198
|
+
*
|
|
6199
|
+
* The slot list the store knows about (`readPersisted().slots`) is deliberately
|
|
6200
|
+
* NOT hydrated any more: an orphaned slot that no rule references has no card to
|
|
6201
|
+
* appear on, so pulling its bytes over the channel at boot is pure waste. It
|
|
6202
|
+
* stays on disk — and is still reported by the node half — until a rule
|
|
6203
|
+
* references it again or it is deleted with its rule.
|
|
6204
|
+
*/
|
|
6205
|
+
const bootSlots = () => {
|
|
6206
|
+
const holiday = todayHolidaySlot();
|
|
6207
|
+
const firsts = cfg.rules.map((r) => r.images[0]?.slot).filter((slot) => slot !== void 0);
|
|
6208
|
+
const current = prioritySlot();
|
|
6209
|
+
return Array.from(/* @__PURE__ */ new Set([
|
|
6210
|
+
...firsts,
|
|
6211
|
+
...holiday === null ? [] : [holiday],
|
|
6212
|
+
...current === null ? [] : [current]
|
|
6213
|
+
]));
|
|
6214
|
+
};
|
|
6215
|
+
/**
|
|
6216
|
+
* The slot that paints before anything else: the holiday in force when there
|
|
6217
|
+
* is one, otherwise the FIRST image of the current model's rule. A holiday
|
|
6218
|
+
* outranks the rule here for the same reason it outranks it when painting.
|
|
6219
|
+
*
|
|
6220
|
+
* Always the first image (never the one `advanceOnSwitch` would step to): boot
|
|
6221
|
+
* paints without an advance, and hydrating an image the first frame is not going
|
|
6222
|
+
* to use would leave the frame blank until the real one arrived.
|
|
6223
|
+
*/
|
|
6224
|
+
const prioritySlot = () => todayHolidaySlot() ?? matchRule(cfg.rules, modelText).rule?.images[0]?.slot ?? null;
|
|
4876
6225
|
(async () => {
|
|
4877
6226
|
const persisted = await loadPersisted();
|
|
4878
6227
|
if (persisted !== null) {
|
|
4879
6228
|
adoptConfig(persisted.config);
|
|
4880
|
-
const
|
|
4881
|
-
const
|
|
4882
|
-
const first = priority === null ? null : priority.slot;
|
|
6229
|
+
const first = prioritySlot();
|
|
6230
|
+
const slots = bootSlots();
|
|
4883
6231
|
const order = first === null ? slots : [first, ...slots.filter((s) => s !== first)];
|
|
4884
6232
|
for (const slot of order) {
|
|
4885
|
-
|
|
4886
|
-
const url = await readImage(slot);
|
|
4887
|
-
if (url !== null) setImage(slot, url);
|
|
6233
|
+
await loadSlot(slot);
|
|
4888
6234
|
if (slot === first) applyActive();
|
|
4889
6235
|
}
|
|
4890
6236
|
}
|
|
@@ -4895,7 +6241,7 @@ window.__ModuleLoader__.load({
|
|
|
4895
6241
|
}, "dsh-background-by-model: wp cleanup");
|
|
4896
6242
|
ctx.effect(() => ctx.on("theme/change", () => {
|
|
4897
6243
|
syncHostScheme();
|
|
4898
|
-
if (
|
|
6244
|
+
if (activeColor() !== null) {
|
|
4899
6245
|
const snapshot = ctx.theme.getTheme();
|
|
4900
6246
|
if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
|
|
4901
6247
|
}
|
|
@@ -4926,34 +6272,47 @@ window.__ModuleLoader__.load({
|
|
|
4926
6272
|
en
|
|
4927
6273
|
}), "dsh-background-by-model: i18n");
|
|
4928
6274
|
/**
|
|
4929
|
-
* The automatic half of "extract from image": fill
|
|
4930
|
-
*
|
|
4931
|
-
*
|
|
4932
|
-
*
|
|
6275
|
+
* The automatic half of "extract from image": fill an IMAGE's theme color from
|
|
6276
|
+
* its own bytes when it has none of its own, and only then — a color the user
|
|
6277
|
+
* picked (or a previous extraction produced) is never overwritten, so this
|
|
6278
|
+
* cannot fight a deliberate choice.
|
|
6279
|
+
*
|
|
6280
|
+
* `slot` names the image that just arrived, because a rule's images each carry
|
|
6281
|
+
* their own palette since 0.7.1 and the one the user just added is the one they
|
|
6282
|
+
* are looking at. Omitted (or unknown) falls back to the rule's first image.
|
|
6283
|
+
*
|
|
6284
|
+
* The question is asked of the IMAGE, not of the rule: a rule whose pictures
|
|
6285
|
+
* were all cleared and then re-filled must theme each new picture, and a rule
|
|
6286
|
+
* with a color of its own still has no say over the colors of its pictures.
|
|
4933
6287
|
*
|
|
4934
6288
|
* The decode is asynchronous, so the "still empty?" question is asked AGAIN on
|
|
4935
6289
|
* arrival: a color chosen while it ran must win over the one being computed.
|
|
4936
6290
|
*/
|
|
4937
|
-
const maybeAutoExtract = async (id) => {
|
|
6291
|
+
const maybeAutoExtract = async (id, slot) => {
|
|
4938
6292
|
if (!cfg.autoExtract) return;
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
6293
|
+
/** The entry this extraction is about, re-resolved on every read. */
|
|
6294
|
+
const target = (rule) => {
|
|
6295
|
+
if (rule === null) return void 0;
|
|
6296
|
+
return (slot === void 0 ? void 0 : rule.images.find((i) => i.slot === slot)) ?? rule.images[0];
|
|
6297
|
+
};
|
|
6298
|
+
const image = target(ruleById(id));
|
|
6299
|
+
if (image === void 0 || image.color !== null) return;
|
|
6300
|
+
const url = displayImageOf(image.slot);
|
|
4942
6301
|
if (url === null) return;
|
|
4943
6302
|
let hsl = null;
|
|
4944
6303
|
try {
|
|
4945
|
-
hsl = await extractWallpaperColor(url,
|
|
6304
|
+
hsl = await extractWallpaperColor(url, image.bgState);
|
|
4946
6305
|
} catch {
|
|
4947
6306
|
hsl = null;
|
|
4948
6307
|
}
|
|
4949
6308
|
if (hsl === null) return;
|
|
4950
|
-
const current = ruleById(id);
|
|
4951
|
-
if (current ===
|
|
6309
|
+
const current = target(ruleById(id));
|
|
6310
|
+
if (current === void 0 || current.color !== null) return;
|
|
6311
|
+
const winner = winnerId();
|
|
4952
6312
|
current.color = hsl;
|
|
4953
6313
|
rulesRev++;
|
|
4954
6314
|
saveConfig();
|
|
4955
|
-
|
|
4956
|
-
else sync();
|
|
6315
|
+
repaintIfMoved(id, winner);
|
|
4957
6316
|
};
|
|
4958
6317
|
const activeLang = () => {
|
|
4959
6318
|
try {
|
|
@@ -4984,7 +6343,7 @@ window.__ModuleLoader__.load({
|
|
|
4984
6343
|
readModelFacts,
|
|
4985
6344
|
imageOf: (slot) => displayImageOf(slot),
|
|
4986
6345
|
addRule: () => {
|
|
4987
|
-
const rule = newRule(nextRuleId()
|
|
6346
|
+
const rule = newRule(nextRuleId());
|
|
4988
6347
|
cfg.rules.push(rule);
|
|
4989
6348
|
rulesRev++;
|
|
4990
6349
|
persistConfig();
|
|
@@ -4995,9 +6354,13 @@ window.__ModuleLoader__.load({
|
|
|
4995
6354
|
const idx = cfg.rules.findIndex((r) => r.id === id);
|
|
4996
6355
|
if (idx < 0) return;
|
|
4997
6356
|
const [rule] = cfg.rules.splice(idx, 1);
|
|
4998
|
-
if (rule !== void 0
|
|
4999
|
-
|
|
5000
|
-
|
|
6357
|
+
if (rule !== void 0) {
|
|
6358
|
+
const stillUsed = takenSlots();
|
|
6359
|
+
for (const slot of ruleSlots(rule)) {
|
|
6360
|
+
if (stillUsed.has(slot)) continue;
|
|
6361
|
+
setImage(slot, null);
|
|
6362
|
+
deleteImage(slot);
|
|
6363
|
+
}
|
|
5001
6364
|
}
|
|
5002
6365
|
rulesRev++;
|
|
5003
6366
|
persistConfig();
|
|
@@ -5016,50 +6379,183 @@ window.__ModuleLoader__.load({
|
|
|
5016
6379
|
setRule: (id, patch) => {
|
|
5017
6380
|
const rule = ruleById(id);
|
|
5018
6381
|
if (rule === null) return;
|
|
6382
|
+
const winner = winnerId();
|
|
5019
6383
|
Object.assign(rule, patch);
|
|
5020
|
-
|
|
5021
|
-
|
|
6384
|
+
normalizeRuleInPlace(rule);
|
|
6385
|
+
rulesRev++;
|
|
6386
|
+
saveConfig();
|
|
6387
|
+
repaintIfMoved(id, winner);
|
|
6388
|
+
},
|
|
6389
|
+
addRuleImages: (id, dataUrls) => {
|
|
6390
|
+
const rule = ruleById(id);
|
|
6391
|
+
if (rule === null || dataUrls.length === 0) return;
|
|
6392
|
+
const winner = winnerId();
|
|
6393
|
+
for (const dataUrl of dataUrls) {
|
|
6394
|
+
const slot = nextSlot();
|
|
6395
|
+
const image = {
|
|
6396
|
+
slot,
|
|
6397
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
6398
|
+
color: null
|
|
6399
|
+
};
|
|
6400
|
+
rule.images.push(image);
|
|
6401
|
+
setImage(slot, dataUrl);
|
|
6402
|
+
writeImage(slot, dataUrl);
|
|
6403
|
+
}
|
|
6404
|
+
rulesRev++;
|
|
6405
|
+
persistConfig();
|
|
6406
|
+
repaintIfMoved(id, winner);
|
|
6407
|
+
maybeAutoExtract(id, rule.images[rule.images.length - 1]?.slot);
|
|
6408
|
+
},
|
|
6409
|
+
removeRuleImage: (id, slot) => {
|
|
6410
|
+
const rule = ruleById(id);
|
|
6411
|
+
if (rule === null) return;
|
|
6412
|
+
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
6413
|
+
if (idx < 0) return;
|
|
6414
|
+
const winner = winnerId();
|
|
6415
|
+
rule.images.splice(idx, 1);
|
|
6416
|
+
if (id === activeRuleId) setRotIndex(0);
|
|
6417
|
+
rulesRev++;
|
|
6418
|
+
persistConfig();
|
|
6419
|
+
if (!takenSlots().has(slot)) {
|
|
6420
|
+
setImage(slot, null);
|
|
6421
|
+
deleteImage(slot);
|
|
6422
|
+
}
|
|
6423
|
+
repaintIfMoved(id, winner);
|
|
6424
|
+
},
|
|
6425
|
+
moveRuleImage: (id, slot, dir) => {
|
|
6426
|
+
const rule = ruleById(id);
|
|
6427
|
+
if (rule === null) return;
|
|
6428
|
+
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
6429
|
+
const to = idx + dir;
|
|
6430
|
+
if (idx < 0 || to < 0 || to >= rule.images.length) return;
|
|
6431
|
+
const winner = winnerId();
|
|
6432
|
+
const [image] = rule.images.splice(idx, 1);
|
|
6433
|
+
rule.images.splice(to, 0, image);
|
|
6434
|
+
rulesRev++;
|
|
6435
|
+
if (id === activeRuleId) {
|
|
6436
|
+
setRotIndex(Math.max(0, to));
|
|
6437
|
+
scheduleRotation(true);
|
|
6438
|
+
}
|
|
6439
|
+
persistConfig();
|
|
6440
|
+
repaintIfMoved(id, winner);
|
|
6441
|
+
},
|
|
6442
|
+
setCurrentImage: (id, slot) => {
|
|
6443
|
+
const rule = ruleById(id);
|
|
6444
|
+
if (rule === null) return;
|
|
6445
|
+
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
6446
|
+
if (idx < 0) return;
|
|
6447
|
+
const winner = winnerId();
|
|
6448
|
+
const [image] = rule.images.splice(idx, 1);
|
|
6449
|
+
rule.images.unshift(image);
|
|
6450
|
+
rulesRev++;
|
|
6451
|
+
if (id === activeRuleId) {
|
|
6452
|
+
setRotIndex(0);
|
|
6453
|
+
scheduleRotation(true);
|
|
6454
|
+
}
|
|
6455
|
+
persistConfig();
|
|
6456
|
+
repaintIfMoved(id, winner);
|
|
6457
|
+
},
|
|
6458
|
+
setImageFraming: (id, slot, bgState) => {
|
|
6459
|
+
const rule = ruleById(id);
|
|
6460
|
+
const image = rule?.images.find((i) => i.slot === slot);
|
|
6461
|
+
if (image === void 0) return;
|
|
6462
|
+
const winner = winnerId();
|
|
6463
|
+
image.bgState = bgState;
|
|
6464
|
+
normalizeRuleInPlace(rule);
|
|
5022
6465
|
rulesRev++;
|
|
5023
6466
|
saveConfig();
|
|
5024
|
-
|
|
5025
|
-
else sync();
|
|
6467
|
+
repaintIfMoved(id, winner);
|
|
5026
6468
|
},
|
|
5027
|
-
|
|
6469
|
+
setRuleRotation: (id, patch) => {
|
|
5028
6470
|
const rule = ruleById(id);
|
|
5029
6471
|
if (rule === null) return;
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
6472
|
+
patchRotation(rule, patch);
|
|
6473
|
+
rulesRev++;
|
|
6474
|
+
persistConfig();
|
|
6475
|
+
scheduleRotation();
|
|
6476
|
+
sync();
|
|
6477
|
+
},
|
|
6478
|
+
rotateNow: (id) => {
|
|
6479
|
+
const rule = ruleById(id);
|
|
6480
|
+
if (rule === null || id !== activeRuleId) return;
|
|
6481
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
6482
|
+
if (next === null) return;
|
|
6483
|
+
setRotIndex(next);
|
|
6484
|
+
paintImage(rule, next);
|
|
6485
|
+
scheduleRotation(true);
|
|
6486
|
+
},
|
|
6487
|
+
loadRuleImages: async (id) => {
|
|
6488
|
+
const rule = ruleById(id);
|
|
6489
|
+
if (rule === null) return;
|
|
6490
|
+
for (const slot of ruleSlots(rule)) await loadSlot(slot);
|
|
6491
|
+
sync();
|
|
6492
|
+
},
|
|
6493
|
+
setRuleImage: (id, slot, dataUrl) => {
|
|
6494
|
+
const rule = ruleById(id);
|
|
6495
|
+
if (rule === null || !rule.images.some((i) => i.slot === slot)) return;
|
|
6496
|
+
const winner = winnerId();
|
|
6497
|
+
setImage(slot, dataUrl);
|
|
6498
|
+
dataUrl === null ? deleteImage(slot) : writeImage(slot, dataUrl);
|
|
6499
|
+
repaintIfMoved(id, winner);
|
|
6500
|
+
if (dataUrl !== null) maybeAutoExtract(id, slot);
|
|
5035
6501
|
},
|
|
5036
|
-
|
|
6502
|
+
addRuleImageFromUrl: async (id, url) => {
|
|
5037
6503
|
const rule = ruleById(id);
|
|
5038
6504
|
if (rule === null) return {
|
|
5039
6505
|
ok: false,
|
|
5040
6506
|
error: "unknown rule"
|
|
5041
6507
|
};
|
|
5042
|
-
const
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
6508
|
+
const slot = nextSlot();
|
|
6509
|
+
const res = await fetchImageUrl(slot, url);
|
|
6510
|
+
if (!res.ok) return res;
|
|
6511
|
+
const winner = winnerId();
|
|
6512
|
+
rule.images.push({
|
|
6513
|
+
slot,
|
|
6514
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
6515
|
+
color: null
|
|
6516
|
+
});
|
|
6517
|
+
setImage(slot, res.dataUrl ?? null);
|
|
6518
|
+
rulesRev++;
|
|
6519
|
+
persistConfig();
|
|
6520
|
+
repaintIfMoved(id, winner);
|
|
6521
|
+
maybeAutoExtract(id, slot);
|
|
5049
6522
|
return res;
|
|
5050
6523
|
},
|
|
5051
|
-
|
|
6524
|
+
/**
|
|
6525
|
+
* Store ONE image's theme color (null = that image follows the system
|
|
6526
|
+
* theme). Addressed by slot exactly like `setImageFraming`, because the
|
|
6527
|
+
* color belongs to the image: editing image 3's color must not rewrite
|
|
6528
|
+
* image 1's, and it must not touch the RULE's color either — that one is
|
|
6529
|
+
* what the rule paints once its last picture is gone, and it is edited only
|
|
6530
|
+
* while there is no image to edit (the panel's own branch).
|
|
6531
|
+
*/
|
|
6532
|
+
setImageColor: (id, slot, color) => {
|
|
6533
|
+
const rule = ruleById(id);
|
|
6534
|
+
const image = rule?.images.find((i) => i.slot === slot);
|
|
6535
|
+
if (rule === null || rule === void 0 || image === void 0) return;
|
|
6536
|
+
const winner = winnerId();
|
|
6537
|
+
image.color = color;
|
|
6538
|
+
normalizeRuleInPlace(rule);
|
|
6539
|
+
rulesRev++;
|
|
6540
|
+
saveConfig();
|
|
6541
|
+
repaintIfMoved(id, winner);
|
|
6542
|
+
},
|
|
6543
|
+
extractColor: async (id, slot) => {
|
|
5052
6544
|
const rule = ruleById(id);
|
|
5053
6545
|
if (rule === null) return false;
|
|
5054
|
-
const
|
|
6546
|
+
const image = rule.images.find((i) => i.slot === slot) ?? rule.images[0];
|
|
6547
|
+
if (image === void 0) return false;
|
|
6548
|
+
const url = displayImageOf(image.slot);
|
|
5055
6549
|
if (url === null) return false;
|
|
5056
|
-
const hsl = await extractWallpaperColor(url,
|
|
6550
|
+
const hsl = await extractWallpaperColor(url, image.bgState);
|
|
5057
6551
|
if (hsl === null) return false;
|
|
5058
|
-
|
|
6552
|
+
const current = ruleById(id)?.images.find((i) => i.slot === image.slot);
|
|
6553
|
+
if (current === void 0) return false;
|
|
6554
|
+
const winner = winnerId();
|
|
6555
|
+
current.color = hsl;
|
|
5059
6556
|
rulesRev++;
|
|
5060
6557
|
saveConfig();
|
|
5061
|
-
|
|
5062
|
-
else sync();
|
|
6558
|
+
repaintIfMoved(id, winner);
|
|
5063
6559
|
return true;
|
|
5064
6560
|
},
|
|
5065
6561
|
setOps: (ops) => {
|
|
@@ -5089,14 +6585,22 @@ window.__ModuleLoader__.load({
|
|
|
5089
6585
|
saveConfig();
|
|
5090
6586
|
sync();
|
|
5091
6587
|
},
|
|
6588
|
+
setHolidaysEnabled: (v) => {
|
|
6589
|
+
cfg.holidays.enabled = v;
|
|
6590
|
+
persistConfig();
|
|
6591
|
+
const slot = todayHolidaySlot();
|
|
6592
|
+
if (v && slot !== null) loadSlot(slot).then(() => applyActive());
|
|
6593
|
+
applyActive();
|
|
6594
|
+
},
|
|
5092
6595
|
exportTheme: () => {
|
|
5093
6596
|
const images = {};
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
6597
|
+
const slots = cfg.rules.flatMap(ruleSlots);
|
|
6598
|
+
for (const slot of slots) {
|
|
6599
|
+
const url = imageOf(slot);
|
|
6600
|
+
if (url !== null) images[slot] = url;
|
|
5097
6601
|
}
|
|
5098
6602
|
const payload = {
|
|
5099
|
-
version:
|
|
6603
|
+
version: 5,
|
|
5100
6604
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5101
6605
|
config: cfg,
|
|
5102
6606
|
images
|
|
@@ -5118,15 +6622,15 @@ window.__ModuleLoader__.load({
|
|
|
5118
6622
|
adoptConfig(d.config);
|
|
5119
6623
|
const incoming = d.images ?? {};
|
|
5120
6624
|
const keep = /* @__PURE__ */ new Set();
|
|
5121
|
-
for (const
|
|
5122
|
-
keep.add(
|
|
5123
|
-
const raw = incoming[
|
|
6625
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
6626
|
+
keep.add(slot);
|
|
6627
|
+
const raw = incoming[slot];
|
|
5124
6628
|
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
5125
|
-
setImage(
|
|
5126
|
-
writeImage(
|
|
6629
|
+
setImage(slot, raw);
|
|
6630
|
+
writeImage(slot, raw);
|
|
5127
6631
|
} else {
|
|
5128
|
-
setImage(
|
|
5129
|
-
deleteImage(
|
|
6632
|
+
setImage(slot, null);
|
|
6633
|
+
deleteImage(slot);
|
|
5130
6634
|
}
|
|
5131
6635
|
}
|
|
5132
6636
|
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && /^[A-Za-z0-9_-]{1,32}$/.test(slot)) deleteImage(slot);
|
|
@@ -5198,7 +6702,7 @@ window.__ModuleLoader__.load({
|
|
|
5198
6702
|
}, "dsh-background-by-model: nav icon watch");
|
|
5199
6703
|
const restoreSaved = () => {
|
|
5200
6704
|
syncHostScheme();
|
|
5201
|
-
const color =
|
|
6705
|
+
const color = activeColor();
|
|
5202
6706
|
if (color !== null) {
|
|
5203
6707
|
const snapshot = ctx.theme.getTheme();
|
|
5204
6708
|
if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) registerCustom(color[0], color[1], color[2]);
|
|
@@ -5212,7 +6716,7 @@ window.__ModuleLoader__.load({
|
|
|
5212
6716
|
}, "dsh-background-by-model: boot restore");
|
|
5213
6717
|
const watchdogId = window.setInterval(() => {
|
|
5214
6718
|
syncHostScheme();
|
|
5215
|
-
const color =
|
|
6719
|
+
const color = activeColor();
|
|
5216
6720
|
if (color === null) {
|
|
5217
6721
|
refreshSystemTheme();
|
|
5218
6722
|
return;
|
|
@@ -5239,12 +6743,6 @@ window.__ModuleLoader__.load({
|
|
|
5239
6743
|
window.addEventListener("pagehide", onPageHide);
|
|
5240
6744
|
ctx.effect(() => () => window.removeEventListener("pagehide", onPageHide), "dsh-background-by-model: pagehide flush");
|
|
5241
6745
|
}
|
|
5242
|
-
/** Color of the currently active rule, or null when it uses the system theme. */
|
|
5243
|
-
function activeRuleColor() {
|
|
5244
|
-
if (activeRuleId === null) return null;
|
|
5245
|
-
const rule = ruleById(activeRuleId);
|
|
5246
|
-
return rule === null ? null : rule.color;
|
|
5247
|
-
}
|
|
5248
6746
|
//#endregion
|
|
5249
6747
|
exports.apply = apply;
|
|
5250
6748
|
exports.inject = inject;
|