dsh-background-by-model 0.6.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -1
- package/CHANGELOG.zh.md +40 -1
- package/README.md +63 -21
- package/README.zh.md +65 -23
- package/lib/client.js +1413 -204
- package/lib/client.js.map +1 -1
- package/lib/index.js +150 -21
- package/package.json +5 -3
package/lib/client.js
CHANGED
|
@@ -25,7 +25,7 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
pageModelBg: "模型背景",
|
|
26
26
|
pageProfile: "配置",
|
|
27
27
|
descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感。这里的设置对所有模型生效",
|
|
28
|
-
descModelBg: "每条规则 = 一个匹配串 +
|
|
28
|
+
descModelBg: "每条规则 = 一个匹配串 + 一组背景图与外观。切换模型时,从上往下找第一条匹配串出现在模型名里的规则;都没中就使用第 1 条。一条规则可以放多张图,按你设的节奏轮换",
|
|
29
29
|
descProfile: "把当前全部规则连同图片导出备份,或从文件一键恢复",
|
|
30
30
|
uiTitle: "主界面",
|
|
31
31
|
uiOpacity: "透明度",
|
|
@@ -44,17 +44,15 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
statusNoteNoSession: "会话服务已就绪,但还没有当前会话 id",
|
|
45
45
|
statusNoteNoProjection: "拿不到本会话的模型选择投影(binding / projections 不可用)",
|
|
46
46
|
statusNoteEmptySelection: "本会话的模型选择投影还没有值(切一次模型即可写入)",
|
|
47
|
-
|
|
48
|
-
rulesHint: "从上到下即为优先级。规则 1 同时兼任兜底:所有匹配串都没命中时使用它",
|
|
47
|
+
hostStaleHint: "检测到 DSH 主进程仍在运行旧版插件:新版配置格式它还读不懂,为避免丢规则,本次只改界面不写入配置。重启 DSH 后多图与轮换即可正常保存",
|
|
49
48
|
tryoutTitle: "匹配试跑",
|
|
50
49
|
tryoutPlaceholder: "输入模型名,如 deepseek-flash",
|
|
51
50
|
tryoutUseCurrent: "填入当前模型",
|
|
52
51
|
tryoutHit: "命中规则",
|
|
53
52
|
tryoutFallback: "无匹配 · 走兜底规则",
|
|
54
|
-
tryoutNone: "
|
|
53
|
+
tryoutNone: "没有可用规则(规则需要启用,并且有图片或主题色)",
|
|
55
54
|
tryoutEmpty: "输入模型名即可试跑",
|
|
56
55
|
autoExtract: "选图后自动取主题色",
|
|
57
|
-
autoExtractHint: "只在规则还没有颜色时自动取色,绝不覆盖你手动选的颜色",
|
|
58
56
|
ruleAdd: "新增规则",
|
|
59
57
|
ruleFallbackBadge: "兜底",
|
|
60
58
|
ruleMatchPlaceholder: "如 flash / glm(忽略大小写,留空则不参与匹配)",
|
|
@@ -63,8 +61,6 @@ window.__ModuleLoader__.load({
|
|
|
63
61
|
ruleDown: "下移",
|
|
64
62
|
ruleRemove: "删除规则",
|
|
65
63
|
ruleNoImage: "尚未选择图片",
|
|
66
|
-
rulePickImage: "选择图片",
|
|
67
|
-
ruleChangeImage: "更换图片",
|
|
68
64
|
ruleFromUrl: "从网址",
|
|
69
65
|
ruleUrlPlaceholder: "粘贴图片网址 https://…",
|
|
70
66
|
ruleUrlApply: "应用",
|
|
@@ -72,19 +68,49 @@ window.__ModuleLoader__.load({
|
|
|
72
68
|
ruleUrlApplying: "加载中…",
|
|
73
69
|
ruleUrlBadHttp: "仅支持 http/https 图片网址",
|
|
74
70
|
ruleUrlFail: "获取图片失败",
|
|
75
|
-
ruleImageRemove: "
|
|
76
|
-
ruleEmptyHint: "
|
|
71
|
+
ruleImageRemove: "移除",
|
|
72
|
+
ruleEmptyHint: "这条规则还没有图片,也没有自己的主题色:没有可显示的内容,匹配和兜底都会跳过它",
|
|
73
|
+
ruleColorOnlyHint: "这条规则还没有图片:不会铺壁纸,界面只用这个主题色",
|
|
74
|
+
ruleImagePendingHint: "这张图片还没有取到内容,稍后会自己出现(也可以直接替换它)",
|
|
77
75
|
ruleColor: "主题色",
|
|
78
76
|
ruleColorNone: "跟随系统主题",
|
|
79
77
|
ruleColorNoneHint: "未设颜色时跟随系统主题:界面用宿主的配色,壁纸与各部位的透明度、模糊照旧生效",
|
|
80
78
|
ruleColorPick: "自定义颜色",
|
|
81
79
|
ruleColorPickHint: "优先从本图取色,取不到就用默认色",
|
|
80
|
+
ruleColorNoImageHint: "这条规则还没有图片,取不到色:选一个颜色可以只铺主题色,或先添加图片",
|
|
81
|
+
ruleColorPendingImageHint: "这张图片还没有取到内容,暂时取不了色:可以直接选一个颜色,或等它加载完",
|
|
82
82
|
ruleColorExtract: "从本图提取",
|
|
83
|
+
ruleColorTargetImage: "作用于选中的图片:",
|
|
84
|
+
ruleColorTargetRule: "作用于本规则(它还没有图片)",
|
|
83
85
|
ruleLayout: "布局模式",
|
|
84
86
|
ruleFramingEdit: "编辑位置",
|
|
85
87
|
ruleOpacity: "背景透明度",
|
|
86
88
|
ruleBlur: "背景模糊",
|
|
87
89
|
ruleBlurHint: "背景模糊作用于壁纸层;界面页里的模糊作用于界面各区域,两者互不影响",
|
|
90
|
+
rotTitle: "多图轮换",
|
|
91
|
+
rotEnable: "轮换这组图",
|
|
92
|
+
rotNeedTwo: "至少两张图才能轮换",
|
|
93
|
+
rotEvery: "每张停留",
|
|
94
|
+
rotCustom: "自定义秒数",
|
|
95
|
+
rotSeconds: "秒",
|
|
96
|
+
rotOrder: "换图顺序",
|
|
97
|
+
rotOrderSeq: "按顺序",
|
|
98
|
+
rotOrderShuffle: "随机",
|
|
99
|
+
rotOnSwitch: "切换模型时也换一张",
|
|
100
|
+
rotNext: "下一张",
|
|
101
|
+
rotNextInactive: "只有正在生效的规则能换画面上的图",
|
|
102
|
+
rotPos: "第",
|
|
103
|
+
rotShowing: "正在显示",
|
|
104
|
+
rotAddImage: "添加",
|
|
105
|
+
rotReplaceImage: "替换",
|
|
106
|
+
rotDragHint: "拖动排序",
|
|
107
|
+
rotDblHint: "双击切换",
|
|
108
|
+
rotEvery10s: "10 秒",
|
|
109
|
+
rotEvery30s: "30 秒",
|
|
110
|
+
rotEvery1m: "1 分钟",
|
|
111
|
+
rotEvery5m: "5 分钟",
|
|
112
|
+
rotEvery30m: "30 分钟",
|
|
113
|
+
rotEvery1h: "1 小时",
|
|
88
114
|
bgModeFit: "适应",
|
|
89
115
|
bgModeFill: "填充",
|
|
90
116
|
bgModeStretch: "拉伸",
|
|
@@ -125,7 +151,7 @@ window.__ModuleLoader__.load({
|
|
|
125
151
|
pageModelBg: "Model background",
|
|
126
152
|
pageProfile: "Profile",
|
|
127
153
|
descInterface: "Tune opacity and blur per surface to build depth. These settings are global and apply to every model",
|
|
128
|
-
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",
|
|
154
|
+
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",
|
|
129
155
|
descProfile: "Back up every rule together with its image, or restore one from a file",
|
|
130
156
|
uiTitle: "Interface",
|
|
131
157
|
uiOpacity: "Opacity",
|
|
@@ -144,17 +170,15 @@ window.__ModuleLoader__.load({
|
|
|
144
170
|
statusNoteNoSession: "The sessions service is up but reports no current session yet",
|
|
145
171
|
statusNoteNoProjection: "This session's model-selection projection is unreachable (no binding / projections)",
|
|
146
172
|
statusNoteEmptySelection: "The model-selection projection exists but carries no value yet (selecting a model once writes it)",
|
|
147
|
-
|
|
148
|
-
rulesHint: "Top to bottom is the priority. Rule 1 doubles as the fallback — it is used when no match string hits",
|
|
173
|
+
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",
|
|
149
174
|
tryoutTitle: "Match test",
|
|
150
175
|
tryoutPlaceholder: "Type a model name, e.g. deepseek-flash",
|
|
151
176
|
tryoutUseCurrent: "Use current model",
|
|
152
177
|
tryoutHit: "matches rule",
|
|
153
178
|
tryoutFallback: "no match · fallback rule",
|
|
154
|
-
tryoutNone: "No usable rule (a rule needs to be enabled and
|
|
179
|
+
tryoutNone: "No usable rule (a rule needs to be enabled and carry an image or a theme color)",
|
|
155
180
|
tryoutEmpty: "Type a model name to test the list",
|
|
156
181
|
autoExtract: "Extract the theme color when an image is chosen",
|
|
157
|
-
autoExtractHint: "Only fills a rule that has no color yet — a color you picked is never overwritten",
|
|
158
182
|
ruleAdd: "Add rule",
|
|
159
183
|
ruleFallbackBadge: "fallback",
|
|
160
184
|
ruleMatchPlaceholder: "e.g. flash / glm (case-insensitive, empty never matches)",
|
|
@@ -163,8 +187,6 @@ window.__ModuleLoader__.load({
|
|
|
163
187
|
ruleDown: "Move down",
|
|
164
188
|
ruleRemove: "Remove rule",
|
|
165
189
|
ruleNoImage: "No image yet",
|
|
166
|
-
rulePickImage: "Choose image",
|
|
167
|
-
ruleChangeImage: "Replace image",
|
|
168
190
|
ruleFromUrl: "From URL",
|
|
169
191
|
ruleUrlPlaceholder: "Paste an image URL https://…",
|
|
170
192
|
ruleUrlApply: "Apply",
|
|
@@ -172,19 +194,49 @@ window.__ModuleLoader__.load({
|
|
|
172
194
|
ruleUrlApplying: "Loading…",
|
|
173
195
|
ruleUrlBadHttp: "Only http/https image URLs are supported",
|
|
174
196
|
ruleUrlFail: "Could not fetch the image",
|
|
175
|
-
ruleImageRemove: "Remove
|
|
176
|
-
ruleEmptyHint: "This rule has no image
|
|
197
|
+
ruleImageRemove: "Remove",
|
|
198
|
+
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",
|
|
199
|
+
ruleColorOnlyHint: "No image in this rule yet: no wallpaper is shown, and the interface is painted from this theme color alone",
|
|
200
|
+
ruleImagePendingHint: "This image has no content yet — it appears once it loads; you can also replace it",
|
|
177
201
|
ruleColor: "Theme color",
|
|
178
202
|
ruleColorNone: "Follow system theme",
|
|
179
203
|
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",
|
|
180
204
|
ruleColorPick: "Custom color",
|
|
181
205
|
ruleColorPickHint: "Extracts from this image when it can, otherwise uses a default color",
|
|
206
|
+
ruleColorNoImageHint: "This rule has no image to take a color from: pick a color to tint the interface alone, or add an image first",
|
|
207
|
+
ruleColorPendingImageHint: "This image has no content yet, so there is nothing to extract from: pick a color directly, or wait for it to load",
|
|
208
|
+
ruleColorTargetImage: "Applies to the selected image:",
|
|
209
|
+
ruleColorTargetRule: "Applies to this rule (it has no image yet)",
|
|
182
210
|
ruleColorExtract: "Extract from image",
|
|
183
211
|
ruleLayout: "Layout mode",
|
|
184
212
|
ruleFramingEdit: "Edit position",
|
|
185
213
|
ruleOpacity: "Background opacity",
|
|
186
214
|
ruleBlur: "Background blur",
|
|
187
215
|
ruleBlurHint: "Background blur affects the wallpaper layer; the blur on the Interface page affects interface surfaces — they are independent",
|
|
216
|
+
rotTitle: "Image rotation",
|
|
217
|
+
rotEnable: "Rotate these images",
|
|
218
|
+
rotNeedTwo: "At least two images are needed to rotate",
|
|
219
|
+
rotEvery: "Dwell time per image",
|
|
220
|
+
rotCustom: "Custom seconds",
|
|
221
|
+
rotSeconds: "s",
|
|
222
|
+
rotOrder: "Order",
|
|
223
|
+
rotOrderSeq: "In order",
|
|
224
|
+
rotOrderShuffle: "Shuffle",
|
|
225
|
+
rotOnSwitch: "Also step on a model switch",
|
|
226
|
+
rotNext: "Next image",
|
|
227
|
+
rotNextInactive: "Only the active rule can step the background",
|
|
228
|
+
rotPos: "Image",
|
|
229
|
+
rotShowing: "On screen",
|
|
230
|
+
rotAddImage: "Add",
|
|
231
|
+
rotReplaceImage: "Replace",
|
|
232
|
+
rotDragHint: "drag to reorder",
|
|
233
|
+
rotDblHint: "double-click to show",
|
|
234
|
+
rotEvery10s: "10 s",
|
|
235
|
+
rotEvery30s: "30 s",
|
|
236
|
+
rotEvery1m: "1 min",
|
|
237
|
+
rotEvery5m: "5 min",
|
|
238
|
+
rotEvery30m: "30 min",
|
|
239
|
+
rotEvery1h: "1 h",
|
|
188
240
|
bgModeFit: "Fit",
|
|
189
241
|
bgModeFill: "Fill",
|
|
190
242
|
bgModeStretch: "Stretch",
|
|
@@ -351,7 +403,7 @@ window.__ModuleLoader__.load({
|
|
|
351
403
|
if (!master || todayId === null) return null;
|
|
352
404
|
const item = items.find((i) => i.id === todayId);
|
|
353
405
|
if (item === void 0 || !item.enabled) return null;
|
|
354
|
-
return hasImage(
|
|
406
|
+
return item.images.some((image) => hasImage(image.slot)) ? item : null;
|
|
355
407
|
}
|
|
356
408
|
//#endregion
|
|
357
409
|
//#region src/schema.ts
|
|
@@ -391,6 +443,26 @@ window.__ModuleLoader__.load({
|
|
|
391
443
|
"tile",
|
|
392
444
|
"center"
|
|
393
445
|
];
|
|
446
|
+
/** Every accepted `rotate.order`, in UI order. */
|
|
447
|
+
const ROTATE_ORDERS = ["order", "shuffle"];
|
|
448
|
+
/** Fastest allowed dwell time — below this the cross-fade never settles. */
|
|
449
|
+
const ROTATE_MIN_MS = 5e3;
|
|
450
|
+
/** Slowest allowed dwell time (24 h): one image per day is still "rotation". */
|
|
451
|
+
const ROTATE_MAX_MS = 864e5;
|
|
452
|
+
/** Longest allowed cross-fade; 0 is a hard cut. */
|
|
453
|
+
const ROTATE_FADE_MAX_MS = 3e3;
|
|
454
|
+
/** Everything off: the shipped default, and what a config predating this feature gets. */
|
|
455
|
+
const DEFAULT_ROTATION = {
|
|
456
|
+
enabled: false,
|
|
457
|
+
intervalMs: 6e4,
|
|
458
|
+
order: "order",
|
|
459
|
+
advanceOnSwitch: false,
|
|
460
|
+
fadeMs: 320
|
|
461
|
+
};
|
|
462
|
+
/** A fresh rotation block (never hand out the shared default object). */
|
|
463
|
+
function defaultRotation() {
|
|
464
|
+
return { ...DEFAULT_ROTATION };
|
|
465
|
+
}
|
|
394
466
|
/**
|
|
395
467
|
* Main interface opacities (0..1), global (Interface page):
|
|
396
468
|
* `bg` main background (`--dsw-alias-bg-base`)
|
|
@@ -442,6 +514,37 @@ window.__ModuleLoader__.load({
|
|
|
442
514
|
rightbar: 0,
|
|
443
515
|
input: 0
|
|
444
516
|
};
|
|
517
|
+
/** Every slot a rule owns, in order. The one thing the render and store layers
|
|
518
|
+
* ask of the image list, so a rule's slots are never enumerated ad hoc. */
|
|
519
|
+
function ruleSlots(rule) {
|
|
520
|
+
return rule.images.map((i) => i.slot);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Whether a rule has anything to show at all: an image, or a theme color of its
|
|
524
|
+
* own. Used by matching (`usable` in ./client/modelbg) and by the panel's copy.
|
|
525
|
+
*
|
|
526
|
+
* An image is the obvious way a rule paints, and until 0.7 it was the ONLY one —
|
|
527
|
+
* which stopped being true the moment the last image became removable. A
|
|
528
|
+
* rule-level theme color is the second: it recolors the interface through the
|
|
529
|
+
* token palette with no wallpaper involved, so a rule whose pictures were all
|
|
530
|
+
* removed still paints, and treating it as unusable made every control left on
|
|
531
|
+
* its card (the wheel, the swatches, the clear-to-system-theme button) dead — the
|
|
532
|
+
* one state where the panel was confidently showing a lie. It also silently
|
|
533
|
+
* handed the model to the NEXT rule, so clearing the default rule's images put
|
|
534
|
+
* another model's wallpaper on the screen.
|
|
535
|
+
*
|
|
536
|
+
* Note that a per-image color (0.7.1) does not enter this question at all: an
|
|
537
|
+
* image is already enough, so the answer here is about the IMAGE LIST being
|
|
538
|
+
* non-empty, never about what those images are painted in.
|
|
539
|
+
*
|
|
540
|
+
* A rule with no image and no color of its own paints nothing: the system theme
|
|
541
|
+
* is not a paint of this rule's own, so such a rule is skipped by matching, which
|
|
542
|
+
* is what makes a freshly added rule (empty, colorless) invisible until the user
|
|
543
|
+
* gives it something.
|
|
544
|
+
*/
|
|
545
|
+
function ruleCanPaint(rule) {
|
|
546
|
+
return rule.enabled && (ruleSlots(rule).length > 0 || rule.color !== null);
|
|
547
|
+
}
|
|
445
548
|
/**
|
|
446
549
|
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
447
550
|
* null when the string is not a hex color.
|
|
@@ -487,16 +590,21 @@ window.__ModuleLoader__.load({
|
|
|
487
590
|
* art was not cut for.
|
|
488
591
|
*/
|
|
489
592
|
function defaultHolidayRule(def) {
|
|
593
|
+
const color = hexToHsl(def.color);
|
|
490
594
|
return {
|
|
491
595
|
id: def.id,
|
|
492
|
-
|
|
596
|
+
images: [{
|
|
597
|
+
slot: def.slot,
|
|
598
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
599
|
+
color
|
|
600
|
+
}],
|
|
493
601
|
match: "",
|
|
494
602
|
enabled: true,
|
|
495
|
-
color
|
|
603
|
+
color,
|
|
496
604
|
bgMode: "fill",
|
|
497
605
|
wallpaperOpacity: 1,
|
|
498
606
|
blur: 0,
|
|
499
|
-
|
|
607
|
+
rotate: defaultRotation()
|
|
500
608
|
};
|
|
501
609
|
}
|
|
502
610
|
/** Every built-in holiday at its default, in `HOLIDAYS` order. */
|
|
@@ -546,34 +654,115 @@ window.__ModuleLoader__.load({
|
|
|
546
654
|
clamp$1(raw[2], 0, 1, .25)
|
|
547
655
|
];
|
|
548
656
|
}
|
|
549
|
-
/** Coerce one persisted
|
|
657
|
+
/** Coerce one persisted rotation block; a config without one gets the shipped default. */
|
|
658
|
+
function normalizeRotation(raw) {
|
|
659
|
+
const r = raw ?? {};
|
|
660
|
+
const d = DEFAULT_ROTATION;
|
|
661
|
+
return {
|
|
662
|
+
enabled: r.enabled === true,
|
|
663
|
+
intervalMs: clamp$1(r.intervalMs, ROTATE_MIN_MS, ROTATE_MAX_MS, d.intervalMs),
|
|
664
|
+
order: ROTATE_ORDERS.includes(r.order) ? r.order : d.order,
|
|
665
|
+
advanceOnSwitch: r.advanceOnSwitch === true,
|
|
666
|
+
fadeMs: clamp$1(r.fadeMs, 0, ROTATE_FADE_MAX_MS, d.fadeMs)
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/** Own-property test for values that may be anything at all (a raw JSON entry). */
|
|
670
|
+
function hasOwn(o, key) {
|
|
671
|
+
return Object.prototype.hasOwnProperty.call(o, key);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Coerce one persisted image entry, or null when it lacks a usable slot.
|
|
675
|
+
*
|
|
676
|
+
* `inherit` is the RULE's own color, and it is applied only to an entry that has
|
|
677
|
+
* no `color` key at all — the shape every release before 0.7.1 wrote, where a
|
|
678
|
+
* rule's color was the only color there was. Lifting it here, once, is what makes
|
|
679
|
+
* an upgrade a no-op on screen: a themed wallpaper keeps its theme instead of
|
|
680
|
+
* silently dropping to the system palette, and it happens in the shared sanitizer
|
|
681
|
+
* so both halves lift identically.
|
|
682
|
+
*
|
|
683
|
+
* The KEY's presence is the whole distinction, which is why `null` is not the
|
|
684
|
+
* same as absent: an entry with `color: null` is an image the user explicitly set
|
|
685
|
+
* to "follow the system theme", and re-inheriting the rule's color over that
|
|
686
|
+
* would undo a deliberate choice on every load.
|
|
687
|
+
*/
|
|
688
|
+
function normalizeImage(raw, inherit = null) {
|
|
689
|
+
const i = raw ?? {};
|
|
690
|
+
if (typeof i.slot !== "string" || !SLOT_RE.test(i.slot)) return null;
|
|
691
|
+
return {
|
|
692
|
+
slot: i.slot,
|
|
693
|
+
bgState: normalizeBgState(i.bgState),
|
|
694
|
+
color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Coerce one persisted rule, or null when it names no image AT ALL.
|
|
699
|
+
*
|
|
700
|
+
* Accepts BOTH image shapes: the 0.7 list (`images`), and the single
|
|
701
|
+
* `slot`/`bgState` pair every earlier version wrote — the latter is lifted to a
|
|
702
|
+
* one-entry list here rather than at a dozen call sites, which is what keeps the
|
|
703
|
+
* rest of the codebase free of "old config" branches. Duplicate slots inside one
|
|
704
|
+
* rule are dropped: two entries pointing at the same bytes would only make the
|
|
705
|
+
* rotation look stuck.
|
|
706
|
+
*
|
|
707
|
+
* The rule's color is resolved FIRST and handed to every image entry, because it
|
|
708
|
+
* is what an entry written before 0.7.1 inherits (`normalizeImage`). The rule keeps
|
|
709
|
+
* it as well: it is not a default for its images at paint time any more, but it
|
|
710
|
+
* is still what the rule paints once its last picture is gone.
|
|
711
|
+
*
|
|
712
|
+
* An EMPTY list is a legitimate rule, not a broken one: removing a rule's last
|
|
713
|
+
* picture has to be expressible, and forcing an entry back in is what produced a
|
|
714
|
+
* phantom blank image that took slot 1 and could never be deleted again (the next
|
|
715
|
+
* upload landed at position 2 and the rule kept painting nothing). What such a
|
|
716
|
+
* rule then does is `ruleCanPaint`'s business: with a color of its own it paints
|
|
717
|
+
* the interface, with neither it is skipped by matching — which the card's own
|
|
718
|
+
* hint says in so many words. Only a rule that names no image anywhere — no list,
|
|
719
|
+
* no legacy slot, or a slot that could never be a filename — is dropped.
|
|
720
|
+
*/
|
|
550
721
|
function normalizeRule(raw) {
|
|
551
722
|
const r = raw ?? {};
|
|
552
723
|
const id = typeof r.id === "string" && r.id !== "" ? r.id : null;
|
|
553
|
-
|
|
554
|
-
|
|
724
|
+
if (id === null) return null;
|
|
725
|
+
const color = normalizeHsl(r.color);
|
|
726
|
+
const images = [];
|
|
727
|
+
const seen = /* @__PURE__ */ new Set();
|
|
728
|
+
const hadList = Array.isArray(r.images);
|
|
729
|
+
if (hadList) for (const entry of r.images) {
|
|
730
|
+
const image = normalizeImage(entry, color);
|
|
731
|
+
if (image === null || seen.has(image.slot)) continue;
|
|
732
|
+
seen.add(image.slot);
|
|
733
|
+
images.push(image);
|
|
734
|
+
}
|
|
735
|
+
const legacySlot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
|
|
736
|
+
if (images.length === 0 && legacySlot !== null) images.push({
|
|
737
|
+
slot: legacySlot,
|
|
738
|
+
bgState: normalizeBgState(r.bgState),
|
|
739
|
+
color
|
|
740
|
+
});
|
|
741
|
+
if (images.length === 0 && !hadList) return null;
|
|
555
742
|
const mode = BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit";
|
|
556
743
|
return {
|
|
557
744
|
id,
|
|
558
|
-
|
|
745
|
+
images,
|
|
559
746
|
match: typeof r.match === "string" ? r.match : "",
|
|
560
747
|
enabled: r.enabled !== false,
|
|
561
|
-
color
|
|
748
|
+
color,
|
|
562
749
|
bgMode: mode,
|
|
563
750
|
wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
|
|
564
751
|
blur: clamp$1(r.blur, 0, 60, 0),
|
|
565
|
-
|
|
752
|
+
rotate: normalizeRotation(r.rotate)
|
|
566
753
|
};
|
|
567
754
|
}
|
|
568
755
|
/**
|
|
569
756
|
* Coerce one persisted holiday entry.
|
|
570
757
|
*
|
|
571
|
-
* `id`, `
|
|
572
|
-
* slot is the only thing tying it to
|
|
573
|
-
* part of what that holiday looks like —
|
|
574
|
-
*
|
|
575
|
-
* festival in a color it does not have.
|
|
576
|
-
* a rule's field
|
|
758
|
+
* `id`, `images` and every theme color (the rule's and each image's) come from
|
|
759
|
+
* the DEFINITION, never from disk: a holiday's slot is the only thing tying it to
|
|
760
|
+
* its bytes, and its theme color is a fixed part of what that holiday looks like —
|
|
761
|
+
* so a stale or hand-edited value would either orphan the wallpaper, point the
|
|
762
|
+
* holiday at a rule's image, or paint a festival in a color it does not have.
|
|
763
|
+
* Everything else is sanitized exactly like a rule's field — except the image
|
|
764
|
+
* list and the rotation, which stay the definition's own single image and stay
|
|
765
|
+
* off, so a festival can never be swapped or cycled.
|
|
577
766
|
*/
|
|
578
767
|
function normalizeHolidayRule(def, raw) {
|
|
579
768
|
const r = raw ?? {};
|
|
@@ -584,7 +773,11 @@ window.__ModuleLoader__.load({
|
|
|
584
773
|
bgMode: BG_MODES$1.includes(r.bgMode) ? r.bgMode : base.bgMode,
|
|
585
774
|
wallpaperOpacity: clamp01(r.wallpaperOpacity, base.wallpaperOpacity),
|
|
586
775
|
blur: clamp$1(r.blur, 0, 60, base.blur),
|
|
587
|
-
|
|
776
|
+
images: base.images.map((image) => ({
|
|
777
|
+
slot: image.slot,
|
|
778
|
+
bgState: normalizeBgState(r.images?.[0]?.bgState ?? r.bgState),
|
|
779
|
+
color: image.color
|
|
780
|
+
}))
|
|
588
781
|
};
|
|
589
782
|
}
|
|
590
783
|
/** Coerce the whole holiday block; unknown ids are dropped, missing ones defaulted. */
|
|
@@ -720,13 +913,68 @@ window.__ModuleLoader__.load({
|
|
|
720
913
|
let activeRuleId = null;
|
|
721
914
|
let activeMatched = false;
|
|
722
915
|
let modelLabel = "";
|
|
916
|
+
/**
|
|
917
|
+
* Which image of the ACTIVE rule is painted right now.
|
|
918
|
+
*
|
|
919
|
+
* Deliberately NOT persisted: a background that comes back on image 3 of 7 after
|
|
920
|
+
* a reload, with no way to say why, is worse than one that starts at the first
|
|
921
|
+
* image every time. Clamped to the rule's own length on every read, so an index
|
|
922
|
+
* left over from a longer list can never point at nothing.
|
|
923
|
+
*/
|
|
924
|
+
let rotIndex = 0;
|
|
723
925
|
function setActive(ruleId, matched) {
|
|
926
|
+
if (ruleId !== activeRuleId) rotIndex = 0;
|
|
724
927
|
activeRuleId = ruleId;
|
|
725
928
|
activeMatched = matched;
|
|
726
929
|
}
|
|
727
930
|
function setModelLabel(label) {
|
|
728
931
|
modelLabel = label;
|
|
729
932
|
}
|
|
933
|
+
/** The image index the render layer is on, clamped into `rule`'s own range. */
|
|
934
|
+
function imageIndexOf(rule) {
|
|
935
|
+
if (rule.images.length === 0) return 0;
|
|
936
|
+
const i = Math.floor(rotIndex);
|
|
937
|
+
return i >= 0 && i < rule.images.length ? i : 0;
|
|
938
|
+
}
|
|
939
|
+
/** Point the active rule at another of its images (the rotation's one mutator). */
|
|
940
|
+
function setRotIndex(i) {
|
|
941
|
+
rotIndex = Math.floor(i);
|
|
942
|
+
}
|
|
943
|
+
/** The image the active rule is painting now, or null when it has none. */
|
|
944
|
+
function activeImage() {
|
|
945
|
+
const rule = activeRule();
|
|
946
|
+
if (rule === null) return null;
|
|
947
|
+
return rule.images[imageIndexOf(rule)] ?? null;
|
|
948
|
+
}
|
|
949
|
+
/** Slot of the active rule's current image; '' when there is nothing to paint. */
|
|
950
|
+
function activeSlot() {
|
|
951
|
+
return activeImage()?.slot ?? "";
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* The theme color in force right now, or null for "follow the system theme".
|
|
955
|
+
*
|
|
956
|
+
* Two rungs, in this order and no others:
|
|
957
|
+
* 1. the color of the IMAGE that is on screen, when the rule has one;
|
|
958
|
+
* 2. the rule's own color, which is all an image-less rule has to paint with.
|
|
959
|
+
*
|
|
960
|
+
* There is deliberately no third rung where an image INHERITS its rule's color:
|
|
961
|
+
* a rule's color is not a default for its pictures (see `BgRule.color`). If it
|
|
962
|
+
* were, a rotation through a green picture, a red one and a system-themed one
|
|
963
|
+
* could not be expressed at all — clearing a picture's color would only hand it
|
|
964
|
+
* back to the rule's, and the clear button would look broken. Images that predate
|
|
965
|
+
* per-image colors get the rule's color LIFTED onto them at read time instead
|
|
966
|
+
* (`normalizeImage`), so nothing loses its theme on upgrade.
|
|
967
|
+
*
|
|
968
|
+
* The image is the one being PAINTED (`imageIndexOf`), not the one the panel has
|
|
969
|
+
* selected — the panel's selection is an editing cursor and must not repaint the
|
|
970
|
+
* interface while the user is fixing another picture's framing.
|
|
971
|
+
*/
|
|
972
|
+
function activeColor() {
|
|
973
|
+
const rule = activeRule();
|
|
974
|
+
if (rule === null) return null;
|
|
975
|
+
if (rule.images.length === 0) return rule.color;
|
|
976
|
+
return activeImage()?.color ?? null;
|
|
977
|
+
}
|
|
730
978
|
/**
|
|
731
979
|
* One rule by id — from the USER's list first, then from the holiday overrides.
|
|
732
980
|
*
|
|
@@ -745,22 +993,46 @@ window.__ModuleLoader__.load({
|
|
|
745
993
|
function holidayById(id) {
|
|
746
994
|
return cfg.holidays.items.find((r) => r.id === id) ?? null;
|
|
747
995
|
}
|
|
748
|
-
|
|
996
|
+
/**
|
|
997
|
+
* A fresh rule, holding NO image yet.
|
|
998
|
+
*
|
|
999
|
+
* It used to be born with an allocated slot and an empty entry, which is what put
|
|
1000
|
+
* a blank tile at position 1 of a new rule's strip — and, worse, stopped the
|
|
1001
|
+
* picture the user added next from being the first one (a rule paints its first
|
|
1002
|
+
* image). A rule with no pictures is a valid state: `ruleCanPaint` (./schema) skips
|
|
1003
|
+
* it — it has no image and no color yet — until either arrives, and the slot is
|
|
1004
|
+
* allocated when the first picture does.
|
|
1005
|
+
*/
|
|
1006
|
+
function newRule(id) {
|
|
749
1007
|
return {
|
|
750
1008
|
id,
|
|
751
|
-
|
|
1009
|
+
images: [],
|
|
752
1010
|
match: "",
|
|
753
1011
|
enabled: true,
|
|
754
1012
|
color: null,
|
|
755
1013
|
bgMode: "fit",
|
|
756
1014
|
wallpaperOpacity: 1,
|
|
757
1015
|
blur: 0,
|
|
758
|
-
|
|
1016
|
+
rotate: defaultRotation()
|
|
759
1017
|
};
|
|
760
1018
|
}
|
|
761
|
-
/**
|
|
1019
|
+
/** Every slot any rule owns — the user's rules and the holiday entries alike. */
|
|
1020
|
+
function takenSlots() {
|
|
1021
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1022
|
+
for (const rule of cfg.rules) for (const slot of ruleSlots(rule)) taken.add(slot);
|
|
1023
|
+
for (const rule of cfg.holidays.items) for (const slot of ruleSlots(rule)) taken.add(slot);
|
|
1024
|
+
return taken;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* First free image slot (`m1`, `m2`, …).
|
|
1028
|
+
*
|
|
1029
|
+
* Scans EVERY slot in the config, not just the primary one of each rule: with
|
|
1030
|
+
* several images per rule, "the slots this file already uses" is the only set
|
|
1031
|
+
* that can be safely handed out, and a collision here would silently point two
|
|
1032
|
+
* cards at the same bytes (and let one card's delete orphan the other).
|
|
1033
|
+
*/
|
|
762
1034
|
function nextSlot() {
|
|
763
|
-
const taken =
|
|
1035
|
+
const taken = takenSlots();
|
|
764
1036
|
for (let i = 1; i < 1e3; i++) {
|
|
765
1037
|
const slot = `m${i}`;
|
|
766
1038
|
if (!taken.has(slot)) return slot;
|
|
@@ -776,11 +1048,24 @@ window.__ModuleLoader__.load({
|
|
|
776
1048
|
}
|
|
777
1049
|
return `r${Date.now()}`;
|
|
778
1050
|
}
|
|
1051
|
+
/** Patch one rule's rotation block, through the shared sanitizer so a live edit
|
|
1052
|
+
* can never hold a value the next load would clamp differently. */
|
|
1053
|
+
function patchRotation(rule, patch) {
|
|
1054
|
+
rule.rotate = normalizeRotation({
|
|
1055
|
+
...rule.rotate,
|
|
1056
|
+
...patch
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
/** Re-sanitize one rule in place, so what the UI holds equals what would persist. */
|
|
1060
|
+
function normalizeRuleInPlace(rule) {
|
|
1061
|
+
const normalized = normalizeRule(rule);
|
|
1062
|
+
if (normalized !== null) Object.assign(rule, normalized);
|
|
1063
|
+
}
|
|
779
1064
|
function rHasColor() {
|
|
780
|
-
return
|
|
1065
|
+
return activeColor() !== null;
|
|
781
1066
|
}
|
|
782
1067
|
function rColor() {
|
|
783
|
-
return
|
|
1068
|
+
return activeColor() ?? [
|
|
784
1069
|
220,
|
|
785
1070
|
.55,
|
|
786
1071
|
.25
|
|
@@ -795,13 +1080,22 @@ window.__ModuleLoader__.load({
|
|
|
795
1080
|
function rBl() {
|
|
796
1081
|
return clamp$1(activeRule()?.blur, 0, 60, 0);
|
|
797
1082
|
}
|
|
1083
|
+
/** Framing of the CURRENT image (per image, see BgImage). */
|
|
798
1084
|
function rBgState() {
|
|
799
|
-
return
|
|
1085
|
+
return activeImage()?.bgState ?? DEFAULT_BG_STATE;
|
|
800
1086
|
}
|
|
801
|
-
/**
|
|
1087
|
+
/** Rotation of the active rule; the shipped default when nothing is active. */
|
|
1088
|
+
function rRotation() {
|
|
1089
|
+
return activeRule()?.rotate ?? defaultRotation();
|
|
1090
|
+
}
|
|
1091
|
+
/** Cross-fade duration the active rule asks for, in ms (0 = hard cut). */
|
|
1092
|
+
function rFadeMs() {
|
|
1093
|
+
return rRotation().fadeMs;
|
|
1094
|
+
}
|
|
1095
|
+
/** Paintable URL of the active rule's CURRENT image, or null when it has none. */
|
|
802
1096
|
function rWp() {
|
|
803
|
-
const
|
|
804
|
-
return
|
|
1097
|
+
const slot = activeSlot();
|
|
1098
|
+
return slot === "" ? null : displayImageOf(slot);
|
|
805
1099
|
}
|
|
806
1100
|
function rOps() {
|
|
807
1101
|
const o = cfg.opacities ?? {};
|
|
@@ -836,12 +1130,126 @@ window.__ModuleLoader__.load({
|
|
|
836
1130
|
activeRuleId = null;
|
|
837
1131
|
activeMatched = false;
|
|
838
1132
|
}
|
|
1133
|
+
rotIndex = 0;
|
|
1134
|
+
}
|
|
1135
|
+
//#endregion
|
|
1136
|
+
//#region src/client/rotation.ts
|
|
1137
|
+
/** Dwell-time presets the settings UI offers, in ms. */
|
|
1138
|
+
const ROTATE_PRESETS = [
|
|
1139
|
+
{
|
|
1140
|
+
ms: 1e4,
|
|
1141
|
+
key: "rotEvery10s"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
ms: 3e4,
|
|
1145
|
+
key: "rotEvery30s"
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
ms: 6e4,
|
|
1149
|
+
key: "rotEvery1m"
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
ms: 3e5,
|
|
1153
|
+
key: "rotEvery5m"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
ms: 18e5,
|
|
1157
|
+
key: "rotEvery30m"
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
ms: 36e5,
|
|
1161
|
+
key: "rotEvery1h"
|
|
1162
|
+
}
|
|
1163
|
+
];
|
|
1164
|
+
/**
|
|
1165
|
+
* The index to paint after the current one, or null when there is nowhere to go
|
|
1166
|
+
* (fewer than two images — the caller must not start a timer at all then).
|
|
1167
|
+
*
|
|
1168
|
+
* `order` walks the list and wraps; `shuffle` picks uniformly among every index
|
|
1169
|
+
* EXCEPT the current one, because landing on the same image reads as a missed
|
|
1170
|
+
* tick. `current` outside the list is treated as 0 rather than trusted: it comes
|
|
1171
|
+
* from the UI as often as from the timer, and a stale index must not be able to
|
|
1172
|
+
* index past the end.
|
|
1173
|
+
*
|
|
1174
|
+
* `rand` is injectable so the shuffle branch is testable without stubbing a
|
|
1175
|
+
* global.
|
|
1176
|
+
*/
|
|
1177
|
+
function nextIndex(len, order, current, rand = Math.random) {
|
|
1178
|
+
if (!Number.isFinite(len) || len < 2) return null;
|
|
1179
|
+
const count = Math.floor(len);
|
|
1180
|
+
const cur = Number.isFinite(current) && current >= 0 && current < count ? Math.floor(current) : 0;
|
|
1181
|
+
if (order === "shuffle") {
|
|
1182
|
+
const roll = Math.min(Math.floor(Math.max(rand(), 0) * (count - 1)), count - 2);
|
|
1183
|
+
return roll >= cur ? roll + 1 : roll;
|
|
1184
|
+
}
|
|
1185
|
+
return (cur + 1) % count;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Whether a rotation is worth running: two images, the switch on, and a positive
|
|
1189
|
+
* dwell time. Kept here (rather than at the call site) so the UI's "rotating"
|
|
1190
|
+
* badge and the timer agree by construction — a badge that says "rotating" while
|
|
1191
|
+
* no timer exists is the one failure this feature can produce silently.
|
|
1192
|
+
*/
|
|
1193
|
+
function isRotating(len, enabled, intervalMs) {
|
|
1194
|
+
return enabled && len >= 2 && Number.isFinite(intervalMs) && intervalMs > 0;
|
|
1195
|
+
}
|
|
1196
|
+
//#endregion
|
|
1197
|
+
//#region src/client/repaint.ts
|
|
1198
|
+
/**
|
|
1199
|
+
* The repaint decision, as a pure function.
|
|
1200
|
+
*
|
|
1201
|
+
* Every edit to a rule ends with the same question: does the interface have to be
|
|
1202
|
+
* repainted, or is refreshing the panel's readout enough? Repainting is the
|
|
1203
|
+
* expensive half (the theme skin is re-registered, the wallpaper layer is
|
|
1204
|
+
* rewritten, the host's theme is switched), so it must not be done for a rule
|
|
1205
|
+
* that is not the one on screen — but it MUST be done the moment the edit
|
|
1206
|
+
* changes what should be on screen.
|
|
1207
|
+
*
|
|
1208
|
+
* The naive test — "is the edited rule the active one?" — is wrong in exactly the
|
|
1209
|
+
* case that kept being reported as "更新的不太及时,只有我动调色盘或者切模型的时候
|
|
1210
|
+
* 才出现": an edit that CREATES the winner. Uploading a rule's first picture, or
|
|
1211
|
+
* an auto-extracted color landing on a rule that had none, makes that rule
|
|
1212
|
+
* paintable *through that very edit* (see `ruleCanPaint`), so the rule is not the
|
|
1213
|
+
* active one at the moment of the check; the wallpaper and the skin then only
|
|
1214
|
+
* showed up later, when an unrelated drag or model switch happened to re-run the
|
|
1215
|
+
* resolution.
|
|
1216
|
+
*
|
|
1217
|
+
* Sampling the winner on BOTH sides of the edit is what makes such a change land
|
|
1218
|
+
* at once, and `editedId === winnerAfter` additionally covers the edit that does
|
|
1219
|
+
* not move the resolution but changes the look of the rule that IS painting (a
|
|
1220
|
+
* color, a slider, a framing).
|
|
1221
|
+
*
|
|
1222
|
+
* `null` is "nothing of ours is painting" (the interface falls back to the host's
|
|
1223
|
+
* own look), which is a winner like any other id.
|
|
1224
|
+
*
|
|
1225
|
+
* Kept free of imports on purpose: `scripts/repaint-check.ts` exercises it on
|
|
1226
|
+
* plain node, the same way `rotation.ts` is exercised. A rule's `color`/`match`
|
|
1227
|
+
* are the inputs that make the resolution move, which is why they are the fields
|
|
1228
|
+
* whose write paths all end here.
|
|
1229
|
+
*/
|
|
1230
|
+
function shouldRepaint(editedId, winnerBefore, winnerAfter) {
|
|
1231
|
+
return winnerAfter === editedId || winnerAfter !== winnerBefore;
|
|
839
1232
|
}
|
|
840
1233
|
//#endregion
|
|
841
1234
|
//#region src/client/rpc.ts
|
|
842
1235
|
const RPC_CHANNEL = "/dsh-background-by-model";
|
|
843
1236
|
const RPC_NS = "dshBackgroundByModel";
|
|
844
1237
|
const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
|
|
1238
|
+
let hostShape = "unknown";
|
|
1239
|
+
let warnedLegacy = false;
|
|
1240
|
+
/** Whether this bundle may write its config to the host it is talking to. */
|
|
1241
|
+
function canWriteConfig() {
|
|
1242
|
+
return hostShape !== "legacy";
|
|
1243
|
+
}
|
|
1244
|
+
/** Whether the running host process is older than this client bundle. */
|
|
1245
|
+
function hostIsLegacy() {
|
|
1246
|
+
return hostShape === "legacy";
|
|
1247
|
+
}
|
|
1248
|
+
function holdWrite() {
|
|
1249
|
+
if (warnedLegacy) return;
|
|
1250
|
+
warnedLegacy = true;
|
|
1251
|
+
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.");
|
|
1252
|
+
}
|
|
845
1253
|
let rpcCallFn = null;
|
|
846
1254
|
function initRpc(call) {
|
|
847
1255
|
rpcCallFn = call;
|
|
@@ -870,6 +1278,10 @@ window.__ModuleLoader__.load({
|
|
|
870
1278
|
const SAVE_DEBOUNCE_MS = 250;
|
|
871
1279
|
let saveTimer;
|
|
872
1280
|
function saveConfig() {
|
|
1281
|
+
if (!canWriteConfig()) {
|
|
1282
|
+
holdWrite();
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
873
1285
|
if (saveTimer !== void 0) window.clearTimeout(saveTimer);
|
|
874
1286
|
saveTimer = window.setTimeout(() => {
|
|
875
1287
|
saveTimer = void 0;
|
|
@@ -880,10 +1292,18 @@ window.__ModuleLoader__.load({
|
|
|
880
1292
|
if (saveTimer === void 0) return;
|
|
881
1293
|
window.clearTimeout(saveTimer);
|
|
882
1294
|
saveTimer = void 0;
|
|
1295
|
+
if (!canWriteConfig()) {
|
|
1296
|
+
holdWrite();
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
883
1299
|
rpcCall("writeConfig", { config: cfg });
|
|
884
1300
|
}
|
|
885
1301
|
/** Persist the current config immediately (no debounce). */
|
|
886
1302
|
function persistConfig() {
|
|
1303
|
+
if (!canWriteConfig()) {
|
|
1304
|
+
holdWrite();
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
887
1307
|
rpcCall("writeConfig", { config: cfg });
|
|
888
1308
|
}
|
|
889
1309
|
/** Load the persisted config plus the list of stored image slots. */
|
|
@@ -892,9 +1312,13 @@ window.__ModuleLoader__.load({
|
|
|
892
1312
|
if (data === null || typeof data !== "object") return null;
|
|
893
1313
|
const d = data;
|
|
894
1314
|
const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
|
|
1315
|
+
const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
|
|
1316
|
+
hostShape = schema === null ? "legacy" : schema >= 4 ? "current" : "legacy";
|
|
1317
|
+
if (hostShape === "legacy") holdWrite();
|
|
895
1318
|
return {
|
|
896
1319
|
config: d.config,
|
|
897
|
-
slots
|
|
1320
|
+
slots,
|
|
1321
|
+
schema
|
|
898
1322
|
};
|
|
899
1323
|
}
|
|
900
1324
|
/** Read one rule image as a data URL (null when the slot is empty). */
|
|
@@ -1371,6 +1795,9 @@ window.__ModuleLoader__.load({
|
|
|
1371
1795
|
.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}
|
|
1372
1796
|
.dab-wheel{position:relative;cursor:crosshair;border-radius:50%;box-shadow:0 12px 32px -14px rgba(0,0,0,.4)}
|
|
1373
1797
|
.dab-hint{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);padding:0 4px}
|
|
1798
|
+
/* The "your edits are being held" notice: it reports a state the user has to act
|
|
1799
|
+
on, so it is tinted like a warning rather than left as grey hint text. */
|
|
1800
|
+
.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}
|
|
1374
1801
|
|
|
1375
1802
|
/* ── precise color inputs ────────────────────────────────────────────────── */
|
|
1376
1803
|
.dab-inputs{display:flex;flex-direction:column;gap:10px;min-width:172px}
|
|
@@ -1505,12 +1932,60 @@ window.__ModuleLoader__.load({
|
|
|
1505
1932
|
.dab-icon-btn:disabled{opacity:.35;cursor:not-allowed}
|
|
1506
1933
|
.dab-icon-btn-danger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
|
|
1507
1934
|
.dab-rule-body{margin-top:13px;display:flex;flex-direction:column;gap:13px}
|
|
1508
|
-
.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)
|
|
1935
|
+
.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)}
|
|
1509
1936
|
.dab-rule-thumb img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1510
1937
|
.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}
|
|
1511
|
-
|
|
1938
|
+
/* The empty tile is the upload button while the rule has no picture at all, so
|
|
1939
|
+
it is a real <button> (focusable, keyboard-reachable) reset down to the box the
|
|
1940
|
+
plain preview occupies — a button brings its own padding, font and border. The
|
|
1941
|
+
dashed frame it looks like is .dab-rule-thumb-empty, which is why the hover and
|
|
1942
|
+
the drag highlight have to reach it through a descendant selector too. */
|
|
1943
|
+
.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}
|
|
1944
|
+
.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))}
|
|
1945
|
+
.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)}
|
|
1946
|
+
.dab-rule-thumb-add:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
1512
1947
|
.dab-rule-cols{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:18px;align-items:start}
|
|
1513
1948
|
.dab-rule-section-title{font-size:12px;font-weight:600;margin-bottom:9px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
1949
|
+
|
|
1950
|
+
/* ── image strip (multi-image rules) ─────────────────────────────────────── */
|
|
1951
|
+
/* A horizontally scrolling row of thumbnails: the one open for editing is
|
|
1952
|
+
ringed, and the one the wallpaper is actually painting carries a live dot —
|
|
1953
|
+
but deliberately NO ring of its own. Two tiles ringed at once read as "two
|
|
1954
|
+
things are selected", which is the one question a row of near-identical
|
|
1955
|
+
thumbnails must not leave open: being on screen is the dot, and nothing
|
|
1956
|
+
else. FILES dropped here are added, which is why the container's border
|
|
1957
|
+
reacts to a drag; a TILE dragged across the row reorders it instead, and that
|
|
1958
|
+
is what the is-dragging / dab-strip-bar pair below is for. */
|
|
1959
|
+
.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}
|
|
1960
|
+
.dab-strip.is-over{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
|
|
1961
|
+
.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:grab;user-select:none;transition:border-color .2s,box-shadow .2s,transform .18s}
|
|
1962
|
+
.dab-strip-item img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1963
|
+
.dab-strip-item:hover{transform:translateY(-1px);border-color:var(--dsw-alias-brand-primary)}
|
|
1964
|
+
.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)}
|
|
1965
|
+
/* The tile being dragged out of the row, and the 2px bar that says where it will
|
|
1966
|
+
land. The bar is drawn INSIDE the tile it precedes (at its left edge) or inside
|
|
1967
|
+
the last tile (at its right edge, for a drop past the end): a bar as its own flex
|
|
1968
|
+
item would push the tiles sideways, and a target that slides away as the pointer
|
|
1969
|
+
approaches it is how a drop ends up one slot off. */
|
|
1970
|
+
.dab-strip-item.is-dragging{cursor:grabbing;opacity:.45;transform:none;border-style:dashed}
|
|
1971
|
+
.dab-strip-bar{position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--dsw-alias-brand-primary);z-index:2}
|
|
1972
|
+
.dab-strip-bar.is-end{left:auto;right:0}
|
|
1973
|
+
.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)}
|
|
1974
|
+
.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}
|
|
1975
|
+
.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}
|
|
1976
|
+
/* Each image's OWN theme color, when it has one. It shares the right edge with
|
|
1977
|
+
the live dot but sits at the bottom, where nothing else is: the item number is
|
|
1978
|
+
bottom-LEFT. Two rings (dark outside, light inside) keep the swatch readable
|
|
1979
|
+
over a dark and a light thumbnail alike. */
|
|
1980
|
+
.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)}
|
|
1981
|
+
.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}
|
|
1982
|
+
.dab-strip-add:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
|
|
1983
|
+
.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}
|
|
1984
|
+
|
|
1985
|
+
/* ── rotation panel ──────────────────────────────────────────────────────── */
|
|
1986
|
+
.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)}
|
|
1987
|
+
.dab-num-sec{flex:none;width:88px;min-width:0}
|
|
1988
|
+
|
|
1514
1989
|
@container (max-width:760px){.dab-rule-cols{grid-template-columns:1fr}}
|
|
1515
1990
|
|
|
1516
1991
|
/* ── host self-check ─────────────────────────────────────────────────────── */
|
|
@@ -1570,6 +2045,9 @@ window.__ModuleLoader__.load({
|
|
|
1570
2045
|
@keyframes dab-toast-in{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
1571
2046
|
@keyframes dab-zoom-in{from{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:none}}
|
|
1572
2047
|
@keyframes dab-flow{to{background-position:300% 50%}}
|
|
2048
|
+
/* The strip's "this is the image on screen" dot. It is the only marker that says
|
|
2049
|
+
the rotation is alive, so it breathes rather than merely sitting there. */
|
|
2050
|
+
@keyframes dab-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.72)}}
|
|
1573
2051
|
|
|
1574
2052
|
/* ── responsive & motion preferences ─────────────────────────────────────── */
|
|
1575
2053
|
@container (max-width:620px){
|
|
@@ -2315,7 +2793,6 @@ window.__ModuleLoader__.load({
|
|
|
2315
2793
|
themeObserver = null;
|
|
2316
2794
|
};
|
|
2317
2795
|
}
|
|
2318
|
-
const FADE_MS = 320;
|
|
2319
2796
|
let layers = null;
|
|
2320
2797
|
/** Index of the layer the user is looking at. */
|
|
2321
2798
|
let front = 0;
|
|
@@ -2521,7 +2998,8 @@ window.__ModuleLoader__.load({
|
|
|
2521
2998
|
from.el.style.zIndex = "0";
|
|
2522
2999
|
paintLayer(target, url);
|
|
2523
3000
|
applyWpEffects();
|
|
2524
|
-
|
|
3001
|
+
const fadeMs = rFadeMs();
|
|
3002
|
+
if (!canFade(from) || fadeMs <= 0) {
|
|
2525
3003
|
el.style.opacity = "1";
|
|
2526
3004
|
front = idx;
|
|
2527
3005
|
clearLayer(from);
|
|
@@ -2531,10 +3009,10 @@ window.__ModuleLoader__.load({
|
|
|
2531
3009
|
pending = idx;
|
|
2532
3010
|
requestAnimationFrame(() => {
|
|
2533
3011
|
if (pending !== idx || layers === null) return;
|
|
2534
|
-
el.style.transition = `opacity ${
|
|
3012
|
+
el.style.transition = `opacity ${fadeMs}ms ease`;
|
|
2535
3013
|
el.style.opacity = "1";
|
|
2536
3014
|
});
|
|
2537
|
-
fadeTimer = window.setTimeout(() => finishFade(idx),
|
|
3015
|
+
fadeTimer = window.setTimeout(() => finishFade(idx), fadeMs + 160);
|
|
2538
3016
|
}
|
|
2539
3017
|
function applyWpEffects() {
|
|
2540
3018
|
if (wpEl === null || layers === null) return;
|
|
@@ -2599,9 +3077,9 @@ window.__ModuleLoader__.load({
|
|
|
2599
3077
|
}
|
|
2600
3078
|
//#endregion
|
|
2601
3079
|
//#region src/client/modelbg.ts
|
|
2602
|
-
/** Rules that can actually paint:
|
|
3080
|
+
/** Rules that can actually paint: see `ruleCanPaint` (./schema). */
|
|
2603
3081
|
function usable(rules) {
|
|
2604
|
-
return rules.filter(
|
|
3082
|
+
return rules.filter(ruleCanPaint);
|
|
2605
3083
|
}
|
|
2606
3084
|
/**
|
|
2607
3085
|
* Resolve the rule for one model text.
|
|
@@ -2609,6 +3087,10 @@ window.__ModuleLoader__.load({
|
|
|
2609
3087
|
* Top→bottom, first case-insensitive substring hit wins; an empty `match` never
|
|
2610
3088
|
* hits on its own. When nothing hits, the FIRST usable rule is the fallback
|
|
2611
3089
|
* (rule 1 deliberately doubles as both a matcher and the fallback).
|
|
3090
|
+
*
|
|
3091
|
+
* "Usable" is `ruleCanPaint` — an image, or a theme color of its own — so a rule
|
|
3092
|
+
* whose pictures were all removed still holds the models it matches (and paints
|
|
3093
|
+
* its color alone) instead of quietly handing them to the next rule.
|
|
2612
3094
|
*/
|
|
2613
3095
|
function matchRule(rules, modelText) {
|
|
2614
3096
|
const list = usable(rules);
|
|
@@ -4232,6 +4714,11 @@ window.__ModuleLoader__.load({
|
|
|
4232
4714
|
className: "dab-card dab-rise",
|
|
4233
4715
|
style: { "--d": 1 },
|
|
4234
4716
|
children: [
|
|
4717
|
+
store.hostStale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4718
|
+
className: "dab-hint dab-stale",
|
|
4719
|
+
style: { marginBottom: 10 },
|
|
4720
|
+
children: t("hostStaleHint")
|
|
4721
|
+
}) : null,
|
|
4235
4722
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4236
4723
|
className: "dab-swatch-title",
|
|
4237
4724
|
children: t("tryoutTitle")
|
|
@@ -4289,15 +4776,6 @@ window.__ModuleLoader__.load({
|
|
|
4289
4776
|
className: "dab-rise",
|
|
4290
4777
|
style: { "--d": 2 },
|
|
4291
4778
|
children: [
|
|
4292
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4293
|
-
className: "dab-swatch-title",
|
|
4294
|
-
children: t("rulesTitle")
|
|
4295
|
-
}),
|
|
4296
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4297
|
-
className: "dab-hint",
|
|
4298
|
-
style: { marginBottom: 11 },
|
|
4299
|
-
children: t("rulesHint")
|
|
4300
|
-
}),
|
|
4301
4779
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4302
4780
|
className: "dab-chip-row",
|
|
4303
4781
|
style: {
|
|
@@ -4317,11 +4795,6 @@ window.__ModuleLoader__.load({
|
|
|
4317
4795
|
children: t("autoExtract")
|
|
4318
4796
|
})]
|
|
4319
4797
|
}),
|
|
4320
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4321
|
-
className: "dab-hint",
|
|
4322
|
-
style: { marginBottom: 11 },
|
|
4323
|
-
children: t("autoExtractHint")
|
|
4324
|
-
}),
|
|
4325
4798
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4326
4799
|
className: "dab-rules",
|
|
4327
4800
|
children: rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleCard, {
|
|
@@ -4329,6 +4802,7 @@ window.__ModuleLoader__.load({
|
|
|
4329
4802
|
rule,
|
|
4330
4803
|
index: i,
|
|
4331
4804
|
total: rules.length,
|
|
4805
|
+
liveIndex: rule.id === store.activeRuleId ? store.rotIndex : -1,
|
|
4332
4806
|
active: rule.id === store.activeRuleId,
|
|
4333
4807
|
notify
|
|
4334
4808
|
}, rule.id))
|
|
@@ -4346,10 +4820,16 @@ window.__ModuleLoader__.load({
|
|
|
4346
4820
|
})
|
|
4347
4821
|
] });
|
|
4348
4822
|
}
|
|
4349
|
-
function RuleCard({ p, rule, index, total, active, notify }) {
|
|
4823
|
+
function RuleCard({ p, rule, index, total, active, liveIndex, notify }) {
|
|
4350
4824
|
const { t } = p;
|
|
4351
4825
|
const [open, setOpen] = (0, react.useState)(active);
|
|
4352
4826
|
const [dragOver, setDragOver] = (0, react.useState)(false);
|
|
4827
|
+
const pendingDrag = (0, react.useRef)(null);
|
|
4828
|
+
/** Insertion slot of the drag in flight (index of the tile it goes BEFORE). */
|
|
4829
|
+
const dragAt = (0, react.useRef)(null);
|
|
4830
|
+
const [drop, setDrop] = (0, react.useState)(null);
|
|
4831
|
+
/** Set while a drag is real, so the click that ends it is not also a selection. */
|
|
4832
|
+
const draggedRef = (0, react.useRef)(false);
|
|
4353
4833
|
const [urlOpen, setUrlOpen] = (0, react.useState)(false);
|
|
4354
4834
|
const [urlVal, setUrlVal] = (0, react.useState)("");
|
|
4355
4835
|
const [urlBusy, setUrlBusy] = (0, react.useState)(false);
|
|
@@ -4357,49 +4837,98 @@ window.__ModuleLoader__.load({
|
|
|
4357
4837
|
const [editorOpen, setEditorOpen] = (0, react.useState)(false);
|
|
4358
4838
|
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
4359
4839
|
const [extracting, setExtracting] = (0, react.useState)(false);
|
|
4840
|
+
const [secDraft, setSecDraft] = (0, react.useState)(null);
|
|
4841
|
+
/** Whether the next file picked replaces the selected image or is added. */
|
|
4842
|
+
const [replace, setReplace] = (0, react.useState)(false);
|
|
4360
4843
|
const fileRef = (0, react.useRef)(null);
|
|
4361
|
-
const
|
|
4362
|
-
const
|
|
4844
|
+
const [sel, setSel] = (0, react.useState)(0);
|
|
4845
|
+
const selIdx = rule.images.length === 0 ? 0 : Math.min(sel, rule.images.length - 1);
|
|
4846
|
+
const image = rule.images[selIdx];
|
|
4847
|
+
const url = image === void 0 ? null : p.imageOf(image.slot);
|
|
4848
|
+
const ownColor = image === void 0 ? rule.color : image.color;
|
|
4849
|
+
const setColor = (color) => {
|
|
4850
|
+
if (image === void 0) p.setRule(rule.id, { color });
|
|
4851
|
+
else p.setImageColor(rule.id, image.slot, color);
|
|
4852
|
+
};
|
|
4853
|
+
const [h, s, l] = ownColor ?? SEED_COLOR;
|
|
4363
4854
|
const wheel = hslToHsv(h, s, l);
|
|
4855
|
+
(0, react.useEffect)(() => {
|
|
4856
|
+
if (open) p.loadRuleImages(rule.id);
|
|
4857
|
+
}, [
|
|
4858
|
+
open,
|
|
4859
|
+
rule.id,
|
|
4860
|
+
rule.images.length,
|
|
4861
|
+
p
|
|
4862
|
+
]);
|
|
4364
4863
|
const onColor = (nh, ns, nl) => {
|
|
4365
4864
|
const [sh, ss, sl] = hsvToHsl(nh, ns, nl);
|
|
4366
|
-
|
|
4865
|
+
setColor([
|
|
4367
4866
|
sh,
|
|
4368
4867
|
ss,
|
|
4369
4868
|
sl
|
|
4370
|
-
]
|
|
4869
|
+
]);
|
|
4371
4870
|
};
|
|
4372
|
-
const
|
|
4373
|
-
|
|
4374
|
-
|
|
4871
|
+
const onFiles = (files) => {
|
|
4872
|
+
const list = files.filter((f) => f.type.startsWith("image/"));
|
|
4873
|
+
if (list.length === 0) return;
|
|
4874
|
+
if (replace) {
|
|
4875
|
+
const slot = rule.images[selIdx]?.slot;
|
|
4876
|
+
if (slot !== void 0) readImg(list[0], (d) => {
|
|
4877
|
+
if (d !== null) p.setRuleImage(rule.id, slot, d);
|
|
4878
|
+
});
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
const out = [];
|
|
4882
|
+
const first = rule.images.length;
|
|
4883
|
+
let pending = list.length;
|
|
4884
|
+
list.forEach((f, i) => {
|
|
4885
|
+
readImg(f, (d) => {
|
|
4886
|
+
if (d !== null) out[i] = d;
|
|
4887
|
+
pending--;
|
|
4888
|
+
if (pending === 0) {
|
|
4889
|
+
p.addRuleImages(rule.id, out.filter((x) => typeof x === "string"));
|
|
4890
|
+
setSel(first);
|
|
4891
|
+
}
|
|
4892
|
+
});
|
|
4375
4893
|
});
|
|
4376
4894
|
};
|
|
4377
4895
|
const onDrop = (e) => {
|
|
4378
4896
|
e.preventDefault();
|
|
4379
4897
|
setDragOver(false);
|
|
4380
|
-
const
|
|
4381
|
-
if (
|
|
4898
|
+
const files = Array.from(e.dataTransfer.files ?? []);
|
|
4899
|
+
if (files.length > 0) onFiles(files);
|
|
4382
4900
|
};
|
|
4383
4901
|
const applyUrl = async () => {
|
|
4384
|
-
const
|
|
4385
|
-
if (
|
|
4902
|
+
const urls = urlVal.split(/\s+/).map((u) => u.trim()).filter((u) => u !== "");
|
|
4903
|
+
if (urls.length === 0) {
|
|
4904
|
+
setUrlErr(t("ruleUrlBadHttp"));
|
|
4905
|
+
return;
|
|
4906
|
+
}
|
|
4907
|
+
if (urls.some((u) => !/^https?:\/\//i.test(u))) {
|
|
4386
4908
|
setUrlErr(t("ruleUrlBadHttp"));
|
|
4387
4909
|
return;
|
|
4388
4910
|
}
|
|
4389
4911
|
setUrlBusy(true);
|
|
4390
4912
|
setUrlErr(null);
|
|
4391
|
-
const
|
|
4913
|
+
const first = rule.images.length;
|
|
4914
|
+
for (const u of urls) {
|
|
4915
|
+
const res = await p.addRuleImageFromUrl(rule.id, u);
|
|
4916
|
+
if (!res.ok) {
|
|
4917
|
+
setUrlBusy(false);
|
|
4918
|
+
setUrlErr(res.error === "invalid url" || res.error === "unsupported scheme" ? t("ruleUrlBadHttp") : res.error ?? t("ruleUrlFail"));
|
|
4919
|
+
return;
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4392
4922
|
setUrlBusy(false);
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
} else setUrlErr(res.error === "invalid url" || res.error === "unsupported scheme" ? t("ruleUrlBadHttp") : res.error ?? t("ruleUrlFail"));
|
|
4923
|
+
setUrlOpen(false);
|
|
4924
|
+
setUrlVal("");
|
|
4925
|
+
setSel(first);
|
|
4397
4926
|
};
|
|
4398
4927
|
const onExtract = async () => {
|
|
4399
|
-
if (url === null || extracting) return;
|
|
4928
|
+
if (url === null || extracting || image === void 0) return;
|
|
4400
4929
|
setExtracting(true);
|
|
4401
4930
|
try {
|
|
4402
|
-
const ok = await p.extractColor(rule.id);
|
|
4931
|
+
const ok = await p.extractColor(rule.id, image.slot);
|
|
4403
4932
|
notify(ok ? t("extractDone") : t("extractFail"), ok);
|
|
4404
4933
|
} catch {
|
|
4405
4934
|
notify(t("extractFail"), false);
|
|
@@ -4414,17 +4943,90 @@ window.__ModuleLoader__.load({
|
|
|
4414
4943
|
* extract button runs and returns false when nothing vivid was found.)
|
|
4415
4944
|
*/
|
|
4416
4945
|
const onPickColor = async () => {
|
|
4417
|
-
if (url !== null) {
|
|
4946
|
+
if (url !== null && image !== void 0) {
|
|
4418
4947
|
setExtracting(true);
|
|
4419
4948
|
try {
|
|
4420
|
-
if (await p.extractColor(rule.id)) return;
|
|
4949
|
+
if (await p.extractColor(rule.id, image.slot)) return;
|
|
4421
4950
|
} catch {} finally {
|
|
4422
4951
|
setExtracting(false);
|
|
4423
4952
|
}
|
|
4424
4953
|
}
|
|
4425
|
-
|
|
4954
|
+
setColor(SEED_COLOR);
|
|
4426
4955
|
};
|
|
4427
4956
|
const cls = `dab-rule${active ? " is-active" : ""}${rule.enabled ? "" : " is-off"}`;
|
|
4957
|
+
const rotatable = rule.images.length >= 2;
|
|
4958
|
+
const preset = ROTATE_PRESETS.find((x) => x.ms === rule.rotate.intervalMs);
|
|
4959
|
+
const selSlot = image?.slot ?? null;
|
|
4960
|
+
/** How far the pointer must travel before a click turns into a drag. */
|
|
4961
|
+
const DRAG_SLOP = 5;
|
|
4962
|
+
const tileDown = (i, slot) => (e) => {
|
|
4963
|
+
if (e.button !== 0 || e.pointerType === "touch") return;
|
|
4964
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
4965
|
+
pendingDrag.current = {
|
|
4966
|
+
x: e.clientX,
|
|
4967
|
+
y: e.clientY,
|
|
4968
|
+
from: i,
|
|
4969
|
+
slot,
|
|
4970
|
+
id: e.pointerId
|
|
4971
|
+
};
|
|
4972
|
+
draggedRef.current = false;
|
|
4973
|
+
};
|
|
4974
|
+
const tileMove = (e) => {
|
|
4975
|
+
const pen = pendingDrag.current;
|
|
4976
|
+
if (pen === null || pen.id !== e.pointerId) return;
|
|
4977
|
+
const cur = dragAt.current;
|
|
4978
|
+
if (cur === null) {
|
|
4979
|
+
if (Math.abs(e.clientX - pen.x) + Math.abs(e.clientY - pen.y) < DRAG_SLOP) return;
|
|
4980
|
+
draggedRef.current = true;
|
|
4981
|
+
}
|
|
4982
|
+
const strip = e.currentTarget.parentElement;
|
|
4983
|
+
const list = strip === null ? [] : [...strip.querySelectorAll(".dab-strip-item")];
|
|
4984
|
+
let at = list.length;
|
|
4985
|
+
for (let i = 0; i < list.length; i++) {
|
|
4986
|
+
const box = list[i].getBoundingClientRect();
|
|
4987
|
+
if (e.clientX < box.left + box.width / 2) {
|
|
4988
|
+
at = i;
|
|
4989
|
+
break;
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
if (cur !== null && cur.at === at) return;
|
|
4993
|
+
dragAt.current = {
|
|
4994
|
+
from: pen.from,
|
|
4995
|
+
at
|
|
4996
|
+
};
|
|
4997
|
+
setDrop({
|
|
4998
|
+
from: pen.from,
|
|
4999
|
+
at
|
|
5000
|
+
});
|
|
5001
|
+
};
|
|
5002
|
+
/** End a drag: commit the new order, or just forget it on a cancel. */
|
|
5003
|
+
const tileUp = (commit) => (e) => {
|
|
5004
|
+
const pen = pendingDrag.current;
|
|
5005
|
+
pendingDrag.current = null;
|
|
5006
|
+
if (pen !== null && e.currentTarget.hasPointerCapture(pen.id)) e.currentTarget.releasePointerCapture(pen.id);
|
|
5007
|
+
const end = dragAt.current;
|
|
5008
|
+
dragAt.current = null;
|
|
5009
|
+
setDrop(null);
|
|
5010
|
+
if (!commit || pen === null || end === null) return;
|
|
5011
|
+
const to = end.at > pen.from ? end.at - 1 : end.at;
|
|
5012
|
+
if (to === pen.from) return;
|
|
5013
|
+
p.moveRuleImageTo(rule.id, pen.slot, to);
|
|
5014
|
+
setSel(to);
|
|
5015
|
+
};
|
|
5016
|
+
const removeSel = () => {
|
|
5017
|
+
if (selSlot === null) return;
|
|
5018
|
+
const at = selIdx;
|
|
5019
|
+
p.removeRuleImage(rule.id, selSlot);
|
|
5020
|
+
setSel(Math.max(0, Math.min(at, rule.images.length - 2)));
|
|
5021
|
+
};
|
|
5022
|
+
/** Commit the custom dwell time the moment it is typed (seconds → ms). */
|
|
5023
|
+
const commitSeconds = () => {
|
|
5024
|
+
if (secDraft === null) return;
|
|
5025
|
+
const secs = Number(secDraft.trim());
|
|
5026
|
+
setSecDraft(null);
|
|
5027
|
+
if (!Number.isFinite(secs) || secs <= 0) return;
|
|
5028
|
+
p.setRuleRotation(rule.id, { intervalMs: Math.round(secs * 1e3) });
|
|
5029
|
+
};
|
|
4428
5030
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4429
5031
|
className: cls,
|
|
4430
5032
|
children: [
|
|
@@ -4492,33 +5094,126 @@ window.__ModuleLoader__.load({
|
|
|
4492
5094
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4493
5095
|
className: "dab-rule-cols",
|
|
4494
5096
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
4495
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4496
|
-
className:
|
|
4497
|
-
|
|
5097
|
+
url !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5098
|
+
className: "dab-rule-thumb",
|
|
5099
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
5100
|
+
src: url,
|
|
5101
|
+
alt: "",
|
|
5102
|
+
draggable: false
|
|
5103
|
+
})
|
|
5104
|
+
}) : rule.images.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5105
|
+
type: "button",
|
|
5106
|
+
className: `dab-rule-thumb dab-rule-thumb-add${dragOver ? " is-over" : ""}`,
|
|
5107
|
+
title: t("rotAddImage"),
|
|
4498
5108
|
onDragOver: (e) => {
|
|
4499
5109
|
e.preventDefault();
|
|
4500
5110
|
setDragOver(true);
|
|
4501
5111
|
},
|
|
4502
5112
|
onDragLeave: () => setDragOver(false),
|
|
4503
5113
|
onDrop,
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
5114
|
+
onClick: () => {
|
|
5115
|
+
setReplace(false);
|
|
5116
|
+
fileRef.current?.click();
|
|
5117
|
+
},
|
|
5118
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5119
|
+
className: "dab-rule-thumb-empty",
|
|
5120
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rotAddImage") })]
|
|
5121
|
+
})
|
|
5122
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5123
|
+
className: "dab-rule-thumb",
|
|
5124
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4509
5125
|
className: "dab-rule-thumb-empty",
|
|
4510
5126
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("ruleNoImage") })]
|
|
4511
5127
|
})
|
|
4512
5128
|
}),
|
|
5129
|
+
rule.images.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5130
|
+
className: `dab-strip${dragOver ? " is-over" : ""}`,
|
|
5131
|
+
onDragOver: (e) => {
|
|
5132
|
+
e.preventDefault();
|
|
5133
|
+
setDragOver(true);
|
|
5134
|
+
},
|
|
5135
|
+
onDragLeave: () => setDragOver(false),
|
|
5136
|
+
onDrop,
|
|
5137
|
+
children: [rule.images.map((img, i) => {
|
|
5138
|
+
const thumb = p.imageOf(img.slot);
|
|
5139
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5140
|
+
type: "button",
|
|
5141
|
+
className: `dab-strip-item${i === selIdx ? " is-sel" : ""}${drop !== null && drop.from === i ? " is-dragging" : ""}`,
|
|
5142
|
+
title: `${i + 1} / ${rule.images.length} · ${t("rotDragHint")} · ${t("rotDblHint")}`,
|
|
5143
|
+
onPointerDown: tileDown(i, img.slot),
|
|
5144
|
+
onPointerMove: tileMove,
|
|
5145
|
+
onPointerUp: tileUp(true),
|
|
5146
|
+
onPointerCancel: tileUp(false),
|
|
5147
|
+
onClick: () => {
|
|
5148
|
+
if (draggedRef.current) {
|
|
5149
|
+
draggedRef.current = false;
|
|
5150
|
+
return;
|
|
5151
|
+
}
|
|
5152
|
+
setSel(i);
|
|
5153
|
+
},
|
|
5154
|
+
onDoubleClick: () => {
|
|
5155
|
+
if (!active) {
|
|
5156
|
+
notify(t("rotNextInactive"));
|
|
5157
|
+
return;
|
|
5158
|
+
}
|
|
5159
|
+
p.showRuleImage(rule.id, img.slot);
|
|
5160
|
+
},
|
|
5161
|
+
children: [
|
|
5162
|
+
thumb !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
5163
|
+
src: thumb,
|
|
5164
|
+
alt: "",
|
|
5165
|
+
draggable: false
|
|
5166
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-strip-wait" }),
|
|
5167
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5168
|
+
className: "dab-strip-num",
|
|
5169
|
+
children: i + 1
|
|
5170
|
+
}),
|
|
5171
|
+
drop !== null && drop.at === i ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-strip-bar" }) : null,
|
|
5172
|
+
drop !== null && drop.at === rule.images.length && i === rule.images.length - 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-strip-bar is-end" }) : null,
|
|
5173
|
+
img.color !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5174
|
+
className: "dab-strip-dot",
|
|
5175
|
+
title: toHex(hslToRgb(img.color[0], img.color[1], img.color[2])).toUpperCase(),
|
|
5176
|
+
style: { background: `hsl(${img.color[0]} ${Math.round(img.color[1] * 100)}% ${Math.round(img.color[2] * 100)}%)` }
|
|
5177
|
+
}) : null,
|
|
5178
|
+
liveIndex === i ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5179
|
+
className: "dab-strip-live",
|
|
5180
|
+
title: t("rotShowing")
|
|
5181
|
+
}) : null
|
|
5182
|
+
]
|
|
5183
|
+
}, img.slot);
|
|
5184
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5185
|
+
type: "button",
|
|
5186
|
+
className: "dab-strip-add",
|
|
5187
|
+
title: t("rotAddImage"),
|
|
5188
|
+
onClick: () => {
|
|
5189
|
+
setReplace(false);
|
|
5190
|
+
fileRef.current?.click();
|
|
5191
|
+
},
|
|
5192
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 16 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rotAddImage") })]
|
|
5193
|
+
})]
|
|
5194
|
+
}) : null,
|
|
4513
5195
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4514
5196
|
className: "dab-chip-row",
|
|
4515
5197
|
style: { marginTop: 10 },
|
|
4516
5198
|
children: [
|
|
5199
|
+
rule.images.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5200
|
+
className: "dab-strip-pos",
|
|
5201
|
+
children: [
|
|
5202
|
+
t("rotPos"),
|
|
5203
|
+
" ",
|
|
5204
|
+
selIdx + 1,
|
|
5205
|
+
" / ",
|
|
5206
|
+
rule.images.length
|
|
5207
|
+
]
|
|
5208
|
+
}) : null,
|
|
4517
5209
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4518
5210
|
type: "button",
|
|
4519
5211
|
className: "dab-btn",
|
|
4520
|
-
onClick: () =>
|
|
4521
|
-
|
|
5212
|
+
onClick: () => {
|
|
5213
|
+
setReplace(false);
|
|
5214
|
+
fileRef.current?.click();
|
|
5215
|
+
},
|
|
5216
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 13 }), t("rotAddImage")]
|
|
4522
5217
|
}),
|
|
4523
5218
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4524
5219
|
type: "button",
|
|
@@ -4526,16 +5221,19 @@ window.__ModuleLoader__.load({
|
|
|
4526
5221
|
onClick: () => setUrlOpen((o) => !o),
|
|
4527
5222
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkIcon, { size: 13 }), t("ruleFromUrl")]
|
|
4528
5223
|
}),
|
|
4529
|
-
|
|
5224
|
+
selSlot !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4530
5225
|
type: "button",
|
|
4531
|
-
className: "dab-btn",
|
|
4532
|
-
onClick: () =>
|
|
4533
|
-
|
|
5226
|
+
className: "dab-btn dab-btn-ghost",
|
|
5227
|
+
onClick: () => {
|
|
5228
|
+
setReplace(true);
|
|
5229
|
+
fileRef.current?.click();
|
|
5230
|
+
},
|
|
5231
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 13 }), t("rotReplaceImage")]
|
|
4534
5232
|
}) : null,
|
|
4535
|
-
|
|
5233
|
+
selSlot !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4536
5234
|
type: "button",
|
|
4537
5235
|
className: "dab-btn dab-btn-ghost dab-btn-danger",
|
|
4538
|
-
onClick:
|
|
5236
|
+
onClick: removeSel,
|
|
4539
5237
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 }), t("ruleImageRemove")]
|
|
4540
5238
|
}) : null
|
|
4541
5239
|
]
|
|
@@ -4590,24 +5288,168 @@ window.__ModuleLoader__.load({
|
|
|
4590
5288
|
},
|
|
4591
5289
|
children: urlErr
|
|
4592
5290
|
}) : null,
|
|
4593
|
-
|
|
5291
|
+
rule.images.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4594
5292
|
className: "dab-hint",
|
|
4595
5293
|
style: { marginTop: 8 },
|
|
4596
|
-
children: t("ruleEmptyHint")
|
|
5294
|
+
children: rule.color === null ? t("ruleEmptyHint") : t("ruleColorOnlyHint")
|
|
4597
5295
|
}) : null,
|
|
5296
|
+
rule.images.length > 0 && url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5297
|
+
className: "dab-hint",
|
|
5298
|
+
style: { marginTop: 8 },
|
|
5299
|
+
children: t("ruleImagePendingHint")
|
|
5300
|
+
}) : null,
|
|
5301
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5302
|
+
style: { marginTop: 14 },
|
|
5303
|
+
children: [
|
|
5304
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5305
|
+
className: "dab-rule-section-title",
|
|
5306
|
+
children: t("rotTitle")
|
|
5307
|
+
}),
|
|
5308
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5309
|
+
className: "dab-chip-row",
|
|
5310
|
+
style: { alignItems: "center" },
|
|
5311
|
+
children: [
|
|
5312
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5313
|
+
type: "button",
|
|
5314
|
+
className: `dab-toggle${rule.rotate.enabled ? " is-on" : ""}`,
|
|
5315
|
+
role: "switch",
|
|
5316
|
+
"aria-checked": rule.rotate.enabled,
|
|
5317
|
+
title: t("rotEnable"),
|
|
5318
|
+
disabled: !rotatable,
|
|
5319
|
+
onClick: () => p.setRuleRotation(rule.id, { enabled: !rule.rotate.enabled }),
|
|
5320
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5321
|
+
}),
|
|
5322
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5323
|
+
className: "dab-hint",
|
|
5324
|
+
style: { padding: 0 },
|
|
5325
|
+
children: t("rotEnable")
|
|
5326
|
+
}),
|
|
5327
|
+
!rotatable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5328
|
+
className: "dab-hint",
|
|
5329
|
+
style: { padding: 0 },
|
|
5330
|
+
children: t("rotNeedTwo")
|
|
5331
|
+
}) : null
|
|
5332
|
+
]
|
|
5333
|
+
}),
|
|
5334
|
+
rotatable && rule.rotate.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5335
|
+
className: "dab-rot-panel",
|
|
5336
|
+
children: [
|
|
5337
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5338
|
+
className: "dab-rule-section-title",
|
|
5339
|
+
children: t("rotEvery")
|
|
5340
|
+
}),
|
|
5341
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5342
|
+
className: "dab-chip-row",
|
|
5343
|
+
children: [
|
|
5344
|
+
ROTATE_PRESETS.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5345
|
+
type: "button",
|
|
5346
|
+
className: `dab-chip${item.ms === rule.rotate.intervalMs ? " is-active" : ""}`,
|
|
5347
|
+
onClick: () => p.setRuleRotation(rule.id, { intervalMs: item.ms }),
|
|
5348
|
+
children: t(item.key)
|
|
5349
|
+
}, item.ms)),
|
|
5350
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5351
|
+
type: "number",
|
|
5352
|
+
className: "dab-num dab-num-sec",
|
|
5353
|
+
min: 5,
|
|
5354
|
+
max: 86400,
|
|
5355
|
+
step: 5,
|
|
5356
|
+
value: secDraft ?? (preset === void 0 ? Math.round(rule.rotate.intervalMs / 1e3) : ""),
|
|
5357
|
+
placeholder: t("rotCustom"),
|
|
5358
|
+
onChange: (e) => setSecDraft(e.target.value),
|
|
5359
|
+
onBlur: commitSeconds,
|
|
5360
|
+
onKeyDown: (e) => {
|
|
5361
|
+
if (e.key === "Enter") {
|
|
5362
|
+
e.preventDefault();
|
|
5363
|
+
commitSeconds();
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
}),
|
|
5367
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5368
|
+
className: "dab-hint",
|
|
5369
|
+
style: { padding: 0 },
|
|
5370
|
+
children: t("rotSeconds")
|
|
5371
|
+
})
|
|
5372
|
+
]
|
|
5373
|
+
}),
|
|
5374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5375
|
+
className: "dab-rule-section-title",
|
|
5376
|
+
style: { marginTop: 12 },
|
|
5377
|
+
children: t("rotOrder")
|
|
5378
|
+
}),
|
|
5379
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5380
|
+
className: "dab-chip-row",
|
|
5381
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5382
|
+
type: "button",
|
|
5383
|
+
className: `dab-chip${rule.rotate.order === "order" ? " is-active" : ""}`,
|
|
5384
|
+
onClick: () => p.setRuleRotation(rule.id, { order: "order" }),
|
|
5385
|
+
children: t("rotOrderSeq")
|
|
5386
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5387
|
+
type: "button",
|
|
5388
|
+
className: `dab-chip${rule.rotate.order === "shuffle" ? " is-active" : ""}`,
|
|
5389
|
+
onClick: () => p.setRuleRotation(rule.id, { order: "shuffle" }),
|
|
5390
|
+
children: t("rotOrderShuffle")
|
|
5391
|
+
})]
|
|
5392
|
+
}),
|
|
5393
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5394
|
+
className: "dab-chip-row",
|
|
5395
|
+
style: {
|
|
5396
|
+
marginTop: 12,
|
|
5397
|
+
alignItems: "center"
|
|
5398
|
+
},
|
|
5399
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5400
|
+
type: "button",
|
|
5401
|
+
className: `dab-toggle${rule.rotate.advanceOnSwitch ? " is-on" : ""}`,
|
|
5402
|
+
role: "switch",
|
|
5403
|
+
"aria-checked": rule.rotate.advanceOnSwitch,
|
|
5404
|
+
title: t("rotOnSwitch"),
|
|
5405
|
+
onClick: () => p.setRuleRotation(rule.id, { advanceOnSwitch: !rule.rotate.advanceOnSwitch }),
|
|
5406
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5407
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5408
|
+
className: "dab-hint",
|
|
5409
|
+
style: { padding: 0 },
|
|
5410
|
+
children: t("rotOnSwitch")
|
|
5411
|
+
})]
|
|
5412
|
+
}),
|
|
5413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5414
|
+
className: "dab-chip-row",
|
|
5415
|
+
style: {
|
|
5416
|
+
marginTop: 12,
|
|
5417
|
+
alignItems: "center"
|
|
5418
|
+
},
|
|
5419
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5420
|
+
type: "button",
|
|
5421
|
+
className: "dab-btn",
|
|
5422
|
+
disabled: !active,
|
|
5423
|
+
onClick: () => p.rotateNow(rule.id),
|
|
5424
|
+
children: t("rotNext")
|
|
5425
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5426
|
+
className: "dab-hint",
|
|
5427
|
+
style: { padding: 0 },
|
|
5428
|
+
children: active ? `${t("rotShowing")} ${rule.images.length === 0 ? 0 : Math.min(liveIndex + 1, rule.images.length)} / ${rule.images.length}` : t("rotNextInactive")
|
|
5429
|
+
})]
|
|
5430
|
+
})
|
|
5431
|
+
]
|
|
5432
|
+
}) : null
|
|
5433
|
+
]
|
|
5434
|
+
}),
|
|
4598
5435
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4599
5436
|
style: { marginTop: 14 },
|
|
4600
5437
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4601
5438
|
className: "dab-rule-section-title",
|
|
4602
5439
|
children: t("ruleLayout")
|
|
4603
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5440
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4604
5441
|
className: "dab-chip-row",
|
|
4605
|
-
children: BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5442
|
+
children: [BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4606
5443
|
type: "button",
|
|
4607
5444
|
className: `dab-chip${rule.bgMode === m.mode ? " is-active" : ""}`,
|
|
4608
5445
|
onClick: () => p.setRule(rule.id, { bgMode: m.mode }),
|
|
4609
5446
|
children: t(m.key)
|
|
4610
|
-
}, m.mode))
|
|
5447
|
+
}, m.mode)), url !== null && rule.bgMode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5448
|
+
type: "button",
|
|
5449
|
+
className: "dab-btn",
|
|
5450
|
+
onClick: () => setEditorOpen(true),
|
|
5451
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 13 }), t("ruleFramingEdit")]
|
|
5452
|
+
}) : null]
|
|
4611
5453
|
})]
|
|
4612
5454
|
})
|
|
4613
5455
|
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
@@ -4615,7 +5457,12 @@ window.__ModuleLoader__.load({
|
|
|
4615
5457
|
className: "dab-rule-section-title",
|
|
4616
5458
|
children: t("ruleColor")
|
|
4617
5459
|
}),
|
|
4618
|
-
|
|
5460
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5461
|
+
className: "dab-hint",
|
|
5462
|
+
style: { marginTop: 6 },
|
|
5463
|
+
children: image === void 0 ? t("ruleColorTargetRule") : `${t("ruleColorTargetImage")} ${selIdx + 1} / ${rule.images.length}`
|
|
5464
|
+
}),
|
|
5465
|
+
ownColor === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4619
5466
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4620
5467
|
className: "dab-chip-row",
|
|
4621
5468
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -4637,10 +5484,18 @@ window.__ModuleLoader__.load({
|
|
|
4637
5484
|
style: { marginTop: 8 },
|
|
4638
5485
|
children: t("ruleColorNoneHint")
|
|
4639
5486
|
}),
|
|
4640
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5487
|
+
url !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4641
5488
|
className: "dab-hint",
|
|
4642
5489
|
style: { marginTop: 6 },
|
|
4643
5490
|
children: t("ruleColorPickHint")
|
|
5491
|
+
}) : image === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5492
|
+
className: "dab-hint",
|
|
5493
|
+
style: { marginTop: 6 },
|
|
5494
|
+
children: t("ruleColorNoImageHint")
|
|
5495
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5496
|
+
className: "dab-hint",
|
|
5497
|
+
style: { marginTop: 6 },
|
|
5498
|
+
children: t("ruleColorPendingImageHint")
|
|
4644
5499
|
})
|
|
4645
5500
|
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4646
5501
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4686,7 +5541,7 @@ window.__ModuleLoader__.load({
|
|
|
4686
5541
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4687
5542
|
type: "button",
|
|
4688
5543
|
className: "dab-btn dab-btn-ghost",
|
|
4689
|
-
onClick: () =>
|
|
5544
|
+
onClick: () => setColor(null),
|
|
4690
5545
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }), t("ruleColorNone")]
|
|
4691
5546
|
})
|
|
4692
5547
|
]
|
|
@@ -4699,11 +5554,11 @@ window.__ModuleLoader__.load({
|
|
|
4699
5554
|
className: "dab-swatch",
|
|
4700
5555
|
style: { background: `hsl(${sh} ${Math.round(ss * 100)}% ${Math.round(sl * 100)}%)` },
|
|
4701
5556
|
title: toHex(hslToRgb(sh, ss, sl)).toUpperCase(),
|
|
4702
|
-
onClick: () =>
|
|
5557
|
+
onClick: () => setColor([
|
|
4703
5558
|
sh,
|
|
4704
5559
|
ss,
|
|
4705
5560
|
sl
|
|
4706
|
-
]
|
|
5561
|
+
])
|
|
4707
5562
|
}, i))
|
|
4708
5563
|
})
|
|
4709
5564
|
] }),
|
|
@@ -4742,27 +5597,29 @@ window.__ModuleLoader__.load({
|
|
|
4742
5597
|
ref: fileRef,
|
|
4743
5598
|
type: "file",
|
|
4744
5599
|
accept: "image/*",
|
|
5600
|
+
multiple: true,
|
|
4745
5601
|
style: { display: "none" },
|
|
4746
5602
|
onChange: (e) => {
|
|
4747
|
-
const
|
|
4748
|
-
if (
|
|
4749
|
-
|
|
5603
|
+
const files = Array.from(e.target.files ?? []);
|
|
5604
|
+
if (files.length === 0) return;
|
|
5605
|
+
onFiles(files);
|
|
4750
5606
|
e.target.value = "";
|
|
5607
|
+
setReplace(false);
|
|
4751
5608
|
}
|
|
4752
5609
|
}),
|
|
4753
|
-
editorOpen && url !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
|
|
5610
|
+
editorOpen && url !== null && image !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
|
|
4754
5611
|
url,
|
|
4755
|
-
state:
|
|
5612
|
+
state: image.bgState,
|
|
4756
5613
|
t,
|
|
4757
5614
|
onClose: () => setEditorOpen(false),
|
|
4758
5615
|
onCommit: (z, x, y, iw, ih) => {
|
|
4759
|
-
p.
|
|
5616
|
+
p.setImageFraming(rule.id, image.slot, {
|
|
4760
5617
|
zoom: z,
|
|
4761
5618
|
x,
|
|
4762
5619
|
y,
|
|
4763
5620
|
iw,
|
|
4764
5621
|
ih
|
|
4765
|
-
}
|
|
5622
|
+
});
|
|
4766
5623
|
setEditorOpen(false);
|
|
4767
5624
|
}
|
|
4768
5625
|
}) : null,
|
|
@@ -5047,6 +5904,35 @@ window.__ModuleLoader__.load({
|
|
|
5047
5904
|
if (ctx.theme.getTheme().preference === CUSTOM_ID) ctx.theme.setTheme("system");
|
|
5048
5905
|
} catch {}
|
|
5049
5906
|
};
|
|
5907
|
+
/**
|
|
5908
|
+
* Re-emit the interface palette for whatever is painted RIGHT NOW.
|
|
5909
|
+
*
|
|
5910
|
+
* Called from two places, and the second one is why this is a function: a
|
|
5911
|
+
* model switch (`applyActive`) and a ROTATION STEP (`paintImage`). A theme color
|
|
5912
|
+
* belongs to an image since 0.7.1, so stepping to the next picture changes the
|
|
5913
|
+
* palette as much as switching models does — and a step that only repainted the
|
|
5914
|
+
* wallpaper would leave the previous picture's color on the interface until
|
|
5915
|
+
* something else happened to re-run an apply (dragging the wheel, switching
|
|
5916
|
+
* models), which is precisely the "it only updates when I touch the color wheel"
|
|
5917
|
+
* report this plugin already had once.
|
|
5918
|
+
*
|
|
5919
|
+
* The debounce is for the OTHER caller: a color drag runs a full apply dozens of
|
|
5920
|
+
* times per second, and the host-visible skin switch (dispose + register +
|
|
5921
|
+
* activate) must not happen on every pointer move. A rotation step pays the same
|
|
5922
|
+
* 60 ms, which is invisible next to a dwell time of 5 s or more.
|
|
5923
|
+
*/
|
|
5924
|
+
const applyPalette = () => {
|
|
5925
|
+
const color = activeColor();
|
|
5926
|
+
if (skinTimer !== null) window.clearTimeout(skinTimer);
|
|
5927
|
+
if (color === null) {
|
|
5928
|
+
skinTimer = null;
|
|
5929
|
+
dropCustom();
|
|
5930
|
+
} else skinTimer = window.setTimeout(() => {
|
|
5931
|
+
skinTimer = null;
|
|
5932
|
+
registerCustom(color[0], color[1], color[2]);
|
|
5933
|
+
}, 60);
|
|
5934
|
+
syncHostScheme();
|
|
5935
|
+
};
|
|
5050
5936
|
ctx.effect(() => () => {
|
|
5051
5937
|
if (skinTimer !== null) window.clearTimeout(skinTimer);
|
|
5052
5938
|
customDispose?.();
|
|
@@ -5074,9 +5960,13 @@ window.__ModuleLoader__.load({
|
|
|
5074
5960
|
modelSource: "session",
|
|
5075
5961
|
modelNote: "waiting",
|
|
5076
5962
|
activeRuleId: null,
|
|
5077
|
-
matched: false
|
|
5963
|
+
matched: false,
|
|
5964
|
+
rotIndex: 0,
|
|
5965
|
+
rotTotal: 0,
|
|
5966
|
+
rotating: false,
|
|
5967
|
+
hostStale: false
|
|
5078
5968
|
}),
|
|
5079
|
-
actions: { sync: (d, url, r, rr, model, source, note, id, matched) => {
|
|
5969
|
+
actions: { sync: (d, url, r, rr, model, source, note, id, matched, rotIndex, rotTotal, rotating, hostStale) => {
|
|
5080
5970
|
if (r > d.rev) {
|
|
5081
5971
|
d.url = url;
|
|
5082
5972
|
d.rev = r;
|
|
@@ -5087,12 +5977,31 @@ window.__ModuleLoader__.load({
|
|
|
5087
5977
|
d.modelNote = note;
|
|
5088
5978
|
d.activeRuleId = id;
|
|
5089
5979
|
d.matched = matched;
|
|
5980
|
+
d.rotIndex = rotIndex;
|
|
5981
|
+
d.rotTotal = rotTotal;
|
|
5982
|
+
d.rotating = rotating;
|
|
5983
|
+
d.hostStale = hostStale;
|
|
5090
5984
|
} }
|
|
5091
5985
|
});
|
|
5092
5986
|
let bound = null;
|
|
5987
|
+
/** Rotation facts of whatever is painted now, for the section's readout. */
|
|
5988
|
+
const rotationFacts = () => {
|
|
5989
|
+
const rule = activeRule();
|
|
5990
|
+
if (rule === null) return {
|
|
5991
|
+
index: 0,
|
|
5992
|
+
total: 0,
|
|
5993
|
+
rotating: false
|
|
5994
|
+
};
|
|
5995
|
+
return {
|
|
5996
|
+
index: imageIndexOf(rule),
|
|
5997
|
+
total: rule.images.length,
|
|
5998
|
+
rotating: isRotating(rule.images.length, rule.rotate.enabled, rule.rotate.intervalMs) && !document.hidden
|
|
5999
|
+
};
|
|
6000
|
+
};
|
|
5093
6001
|
const sync = () => {
|
|
5094
6002
|
rev++;
|
|
5095
|
-
|
|
6003
|
+
const rot = rotationFacts();
|
|
6004
|
+
bound?.sync(rWp(), rev, rulesRev, modelLabel !== "" ? modelLabel : modelText, modelSource, modelNote, activeRuleId, activeMatched, rot.index, rot.total, rot.rotating, hostIsLegacy());
|
|
5096
6005
|
};
|
|
5097
6006
|
/**
|
|
5098
6007
|
* Publish the scheme the host's own preference resolves to. The theme service
|
|
@@ -5108,27 +6017,141 @@ window.__ModuleLoader__.load({
|
|
|
5108
6017
|
setHostScheme(null);
|
|
5109
6018
|
}
|
|
5110
6019
|
};
|
|
5111
|
-
/**
|
|
5112
|
-
|
|
6020
|
+
/**
|
|
6021
|
+
* Which rule wins for the current model RIGHT NOW, and whether it was a
|
|
6022
|
+
* substring hit (a holiday override is never a "match" of the model).
|
|
6023
|
+
*
|
|
6024
|
+
* Read by the repaint AND by the panel's write path, which has to know whether
|
|
6025
|
+
* an edit changed the answer: `color`, `enabled` and `match` all feed the
|
|
6026
|
+
* resolution (a rule with no image still paints from its own color — see
|
|
6027
|
+
* `ruleCanPaint`). Without this, editing a rule that was not the active one —
|
|
6028
|
+
* giving an emptied rule its first color, typing a match string that should
|
|
6029
|
+
* promote it — changed nothing on screen until some unrelated event happened to
|
|
6030
|
+
* re-run the resolution.
|
|
6031
|
+
*/
|
|
6032
|
+
const resolveRule = () => {
|
|
5113
6033
|
const holiday = activeHolidayRule();
|
|
5114
|
-
|
|
6034
|
+
return holiday !== null ? {
|
|
5115
6035
|
rule: holiday,
|
|
5116
6036
|
matched: false
|
|
5117
6037
|
} : matchRule(cfg.rules, modelText);
|
|
6038
|
+
};
|
|
6039
|
+
/** Id of the rule that would paint right now; `null` = the host's own look. */
|
|
6040
|
+
const winnerId = () => resolveRule().rule?.id ?? null;
|
|
6041
|
+
/**
|
|
6042
|
+
* Close a rule edit: repaint when it could have moved what is on screen, and
|
|
6043
|
+
* merely refresh the panel's readout when it could not.
|
|
6044
|
+
*
|
|
6045
|
+
* `winnerBefore` is sampled by the caller BEFORE its mutation (`winnerId()`),
|
|
6046
|
+
* and the comparison itself lives in ./repaint, where it is checked on plain
|
|
6047
|
+
* node. Every write path below ends here — the old per-path test
|
|
6048
|
+
* (`id === activeRuleId`) missed the edit that CREATES the winner, which is the
|
|
6049
|
+
* "上传图片 / 提取主题色之后界面不动,动一下调色盘或切模型才出现" report.
|
|
6050
|
+
*/
|
|
6051
|
+
const repaintIfMoved = (id, winnerBefore) => {
|
|
6052
|
+
if (shouldRepaint(id, winnerBefore, winnerId())) applyActive();
|
|
6053
|
+
else sync();
|
|
6054
|
+
};
|
|
6055
|
+
/**
|
|
6056
|
+
* Resolve the active rule for the current model and repaint everything.
|
|
6057
|
+
*
|
|
6058
|
+
* @param advance - true only on a MODEL switch: the rotation of the rule that
|
|
6059
|
+
* took over may then step once (`rotate.advanceOnSwitch`), which is how a
|
|
6060
|
+
* model gets "a different picture every time" without any timer at all.
|
|
6061
|
+
*/
|
|
6062
|
+
const applyActive = (advance = false) => {
|
|
6063
|
+
const { rule, matched } = resolveRule();
|
|
5118
6064
|
setActive(rule === null ? null : rule.id, matched);
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
skinTimer = null;
|
|
5126
|
-
registerCustom(color[0], color[1], color[2]);
|
|
5127
|
-
}, 60);
|
|
5128
|
-
syncHostScheme();
|
|
6065
|
+
if (advance && rule !== null && rule.rotate.advanceOnSwitch) {
|
|
6066
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
6067
|
+
if (next !== null) setRotIndex(next);
|
|
6068
|
+
}
|
|
6069
|
+
scheduleRotation();
|
|
6070
|
+
applyPalette();
|
|
5129
6071
|
applyWp();
|
|
5130
6072
|
sync();
|
|
5131
6073
|
};
|
|
6074
|
+
let rotTimer = null;
|
|
6075
|
+
/** Identity of the schedule currently armed; a change means "re-arm". */
|
|
6076
|
+
let rotSig = "";
|
|
6077
|
+
/** Warm one image so a later switch fades toward bytes that are already here. */
|
|
6078
|
+
const warmImage = (rule, idx) => {
|
|
6079
|
+
const slot = rule.images[idx]?.slot;
|
|
6080
|
+
if (slot !== void 0) loadSlot(slot);
|
|
6081
|
+
};
|
|
6082
|
+
/** Show the image at `idx` at once, loading it first when it is not in memory. */
|
|
6083
|
+
const paintImage = (rule, idx) => {
|
|
6084
|
+
const slot = rule.images[idx]?.slot;
|
|
6085
|
+
if (slot === void 0) return;
|
|
6086
|
+
applyPalette();
|
|
6087
|
+
if (imageOf(slot) !== null) {
|
|
6088
|
+
applyWp();
|
|
6089
|
+
sync();
|
|
6090
|
+
return;
|
|
6091
|
+
}
|
|
6092
|
+
loadSlot(slot).then(() => {
|
|
6093
|
+
applyWp();
|
|
6094
|
+
sync();
|
|
6095
|
+
});
|
|
6096
|
+
};
|
|
6097
|
+
const rotateTick = () => {
|
|
6098
|
+
const rule = activeRule();
|
|
6099
|
+
if (rule === null) return;
|
|
6100
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
6101
|
+
if (next === null) {
|
|
6102
|
+
scheduleRotation(true);
|
|
6103
|
+
return;
|
|
6104
|
+
}
|
|
6105
|
+
setRotIndex(next);
|
|
6106
|
+
paintImage(rule, next);
|
|
6107
|
+
const after = nextIndex(rule.images.length, rule.rotate.order, next);
|
|
6108
|
+
if (after !== null) warmImage(rule, after);
|
|
6109
|
+
};
|
|
6110
|
+
/**
|
|
6111
|
+
* Arm (or re-arm) the rotation for the active rule.
|
|
6112
|
+
*
|
|
6113
|
+
* Idempotent by SIGNATURE rather than by "already armed": a slider drag on the
|
|
6114
|
+
* active rule re-runs the whole apply dozens of times per second, and restarting
|
|
6115
|
+
* the interval on each of those would mean the dwell timer never elapses while
|
|
6116
|
+
* the user is dragging (and a 10 s rotation would never advance). The signature
|
|
6117
|
+
* carries exactly the inputs the schedule depends on, so a real change — another
|
|
6118
|
+
* rule, another interval, the tab becoming visible again — re-arms while a
|
|
6119
|
+
* repaint does not.
|
|
6120
|
+
*/
|
|
6121
|
+
const scheduleRotation = (force = false) => {
|
|
6122
|
+
const rule = activeRule();
|
|
6123
|
+
const sig = rule === null ? "" : [
|
|
6124
|
+
rule.id,
|
|
6125
|
+
rule.rotate.enabled,
|
|
6126
|
+
rule.rotate.intervalMs,
|
|
6127
|
+
rule.rotate.order,
|
|
6128
|
+
rule.images.length,
|
|
6129
|
+
document.hidden
|
|
6130
|
+
].join("|");
|
|
6131
|
+
if (!force && sig === rotSig) return;
|
|
6132
|
+
rotSig = sig;
|
|
6133
|
+
if (rotTimer !== null) {
|
|
6134
|
+
window.clearInterval(rotTimer);
|
|
6135
|
+
rotTimer = null;
|
|
6136
|
+
}
|
|
6137
|
+
if (rule === null || document.hidden) return;
|
|
6138
|
+
if (!isRotating(rule.images.length, rule.rotate.enabled, rule.rotate.intervalMs)) return;
|
|
6139
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
6140
|
+
if (next !== null) warmImage(rule, next);
|
|
6141
|
+
rotTimer = window.setInterval(rotateTick, rule.rotate.intervalMs);
|
|
6142
|
+
};
|
|
6143
|
+
ctx.effect(() => () => {
|
|
6144
|
+
if (rotTimer !== null) {
|
|
6145
|
+
window.clearInterval(rotTimer);
|
|
6146
|
+
rotTimer = null;
|
|
6147
|
+
}
|
|
6148
|
+
}, "dsh-background-by-model: rotation timer");
|
|
6149
|
+
const onVisibility = () => {
|
|
6150
|
+
scheduleRotation();
|
|
6151
|
+
sync();
|
|
6152
|
+
};
|
|
6153
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
6154
|
+
ctx.effect(() => () => document.removeEventListener("visibilitychange", onVisibility), "dsh-background-by-model: rotation visibility");
|
|
5132
6155
|
/**
|
|
5133
6156
|
* The holiday the background must follow right now, or null.
|
|
5134
6157
|
*
|
|
@@ -5150,7 +6173,7 @@ window.__ModuleLoader__.load({
|
|
|
5150
6173
|
modelSource = source;
|
|
5151
6174
|
modelNote = note;
|
|
5152
6175
|
setModelLabel(label !== "" ? label : text);
|
|
5153
|
-
if (textChanged) applyActive();
|
|
6176
|
+
if (textChanged) applyActive(true);
|
|
5154
6177
|
else sync();
|
|
5155
6178
|
});
|
|
5156
6179
|
ctx.effect(() => () => offModel(), "dsh-background-by-model: model watch");
|
|
@@ -5197,29 +6220,55 @@ window.__ModuleLoader__.load({
|
|
|
5197
6220
|
const id = activeHoliday();
|
|
5198
6221
|
if (id === null) return null;
|
|
5199
6222
|
const item = holidayById(id);
|
|
5200
|
-
|
|
6223
|
+
if (item === null || !item.enabled) return null;
|
|
6224
|
+
return ruleSlots(item)[0] ?? null;
|
|
5201
6225
|
};
|
|
5202
|
-
/**
|
|
5203
|
-
|
|
6226
|
+
/**
|
|
6227
|
+
* The slots boot hydrates, in no particular order: the FIRST image of every
|
|
6228
|
+
* rule, today's holiday, and the image the current model paints.
|
|
6229
|
+
*
|
|
6230
|
+
* That first-image set is exactly what this plugin loaded per rule before
|
|
6231
|
+
* multi-image existed — a rule card draws its first image as its thumbnail, so
|
|
6232
|
+
* the settings page looks the same as it always did. Everything past the first
|
|
6233
|
+
* image of a rule is read ON DEMAND: by the rotation (which warms the image it
|
|
6234
|
+
* is about to need as soon as its schedule is armed) and by the settings page
|
|
6235
|
+
* when a card is expanded. Hydrating every image of every rule here would mean
|
|
6236
|
+
* twenty full-size data URLs over this channel before the first frame on a
|
|
6237
|
+
* five-rule setup with four pictures each, which is the one cost this feature
|
|
6238
|
+
* must not pay.
|
|
6239
|
+
*
|
|
6240
|
+
* The slot list the store knows about (`readPersisted().slots`) is deliberately
|
|
6241
|
+
* NOT hydrated any more: an orphaned slot that no rule references has no card to
|
|
6242
|
+
* appear on, so pulling its bytes over the channel at boot is pure waste. It
|
|
6243
|
+
* stays on disk — and is still reported by the node half — until a rule
|
|
6244
|
+
* references it again or it is deleted with its rule.
|
|
6245
|
+
*/
|
|
6246
|
+
const bootSlots = () => {
|
|
5204
6247
|
const holiday = todayHolidaySlot();
|
|
6248
|
+
const firsts = cfg.rules.map((r) => r.images[0]?.slot).filter((slot) => slot !== void 0);
|
|
6249
|
+
const current = prioritySlot();
|
|
5205
6250
|
return Array.from(/* @__PURE__ */ new Set([
|
|
5206
|
-
...
|
|
5207
|
-
...
|
|
5208
|
-
...
|
|
6251
|
+
...firsts,
|
|
6252
|
+
...holiday === null ? [] : [holiday],
|
|
6253
|
+
...current === null ? [] : [current]
|
|
5209
6254
|
]));
|
|
5210
6255
|
};
|
|
5211
6256
|
/**
|
|
5212
6257
|
* The slot that paints before anything else: the holiday in force when there
|
|
5213
|
-
* is one, otherwise the
|
|
5214
|
-
* the rule here for the same reason it outranks it when painting.
|
|
6258
|
+
* is one, otherwise the FIRST image of the current model's rule. A holiday
|
|
6259
|
+
* outranks the rule here for the same reason it outranks it when painting.
|
|
6260
|
+
*
|
|
6261
|
+
* Always the first image (never the one `advanceOnSwitch` would step to): boot
|
|
6262
|
+
* paints without an advance, and hydrating an image the first frame is not going
|
|
6263
|
+
* to use would leave the frame blank until the real one arrived.
|
|
5215
6264
|
*/
|
|
5216
|
-
const prioritySlot = () => todayHolidaySlot() ?? matchRule(cfg.rules, modelText).rule?.slot ?? null;
|
|
6265
|
+
const prioritySlot = () => todayHolidaySlot() ?? matchRule(cfg.rules, modelText).rule?.images[0]?.slot ?? null;
|
|
5217
6266
|
(async () => {
|
|
5218
6267
|
const persisted = await loadPersisted();
|
|
5219
6268
|
if (persisted !== null) {
|
|
5220
6269
|
adoptConfig(persisted.config);
|
|
5221
6270
|
const first = prioritySlot();
|
|
5222
|
-
const slots =
|
|
6271
|
+
const slots = bootSlots();
|
|
5223
6272
|
const order = first === null ? slots : [first, ...slots.filter((s) => s !== first)];
|
|
5224
6273
|
for (const slot of order) {
|
|
5225
6274
|
await loadSlot(slot);
|
|
@@ -5233,7 +6282,7 @@ window.__ModuleLoader__.load({
|
|
|
5233
6282
|
}, "dsh-background-by-model: wp cleanup");
|
|
5234
6283
|
ctx.effect(() => ctx.on("theme/change", () => {
|
|
5235
6284
|
syncHostScheme();
|
|
5236
|
-
if (
|
|
6285
|
+
if (activeColor() !== null) {
|
|
5237
6286
|
const snapshot = ctx.theme.getTheme();
|
|
5238
6287
|
if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
|
|
5239
6288
|
}
|
|
@@ -5264,34 +6313,64 @@ window.__ModuleLoader__.load({
|
|
|
5264
6313
|
en
|
|
5265
6314
|
}), "dsh-background-by-model: i18n");
|
|
5266
6315
|
/**
|
|
5267
|
-
* The automatic half of "extract from image": fill
|
|
5268
|
-
*
|
|
5269
|
-
*
|
|
5270
|
-
*
|
|
6316
|
+
* The automatic half of "extract from image": fill an IMAGE's theme color from
|
|
6317
|
+
* its own bytes when it has none of its own, and only then — a color the user
|
|
6318
|
+
* picked (or a previous extraction produced) is never overwritten, so this
|
|
6319
|
+
* cannot fight a deliberate choice.
|
|
6320
|
+
*
|
|
6321
|
+
* `slot` names the image that just arrived, because a rule's images each carry
|
|
6322
|
+
* their own palette since 0.7.1 and the one the user just added is the one they
|
|
6323
|
+
* are looking at. Omitted (or unknown) falls back to the rule's first image.
|
|
6324
|
+
*
|
|
6325
|
+
* The question is asked of the IMAGE, not of the rule: a rule whose pictures
|
|
6326
|
+
* were all cleared and then re-filled must theme each new picture, and a rule
|
|
6327
|
+
* with a color of its own still has no say over the colors of its pictures.
|
|
5271
6328
|
*
|
|
5272
6329
|
* The decode is asynchronous, so the "still empty?" question is asked AGAIN on
|
|
5273
6330
|
* arrival: a color chosen while it ran must win over the one being computed.
|
|
5274
6331
|
*/
|
|
5275
|
-
const maybeAutoExtract = async (id) => {
|
|
6332
|
+
const maybeAutoExtract = async (id, slot) => {
|
|
5276
6333
|
if (!cfg.autoExtract) return;
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
6334
|
+
/** The entry this extraction is about, re-resolved on every read. */
|
|
6335
|
+
const target = (rule) => {
|
|
6336
|
+
if (rule === null) return void 0;
|
|
6337
|
+
return (slot === void 0 ? void 0 : rule.images.find((i) => i.slot === slot)) ?? rule.images[0];
|
|
6338
|
+
};
|
|
6339
|
+
const image = target(ruleById(id));
|
|
6340
|
+
if (image === void 0 || image.color !== null) return;
|
|
6341
|
+
const url = displayImageOf(image.slot);
|
|
5280
6342
|
if (url === null) return;
|
|
5281
6343
|
let hsl = null;
|
|
5282
6344
|
try {
|
|
5283
|
-
hsl = await extractWallpaperColor(url,
|
|
6345
|
+
hsl = await extractWallpaperColor(url, image.bgState);
|
|
5284
6346
|
} catch {
|
|
5285
6347
|
hsl = null;
|
|
5286
6348
|
}
|
|
5287
6349
|
if (hsl === null) return;
|
|
5288
|
-
const current = ruleById(id);
|
|
5289
|
-
if (current ===
|
|
6350
|
+
const current = target(ruleById(id));
|
|
6351
|
+
if (current === void 0 || current.color !== null) return;
|
|
6352
|
+
const winner = winnerId();
|
|
5290
6353
|
current.color = hsl;
|
|
5291
6354
|
rulesRev++;
|
|
5292
6355
|
saveConfig();
|
|
5293
|
-
|
|
5294
|
-
|
|
6356
|
+
repaintIfMoved(id, winner);
|
|
6357
|
+
};
|
|
6358
|
+
/**
|
|
6359
|
+
* The batch form, for one upload that carried several pictures.
|
|
6360
|
+
*
|
|
6361
|
+
* EVERY picture that arrived is themed from its own bytes, the first ones
|
|
6362
|
+
* included. The single-slot reading of an upload ("the last image added is the
|
|
6363
|
+
* one being looked at") is true of the CARD's selection and false of the
|
|
6364
|
+
* palette: since a theme color belongs to an image, a batch that themed only
|
|
6365
|
+
* its last file left the rest of a fresh rotation on the system theme, with
|
|
6366
|
+
* nothing in the interface to say why.
|
|
6367
|
+
*
|
|
6368
|
+
* Sequential on purpose: a batch is a handful of full-size photos, and ten
|
|
6369
|
+
* parallel decodes buy nothing the user can see, while landing the colors in
|
|
6370
|
+
* list order does (the card fills in from its first picture).
|
|
6371
|
+
*/
|
|
6372
|
+
const maybeAutoExtractEach = async (id, slots) => {
|
|
6373
|
+
for (const slot of slots) await maybeAutoExtract(id, slot);
|
|
5295
6374
|
};
|
|
5296
6375
|
const activeLang = () => {
|
|
5297
6376
|
try {
|
|
@@ -5322,7 +6401,7 @@ window.__ModuleLoader__.load({
|
|
|
5322
6401
|
readModelFacts,
|
|
5323
6402
|
imageOf: (slot) => displayImageOf(slot),
|
|
5324
6403
|
addRule: () => {
|
|
5325
|
-
const rule = newRule(nextRuleId()
|
|
6404
|
+
const rule = newRule(nextRuleId());
|
|
5326
6405
|
cfg.rules.push(rule);
|
|
5327
6406
|
rulesRev++;
|
|
5328
6407
|
persistConfig();
|
|
@@ -5333,9 +6412,13 @@ window.__ModuleLoader__.load({
|
|
|
5333
6412
|
const idx = cfg.rules.findIndex((r) => r.id === id);
|
|
5334
6413
|
if (idx < 0) return;
|
|
5335
6414
|
const [rule] = cfg.rules.splice(idx, 1);
|
|
5336
|
-
if (rule !== void 0
|
|
5337
|
-
|
|
5338
|
-
|
|
6415
|
+
if (rule !== void 0) {
|
|
6416
|
+
const stillUsed = takenSlots();
|
|
6417
|
+
for (const slot of ruleSlots(rule)) {
|
|
6418
|
+
if (stillUsed.has(slot)) continue;
|
|
6419
|
+
setImage(slot, null);
|
|
6420
|
+
deleteImage(slot);
|
|
6421
|
+
}
|
|
5339
6422
|
}
|
|
5340
6423
|
rulesRev++;
|
|
5341
6424
|
persistConfig();
|
|
@@ -5354,50 +6437,182 @@ window.__ModuleLoader__.load({
|
|
|
5354
6437
|
setRule: (id, patch) => {
|
|
5355
6438
|
const rule = ruleById(id);
|
|
5356
6439
|
if (rule === null) return;
|
|
6440
|
+
const winner = winnerId();
|
|
5357
6441
|
Object.assign(rule, patch);
|
|
5358
|
-
|
|
5359
|
-
if (normalized !== null) Object.assign(rule, normalized);
|
|
6442
|
+
normalizeRuleInPlace(rule);
|
|
5360
6443
|
rulesRev++;
|
|
5361
6444
|
saveConfig();
|
|
5362
|
-
|
|
5363
|
-
|
|
6445
|
+
repaintIfMoved(id, winner);
|
|
6446
|
+
},
|
|
6447
|
+
addRuleImages: (id, dataUrls) => {
|
|
6448
|
+
const rule = ruleById(id);
|
|
6449
|
+
if (rule === null || dataUrls.length === 0) return;
|
|
6450
|
+
const winner = winnerId();
|
|
6451
|
+
/** Slots of THIS batch, in list order — the extraction below is per slot. */
|
|
6452
|
+
const added = [];
|
|
6453
|
+
for (const dataUrl of dataUrls) {
|
|
6454
|
+
const slot = nextSlot();
|
|
6455
|
+
const image = {
|
|
6456
|
+
slot,
|
|
6457
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
6458
|
+
color: null
|
|
6459
|
+
};
|
|
6460
|
+
rule.images.push(image);
|
|
6461
|
+
added.push(slot);
|
|
6462
|
+
setImage(slot, dataUrl);
|
|
6463
|
+
writeImage(slot, dataUrl);
|
|
6464
|
+
}
|
|
6465
|
+
rulesRev++;
|
|
6466
|
+
persistConfig();
|
|
6467
|
+
repaintIfMoved(id, winner);
|
|
6468
|
+
maybeAutoExtractEach(id, added);
|
|
5364
6469
|
},
|
|
5365
|
-
|
|
6470
|
+
removeRuleImage: (id, slot) => {
|
|
5366
6471
|
const rule = ruleById(id);
|
|
5367
6472
|
if (rule === null) return;
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
if (
|
|
6473
|
+
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
6474
|
+
if (idx < 0) return;
|
|
6475
|
+
const winner = winnerId();
|
|
6476
|
+
rule.images.splice(idx, 1);
|
|
6477
|
+
if (id === activeRuleId) setRotIndex(0);
|
|
6478
|
+
rulesRev++;
|
|
6479
|
+
persistConfig();
|
|
6480
|
+
if (!takenSlots().has(slot)) {
|
|
6481
|
+
setImage(slot, null);
|
|
6482
|
+
deleteImage(slot);
|
|
6483
|
+
}
|
|
6484
|
+
repaintIfMoved(id, winner);
|
|
6485
|
+
},
|
|
6486
|
+
moveRuleImageTo: (id, slot, to) => {
|
|
6487
|
+
const rule = ruleById(id);
|
|
6488
|
+
if (rule === null) return;
|
|
6489
|
+
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
6490
|
+
if (idx < 0) return;
|
|
6491
|
+
const at = Math.max(0, Math.min(rule.images.length - 1, Math.trunc(to)));
|
|
6492
|
+
if (at === idx) return;
|
|
6493
|
+
const winner = winnerId();
|
|
6494
|
+
const painted = id === activeRuleId ? rule.images[imageIndexOf(rule)]?.slot ?? null : null;
|
|
6495
|
+
const [image] = rule.images.splice(idx, 1);
|
|
6496
|
+
rule.images.splice(at, 0, image);
|
|
6497
|
+
rulesRev++;
|
|
6498
|
+
if (id === activeRuleId) {
|
|
6499
|
+
const keep = painted === null ? -1 : rule.images.findIndex((i) => i.slot === painted);
|
|
6500
|
+
setRotIndex(keep < 0 ? Math.min(at, rule.images.length - 1) : keep);
|
|
6501
|
+
scheduleRotation(true);
|
|
6502
|
+
}
|
|
6503
|
+
persistConfig();
|
|
6504
|
+
repaintIfMoved(id, winner);
|
|
6505
|
+
},
|
|
6506
|
+
setImageFraming: (id, slot, bgState) => {
|
|
6507
|
+
const rule = ruleById(id);
|
|
6508
|
+
const image = rule?.images.find((i) => i.slot === slot);
|
|
6509
|
+
if (image === void 0) return;
|
|
6510
|
+
const winner = winnerId();
|
|
6511
|
+
image.bgState = bgState;
|
|
6512
|
+
normalizeRuleInPlace(rule);
|
|
6513
|
+
rulesRev++;
|
|
6514
|
+
saveConfig();
|
|
6515
|
+
repaintIfMoved(id, winner);
|
|
6516
|
+
},
|
|
6517
|
+
setRuleRotation: (id, patch) => {
|
|
6518
|
+
const rule = ruleById(id);
|
|
6519
|
+
if (rule === null) return;
|
|
6520
|
+
patchRotation(rule, patch);
|
|
6521
|
+
rulesRev++;
|
|
6522
|
+
persistConfig();
|
|
6523
|
+
scheduleRotation();
|
|
6524
|
+
sync();
|
|
5373
6525
|
},
|
|
5374
|
-
|
|
6526
|
+
rotateNow: (id) => {
|
|
6527
|
+
const rule = ruleById(id);
|
|
6528
|
+
if (rule === null || id !== activeRuleId) return;
|
|
6529
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
6530
|
+
if (next === null) return;
|
|
6531
|
+
setRotIndex(next);
|
|
6532
|
+
paintImage(rule, next);
|
|
6533
|
+
scheduleRotation(true);
|
|
6534
|
+
},
|
|
6535
|
+
showRuleImage: (id, slot) => {
|
|
6536
|
+
const rule = ruleById(id);
|
|
6537
|
+
if (rule === null || id !== activeRuleId) return;
|
|
6538
|
+
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
6539
|
+
if (idx < 0) return;
|
|
6540
|
+
setRotIndex(idx);
|
|
6541
|
+
paintImage(rule, idx);
|
|
6542
|
+
scheduleRotation(true);
|
|
6543
|
+
},
|
|
6544
|
+
loadRuleImages: async (id) => {
|
|
6545
|
+
const rule = ruleById(id);
|
|
6546
|
+
if (rule === null) return;
|
|
6547
|
+
for (const slot of ruleSlots(rule)) await loadSlot(slot);
|
|
6548
|
+
sync();
|
|
6549
|
+
},
|
|
6550
|
+
setRuleImage: (id, slot, dataUrl) => {
|
|
6551
|
+
const rule = ruleById(id);
|
|
6552
|
+
if (rule === null || !rule.images.some((i) => i.slot === slot)) return;
|
|
6553
|
+
const winner = winnerId();
|
|
6554
|
+
setImage(slot, dataUrl);
|
|
6555
|
+
dataUrl === null ? deleteImage(slot) : writeImage(slot, dataUrl);
|
|
6556
|
+
repaintIfMoved(id, winner);
|
|
6557
|
+
if (dataUrl !== null) maybeAutoExtract(id, slot);
|
|
6558
|
+
},
|
|
6559
|
+
addRuleImageFromUrl: async (id, url) => {
|
|
5375
6560
|
const rule = ruleById(id);
|
|
5376
6561
|
if (rule === null) return {
|
|
5377
6562
|
ok: false,
|
|
5378
6563
|
error: "unknown rule"
|
|
5379
6564
|
};
|
|
5380
|
-
const
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
6565
|
+
const slot = nextSlot();
|
|
6566
|
+
const res = await fetchImageUrl(slot, url);
|
|
6567
|
+
if (!res.ok) return res;
|
|
6568
|
+
const winner = winnerId();
|
|
6569
|
+
rule.images.push({
|
|
6570
|
+
slot,
|
|
6571
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
6572
|
+
color: null
|
|
6573
|
+
});
|
|
6574
|
+
setImage(slot, res.dataUrl ?? null);
|
|
6575
|
+
rulesRev++;
|
|
6576
|
+
persistConfig();
|
|
6577
|
+
repaintIfMoved(id, winner);
|
|
6578
|
+
maybeAutoExtract(id, slot);
|
|
5387
6579
|
return res;
|
|
5388
6580
|
},
|
|
5389
|
-
|
|
6581
|
+
/**
|
|
6582
|
+
* Store ONE image's theme color (null = that image follows the system
|
|
6583
|
+
* theme). Addressed by slot exactly like `setImageFraming`, because the
|
|
6584
|
+
* color belongs to the image: editing image 3's color must not rewrite
|
|
6585
|
+
* image 1's, and it must not touch the RULE's color either — that one is
|
|
6586
|
+
* what the rule paints once its last picture is gone, and it is edited only
|
|
6587
|
+
* while there is no image to edit (the panel's own branch).
|
|
6588
|
+
*/
|
|
6589
|
+
setImageColor: (id, slot, color) => {
|
|
6590
|
+
const rule = ruleById(id);
|
|
6591
|
+
const image = rule?.images.find((i) => i.slot === slot);
|
|
6592
|
+
if (rule === null || rule === void 0 || image === void 0) return;
|
|
6593
|
+
const winner = winnerId();
|
|
6594
|
+
image.color = color;
|
|
6595
|
+
normalizeRuleInPlace(rule);
|
|
6596
|
+
rulesRev++;
|
|
6597
|
+
saveConfig();
|
|
6598
|
+
repaintIfMoved(id, winner);
|
|
6599
|
+
},
|
|
6600
|
+
extractColor: async (id, slot) => {
|
|
5390
6601
|
const rule = ruleById(id);
|
|
5391
6602
|
if (rule === null) return false;
|
|
5392
|
-
const
|
|
6603
|
+
const image = rule.images.find((i) => i.slot === slot) ?? rule.images[0];
|
|
6604
|
+
if (image === void 0) return false;
|
|
6605
|
+
const url = displayImageOf(image.slot);
|
|
5393
6606
|
if (url === null) return false;
|
|
5394
|
-
const hsl = await extractWallpaperColor(url,
|
|
6607
|
+
const hsl = await extractWallpaperColor(url, image.bgState);
|
|
5395
6608
|
if (hsl === null) return false;
|
|
5396
|
-
|
|
6609
|
+
const current = ruleById(id)?.images.find((i) => i.slot === image.slot);
|
|
6610
|
+
if (current === void 0) return false;
|
|
6611
|
+
const winner = winnerId();
|
|
6612
|
+
current.color = hsl;
|
|
5397
6613
|
rulesRev++;
|
|
5398
6614
|
saveConfig();
|
|
5399
|
-
|
|
5400
|
-
else sync();
|
|
6615
|
+
repaintIfMoved(id, winner);
|
|
5401
6616
|
return true;
|
|
5402
6617
|
},
|
|
5403
6618
|
setOps: (ops) => {
|
|
@@ -5436,13 +6651,13 @@ window.__ModuleLoader__.load({
|
|
|
5436
6651
|
},
|
|
5437
6652
|
exportTheme: () => {
|
|
5438
6653
|
const images = {};
|
|
5439
|
-
const slots = cfg.rules.
|
|
6654
|
+
const slots = cfg.rules.flatMap(ruleSlots);
|
|
5440
6655
|
for (const slot of slots) {
|
|
5441
6656
|
const url = imageOf(slot);
|
|
5442
6657
|
if (url !== null) images[slot] = url;
|
|
5443
6658
|
}
|
|
5444
6659
|
const payload = {
|
|
5445
|
-
version:
|
|
6660
|
+
version: 5,
|
|
5446
6661
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5447
6662
|
config: cfg,
|
|
5448
6663
|
images
|
|
@@ -5464,7 +6679,7 @@ window.__ModuleLoader__.load({
|
|
|
5464
6679
|
adoptConfig(d.config);
|
|
5465
6680
|
const incoming = d.images ?? {};
|
|
5466
6681
|
const keep = /* @__PURE__ */ new Set();
|
|
5467
|
-
for (const slot of cfg.rules.
|
|
6682
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
5468
6683
|
keep.add(slot);
|
|
5469
6684
|
const raw = incoming[slot];
|
|
5470
6685
|
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
@@ -5544,7 +6759,7 @@ window.__ModuleLoader__.load({
|
|
|
5544
6759
|
}, "dsh-background-by-model: nav icon watch");
|
|
5545
6760
|
const restoreSaved = () => {
|
|
5546
6761
|
syncHostScheme();
|
|
5547
|
-
const color =
|
|
6762
|
+
const color = activeColor();
|
|
5548
6763
|
if (color !== null) {
|
|
5549
6764
|
const snapshot = ctx.theme.getTheme();
|
|
5550
6765
|
if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) registerCustom(color[0], color[1], color[2]);
|
|
@@ -5558,7 +6773,7 @@ window.__ModuleLoader__.load({
|
|
|
5558
6773
|
}, "dsh-background-by-model: boot restore");
|
|
5559
6774
|
const watchdogId = window.setInterval(() => {
|
|
5560
6775
|
syncHostScheme();
|
|
5561
|
-
const color =
|
|
6776
|
+
const color = activeColor();
|
|
5562
6777
|
if (color === null) {
|
|
5563
6778
|
refreshSystemTheme();
|
|
5564
6779
|
return;
|
|
@@ -5585,12 +6800,6 @@ window.__ModuleLoader__.load({
|
|
|
5585
6800
|
window.addEventListener("pagehide", onPageHide);
|
|
5586
6801
|
ctx.effect(() => () => window.removeEventListener("pagehide", onPageHide), "dsh-background-by-model: pagehide flush");
|
|
5587
6802
|
}
|
|
5588
|
-
/** Color of the currently active rule, or null when it uses the system theme. */
|
|
5589
|
-
function activeRuleColor() {
|
|
5590
|
-
if (activeRuleId === null) return null;
|
|
5591
|
-
const rule = ruleById(activeRuleId);
|
|
5592
|
-
return rule === null ? null : rule.color;
|
|
5593
|
-
}
|
|
5594
6803
|
//#endregion
|
|
5595
6804
|
exports.apply = apply;
|
|
5596
6805
|
exports.inject = inject;
|