dsh-background-by-model 0.7.1 → 0.7.3
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 +21 -1
- package/CHANGELOG.zh.md +21 -1
- package/README.md +43 -33
- package/README.zh.md +43 -33
- package/lib/client.js +807 -146
- package/lib/client.js.map +1 -1
- package/lib/index.js +86 -8
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -26,7 +26,7 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
pageProfile: "配置",
|
|
27
27
|
descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感。这里的设置对所有模型生效",
|
|
28
28
|
descModelBg: "每条规则 = 一个匹配串 + 一组背景图与外观。切换模型时,从上往下找第一条匹配串出现在模型名里的规则;都没中就使用第 1 条。一条规则可以放多张图,按你设的节奏轮换",
|
|
29
|
-
descProfile: "
|
|
29
|
+
descProfile: "调整壁纸切换效果,并把当前全部规则连同图片导出备份,或从文件一键恢复",
|
|
30
30
|
uiTitle: "主界面",
|
|
31
31
|
uiOpacity: "透明度",
|
|
32
32
|
uiBlur: "模糊度",
|
|
@@ -44,8 +44,6 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
statusNoteNoSession: "会话服务已就绪,但还没有当前会话 id",
|
|
45
45
|
statusNoteNoProjection: "拿不到本会话的模型选择投影(binding / projections 不可用)",
|
|
46
46
|
statusNoteEmptySelection: "本会话的模型选择投影还没有值(切一次模型即可写入)",
|
|
47
|
-
rulesTitle: "规则列表",
|
|
48
|
-
rulesHint: "从上到下即为优先级。规则 1 同时兼任兜底:所有匹配串都没命中时使用它",
|
|
49
47
|
hostStaleHint: "检测到 DSH 主进程仍在运行旧版插件:新版配置格式它还读不懂,为避免丢规则,本次只改界面不写入配置。重启 DSH 后多图与轮换即可正常保存",
|
|
50
48
|
tryoutTitle: "匹配试跑",
|
|
51
49
|
tryoutPlaceholder: "输入模型名,如 deepseek-flash",
|
|
@@ -55,7 +53,6 @@ window.__ModuleLoader__.load({
|
|
|
55
53
|
tryoutNone: "没有可用规则(规则需要启用,并且有图片或主题色)",
|
|
56
54
|
tryoutEmpty: "输入模型名即可试跑",
|
|
57
55
|
autoExtract: "选图后自动取主题色",
|
|
58
|
-
autoExtractHint: "只在图片还没有自己的颜色时自动取色,绝不覆盖你手动选的颜色",
|
|
59
56
|
ruleAdd: "新增规则",
|
|
60
57
|
ruleFallbackBadge: "兜底",
|
|
61
58
|
ruleMatchPlaceholder: "如 flash / glm(忽略大小写,留空则不参与匹配)",
|
|
@@ -71,7 +68,7 @@ window.__ModuleLoader__.load({
|
|
|
71
68
|
ruleUrlApplying: "加载中…",
|
|
72
69
|
ruleUrlBadHttp: "仅支持 http/https 图片网址",
|
|
73
70
|
ruleUrlFail: "获取图片失败",
|
|
74
|
-
ruleImageRemove: "
|
|
71
|
+
ruleImageRemove: "移除",
|
|
75
72
|
ruleEmptyHint: "这条规则还没有图片,也没有自己的主题色:没有可显示的内容,匹配和兜底都会跳过它",
|
|
76
73
|
ruleColorOnlyHint: "这条规则还没有图片:不会铺壁纸,界面只用这个主题色",
|
|
77
74
|
ruleImagePendingHint: "这张图片还没有取到内容,稍后会自己出现(也可以直接替换它)",
|
|
@@ -102,13 +99,12 @@ window.__ModuleLoader__.load({
|
|
|
102
99
|
rotOnSwitch: "切换模型时也换一张",
|
|
103
100
|
rotNext: "下一张",
|
|
104
101
|
rotNextInactive: "只有正在生效的规则能换画面上的图",
|
|
105
|
-
rotPos: "
|
|
102
|
+
rotPos: "第",
|
|
106
103
|
rotShowing: "正在显示",
|
|
107
|
-
rotAddImage: "
|
|
108
|
-
rotReplaceImage: "
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
rotMakeFirst: "设为第一张",
|
|
104
|
+
rotAddImage: "添加",
|
|
105
|
+
rotReplaceImage: "替换",
|
|
106
|
+
rotDragHint: "拖动排序",
|
|
107
|
+
rotDblHint: "双击切换",
|
|
112
108
|
rotEvery10s: "10 秒",
|
|
113
109
|
rotEvery30s: "30 秒",
|
|
114
110
|
rotEvery1m: "1 分钟",
|
|
@@ -146,7 +142,22 @@ window.__ModuleLoader__.load({
|
|
|
146
142
|
importFail: "导入失败,文件格式不正确",
|
|
147
143
|
footerTag: "按模型换背景插件",
|
|
148
144
|
holidayTitle: "节日特殊背景",
|
|
149
|
-
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)"
|
|
145
|
+
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)",
|
|
146
|
+
trTitle: "切换效果",
|
|
147
|
+
trEffect: "切换方式",
|
|
148
|
+
trEffectFade: "淡入淡出",
|
|
149
|
+
trEffectNone: "直接切换",
|
|
150
|
+
trEffectZoom: "缩放淡入",
|
|
151
|
+
trEffectSlide: "平移推入",
|
|
152
|
+
trEasing: "缓动",
|
|
153
|
+
trEasingEase: "标准",
|
|
154
|
+
trEasingLinear: "匀速",
|
|
155
|
+
trEasingOut: "先快后慢",
|
|
156
|
+
trEasingInOut: "两端慢",
|
|
157
|
+
trDuration: "切换时长",
|
|
158
|
+
trPreview: "试放",
|
|
159
|
+
trHint: "效果对所有换图生效:切模型、轮换、手动下一张、节日。「直接切换」和 0 秒一样是硬切",
|
|
160
|
+
trReducedHint: "系统开启了「减少动态效果」,实际切换不会播放动画"
|
|
150
161
|
};
|
|
151
162
|
const en = {
|
|
152
163
|
nav: "Model background",
|
|
@@ -156,7 +167,7 @@ window.__ModuleLoader__.load({
|
|
|
156
167
|
pageProfile: "Profile",
|
|
157
168
|
descInterface: "Tune opacity and blur per surface to build depth. These settings are global and apply to every model",
|
|
158
169
|
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",
|
|
159
|
-
descProfile: "
|
|
170
|
+
descProfile: "Tune how the wallpaper switches, and back up every rule together with its image — or restore one from a file",
|
|
160
171
|
uiTitle: "Interface",
|
|
161
172
|
uiOpacity: "Opacity",
|
|
162
173
|
uiBlur: "Blur",
|
|
@@ -174,8 +185,6 @@ window.__ModuleLoader__.load({
|
|
|
174
185
|
statusNoteNoSession: "The sessions service is up but reports no current session yet",
|
|
175
186
|
statusNoteNoProjection: "This session's model-selection projection is unreachable (no binding / projections)",
|
|
176
187
|
statusNoteEmptySelection: "The model-selection projection exists but carries no value yet (selecting a model once writes it)",
|
|
177
|
-
rulesTitle: "Rules",
|
|
178
|
-
rulesHint: "Top to bottom is the priority. Rule 1 doubles as the fallback — it is used when no match string hits",
|
|
179
188
|
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",
|
|
180
189
|
tryoutTitle: "Match test",
|
|
181
190
|
tryoutPlaceholder: "Type a model name, e.g. deepseek-flash",
|
|
@@ -185,7 +194,6 @@ window.__ModuleLoader__.load({
|
|
|
185
194
|
tryoutNone: "No usable rule (a rule needs to be enabled and carry an image or a theme color)",
|
|
186
195
|
tryoutEmpty: "Type a model name to test the list",
|
|
187
196
|
autoExtract: "Extract the theme color when an image is chosen",
|
|
188
|
-
autoExtractHint: "Only fills an image that has no color of its own yet — a color you picked is never overwritten",
|
|
189
197
|
ruleAdd: "Add rule",
|
|
190
198
|
ruleFallbackBadge: "fallback",
|
|
191
199
|
ruleMatchPlaceholder: "e.g. flash / glm (case-insensitive, empty never matches)",
|
|
@@ -201,7 +209,7 @@ window.__ModuleLoader__.load({
|
|
|
201
209
|
ruleUrlApplying: "Loading…",
|
|
202
210
|
ruleUrlBadHttp: "Only http/https image URLs are supported",
|
|
203
211
|
ruleUrlFail: "Could not fetch the image",
|
|
204
|
-
ruleImageRemove: "Remove
|
|
212
|
+
ruleImageRemove: "Remove",
|
|
205
213
|
ruleEmptyHint: "This rule has no image and no theme color of its own: it has nothing to show, so matching and the fallback both skip it",
|
|
206
214
|
ruleColorOnlyHint: "No image in this rule yet: no wallpaper is shown, and the interface is painted from this theme color alone",
|
|
207
215
|
ruleImagePendingHint: "This image has no content yet — it appears once it loads; you can also replace it",
|
|
@@ -234,11 +242,10 @@ window.__ModuleLoader__.load({
|
|
|
234
242
|
rotNextInactive: "Only the active rule can step the background",
|
|
235
243
|
rotPos: "Image",
|
|
236
244
|
rotShowing: "On screen",
|
|
237
|
-
rotAddImage: "Add
|
|
238
|
-
rotReplaceImage: "Replace
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
rotMakeFirst: "Make first",
|
|
245
|
+
rotAddImage: "Add",
|
|
246
|
+
rotReplaceImage: "Replace",
|
|
247
|
+
rotDragHint: "drag to reorder",
|
|
248
|
+
rotDblHint: "double-click to show",
|
|
242
249
|
rotEvery10s: "10 s",
|
|
243
250
|
rotEvery30s: "30 s",
|
|
244
251
|
rotEvery1m: "1 min",
|
|
@@ -276,9 +283,106 @@ window.__ModuleLoader__.load({
|
|
|
276
283
|
importFail: "Import failed — invalid file",
|
|
277
284
|
footerTag: "Per-model wallpaper plugin",
|
|
278
285
|
holidayTitle: "Holiday backgrounds",
|
|
279
|
-
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)"
|
|
286
|
+
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)",
|
|
287
|
+
trTitle: "Switch effect",
|
|
288
|
+
trEffect: "Transition",
|
|
289
|
+
trEffectFade: "Cross-fade",
|
|
290
|
+
trEffectNone: "Instant",
|
|
291
|
+
trEffectZoom: "Zoom in",
|
|
292
|
+
trEffectSlide: "Slide in",
|
|
293
|
+
trEasing: "Easing",
|
|
294
|
+
trEasingEase: "Standard",
|
|
295
|
+
trEasingLinear: "Linear",
|
|
296
|
+
trEasingOut: "Ease out",
|
|
297
|
+
trEasingInOut: "Ease in-out",
|
|
298
|
+
trDuration: "Duration",
|
|
299
|
+
trPreview: "Preview",
|
|
300
|
+
trHint: "The effect applies to every change: model switch, rotation, manual next, holidays. “Instant” and 0 s are the same hard cut",
|
|
301
|
+
trReducedHint: "Your system asks for reduced motion, so a real switch will not animate"
|
|
280
302
|
};
|
|
281
303
|
//#endregion
|
|
304
|
+
//#region src/client/transition.ts
|
|
305
|
+
/**
|
|
306
|
+
* Resolve the duration one switch uses, in ms (`0` = a hard cut).
|
|
307
|
+
*
|
|
308
|
+
* 1. `effect: 'none'` is a hard cut whatever the duration says — asking for no
|
|
309
|
+
* animation and asking for 0 ms are the same request, and the effect is the
|
|
310
|
+
* more explicit of the two;
|
|
311
|
+
* 2. otherwise the global `durationMs` decides, because a rule has no notion of
|
|
312
|
+
* a transition and there is exactly one duration for every switch.
|
|
313
|
+
*
|
|
314
|
+
* A non-finite or negative value answers `0` (a hard cut) rather than a default:
|
|
315
|
+
* animating for an unknown length is the one answer that cannot be right. The
|
|
316
|
+
* input is already clamped by the shared sanitizer (`./schema`), so this is a
|
|
317
|
+
* guard against a hand-edited config, not a second clamp.
|
|
318
|
+
*/
|
|
319
|
+
function resolveFadeMs(transition) {
|
|
320
|
+
if (transition.effect === "none") return 0;
|
|
321
|
+
return Number.isFinite(transition.durationMs) && transition.durationMs > 0 ? transition.durationMs : 0;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The transform the incoming layer starts from, per effect.
|
|
325
|
+
*
|
|
326
|
+
* `zoom` starts ABOVE 1 and settles back to exactly `none`, never below or past
|
|
327
|
+
* it: the wallpaper would otherwise be left larger than the viewport, cropping a
|
|
328
|
+
* `fit` framing the user committed by hand. `slide` is a small push because a
|
|
329
|
+
* full-width travel of a full-bleed wallpaper reads as two wallpapers, not one
|
|
330
|
+
* transition.
|
|
331
|
+
*/
|
|
332
|
+
const START_TRANSFORM = {
|
|
333
|
+
fade: "none",
|
|
334
|
+
none: "none",
|
|
335
|
+
zoom: "scale(1.06)",
|
|
336
|
+
slide: "translateX(6%)"
|
|
337
|
+
};
|
|
338
|
+
/** The properties one effect actually animates — an unchanged property must not
|
|
339
|
+
* be put on the transition list, or `fade` would stop being byte-for-byte the
|
|
340
|
+
* cross-fade every release so far performed. */
|
|
341
|
+
function animatedProps(effect) {
|
|
342
|
+
return effect === "fade" ? ["opacity"] : ["opacity", "transform"];
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Decide one switch: whether to animate, from what, to what, and on what curve.
|
|
346
|
+
*
|
|
347
|
+
* `canAnimate` is the caller's answer to "is there anything to animate from" —
|
|
348
|
+
* a first paint has no previous layer, and a wallpaper at 0% opacity is not on
|
|
349
|
+
* screen — plus the window-visibility question the caller already owns. It is
|
|
350
|
+
* passed in rather than computed here so this stays a pure function of its
|
|
351
|
+
* inputs.
|
|
352
|
+
*
|
|
353
|
+
* `reducedMotion` is honoured as a hard veto rather than a shortened duration:
|
|
354
|
+
* the point of the preference is that the interface stops moving, and a fast
|
|
355
|
+
* animation is still an animation.
|
|
356
|
+
*/
|
|
357
|
+
function transitionPlan(input) {
|
|
358
|
+
const still = {
|
|
359
|
+
opacity: "1",
|
|
360
|
+
transform: "none"
|
|
361
|
+
};
|
|
362
|
+
const moves = input.durationMs > 0;
|
|
363
|
+
if (!input.canAnimate || input.reducedMotion || input.effect === "none" || !moves) return {
|
|
364
|
+
animate: false,
|
|
365
|
+
durationMs: 0,
|
|
366
|
+
from: still,
|
|
367
|
+
to: still,
|
|
368
|
+
transition: "none",
|
|
369
|
+
willChange: ""
|
|
370
|
+
};
|
|
371
|
+
const props = animatedProps(input.effect);
|
|
372
|
+
const timing = `${input.durationMs}ms ${input.easing}`;
|
|
373
|
+
return {
|
|
374
|
+
animate: true,
|
|
375
|
+
durationMs: input.durationMs,
|
|
376
|
+
from: {
|
|
377
|
+
opacity: "0",
|
|
378
|
+
transform: START_TRANSFORM[input.effect]
|
|
379
|
+
},
|
|
380
|
+
to: still,
|
|
381
|
+
transition: props.map((p) => `${p} ${timing}`).join(", "),
|
|
382
|
+
willChange: props.join(", ")
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
282
386
|
//#region src/holiday.ts
|
|
283
387
|
const HOLIDAYS = [{
|
|
284
388
|
id: "mid-autumn",
|
|
@@ -457,15 +561,12 @@ window.__ModuleLoader__.load({
|
|
|
457
561
|
const ROTATE_MIN_MS = 5e3;
|
|
458
562
|
/** Slowest allowed dwell time (24 h): one image per day is still "rotation". */
|
|
459
563
|
const ROTATE_MAX_MS = 864e5;
|
|
460
|
-
/** Longest allowed cross-fade; 0 is a hard cut. */
|
|
461
|
-
const ROTATE_FADE_MAX_MS = 3e3;
|
|
462
564
|
/** Everything off: the shipped default, and what a config predating this feature gets. */
|
|
463
565
|
const DEFAULT_ROTATION = {
|
|
464
566
|
enabled: false,
|
|
465
567
|
intervalMs: 6e4,
|
|
466
568
|
order: "order",
|
|
467
|
-
advanceOnSwitch: false
|
|
468
|
-
fadeMs: 320
|
|
569
|
+
advanceOnSwitch: false
|
|
469
570
|
};
|
|
470
571
|
/** A fresh rotation block (never hand out the shared default object). */
|
|
471
572
|
function defaultRotation() {
|
|
@@ -522,6 +623,32 @@ window.__ModuleLoader__.load({
|
|
|
522
623
|
rightbar: 0,
|
|
523
624
|
input: 0
|
|
524
625
|
};
|
|
626
|
+
/** Longest allowed switch duration; 0 is a hard cut. */
|
|
627
|
+
const TRANSITION_MAX_MS = 3e3;
|
|
628
|
+
/** Every accepted `effect`, in UI order. */
|
|
629
|
+
const TRANSITION_EFFECTS = [
|
|
630
|
+
"fade",
|
|
631
|
+
"none",
|
|
632
|
+
"zoom",
|
|
633
|
+
"slide"
|
|
634
|
+
];
|
|
635
|
+
/** Every accepted `easing`, in UI order. */
|
|
636
|
+
const TRANSITION_EASINGS = [
|
|
637
|
+
"ease",
|
|
638
|
+
"linear",
|
|
639
|
+
"ease-out",
|
|
640
|
+
"ease-in-out"
|
|
641
|
+
];
|
|
642
|
+
/** The shipped default: the cross-fade this plugin already performed, at 320 ms. */
|
|
643
|
+
const DEFAULT_TRANSITION = {
|
|
644
|
+
effect: "fade",
|
|
645
|
+
easing: "ease",
|
|
646
|
+
durationMs: 320
|
|
647
|
+
};
|
|
648
|
+
/** A fresh transition block (never hand out the shared default object). */
|
|
649
|
+
function defaultTransition() {
|
|
650
|
+
return { ...DEFAULT_TRANSITION };
|
|
651
|
+
}
|
|
525
652
|
/** Every slot a rule owns, in order. The one thing the render and store layers
|
|
526
653
|
* ask of the image list, so a rule's slots are never enumerated ad hoc. */
|
|
527
654
|
function ruleSlots(rule) {
|
|
@@ -625,6 +752,7 @@ window.__ModuleLoader__.load({
|
|
|
625
752
|
rules: [],
|
|
626
753
|
opacities: { ...DEFAULT_PART_OPACITIES },
|
|
627
754
|
blurs: { ...DEFAULT_PART_BLURS },
|
|
755
|
+
transition: defaultTransition(),
|
|
628
756
|
settingsOpacity: 1,
|
|
629
757
|
chatTextOpacity: 0,
|
|
630
758
|
trajectoryOpacity: 1,
|
|
@@ -670,8 +798,7 @@ window.__ModuleLoader__.load({
|
|
|
670
798
|
enabled: r.enabled === true,
|
|
671
799
|
intervalMs: clamp$1(r.intervalMs, ROTATE_MIN_MS, ROTATE_MAX_MS, d.intervalMs),
|
|
672
800
|
order: ROTATE_ORDERS.includes(r.order) ? r.order : d.order,
|
|
673
|
-
advanceOnSwitch: r.advanceOnSwitch === true
|
|
674
|
-
fadeMs: clamp$1(r.fadeMs, 0, ROTATE_FADE_MAX_MS, d.fadeMs)
|
|
801
|
+
advanceOnSwitch: r.advanceOnSwitch === true
|
|
675
802
|
};
|
|
676
803
|
}
|
|
677
804
|
/** Own-property test for values that may be anything at all (a raw JSON entry). */
|
|
@@ -679,6 +806,54 @@ window.__ModuleLoader__.load({
|
|
|
679
806
|
return Object.prototype.hasOwnProperty.call(o, key);
|
|
680
807
|
}
|
|
681
808
|
/**
|
|
809
|
+
* The duration a config written before the switch effect carries on any of its
|
|
810
|
+
* rules, or `undefined` when it carries none worth adopting.
|
|
811
|
+
*
|
|
812
|
+
* `rotate.fadeMs` was the per-rule duration from 0.7.0 on, and it had NO control
|
|
813
|
+
* anywhere — it could only be hand-edited, which is why the global setting
|
|
814
|
+
* replaced it. This is the one-shot lift that keeps such a hand-tuned value from
|
|
815
|
+
* being silently dropped on upgrade: the first rule whose value differs from the
|
|
816
|
+
* 320 ms every build shipped is adopted as the global duration. Values that are
|
|
817
|
+
* all the default are ignored, so an ordinary config keeps the ordinary default,
|
|
818
|
+
* and rules that DISAGREE cannot be represented by one global number — the first
|
|
819
|
+
* deliberate value wins, and the others are gone with the field they lived on.
|
|
820
|
+
*/
|
|
821
|
+
function legacyFadeMs(rawRules) {
|
|
822
|
+
if (!Array.isArray(rawRules)) return void 0;
|
|
823
|
+
for (const entry of rawRules) {
|
|
824
|
+
const rotate = entry?.rotate;
|
|
825
|
+
if (rotate === null || typeof rotate !== "object") continue;
|
|
826
|
+
const value = rotate.fadeMs;
|
|
827
|
+
if (typeof value !== "number" || !isFinite(value)) continue;
|
|
828
|
+
if (value === DEFAULT_TRANSITION.durationMs) continue;
|
|
829
|
+
return clamp$1(value, 0, TRANSITION_MAX_MS, DEFAULT_TRANSITION.durationMs);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Coerce one persisted transition block; a config written before this setting
|
|
834
|
+
* existed has no `transition` key at all and gets the shipped default, which is
|
|
835
|
+
* the behaviour it already had.
|
|
836
|
+
*
|
|
837
|
+
* `legacyMs` is the per-rule `fadeMs` this shape replaced (see `legacyFadeMs`),
|
|
838
|
+
* adopted only when the incoming config has no `durationMs` of its own — so a
|
|
839
|
+
* deliberate hand-tuned blend survives the upgrade instead of being dropped with
|
|
840
|
+
* the field it used to live on.
|
|
841
|
+
*
|
|
842
|
+
* `durationMs` is clamped with `0` kept as a real value (a hard cut) rather than
|
|
843
|
+
* read as "absent": 0 is exactly how a hard cut is asked for without changing
|
|
844
|
+
* the effect.
|
|
845
|
+
*/
|
|
846
|
+
function normalizeTransition(raw, legacyMs) {
|
|
847
|
+
const t = raw ?? {};
|
|
848
|
+
const d = DEFAULT_TRANSITION;
|
|
849
|
+
const fallback = typeof legacyMs === "number" && isFinite(legacyMs) ? legacyMs : d.durationMs;
|
|
850
|
+
return {
|
|
851
|
+
effect: TRANSITION_EFFECTS.includes(t.effect) ? t.effect : d.effect,
|
|
852
|
+
easing: TRANSITION_EASINGS.includes(t.easing) ? t.easing : d.easing,
|
|
853
|
+
durationMs: clamp$1(t.durationMs, 0, TRANSITION_MAX_MS, fallback)
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
682
857
|
* Coerce one persisted image entry, or null when it lacks a usable slot.
|
|
683
858
|
*
|
|
684
859
|
* `inherit` is the RULE's own color, and it is applied only to an entry that has
|
|
@@ -815,6 +990,7 @@ window.__ModuleLoader__.load({
|
|
|
815
990
|
rules,
|
|
816
991
|
opacities,
|
|
817
992
|
blurs,
|
|
993
|
+
transition: normalizeTransition(r.transition, legacyFadeMs(r.rules)),
|
|
818
994
|
settingsOpacity: clamp01(r.settingsOpacity, 1),
|
|
819
995
|
chatTextOpacity: clamp01(r.chatTextOpacity, 0),
|
|
820
996
|
trajectoryOpacity: clamp01(r.trajectoryOpacity, 1),
|
|
@@ -1092,13 +1268,26 @@ window.__ModuleLoader__.load({
|
|
|
1092
1268
|
function rBgState() {
|
|
1093
1269
|
return activeImage()?.bgState ?? DEFAULT_BG_STATE;
|
|
1094
1270
|
}
|
|
1095
|
-
/**
|
|
1096
|
-
|
|
1097
|
-
|
|
1271
|
+
/**
|
|
1272
|
+
* The GLOBAL wallpaper-switch transition (effect, easing, duration).
|
|
1273
|
+
*
|
|
1274
|
+
* Read from the config on every apply rather than cached: the Config page edits
|
|
1275
|
+
* it while a wallpaper is on screen, and the next switch has to use what the
|
|
1276
|
+
* user just chose.
|
|
1277
|
+
*/
|
|
1278
|
+
function rTransition() {
|
|
1279
|
+
return cfg.transition ?? DEFAULT_TRANSITION;
|
|
1098
1280
|
}
|
|
1099
|
-
/**
|
|
1281
|
+
/**
|
|
1282
|
+
* Duration the next switch runs for, in ms (`0` = a hard cut).
|
|
1283
|
+
*
|
|
1284
|
+
* The two rungs are `resolveFadeMs`'s business (./transition): the effect can
|
|
1285
|
+
* veto the animation outright, and otherwise the global duration decides. It is
|
|
1286
|
+
* deliberately NOT a per-rule value — a rule has no notion of a transition, and
|
|
1287
|
+
* the per-rule field this replaced had no control anywhere to set it with.
|
|
1288
|
+
*/
|
|
1100
1289
|
function rFadeMs() {
|
|
1101
|
-
return
|
|
1290
|
+
return resolveFadeMs(rTransition());
|
|
1102
1291
|
}
|
|
1103
1292
|
/** Paintable URL of the active rule's CURRENT image, or null when it has none. */
|
|
1104
1293
|
function rWp() {
|
|
@@ -1321,7 +1510,7 @@ window.__ModuleLoader__.load({
|
|
|
1321
1510
|
const d = data;
|
|
1322
1511
|
const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
|
|
1323
1512
|
const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
|
|
1324
|
-
hostShape = schema === null ? "legacy" : schema >=
|
|
1513
|
+
hostShape = schema === null ? "legacy" : schema >= 5 ? "current" : "legacy";
|
|
1325
1514
|
if (hostShape === "legacy") holdWrite();
|
|
1326
1515
|
return {
|
|
1327
1516
|
config: d.config,
|
|
@@ -1379,6 +1568,57 @@ window.__ModuleLoader__.load({
|
|
|
1379
1568
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
1380
1569
|
}
|
|
1381
1570
|
//#endregion
|
|
1571
|
+
//#region src/client/palette-fade.ts
|
|
1572
|
+
/**
|
|
1573
|
+
* Whether a palette change may interpolate at all.
|
|
1574
|
+
*
|
|
1575
|
+
* Only while the SCHEME stays put. A light↔dark flip is not a tint of the same
|
|
1576
|
+
* palette, it is a different palette: interpolating between them walks the whole
|
|
1577
|
+
* interface through mid-tones whose ink is wrong in both directions (dark text
|
|
1578
|
+
* half-way into a dark palette), which looks worse than the switch it replaces.
|
|
1579
|
+
* The wallpaper has no such problem — one image fades into another either way —
|
|
1580
|
+
* so its own animation is deliberately untouched by this.
|
|
1581
|
+
*
|
|
1582
|
+
* `previous === null` means nothing has been painted yet (the first apply after a
|
|
1583
|
+
* load): there is no colour to come from, so there is nothing to fade.
|
|
1584
|
+
*/
|
|
1585
|
+
function paletteFadeAllowed(previous, next) {
|
|
1586
|
+
return previous === next;
|
|
1587
|
+
}
|
|
1588
|
+
/** One `@property` block per registered token. */
|
|
1589
|
+
function registrations(names, initial) {
|
|
1590
|
+
return names.map((n) => `@property ${n}{syntax:'<color>';inherits:true;initial-value:${initial[n]}}`).join("");
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* The stylesheet for one state: the registrations always, the transition only
|
|
1594
|
+
* while a palette change is actually being painted.
|
|
1595
|
+
*
|
|
1596
|
+
* `durationMs = 0` deliberately emits NO transition rule. That is not only
|
|
1597
|
+
* "instant": it is what keeps a slider drag instant, because the same call sites
|
|
1598
|
+
* rewrite the `body{…}` rule on every frame of a drag and a live transition on
|
|
1599
|
+
* those tokens would make the alphas lag behind the pointer.
|
|
1600
|
+
*/
|
|
1601
|
+
function paletteFadeCss(input) {
|
|
1602
|
+
const names = Object.keys(input.initial).filter((n) => n.startsWith("--"));
|
|
1603
|
+
if (names.length === 0) return "";
|
|
1604
|
+
const css = registrations(names, input.initial);
|
|
1605
|
+
if (!(input.durationMs > 0) || input.selectors.length === 0) return css;
|
|
1606
|
+
const list = names.map((n) => `${n} ${input.durationMs}ms ${input.easing}`).join(",");
|
|
1607
|
+
return `${css}${input.selectors.join(",")}{transition:${list}}`;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* The same fade for an element the plugin paints INLINE (its columns and cards).
|
|
1611
|
+
*
|
|
1612
|
+
* No custom property is involved there — the element's own `background-color` is
|
|
1613
|
+
* what changes — so the standard property is what has to be told. `0` means
|
|
1614
|
+
* `none` rather than an empty value: an empty value would leave whatever the host
|
|
1615
|
+
* had declared on that element, and the point of the 0 case is that a drag must
|
|
1616
|
+
* not fade at all.
|
|
1617
|
+
*/
|
|
1618
|
+
function paletteFadeInline(durationMs, easing) {
|
|
1619
|
+
return durationMs > 0 ? `background-color ${durationMs}ms ${easing}` : "none";
|
|
1620
|
+
}
|
|
1621
|
+
//#endregion
|
|
1382
1622
|
//#region src/client/utils/color.ts
|
|
1383
1623
|
function hsvToHsl(h, s, v) {
|
|
1384
1624
|
const l = v * (1 - s / 2);
|
|
@@ -1898,6 +2138,11 @@ window.__ModuleLoader__.load({
|
|
|
1898
2138
|
|
|
1899
2139
|
/* ── profile page ────────────────────────────────────────────────────────── */
|
|
1900
2140
|
.dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(238px,1fr));gap:13px}
|
|
2141
|
+
/* The switch-effect preview: a two-tone swatch that replays the chosen effect in
|
|
2142
|
+
the panel. It exists because a wallpaper fading into ITSELF is invisible —
|
|
2143
|
+
the one thing a transition setting must not leave the user guessing about. */
|
|
2144
|
+
.dab-tr-preview{position:relative;overflow:hidden;flex:1;min-width:0;height:74px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);background-image:linear-gradient(120deg,color-mix(in srgb,var(--dsw-alias-label-tertiary) 24%,transparent),transparent 70%)}
|
|
2145
|
+
.dab-tr-preview-in{position:absolute;inset:0;background-image:linear-gradient(120deg,var(--dsw-alias-brand-primary),color-mix(in srgb,var(--dsw-alias-brand-primary) 50%,var(--dsw-alias-bg-layer-1)))}
|
|
1901
2146
|
.dab-profile-ico{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;margin-bottom:13px;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
1902
2147
|
.dab-profile-title{font-size:14px;font-weight:650}
|
|
1903
2148
|
.dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
|
|
@@ -1956,16 +2201,28 @@ window.__ModuleLoader__.load({
|
|
|
1956
2201
|
.dab-rule-section-title{font-size:12px;font-weight:600;margin-bottom:9px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
1957
2202
|
|
|
1958
2203
|
/* ── image strip (multi-image rules) ─────────────────────────────────────── */
|
|
1959
|
-
/* A horizontally scrolling row of thumbnails: the
|
|
1960
|
-
the wallpaper is actually painting carries a live dot
|
|
1961
|
-
|
|
2204
|
+
/* A horizontally scrolling row of thumbnails: the one open for editing is
|
|
2205
|
+
ringed, and the one the wallpaper is actually painting carries a live dot —
|
|
2206
|
+
but deliberately NO ring of its own. Two tiles ringed at once read as "two
|
|
2207
|
+
things are selected", which is the one question a row of near-identical
|
|
2208
|
+
thumbnails must not leave open: being on screen is the dot, and nothing
|
|
2209
|
+
else. FILES dropped here are added, which is why the container's border
|
|
2210
|
+
reacts to a drag; a TILE dragged across the row reorders it instead, and that
|
|
2211
|
+
is what the is-dragging / dab-strip-bar pair below is for. */
|
|
1962
2212
|
.dab-strip{display:flex;gap:7px;margin-top:9px;padding:7px;border-radius:12px;border:1.5px dashed var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);overflow-x:auto;scrollbar-width:thin}
|
|
1963
2213
|
.dab-strip.is-over{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
|
|
1964
|
-
.dab-strip-item{position:relative;flex:none;width:62px;height:44px;padding:0;border-radius:8px;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);cursor:
|
|
2214
|
+
.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}
|
|
1965
2215
|
.dab-strip-item img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1966
2216
|
.dab-strip-item:hover{transform:translateY(-1px);border-color:var(--dsw-alias-brand-primary)}
|
|
1967
2217
|
.dab-strip-item.is-sel{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 40%,transparent)}
|
|
1968
|
-
|
|
2218
|
+
/* The tile being dragged out of the row, and the 2px bar that says where it will
|
|
2219
|
+
land. The bar is drawn INSIDE the tile it precedes (at its left edge) or inside
|
|
2220
|
+
the last tile (at its right edge, for a drop past the end): a bar as its own flex
|
|
2221
|
+
item would push the tiles sideways, and a target that slides away as the pointer
|
|
2222
|
+
approaches it is how a drop ends up one slot off. */
|
|
2223
|
+
.dab-strip-item.is-dragging{cursor:grabbing;opacity:.45;transform:none;border-style:dashed}
|
|
2224
|
+
.dab-strip-bar{position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--dsw-alias-brand-primary);z-index:2}
|
|
2225
|
+
.dab-strip-bar.is-end{left:auto;right:0}
|
|
1969
2226
|
.dab-strip-wait{position:absolute;inset:0;background:repeating-linear-gradient(45deg,transparent 0 4px,color-mix(in srgb,var(--dsw-alias-label-tertiary) 14%,transparent) 4px 8px)}
|
|
1970
2227
|
.dab-strip-num{position:absolute;left:3px;bottom:2px;font-family:var(--dab-mono);font-size:9.5px;line-height:1;padding:1px 4px;border-radius:5px;background:rgba(0,0,0,.45);color:#fff}
|
|
1971
2228
|
.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}
|
|
@@ -2319,19 +2576,70 @@ window.__ModuleLoader__.load({
|
|
|
2319
2576
|
}
|
|
2320
2577
|
let pendingOps = null;
|
|
2321
2578
|
let tokensRaf = null;
|
|
2579
|
+
/** When the pending frame was armed, so a frame that never ran can be spotted. */
|
|
2580
|
+
let tokensArmedAt = 0;
|
|
2581
|
+
/** A frame outstanding for longer than this is treated as lost (see below). */
|
|
2582
|
+
const STALE_TOKENS_RAF_MS = 1e3;
|
|
2583
|
+
/** The fingerprint of the palette + colour in force; see `applyCustomTokensNow`. */
|
|
2584
|
+
function paletteKey(src, color) {
|
|
2585
|
+
return src.own ? `own:${color[0]}|${color[1]}|${color[2]}` : `host:${HOST_SURFACE_TOKENS.map((n) => src.tokens[n] ?? "").join("|")}`;
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Re-emit the interface palette, coalescing a slider drag into one frame.
|
|
2589
|
+
*
|
|
2590
|
+
* A PALETTE change is never coalesced, and that distinction is the whole point:
|
|
2591
|
+
* this call is the only writer of the palette's INK. Every other surface has a
|
|
2592
|
+
* second, synchronous writer (the dialog's layer tokens come from
|
|
2593
|
+
* `applySettingsOverrides`, the columns' backgrounds from the view cards, the
|
|
2594
|
+
* `--dsh-any-op-*` alphas from the same callback as the ink), so a palette update
|
|
2595
|
+
* that is deferred and then lost leaves the surfaces on the NEW colour and the
|
|
2596
|
+
* ink on the previous one — a dark palette with dark text, which reads as
|
|
2597
|
+
* "the theme colour did nothing" and only a reload clears it.
|
|
2598
|
+
*
|
|
2599
|
+
* Measured in the field: a window that is not rendering can swallow the frame
|
|
2600
|
+
* (the id stays armed, so every later update parks behind it), and a failure
|
|
2601
|
+
* inside the write used to be swallowed by an empty `catch`. Both are handled
|
|
2602
|
+
* here — a changed palette goes through synchronously, a stale frame is dropped
|
|
2603
|
+
* instead of blocking forever, and the failure is logged (see `applyCustomTokensNow`).
|
|
2604
|
+
*/
|
|
2322
2605
|
function applyCustomTokens(ops) {
|
|
2323
2606
|
pendingOps = ops;
|
|
2324
|
-
|
|
2607
|
+
const src = palette();
|
|
2608
|
+
if (src !== null && paletteKey(src, rColor()) !== baseTokenKey) {
|
|
2609
|
+
flushTokens();
|
|
2610
|
+
return lastFadeable;
|
|
2611
|
+
}
|
|
2612
|
+
if (tokensRaf !== null) {
|
|
2613
|
+
if (Date.now() - tokensArmedAt <= STALE_TOKENS_RAF_MS) return false;
|
|
2614
|
+
window.cancelAnimationFrame(tokensRaf);
|
|
2615
|
+
tokensRaf = null;
|
|
2616
|
+
}
|
|
2617
|
+
tokensArmedAt = Date.now();
|
|
2325
2618
|
tokensRaf = requestAnimationFrame(() => {
|
|
2326
2619
|
tokensRaf = null;
|
|
2327
|
-
|
|
2328
|
-
const o = pendingOps;
|
|
2329
|
-
pendingOps = null;
|
|
2330
|
-
applyCustomTokensNow(o);
|
|
2620
|
+
flushTokens();
|
|
2331
2621
|
});
|
|
2622
|
+
return false;
|
|
2623
|
+
}
|
|
2624
|
+
/** Run the pending token write now (one funnel, so no path can skip it). */
|
|
2625
|
+
function flushTokens() {
|
|
2626
|
+
if (pendingOps === null) return;
|
|
2627
|
+
const o = pendingOps;
|
|
2628
|
+
pendingOps = null;
|
|
2629
|
+
applyCustomTokensNow(o);
|
|
2332
2630
|
}
|
|
2333
2631
|
let lastBgKey = "";
|
|
2632
|
+
/**
|
|
2633
|
+
* Whether the palette write in flight may fade — the answer `applyCustomTokens`
|
|
2634
|
+
* hands back to the callers that paint their own surfaces inline.
|
|
2635
|
+
*
|
|
2636
|
+
* It has to travel out of `applyCustomTokensNow` (which is where the decision is
|
|
2637
|
+
* made) because those callers run AFTER it in the same task, and re-deciding
|
|
2638
|
+
* there would be a second opinion on the same question.
|
|
2639
|
+
*/
|
|
2640
|
+
let lastFadeable = false;
|
|
2334
2641
|
function applyCustomTokensNow(ops) {
|
|
2642
|
+
lastFadeable = false;
|
|
2335
2643
|
const src = palette();
|
|
2336
2644
|
if (src === null) {
|
|
2337
2645
|
clearCustomTokens();
|
|
@@ -2343,8 +2651,18 @@ window.__ModuleLoader__.load({
|
|
|
2343
2651
|
const [h, s, l] = rColor();
|
|
2344
2652
|
try {
|
|
2345
2653
|
const forceDark = own && l < .55;
|
|
2346
|
-
const key =
|
|
2347
|
-
|
|
2654
|
+
const key = paletteKey(src, [
|
|
2655
|
+
h,
|
|
2656
|
+
s,
|
|
2657
|
+
l
|
|
2658
|
+
]);
|
|
2659
|
+
const paletteChanged = key !== baseTokenKey;
|
|
2660
|
+
const scheme = own ? forceDark ? "dark" : "light" : resolveHostDark() ? "dark" : "light";
|
|
2661
|
+
const fadeable = paletteChanged && paletteFadeAllowed(paletteScheme, scheme);
|
|
2662
|
+
lastFadeable = fadeable;
|
|
2663
|
+
applyPaletteFade(fadeable);
|
|
2664
|
+
if (paletteChanged) {
|
|
2665
|
+
paletteScheme = scheme;
|
|
2348
2666
|
if (own) {
|
|
2349
2667
|
if (forceDark) document.body.setAttribute(DARK_ATTR, OWN_SCHEME);
|
|
2350
2668
|
else document.body.removeAttribute(DARK_ATTR);
|
|
@@ -2367,9 +2685,11 @@ window.__ModuleLoader__.load({
|
|
|
2367
2685
|
const bgKey = `${baseTokenKey}|${ops.bg}`;
|
|
2368
2686
|
if (bgKey !== lastBgKey) {
|
|
2369
2687
|
lastBgKey = bgKey;
|
|
2370
|
-
applyPartOpacities(ops);
|
|
2688
|
+
applyPartOpacities(ops, fadeable);
|
|
2371
2689
|
}
|
|
2372
|
-
} catch {
|
|
2690
|
+
} catch (e) {
|
|
2691
|
+
console.warn("dsh-background-by-model: could not re-emit the palette tokens", e);
|
|
2692
|
+
}
|
|
2373
2693
|
}
|
|
2374
2694
|
const SETTINGS_PANEL_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
|
|
2375
2695
|
const SETTINGS_STYLE_RULE = `${SETTINGS_PANEL_SEL}{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none);}html[${THEMED_ATTR}] ${SETTINGS_PANEL_SEL}{--dsw-alias-bg-layer-1:var(--dsh-any-bg-settings-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-bg-settings-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-bg-settings-layer-3)}${SETTINGS_PANEL_SEL} .dab-card{backdrop-filter:var(--dsh-any-blur-card-panels,none);-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,none)}`;
|
|
@@ -2404,6 +2724,100 @@ window.__ModuleLoader__.load({
|
|
|
2404
2724
|
write("--dsh-any-traj-layer-2", "--dsw-alias-bg-layer-2");
|
|
2405
2725
|
write("--dsh-any-traj-layer-3", "--dsw-alias-bg-layer-3");
|
|
2406
2726
|
}
|
|
2727
|
+
/** Elements whose own token declarations change when the palette does: the body
|
|
2728
|
+
* (which everything else inherits), and the two surfaces that RE-declare the
|
|
2729
|
+
* layer tokens instead of inheriting them. */
|
|
2730
|
+
const FADE_SELECTORS = [
|
|
2731
|
+
"body",
|
|
2732
|
+
SETTINGS_PANEL_SEL,
|
|
2733
|
+
"[data-conversation-composer-overlay]"
|
|
2734
|
+
];
|
|
2735
|
+
/** The token names the plugin re-emits. One palette is enough: every branch
|
|
2736
|
+
* defines the same set, and an unknown name would only be registered when the
|
|
2737
|
+
* host can answer for it (see `fadeInitials`). */
|
|
2738
|
+
function fadeTokenNames() {
|
|
2739
|
+
return Object.keys(genTokens(220, .55, .25).tokens);
|
|
2740
|
+
}
|
|
2741
|
+
let fadeStyleEl = null;
|
|
2742
|
+
let fadeInitials = null;
|
|
2743
|
+
let fadeCssCache = "";
|
|
2744
|
+
/** The scheme the last written palette had, so a FLIP can be told from a tint. */
|
|
2745
|
+
let paletteScheme = null;
|
|
2746
|
+
function ensureFadeSheet() {
|
|
2747
|
+
if (fadeStyleEl?.isConnected) return fadeStyleEl;
|
|
2748
|
+
fadeStyleEl = document.createElement("style");
|
|
2749
|
+
fadeStyleEl.dataset.plugin = "dsh-background-by-model-fade";
|
|
2750
|
+
markOwnSheet(fadeStyleEl);
|
|
2751
|
+
document.head.appendChild(fadeStyleEl);
|
|
2752
|
+
return fadeStyleEl;
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* The host's own value for each token — the `initial-value` a registration needs,
|
|
2756
|
+
* and the test for whether a token may be registered at all.
|
|
2757
|
+
*
|
|
2758
|
+
* Read with the plugin's token sheet muted, for the same reason `readHostPalette`
|
|
2759
|
+
* does it: our rule re-emits the very names being asked for. Cached, because the
|
|
2760
|
+
* answer only changes with the host's scheme and a stale hint is only ever used
|
|
2761
|
+
* for a token the host does not publish anyway.
|
|
2762
|
+
*/
|
|
2763
|
+
function fadeInitialsOf(names) {
|
|
2764
|
+
if (fadeInitials !== null) return fadeInitials;
|
|
2765
|
+
const sheet = tokenStyleEl;
|
|
2766
|
+
const muted = sheet !== null && sheet.isConnected && !sheet.disabled;
|
|
2767
|
+
if (muted) sheet.disabled = true;
|
|
2768
|
+
try {
|
|
2769
|
+
const computed = getComputedStyle(document.body);
|
|
2770
|
+
const out = {};
|
|
2771
|
+
for (const name of names) {
|
|
2772
|
+
const used = resolveCssColor(computed.getPropertyValue(name));
|
|
2773
|
+
if (used !== null) out[name] = used;
|
|
2774
|
+
}
|
|
2775
|
+
fadeInitials = out;
|
|
2776
|
+
return out;
|
|
2777
|
+
} catch {
|
|
2778
|
+
fadeInitials = {};
|
|
2779
|
+
return fadeInitials;
|
|
2780
|
+
} finally {
|
|
2781
|
+
if (muted) sheet.disabled = false;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
/** How long a colour change may take, or `0` for "do not animate it".
|
|
2785
|
+
*
|
|
2786
|
+
* The same decision the wallpaper layer makes (`transitionPlan` over the global
|
|
2787
|
+
* effect, the resolved duration and the OS preference), so the two halves of one
|
|
2788
|
+
* switch can never disagree about whether they are animating. */
|
|
2789
|
+
function paletteFadeMs() {
|
|
2790
|
+
const tr = rTransition();
|
|
2791
|
+
const plan = transitionPlan({
|
|
2792
|
+
effect: tr.effect,
|
|
2793
|
+
easing: tr.easing,
|
|
2794
|
+
durationMs: rFadeMs(),
|
|
2795
|
+
canAnimate: true,
|
|
2796
|
+
reducedMotion: prefersReducedMotion()
|
|
2797
|
+
});
|
|
2798
|
+
return plan.animate ? plan.durationMs : 0;
|
|
2799
|
+
}
|
|
2800
|
+
/** Arm the fade for a palette change, or disarm it for anything else.
|
|
2801
|
+
*
|
|
2802
|
+
* `animate` is false on every pass that only moves an alpha (a slider drag): the
|
|
2803
|
+
* tokens are rewritten there too, and a live transition would make the surface
|
|
2804
|
+
* lag behind the pointer. */
|
|
2805
|
+
function applyPaletteFade(animate) {
|
|
2806
|
+
const initials = fadeInitialsOf(fadeTokenNames());
|
|
2807
|
+
const css = paletteFadeCss({
|
|
2808
|
+
selectors: FADE_SELECTORS,
|
|
2809
|
+
initial: initials,
|
|
2810
|
+
durationMs: animate ? paletteFadeMs() : 0,
|
|
2811
|
+
easing: rTransition().easing
|
|
2812
|
+
});
|
|
2813
|
+
if (css === fadeCssCache) return;
|
|
2814
|
+
fadeCssCache = css;
|
|
2815
|
+
ensureFadeSheet().textContent = css;
|
|
2816
|
+
}
|
|
2817
|
+
/** Inline `transition` for an element the plugin paints itself; `0` disarms it. */
|
|
2818
|
+
function fadeInlineFor(animate) {
|
|
2819
|
+
return paletteFadeInline(animate ? paletteFadeMs() : 0, rTransition().easing);
|
|
2820
|
+
}
|
|
2407
2821
|
const RIGHTBAR_STYLE_RULE = "div[data-sidebar-right-panel]:not(:has([data-dockkit-content],[data-dockkit-empty]))[data-sidebar-right-open],div[data-sidebar-right-panel] [data-dockkit-content]:not([data-dockkit-float]),div[data-sidebar-right-panel] [data-dockkit-empty]{background:var(--dsh-any-bg-rightbar,var(--dsw-alias-bg-base))!important}div[data-sidebar-right-panel][data-sidebar-right-open]{-webkit-backdrop-filter:var(--dsh-any-blur-rightbar,none);backdrop-filter:var(--dsh-any-blur-rightbar,none)}";
|
|
2408
2822
|
/** Paint the file-preview panel's surface from its own slider. */
|
|
2409
2823
|
function applyRightbarOverrides(op) {
|
|
@@ -2481,7 +2895,7 @@ window.__ModuleLoader__.load({
|
|
|
2481
2895
|
* the frame. The frame's translucent bg-base sits UNDER the sidebar, so
|
|
2482
2896
|
* reducing the main-bg opacity stacked a second alpha onto the sidebar; moving
|
|
2483
2897
|
* the alpha onto the columns keeps the sidebar owned by its own slider. */
|
|
2484
|
-
function applyPartOpacities(ops) {
|
|
2898
|
+
function applyPartOpacities(ops, fade = false) {
|
|
2485
2899
|
discoverParts();
|
|
2486
2900
|
if (frameEl === null) return;
|
|
2487
2901
|
const base = palette()?.tokens["--dsw-alias-bg-base"];
|
|
@@ -2494,8 +2908,15 @@ window.__ModuleLoader__.load({
|
|
|
2494
2908
|
return;
|
|
2495
2909
|
}
|
|
2496
2910
|
frameEl.style.background = "transparent";
|
|
2497
|
-
|
|
2498
|
-
if (
|
|
2911
|
+
const transition = fadeInlineFor(fade);
|
|
2912
|
+
if (centerEl !== null) {
|
|
2913
|
+
centerEl.style.transition = transition;
|
|
2914
|
+
centerEl.style.background = withAlpha(base, ops.bg);
|
|
2915
|
+
}
|
|
2916
|
+
if (detailsEl !== null) {
|
|
2917
|
+
detailsEl.style.transition = transition;
|
|
2918
|
+
detailsEl.style.background = withAlpha(base, ops.bg);
|
|
2919
|
+
}
|
|
2499
2920
|
}
|
|
2500
2921
|
/** Blur of the option panels inside the settings dialog (.dab-card), owned by
|
|
2501
2922
|
* the "dialog option panel" (card) blur slider. Written as a plugin-owned
|
|
@@ -2705,10 +3126,11 @@ window.__ModuleLoader__.load({
|
|
|
2705
3126
|
if (!tableFixStyleEl.isConnected) document.head.appendChild(tableFixStyleEl);
|
|
2706
3127
|
}
|
|
2707
3128
|
/** Re-derive the conversation view cards from the current config. */
|
|
2708
|
-
function applyViewCards() {
|
|
3129
|
+
function applyViewCards(fade = false) {
|
|
2709
3130
|
discoverParts();
|
|
2710
3131
|
if (centerEl === null) return;
|
|
2711
3132
|
const surface = palette()?.tokens["--dsw-alias-bg-layer-1"];
|
|
3133
|
+
const transition = fadeInlineFor(fade);
|
|
2712
3134
|
VIEW_CARDS.forEach((spec, i) => {
|
|
2713
3135
|
const target = discoverViewTarget(i, spec);
|
|
2714
3136
|
if (target === null) return;
|
|
@@ -2718,6 +3140,7 @@ window.__ModuleLoader__.load({
|
|
|
2718
3140
|
if (!plain) {
|
|
2719
3141
|
if (!target.hasAttribute(spec.mark)) stashCardPrev(target, spec.prev, false);
|
|
2720
3142
|
const borderAlpha = opacity > 0 ? Math.min(1, opacity * 1.5) : blurPx > 0 ? .35 : 0;
|
|
3143
|
+
target.style.transition = transition;
|
|
2721
3144
|
target.style.background = surface !== void 0 ? withAlpha(surface, opacity) : "transparent";
|
|
2722
3145
|
target.style.border = surface !== void 0 ? `1px solid ${withAlpha(surface, borderAlpha)}` : "1px solid transparent";
|
|
2723
3146
|
target.style.borderRadius = "16px";
|
|
@@ -2840,6 +3263,13 @@ window.__ModuleLoader__.load({
|
|
|
2840
3263
|
l.el.style.backgroundImage = "";
|
|
2841
3264
|
l.el.style.backgroundSize = "";
|
|
2842
3265
|
l.el.style.backgroundPosition = "";
|
|
3266
|
+
l.el.style.transform = "none";
|
|
3267
|
+
}
|
|
3268
|
+
/** Write the two properties a switch animates. Both are always written, so a
|
|
3269
|
+
* layer can never keep half of a previous effect. */
|
|
3270
|
+
function applyLayerStyle(el, style) {
|
|
3271
|
+
el.style.opacity = style.opacity;
|
|
3272
|
+
el.style.transform = style.transform;
|
|
2843
3273
|
}
|
|
2844
3274
|
/** Intrinsic-size cache for the center mode (native pixels of the current image). */
|
|
2845
3275
|
let imgNat = null;
|
|
@@ -2908,13 +3338,18 @@ window.__ModuleLoader__.load({
|
|
|
2908
3338
|
});
|
|
2909
3339
|
}
|
|
2910
3340
|
}
|
|
2911
|
-
/** Whether
|
|
2912
|
-
*
|
|
2913
|
-
|
|
3341
|
+
/** Whether there is anything to animate FROM: a wallpaper on screen that is
|
|
3342
|
+
* actually visible. The duration question belongs to `rFadeMs`, and the
|
|
3343
|
+
* `reduced-motion` veto to `transitionPlan` — this answers only the layer half,
|
|
3344
|
+
* so a first paint (no previous image) never fades in from nothing. */
|
|
3345
|
+
function canAnimateFrom(from) {
|
|
2914
3346
|
if (from.url === null) return false;
|
|
2915
|
-
|
|
3347
|
+
return rWop() > .01;
|
|
3348
|
+
}
|
|
3349
|
+
/** Whether the OS asks for reduced motion right now. */
|
|
3350
|
+
function prefersReducedMotion() {
|
|
2916
3351
|
const mm = typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
2917
|
-
return mm
|
|
3352
|
+
return mm !== null && mm.matches;
|
|
2918
3353
|
}
|
|
2919
3354
|
/** End an in-flight fade: promote the incoming layer, free the one under it. */
|
|
2920
3355
|
function finishFade(idx) {
|
|
@@ -2927,6 +3362,7 @@ window.__ModuleLoader__.load({
|
|
|
2927
3362
|
front = idx;
|
|
2928
3363
|
const el = layers[idx].el;
|
|
2929
3364
|
el.style.opacity = "1";
|
|
3365
|
+
el.style.transform = "none";
|
|
2930
3366
|
el.style.willChange = "";
|
|
2931
3367
|
clearLayer(layers[idx === 0 ? 1 : 0]);
|
|
2932
3368
|
}
|
|
@@ -2943,6 +3379,7 @@ window.__ModuleLoader__.load({
|
|
|
2943
3379
|
const el = layers[idx].el;
|
|
2944
3380
|
el.style.transition = "none";
|
|
2945
3381
|
el.style.opacity = "1";
|
|
3382
|
+
el.style.transform = "none";
|
|
2946
3383
|
el.style.willChange = "";
|
|
2947
3384
|
clearLayer(layers[idx === 0 ? 1 : 0]);
|
|
2948
3385
|
}
|
|
@@ -2958,6 +3395,7 @@ window.__ModuleLoader__.load({
|
|
|
2958
3395
|
const el = layers[idx].el;
|
|
2959
3396
|
el.style.transition = "none";
|
|
2960
3397
|
el.style.opacity = "0";
|
|
3398
|
+
el.style.transform = "none";
|
|
2961
3399
|
el.style.willChange = "";
|
|
2962
3400
|
clearLayer(layers[idx]);
|
|
2963
3401
|
}
|
|
@@ -2988,27 +3426,34 @@ window.__ModuleLoader__.load({
|
|
|
2988
3426
|
const idx = front === 0 ? 1 : 0;
|
|
2989
3427
|
const target = ls[idx];
|
|
2990
3428
|
const el = target.el;
|
|
3429
|
+
const tr = rTransition();
|
|
3430
|
+
const plan = transitionPlan({
|
|
3431
|
+
effect: tr.effect,
|
|
3432
|
+
easing: tr.easing,
|
|
3433
|
+
durationMs: rFadeMs(),
|
|
3434
|
+
canAnimate: canAnimateFrom(from),
|
|
3435
|
+
reducedMotion: prefersReducedMotion()
|
|
3436
|
+
});
|
|
2991
3437
|
el.style.transition = "none";
|
|
2992
|
-
el.style.
|
|
3438
|
+
el.style.willChange = plan.willChange;
|
|
3439
|
+
applyLayerStyle(el, plan.from);
|
|
2993
3440
|
el.style.zIndex = "1";
|
|
2994
3441
|
from.el.style.zIndex = "0";
|
|
2995
3442
|
paintLayer(target, url);
|
|
2996
3443
|
applyWpEffects();
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
el.style.opacity = "1";
|
|
3444
|
+
if (!plan.animate) {
|
|
3445
|
+
applyLayerStyle(el, plan.to);
|
|
3000
3446
|
front = idx;
|
|
3001
3447
|
clearLayer(from);
|
|
3002
3448
|
return;
|
|
3003
3449
|
}
|
|
3004
|
-
el.style.willChange = "opacity";
|
|
3005
3450
|
pending = idx;
|
|
3006
3451
|
requestAnimationFrame(() => {
|
|
3007
3452
|
if (pending !== idx || layers === null) return;
|
|
3008
|
-
el.style.transition =
|
|
3009
|
-
el.
|
|
3453
|
+
el.style.transition = plan.transition;
|
|
3454
|
+
applyLayerStyle(el, plan.to);
|
|
3010
3455
|
});
|
|
3011
|
-
fadeTimer = window.setTimeout(() => finishFade(idx),
|
|
3456
|
+
fadeTimer = window.setTimeout(() => finishFade(idx), plan.durationMs + 160);
|
|
3012
3457
|
}
|
|
3013
3458
|
function applyWpEffects() {
|
|
3014
3459
|
if (wpEl === null || layers === null) return;
|
|
@@ -3035,11 +3480,12 @@ window.__ModuleLoader__.load({
|
|
|
3035
3480
|
clearCustomTokens();
|
|
3036
3481
|
clearOwnedSurfaces();
|
|
3037
3482
|
}
|
|
3038
|
-
applyCustomTokens(rOps());
|
|
3483
|
+
const paletteMoved = applyCustomTokens(rOps());
|
|
3039
3484
|
applySettingsOverrides(rSop());
|
|
3040
3485
|
applyTrajectoryOverrides(rTrajectoryOpacity());
|
|
3041
3486
|
applyRightbarOverrides(rRightbarOpacity());
|
|
3042
3487
|
applyPartBlurs(rBlurs());
|
|
3488
|
+
applyViewCards(paletteMoved);
|
|
3043
3489
|
}
|
|
3044
3490
|
function teardownWp() {
|
|
3045
3491
|
dropWpContainer();
|
|
@@ -4772,15 +5218,6 @@ window.__ModuleLoader__.load({
|
|
|
4772
5218
|
className: "dab-rise",
|
|
4773
5219
|
style: { "--d": 2 },
|
|
4774
5220
|
children: [
|
|
4775
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4776
|
-
className: "dab-swatch-title",
|
|
4777
|
-
children: t("rulesTitle")
|
|
4778
|
-
}),
|
|
4779
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4780
|
-
className: "dab-hint",
|
|
4781
|
-
style: { marginBottom: 11 },
|
|
4782
|
-
children: t("rulesHint")
|
|
4783
|
-
}),
|
|
4784
5221
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4785
5222
|
className: "dab-chip-row",
|
|
4786
5223
|
style: {
|
|
@@ -4800,11 +5237,6 @@ window.__ModuleLoader__.load({
|
|
|
4800
5237
|
children: t("autoExtract")
|
|
4801
5238
|
})]
|
|
4802
5239
|
}),
|
|
4803
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4804
|
-
className: "dab-hint",
|
|
4805
|
-
style: { marginBottom: 11 },
|
|
4806
|
-
children: t("autoExtractHint")
|
|
4807
|
-
}),
|
|
4808
5240
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4809
5241
|
className: "dab-rules",
|
|
4810
5242
|
children: rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleCard, {
|
|
@@ -4834,6 +5266,12 @@ window.__ModuleLoader__.load({
|
|
|
4834
5266
|
const { t } = p;
|
|
4835
5267
|
const [open, setOpen] = (0, react.useState)(active);
|
|
4836
5268
|
const [dragOver, setDragOver] = (0, react.useState)(false);
|
|
5269
|
+
const pendingDrag = (0, react.useRef)(null);
|
|
5270
|
+
/** Insertion slot of the drag in flight (index of the tile it goes BEFORE). */
|
|
5271
|
+
const dragAt = (0, react.useRef)(null);
|
|
5272
|
+
const [drop, setDrop] = (0, react.useState)(null);
|
|
5273
|
+
/** Set while a drag is real, so the click that ends it is not also a selection. */
|
|
5274
|
+
const draggedRef = (0, react.useRef)(false);
|
|
4837
5275
|
const [urlOpen, setUrlOpen] = (0, react.useState)(false);
|
|
4838
5276
|
const [urlVal, setUrlVal] = (0, react.useState)("");
|
|
4839
5277
|
const [urlBusy, setUrlBusy] = (0, react.useState)(false);
|
|
@@ -4961,8 +5399,61 @@ window.__ModuleLoader__.load({
|
|
|
4961
5399
|
const rotatable = rule.images.length >= 2;
|
|
4962
5400
|
const preset = ROTATE_PRESETS.find((x) => x.ms === rule.rotate.intervalMs);
|
|
4963
5401
|
const selSlot = image?.slot ?? null;
|
|
4964
|
-
|
|
4965
|
-
|
|
5402
|
+
/** How far the pointer must travel before a click turns into a drag. */
|
|
5403
|
+
const DRAG_SLOP = 5;
|
|
5404
|
+
const tileDown = (i, slot) => (e) => {
|
|
5405
|
+
if (e.button !== 0 || e.pointerType === "touch") return;
|
|
5406
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
5407
|
+
pendingDrag.current = {
|
|
5408
|
+
x: e.clientX,
|
|
5409
|
+
y: e.clientY,
|
|
5410
|
+
from: i,
|
|
5411
|
+
slot,
|
|
5412
|
+
id: e.pointerId
|
|
5413
|
+
};
|
|
5414
|
+
draggedRef.current = false;
|
|
5415
|
+
};
|
|
5416
|
+
const tileMove = (e) => {
|
|
5417
|
+
const pen = pendingDrag.current;
|
|
5418
|
+
if (pen === null || pen.id !== e.pointerId) return;
|
|
5419
|
+
const cur = dragAt.current;
|
|
5420
|
+
if (cur === null) {
|
|
5421
|
+
if (Math.abs(e.clientX - pen.x) + Math.abs(e.clientY - pen.y) < DRAG_SLOP) return;
|
|
5422
|
+
draggedRef.current = true;
|
|
5423
|
+
}
|
|
5424
|
+
const strip = e.currentTarget.parentElement;
|
|
5425
|
+
const list = strip === null ? [] : [...strip.querySelectorAll(".dab-strip-item")];
|
|
5426
|
+
let at = list.length;
|
|
5427
|
+
for (let i = 0; i < list.length; i++) {
|
|
5428
|
+
const box = list[i].getBoundingClientRect();
|
|
5429
|
+
if (e.clientX < box.left + box.width / 2) {
|
|
5430
|
+
at = i;
|
|
5431
|
+
break;
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
if (cur !== null && cur.at === at) return;
|
|
5435
|
+
dragAt.current = {
|
|
5436
|
+
from: pen.from,
|
|
5437
|
+
at
|
|
5438
|
+
};
|
|
5439
|
+
setDrop({
|
|
5440
|
+
from: pen.from,
|
|
5441
|
+
at
|
|
5442
|
+
});
|
|
5443
|
+
};
|
|
5444
|
+
/** End a drag: commit the new order, or just forget it on a cancel. */
|
|
5445
|
+
const tileUp = (commit) => (e) => {
|
|
5446
|
+
const pen = pendingDrag.current;
|
|
5447
|
+
pendingDrag.current = null;
|
|
5448
|
+
if (pen !== null && e.currentTarget.hasPointerCapture(pen.id)) e.currentTarget.releasePointerCapture(pen.id);
|
|
5449
|
+
const end = dragAt.current;
|
|
5450
|
+
dragAt.current = null;
|
|
5451
|
+
setDrop(null);
|
|
5452
|
+
if (!commit || pen === null || end === null) return;
|
|
5453
|
+
const to = end.at > pen.from ? end.at - 1 : end.at;
|
|
5454
|
+
if (to === pen.from) return;
|
|
5455
|
+
p.moveRuleImageTo(rule.id, pen.slot, to);
|
|
5456
|
+
setSel(to);
|
|
4966
5457
|
};
|
|
4967
5458
|
const removeSel = () => {
|
|
4968
5459
|
if (selSlot === null) return;
|
|
@@ -5089,9 +5580,26 @@ window.__ModuleLoader__.load({
|
|
|
5089
5580
|
const thumb = p.imageOf(img.slot);
|
|
5090
5581
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5091
5582
|
type: "button",
|
|
5092
|
-
className: `dab-strip-item${i === selIdx ? " is-sel" : ""}${
|
|
5093
|
-
title: `${i + 1} / ${rule.images.length}`,
|
|
5094
|
-
|
|
5583
|
+
className: `dab-strip-item${i === selIdx ? " is-sel" : ""}${drop !== null && drop.from === i ? " is-dragging" : ""}`,
|
|
5584
|
+
title: `${i + 1} / ${rule.images.length} · ${t("rotDragHint")} · ${t("rotDblHint")}`,
|
|
5585
|
+
onPointerDown: tileDown(i, img.slot),
|
|
5586
|
+
onPointerMove: tileMove,
|
|
5587
|
+
onPointerUp: tileUp(true),
|
|
5588
|
+
onPointerCancel: tileUp(false),
|
|
5589
|
+
onClick: () => {
|
|
5590
|
+
if (draggedRef.current) {
|
|
5591
|
+
draggedRef.current = false;
|
|
5592
|
+
return;
|
|
5593
|
+
}
|
|
5594
|
+
setSel(i);
|
|
5595
|
+
},
|
|
5596
|
+
onDoubleClick: () => {
|
|
5597
|
+
if (!active) {
|
|
5598
|
+
notify(t("rotNextInactive"));
|
|
5599
|
+
return;
|
|
5600
|
+
}
|
|
5601
|
+
p.showRuleImage(rule.id, img.slot);
|
|
5602
|
+
},
|
|
5095
5603
|
children: [
|
|
5096
5604
|
thumb !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
5097
5605
|
src: thumb,
|
|
@@ -5102,6 +5610,8 @@ window.__ModuleLoader__.load({
|
|
|
5102
5610
|
className: "dab-strip-num",
|
|
5103
5611
|
children: i + 1
|
|
5104
5612
|
}),
|
|
5613
|
+
drop !== null && drop.at === i ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-strip-bar" }) : null,
|
|
5614
|
+
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,
|
|
5105
5615
|
img.color !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5106
5616
|
className: "dab-strip-dot",
|
|
5107
5617
|
title: toHex(hslToRgb(img.color[0], img.color[1], img.color[2])).toUpperCase(),
|
|
@@ -5162,38 +5672,6 @@ window.__ModuleLoader__.load({
|
|
|
5162
5672
|
},
|
|
5163
5673
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 13 }), t("rotReplaceImage")]
|
|
5164
5674
|
}) : null,
|
|
5165
|
-
url !== null && rule.bgMode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5166
|
-
type: "button",
|
|
5167
|
-
className: "dab-btn",
|
|
5168
|
-
onClick: () => setEditorOpen(true),
|
|
5169
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 13 }), t("ruleFramingEdit")]
|
|
5170
|
-
}) : null,
|
|
5171
|
-
rule.images.length >= 2 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5172
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5173
|
-
type: "button",
|
|
5174
|
-
className: "dab-icon-btn",
|
|
5175
|
-
disabled: selIdx === 0,
|
|
5176
|
-
title: t("rotImageEarlier"),
|
|
5177
|
-
onClick: () => moveSel(-1),
|
|
5178
|
-
children: "↑"
|
|
5179
|
-
}),
|
|
5180
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5181
|
-
type: "button",
|
|
5182
|
-
className: "dab-icon-btn",
|
|
5183
|
-
disabled: selIdx === rule.images.length - 1,
|
|
5184
|
-
title: t("rotImageLater"),
|
|
5185
|
-
onClick: () => moveSel(1),
|
|
5186
|
-
children: "↓"
|
|
5187
|
-
}),
|
|
5188
|
-
selIdx !== 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5189
|
-
type: "button",
|
|
5190
|
-
className: "dab-btn dab-btn-ghost",
|
|
5191
|
-
onClick: () => {
|
|
5192
|
-
if (selSlot !== null) p.setCurrentImage(rule.id, selSlot);
|
|
5193
|
-
},
|
|
5194
|
-
children: t("rotMakeFirst")
|
|
5195
|
-
}) : null
|
|
5196
|
-
] }) : null,
|
|
5197
5675
|
selSlot !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5198
5676
|
type: "button",
|
|
5199
5677
|
className: "dab-btn dab-btn-ghost dab-btn-danger",
|
|
@@ -5401,14 +5879,19 @@ window.__ModuleLoader__.load({
|
|
|
5401
5879
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5402
5880
|
className: "dab-rule-section-title",
|
|
5403
5881
|
children: t("ruleLayout")
|
|
5404
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5882
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5405
5883
|
className: "dab-chip-row",
|
|
5406
|
-
children: BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5884
|
+
children: [BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5407
5885
|
type: "button",
|
|
5408
5886
|
className: `dab-chip${rule.bgMode === m.mode ? " is-active" : ""}`,
|
|
5409
5887
|
onClick: () => p.setRule(rule.id, { bgMode: m.mode }),
|
|
5410
5888
|
children: t(m.key)
|
|
5411
|
-
}, m.mode))
|
|
5889
|
+
}, m.mode)), url !== null && rule.bgMode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5890
|
+
type: "button",
|
|
5891
|
+
className: "dab-btn",
|
|
5892
|
+
onClick: () => setEditorOpen(true),
|
|
5893
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 13 }), t("ruleFramingEdit")]
|
|
5894
|
+
}) : null]
|
|
5412
5895
|
})]
|
|
5413
5896
|
})
|
|
5414
5897
|
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
@@ -5596,11 +6079,58 @@ window.__ModuleLoader__.load({
|
|
|
5596
6079
|
}
|
|
5597
6080
|
//#endregion
|
|
5598
6081
|
//#region src/client/components/pages/ProfilePage.tsx
|
|
6082
|
+
/** Chip labels, keyed by the value they set — so no chip can be built from a
|
|
6083
|
+
* value the schema does not accept, and `check:ui` still sees every key used. */
|
|
6084
|
+
const EFFECT_KEYS = {
|
|
6085
|
+
fade: "trEffectFade",
|
|
6086
|
+
none: "trEffectNone",
|
|
6087
|
+
zoom: "trEffectZoom",
|
|
6088
|
+
slide: "trEffectSlide"
|
|
6089
|
+
};
|
|
6090
|
+
const EASING_KEYS = {
|
|
6091
|
+
ease: "trEasingEase",
|
|
6092
|
+
linear: "trEasingLinear",
|
|
6093
|
+
"ease-out": "trEasingOut",
|
|
6094
|
+
"ease-in-out": "trEasingInOut"
|
|
6095
|
+
};
|
|
5599
6096
|
function ProfilePage({ p, notify }) {
|
|
5600
|
-
const { t, exportTheme, importTheme } = p;
|
|
6097
|
+
const { t, exportTheme, importTheme, setTransition } = p;
|
|
5601
6098
|
const importRef = (0, react.useRef)(null);
|
|
5602
6099
|
p.useStore((s) => s.rev + s.rulesRev);
|
|
5603
6100
|
const holidays = cfg.holidays;
|
|
6101
|
+
const tr = cfg.transition;
|
|
6102
|
+
const previewRef = (0, react.useRef)(null);
|
|
6103
|
+
/** The OS preference, read at render: it vetoes the real switch (see
|
|
6104
|
+
* `transitionPlan`), so the card has to say so rather than show a preview of
|
|
6105
|
+
* something the interface will not do. */
|
|
6106
|
+
const reduced = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6107
|
+
/**
|
|
6108
|
+
* Play the effect on the preview swatch, for the duration a real switch would
|
|
6109
|
+
* use right now: the wallpaper fading into ITSELF is invisible, which is the
|
|
6110
|
+
* one thing a transition setting must not leave the user guessing about.
|
|
6111
|
+
*/
|
|
6112
|
+
const play = () => {
|
|
6113
|
+
const el = previewRef.current;
|
|
6114
|
+
if (el === null) return;
|
|
6115
|
+
const plan = transitionPlan({
|
|
6116
|
+
effect: tr.effect,
|
|
6117
|
+
easing: tr.easing,
|
|
6118
|
+
durationMs: rFadeMs(),
|
|
6119
|
+
canAnimate: true,
|
|
6120
|
+
reducedMotion: false
|
|
6121
|
+
});
|
|
6122
|
+
el.style.transition = "none";
|
|
6123
|
+
el.style.opacity = plan.from.opacity;
|
|
6124
|
+
el.style.transform = plan.from.transform;
|
|
6125
|
+
requestAnimationFrame(() => {
|
|
6126
|
+
el.style.transition = plan.transition;
|
|
6127
|
+
el.style.opacity = plan.to.opacity;
|
|
6128
|
+
el.style.transform = plan.to.transform;
|
|
6129
|
+
});
|
|
6130
|
+
};
|
|
6131
|
+
(0, react.useEffect)(() => {
|
|
6132
|
+
if (!reduced) play();
|
|
6133
|
+
}, [tr.effect, tr.easing]);
|
|
5604
6134
|
const onImport = async (file) => {
|
|
5605
6135
|
try {
|
|
5606
6136
|
const ok = await importTheme(file);
|
|
@@ -5648,9 +6178,106 @@ window.__ModuleLoader__.load({
|
|
|
5648
6178
|
})]
|
|
5649
6179
|
})
|
|
5650
6180
|
}),
|
|
6181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6182
|
+
className: "dab-card dab-rise",
|
|
6183
|
+
style: { "--d": 1 },
|
|
6184
|
+
children: [
|
|
6185
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6186
|
+
className: "dab-part-head",
|
|
6187
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6188
|
+
className: "dab-part-ico",
|
|
6189
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 16 })
|
|
6190
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6191
|
+
className: "dab-part-name",
|
|
6192
|
+
children: t("trTitle")
|
|
6193
|
+
})]
|
|
6194
|
+
}),
|
|
6195
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6196
|
+
className: "dab-swatch-title",
|
|
6197
|
+
children: t("trEffect")
|
|
6198
|
+
}),
|
|
6199
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6200
|
+
className: "dab-chip-row",
|
|
6201
|
+
children: TRANSITION_EFFECTS.map((effect) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6202
|
+
type: "button",
|
|
6203
|
+
className: `dab-chip${tr.effect === effect ? " is-active" : ""}`,
|
|
6204
|
+
onClick: () => setTransition({ effect }),
|
|
6205
|
+
children: t(EFFECT_KEYS[effect])
|
|
6206
|
+
}, effect))
|
|
6207
|
+
}),
|
|
6208
|
+
tr.effect === "none" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6209
|
+
className: "dab-swatch-title",
|
|
6210
|
+
style: { marginTop: 12 },
|
|
6211
|
+
children: t("trEasing")
|
|
6212
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6213
|
+
className: "dab-chip-row",
|
|
6214
|
+
children: TRANSITION_EASINGS.map((easing) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6215
|
+
type: "button",
|
|
6216
|
+
className: `dab-chip${tr.easing === easing ? " is-active" : ""}`,
|
|
6217
|
+
onClick: () => setTransition({ easing }),
|
|
6218
|
+
children: t(EASING_KEYS[easing])
|
|
6219
|
+
}, easing))
|
|
6220
|
+
})] }),
|
|
6221
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6222
|
+
className: "dab-swatch-title",
|
|
6223
|
+
style: { marginTop: 12 },
|
|
6224
|
+
children: t("trDuration")
|
|
6225
|
+
}),
|
|
6226
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6227
|
+
style: { marginTop: 2 },
|
|
6228
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
6229
|
+
label: t("trDuration"),
|
|
6230
|
+
min: 0,
|
|
6231
|
+
max: 3e3,
|
|
6232
|
+
step: 50,
|
|
6233
|
+
def: tr.durationMs,
|
|
6234
|
+
fmt: (v) => `${v} ms`,
|
|
6235
|
+
onInput: (v) => {
|
|
6236
|
+
cfg.transition = normalizeTransition({
|
|
6237
|
+
...cfg.transition,
|
|
6238
|
+
durationMs: v
|
|
6239
|
+
});
|
|
6240
|
+
saveConfig();
|
|
6241
|
+
},
|
|
6242
|
+
onChange: (v) => setTransition({ durationMs: v })
|
|
6243
|
+
})
|
|
6244
|
+
}),
|
|
6245
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6246
|
+
style: {
|
|
6247
|
+
display: "flex",
|
|
6248
|
+
alignItems: "center",
|
|
6249
|
+
gap: 12,
|
|
6250
|
+
marginTop: 13
|
|
6251
|
+
},
|
|
6252
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6253
|
+
className: "dab-tr-preview",
|
|
6254
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6255
|
+
ref: previewRef,
|
|
6256
|
+
className: "dab-tr-preview-in"
|
|
6257
|
+
})
|
|
6258
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6259
|
+
type: "button",
|
|
6260
|
+
className: "dab-btn",
|
|
6261
|
+
disabled: reduced,
|
|
6262
|
+
onClick: play,
|
|
6263
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), t("trPreview")]
|
|
6264
|
+
})]
|
|
6265
|
+
}),
|
|
6266
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6267
|
+
className: "dab-hint",
|
|
6268
|
+
style: { marginTop: 10 },
|
|
6269
|
+
children: t("trHint")
|
|
6270
|
+
}),
|
|
6271
|
+
reduced ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6272
|
+
className: "dab-hint",
|
|
6273
|
+
style: { marginTop: 6 },
|
|
6274
|
+
children: t("trReducedHint")
|
|
6275
|
+
}) : null
|
|
6276
|
+
]
|
|
6277
|
+
}),
|
|
5651
6278
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5652
6279
|
className: "dab-profile-grid dab-rise",
|
|
5653
|
-
style: { "--d":
|
|
6280
|
+
style: { "--d": 3 },
|
|
5654
6281
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
5655
6282
|
className: "dab-card dab-card-hover",
|
|
5656
6283
|
children: [
|
|
@@ -5714,7 +6341,7 @@ window.__ModuleLoader__.load({
|
|
|
5714
6341
|
}),
|
|
5715
6342
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
5716
6343
|
className: "dab-footer dab-rise",
|
|
5717
|
-
style: { "--d":
|
|
6344
|
+
style: { "--d": 4 },
|
|
5718
6345
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5719
6346
|
className: "dab-footer-mono",
|
|
5720
6347
|
children: "dsh-background-by-model"
|
|
@@ -6314,6 +6941,23 @@ window.__ModuleLoader__.load({
|
|
|
6314
6941
|
saveConfig();
|
|
6315
6942
|
repaintIfMoved(id, winner);
|
|
6316
6943
|
};
|
|
6944
|
+
/**
|
|
6945
|
+
* The batch form, for one upload that carried several pictures.
|
|
6946
|
+
*
|
|
6947
|
+
* EVERY picture that arrived is themed from its own bytes, the first ones
|
|
6948
|
+
* included. The single-slot reading of an upload ("the last image added is the
|
|
6949
|
+
* one being looked at") is true of the CARD's selection and false of the
|
|
6950
|
+
* palette: since a theme color belongs to an image, a batch that themed only
|
|
6951
|
+
* its last file left the rest of a fresh rotation on the system theme, with
|
|
6952
|
+
* nothing in the interface to say why.
|
|
6953
|
+
*
|
|
6954
|
+
* Sequential on purpose: a batch is a handful of full-size photos, and ten
|
|
6955
|
+
* parallel decodes buy nothing the user can see, while landing the colors in
|
|
6956
|
+
* list order does (the card fills in from its first picture).
|
|
6957
|
+
*/
|
|
6958
|
+
const maybeAutoExtractEach = async (id, slots) => {
|
|
6959
|
+
for (const slot of slots) await maybeAutoExtract(id, slot);
|
|
6960
|
+
};
|
|
6317
6961
|
const activeLang = () => {
|
|
6318
6962
|
try {
|
|
6319
6963
|
const snapshot = ctx.locale.getSnapshot();
|
|
@@ -6390,6 +7034,8 @@ window.__ModuleLoader__.load({
|
|
|
6390
7034
|
const rule = ruleById(id);
|
|
6391
7035
|
if (rule === null || dataUrls.length === 0) return;
|
|
6392
7036
|
const winner = winnerId();
|
|
7037
|
+
/** Slots of THIS batch, in list order — the extraction below is per slot. */
|
|
7038
|
+
const added = [];
|
|
6393
7039
|
for (const dataUrl of dataUrls) {
|
|
6394
7040
|
const slot = nextSlot();
|
|
6395
7041
|
const image = {
|
|
@@ -6398,13 +7044,14 @@ window.__ModuleLoader__.load({
|
|
|
6398
7044
|
color: null
|
|
6399
7045
|
};
|
|
6400
7046
|
rule.images.push(image);
|
|
7047
|
+
added.push(slot);
|
|
6401
7048
|
setImage(slot, dataUrl);
|
|
6402
7049
|
writeImage(slot, dataUrl);
|
|
6403
7050
|
}
|
|
6404
7051
|
rulesRev++;
|
|
6405
7052
|
persistConfig();
|
|
6406
7053
|
repaintIfMoved(id, winner);
|
|
6407
|
-
|
|
7054
|
+
maybeAutoExtractEach(id, added);
|
|
6408
7055
|
},
|
|
6409
7056
|
removeRuleImage: (id, slot) => {
|
|
6410
7057
|
const rule = ruleById(id);
|
|
@@ -6422,34 +7069,21 @@ window.__ModuleLoader__.load({
|
|
|
6422
7069
|
}
|
|
6423
7070
|
repaintIfMoved(id, winner);
|
|
6424
7071
|
},
|
|
6425
|
-
|
|
6426
|
-
const rule = ruleById(id);
|
|
6427
|
-
if (rule === null) return;
|
|
6428
|
-
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
6429
|
-
const to = idx + dir;
|
|
6430
|
-
if (idx < 0 || to < 0 || to >= rule.images.length) return;
|
|
6431
|
-
const winner = winnerId();
|
|
6432
|
-
const [image] = rule.images.splice(idx, 1);
|
|
6433
|
-
rule.images.splice(to, 0, image);
|
|
6434
|
-
rulesRev++;
|
|
6435
|
-
if (id === activeRuleId) {
|
|
6436
|
-
setRotIndex(Math.max(0, to));
|
|
6437
|
-
scheduleRotation(true);
|
|
6438
|
-
}
|
|
6439
|
-
persistConfig();
|
|
6440
|
-
repaintIfMoved(id, winner);
|
|
6441
|
-
},
|
|
6442
|
-
setCurrentImage: (id, slot) => {
|
|
7072
|
+
moveRuleImageTo: (id, slot, to) => {
|
|
6443
7073
|
const rule = ruleById(id);
|
|
6444
7074
|
if (rule === null) return;
|
|
6445
7075
|
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
6446
7076
|
if (idx < 0) return;
|
|
7077
|
+
const at = Math.max(0, Math.min(rule.images.length - 1, Math.trunc(to)));
|
|
7078
|
+
if (at === idx) return;
|
|
6447
7079
|
const winner = winnerId();
|
|
7080
|
+
const painted = id === activeRuleId ? rule.images[imageIndexOf(rule)]?.slot ?? null : null;
|
|
6448
7081
|
const [image] = rule.images.splice(idx, 1);
|
|
6449
|
-
rule.images.
|
|
7082
|
+
rule.images.splice(at, 0, image);
|
|
6450
7083
|
rulesRev++;
|
|
6451
7084
|
if (id === activeRuleId) {
|
|
6452
|
-
|
|
7085
|
+
const keep = painted === null ? -1 : rule.images.findIndex((i) => i.slot === painted);
|
|
7086
|
+
setRotIndex(keep < 0 ? Math.min(at, rule.images.length - 1) : keep);
|
|
6453
7087
|
scheduleRotation(true);
|
|
6454
7088
|
}
|
|
6455
7089
|
persistConfig();
|
|
@@ -6484,6 +7118,15 @@ window.__ModuleLoader__.load({
|
|
|
6484
7118
|
paintImage(rule, next);
|
|
6485
7119
|
scheduleRotation(true);
|
|
6486
7120
|
},
|
|
7121
|
+
showRuleImage: (id, slot) => {
|
|
7122
|
+
const rule = ruleById(id);
|
|
7123
|
+
if (rule === null || id !== activeRuleId) return;
|
|
7124
|
+
const idx = rule.images.findIndex((i) => i.slot === slot);
|
|
7125
|
+
if (idx < 0) return;
|
|
7126
|
+
setRotIndex(idx);
|
|
7127
|
+
paintImage(rule, idx);
|
|
7128
|
+
scheduleRotation(true);
|
|
7129
|
+
},
|
|
6487
7130
|
loadRuleImages: async (id) => {
|
|
6488
7131
|
const rule = ruleById(id);
|
|
6489
7132
|
if (rule === null) return;
|
|
@@ -6585,6 +7228,24 @@ window.__ModuleLoader__.load({
|
|
|
6585
7228
|
saveConfig();
|
|
6586
7229
|
sync();
|
|
6587
7230
|
},
|
|
7231
|
+
/**
|
|
7232
|
+
* Patch the GLOBAL wallpaper-switch transition (effect, easing, where the
|
|
7233
|
+
* duration comes from).
|
|
7234
|
+
*
|
|
7235
|
+
* Deliberately no `applyWp()`: this setting decides what the NEXT switch
|
|
7236
|
+
* looks like and nothing on screen changes right now, so re-running the
|
|
7237
|
+
* whole apply (token rewrite, every interface part) for something that
|
|
7238
|
+
* paints nothing would be the expensive way to say "saved". The Config
|
|
7239
|
+
* page's own preview plays the effect instead.
|
|
7240
|
+
*/
|
|
7241
|
+
setTransition: (patch) => {
|
|
7242
|
+
cfg.transition = normalizeTransition({
|
|
7243
|
+
...cfg.transition,
|
|
7244
|
+
...patch
|
|
7245
|
+
});
|
|
7246
|
+
sync();
|
|
7247
|
+
saveConfig();
|
|
7248
|
+
},
|
|
6588
7249
|
setHolidaysEnabled: (v) => {
|
|
6589
7250
|
cfg.holidays.enabled = v;
|
|
6590
7251
|
persistConfig();
|
|
@@ -6600,7 +7261,7 @@ window.__ModuleLoader__.load({
|
|
|
6600
7261
|
if (url !== null) images[slot] = url;
|
|
6601
7262
|
}
|
|
6602
7263
|
const payload = {
|
|
6603
|
-
version:
|
|
7264
|
+
version: 6,
|
|
6604
7265
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6605
7266
|
config: cfg,
|
|
6606
7267
|
images
|