dsh-any-background 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -18
- package/README.zh.md +32 -18
- package/lib/client.js +1659 -218
- package/lib/client.js.map +1 -1
- package/lib/index.js +486 -55
- package/package.json +7 -2
package/lib/client.js
CHANGED
|
@@ -12,9 +12,15 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
try {
|
|
13
13
|
return require("@deepseek-ai/dsh-client-store");
|
|
14
14
|
} catch {}
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
return require("@deepseek-ai/dsh-client-runtime/client");
|
|
17
|
+
} catch {
|
|
18
|
+
console.warn("dsh-any-background: no client store module resolved on this host; the settings panel will be disabled");
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
/** null when neither store package resolves on this host (see resolveStore). */
|
|
23
|
+
const defineStore = resolveStore()?.defineStore ?? null;
|
|
18
24
|
//#endregion
|
|
19
25
|
//#region src/client/i18n.ts
|
|
20
26
|
const NS = "settings.anyBg";
|
|
@@ -24,10 +30,12 @@ window.__ModuleLoader__.load({
|
|
|
24
30
|
close: "关闭",
|
|
25
31
|
pageColor: "色彩",
|
|
26
32
|
pageInterface: "界面",
|
|
33
|
+
pageFont: "字体",
|
|
27
34
|
pageBackground: "背景",
|
|
28
35
|
pageProfile: "配置",
|
|
29
36
|
descColor: "拖动色轮或输入精确数值,整个界面的配色将实时跟随变化",
|
|
30
37
|
descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感",
|
|
38
|
+
descFont: "上传自定义字体为整套界面换字形,并按区域为文字添加描边,让内容在复杂的壁纸上依然清晰",
|
|
31
39
|
descBackground: "上传一张图片或一段视频作为壁纸并自由摆放,或让算法为你实时生成动态背景",
|
|
32
40
|
descProfile: "一键套用外观预设,保存多套配置随时切换,设置昼夜自动换装,或导出/导入完整主题",
|
|
33
41
|
colorTitle: "主题色",
|
|
@@ -46,6 +54,28 @@ window.__ModuleLoader__.load({
|
|
|
46
54
|
uiTrajectory: "轨迹页",
|
|
47
55
|
uiPanelRegion: "better-sidebar 工作台",
|
|
48
56
|
uiProduced: "产出物/高亮内容",
|
|
57
|
+
fontPageTitle: "字体与描边",
|
|
58
|
+
fontTitle: "自定义字体",
|
|
59
|
+
fontHint: "支持 ttf/otf/woff/woff2 字体文件(上限 100MB),将应用到整个界面的文字;代码块的等宽字体保持不变。中文字体通常有几十 MB,首次上传与刷新加载需要片刻",
|
|
60
|
+
fontUpload: "上传字体",
|
|
61
|
+
fontUploading: "上传中…",
|
|
62
|
+
fontRemove: "移除字体",
|
|
63
|
+
fontEnabled: "启用字体",
|
|
64
|
+
fontNone: "未设置,使用宿主默认字体",
|
|
65
|
+
fontFail: "字体上传失败,请确认是有效的 ttf/otf/woff/woff2 文件",
|
|
66
|
+
uploadTooLarge: "文件过大,上限 {limit}",
|
|
67
|
+
bgUploadFail: "壁纸上传失败",
|
|
68
|
+
videoUploadFail: "视频上传失败,本次播放不会被保存",
|
|
69
|
+
strokeTitle: "文字描边",
|
|
70
|
+
strokeHint: "按区域为文字添加描边:粗细为 0 表示关闭,「自动」会生成与文字颜色相反的颜色,「主题色」跟随当前主题色。代码块与图标已自动豁免,多色语法不会糊成一团",
|
|
71
|
+
strokeWidth: "描边粗细",
|
|
72
|
+
strokeColor: "描边颜色",
|
|
73
|
+
strokeAuto: "自动反色",
|
|
74
|
+
strokeGray: "灰色",
|
|
75
|
+
strokeBlack: "黑色",
|
|
76
|
+
strokeWhite: "白色",
|
|
77
|
+
strokeTheme: "主题色",
|
|
78
|
+
strokeCustom: "自定义",
|
|
49
79
|
bgTitle: "背景",
|
|
50
80
|
bgChoose: "选择图片或视频",
|
|
51
81
|
bgRemove: "移除背景",
|
|
@@ -73,7 +103,7 @@ window.__ModuleLoader__.load({
|
|
|
73
103
|
liveBadge: "动态壁纸",
|
|
74
104
|
bgHint: "「适应」布局模式下悬停预览可编辑图片/视频的位置与缩放(其余布局模式按模式自动排布);动态生成模式下可选择类型、预设与参数。绑定种子后刷新页面壁纸保持不变,取消绑定则每次刷新随机换新",
|
|
75
105
|
editorTitle: "背景编辑器",
|
|
76
|
-
editorHint: "
|
|
106
|
+
editorHint: "拖动移动画面,滚轮或双指缩放大小",
|
|
77
107
|
editorCommit: "确认",
|
|
78
108
|
editorCancel: "取消",
|
|
79
109
|
editorReset: "重置",
|
|
@@ -189,10 +219,12 @@ window.__ModuleLoader__.load({
|
|
|
189
219
|
close: "Close",
|
|
190
220
|
pageColor: "Color",
|
|
191
221
|
pageInterface: "Interface",
|
|
222
|
+
pageFont: "Font",
|
|
192
223
|
pageBackground: "Background",
|
|
193
224
|
pageProfile: "Profile",
|
|
194
225
|
descColor: "Drag the wheel or type exact values — the whole UI follows live",
|
|
195
226
|
descInterface: "Tune opacity and blur per surface to build depth",
|
|
227
|
+
descFont: "Swap the interface typeface with your own font file, and outline text per region so it stays legible over busy wallpapers",
|
|
196
228
|
descBackground: "Upload an image or a video as wallpaper and place it freely, or let the algorithm paint a live background",
|
|
197
229
|
descProfile: "Apply a one-click preset, save multiple named profiles, schedule a day/night switch, or export/import the full theme",
|
|
198
230
|
colorTitle: "Theme color",
|
|
@@ -211,6 +243,28 @@ window.__ModuleLoader__.load({
|
|
|
211
243
|
uiTrajectory: "Trajectory view",
|
|
212
244
|
uiPanelRegion: "better-sidebar workbench",
|
|
213
245
|
uiProduced: "Produced / highlights",
|
|
246
|
+
fontPageTitle: "Font & outline",
|
|
247
|
+
fontTitle: "Custom font",
|
|
248
|
+
fontHint: "Accepts ttf/otf/woff/woff2 files (up to 100MB) and applies them to the whole interface; code blocks keep their monospace stack. CJK fonts routinely span tens of MB, so the first upload and each reload take a moment",
|
|
249
|
+
fontUpload: "Upload font",
|
|
250
|
+
fontUploading: "Uploading…",
|
|
251
|
+
fontRemove: "Remove font",
|
|
252
|
+
fontEnabled: "Font enabled",
|
|
253
|
+
fontNone: "None — host default",
|
|
254
|
+
fontFail: "Upload failed — make sure it is a valid ttf/otf/woff/woff2 file",
|
|
255
|
+
uploadTooLarge: "File is too large (limit {limit})",
|
|
256
|
+
bgUploadFail: "Wallpaper upload failed",
|
|
257
|
+
videoUploadFail: "Video upload failed — this playback will not be saved",
|
|
258
|
+
strokeTitle: "Text outline",
|
|
259
|
+
strokeHint: "Outline text per region: width 0 turns it off, \"auto\" picks the inverse of the text color, and \"theme\" follows the current accent. Code blocks and icons are exempted automatically, so multi-color syntax never smears",
|
|
260
|
+
strokeWidth: "Outline width",
|
|
261
|
+
strokeColor: "Outline color",
|
|
262
|
+
strokeAuto: "Auto contrast",
|
|
263
|
+
strokeGray: "Gray",
|
|
264
|
+
strokeBlack: "Black",
|
|
265
|
+
strokeWhite: "White",
|
|
266
|
+
strokeTheme: "Accent",
|
|
267
|
+
strokeCustom: "Custom",
|
|
214
268
|
bgTitle: "Background",
|
|
215
269
|
bgChoose: "Choose image or video",
|
|
216
270
|
bgRemove: "Remove background",
|
|
@@ -238,7 +292,7 @@ window.__ModuleLoader__.load({
|
|
|
238
292
|
liveBadge: "Live wallpaper",
|
|
239
293
|
bgHint: "In Fit layout mode hover the preview to reposition and zoom the image/video (other modes arrange it automatically); in generated mode pick a type, preset and parameters. With the seed locked the wallpaper stays the same across refreshes; unlocked, every refresh randomizes it",
|
|
240
294
|
editorTitle: "Background editor",
|
|
241
|
-
editorHint: "Drag to move
|
|
295
|
+
editorHint: "Drag to move; scroll or pinch with two fingers to zoom",
|
|
242
296
|
editorCommit: "Confirm",
|
|
243
297
|
editorCancel: "Cancel",
|
|
244
298
|
editorReset: "Reset",
|
|
@@ -353,25 +407,72 @@ window.__ModuleLoader__.load({
|
|
|
353
407
|
const DEFAULT_CONFIG = {
|
|
354
408
|
color: null,
|
|
355
409
|
opacities: {
|
|
356
|
-
bg: .
|
|
357
|
-
sidebar: .
|
|
358
|
-
card:
|
|
359
|
-
input:
|
|
410
|
+
bg: .5,
|
|
411
|
+
sidebar: .5,
|
|
412
|
+
card: .5,
|
|
413
|
+
input: .5
|
|
360
414
|
},
|
|
361
415
|
blurs: {
|
|
362
|
-
bg:
|
|
363
|
-
sidebar:
|
|
364
|
-
card:
|
|
365
|
-
settings:
|
|
366
|
-
chat:
|
|
367
|
-
trajectory:
|
|
368
|
-
input:
|
|
369
|
-
panel:
|
|
370
|
-
produced:
|
|
416
|
+
bg: 30,
|
|
417
|
+
sidebar: 30,
|
|
418
|
+
card: 30,
|
|
419
|
+
settings: 30,
|
|
420
|
+
chat: 30,
|
|
421
|
+
trajectory: 30,
|
|
422
|
+
input: 30,
|
|
423
|
+
panel: 30,
|
|
424
|
+
produced: 30
|
|
425
|
+
},
|
|
426
|
+
strokes: {
|
|
427
|
+
bg: {
|
|
428
|
+
width: 0,
|
|
429
|
+
color: "auto",
|
|
430
|
+
customColor: "#808080"
|
|
431
|
+
},
|
|
432
|
+
sidebar: {
|
|
433
|
+
width: 0,
|
|
434
|
+
color: "auto",
|
|
435
|
+
customColor: "#808080"
|
|
436
|
+
},
|
|
437
|
+
card: {
|
|
438
|
+
width: 0,
|
|
439
|
+
color: "auto",
|
|
440
|
+
customColor: "#808080"
|
|
441
|
+
},
|
|
442
|
+
settings: {
|
|
443
|
+
width: 0,
|
|
444
|
+
color: "auto",
|
|
445
|
+
customColor: "#808080"
|
|
446
|
+
},
|
|
447
|
+
chat: {
|
|
448
|
+
width: 0,
|
|
449
|
+
color: "auto",
|
|
450
|
+
customColor: "#808080"
|
|
451
|
+
},
|
|
452
|
+
trajectory: {
|
|
453
|
+
width: 0,
|
|
454
|
+
color: "auto",
|
|
455
|
+
customColor: "#808080"
|
|
456
|
+
},
|
|
457
|
+
input: {
|
|
458
|
+
width: 0,
|
|
459
|
+
color: "auto",
|
|
460
|
+
customColor: "#808080"
|
|
461
|
+
},
|
|
462
|
+
panel: {
|
|
463
|
+
width: 0,
|
|
464
|
+
color: "auto",
|
|
465
|
+
customColor: "#808080"
|
|
466
|
+
},
|
|
467
|
+
produced: {
|
|
468
|
+
width: 0,
|
|
469
|
+
color: "auto",
|
|
470
|
+
customColor: "#808080"
|
|
471
|
+
}
|
|
371
472
|
},
|
|
372
|
-
settingsOpacity:
|
|
473
|
+
settingsOpacity: .5,
|
|
373
474
|
wallpaperOpacity: 1,
|
|
374
|
-
blur:
|
|
475
|
+
blur: 30,
|
|
375
476
|
bgState: {
|
|
376
477
|
zoom: 1,
|
|
377
478
|
x: 0,
|
|
@@ -389,12 +490,14 @@ window.__ModuleLoader__.load({
|
|
|
389
490
|
backgroundType: "image",
|
|
390
491
|
bgMode: "fit",
|
|
391
492
|
videoMime: null,
|
|
493
|
+
fontMime: null,
|
|
494
|
+
fontEnabled: true,
|
|
392
495
|
generatedBg: null,
|
|
393
496
|
regenerateOnReload: false,
|
|
394
|
-
chatTextOpacity:
|
|
395
|
-
trajectoryOpacity:
|
|
396
|
-
panelOpacity:
|
|
397
|
-
producedOpacity:
|
|
497
|
+
chatTextOpacity: .5,
|
|
498
|
+
trajectoryOpacity: .5,
|
|
499
|
+
panelOpacity: .5,
|
|
500
|
+
producedOpacity: .5,
|
|
398
501
|
profiles: [],
|
|
399
502
|
rotation: {
|
|
400
503
|
enabled: false,
|
|
@@ -420,6 +523,7 @@ window.__ModuleLoader__.load({
|
|
|
420
523
|
...DEFAULT_CONFIG,
|
|
421
524
|
opacities: { ...DEFAULT_CONFIG.opacities },
|
|
422
525
|
blurs: { ...DEFAULT_CONFIG.blurs },
|
|
526
|
+
strokes: structuredClone(DEFAULT_CONFIG.strokes),
|
|
423
527
|
bgState: { ...DEFAULT_CONFIG.bgState },
|
|
424
528
|
videoBgState: { ...DEFAULT_CONFIG.videoBgState }
|
|
425
529
|
};
|
|
@@ -540,6 +644,49 @@ window.__ModuleLoader__.load({
|
|
|
540
644
|
}
|
|
541
645
|
return out;
|
|
542
646
|
}
|
|
647
|
+
const STROKE_GROUPS = [
|
|
648
|
+
"bg",
|
|
649
|
+
"sidebar",
|
|
650
|
+
"card",
|
|
651
|
+
"settings",
|
|
652
|
+
"chat",
|
|
653
|
+
"trajectory",
|
|
654
|
+
"input",
|
|
655
|
+
"panel",
|
|
656
|
+
"produced"
|
|
657
|
+
];
|
|
658
|
+
const STROKE_COLOR_KEYS = [
|
|
659
|
+
"auto",
|
|
660
|
+
"gray",
|
|
661
|
+
"black",
|
|
662
|
+
"white",
|
|
663
|
+
"theme",
|
|
664
|
+
"custom"
|
|
665
|
+
];
|
|
666
|
+
const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
667
|
+
const DEFAULT_STROKE = {
|
|
668
|
+
width: 0,
|
|
669
|
+
color: "auto",
|
|
670
|
+
customColor: "#808080"
|
|
671
|
+
};
|
|
672
|
+
function adoptStroke(raw) {
|
|
673
|
+
const s = raw ?? {};
|
|
674
|
+
return {
|
|
675
|
+
width: typeof s.width === "number" && isFinite(s.width) ? Math.min(4, Math.max(0, s.width)) : DEFAULT_STROKE.width,
|
|
676
|
+
color: STROKE_COLOR_KEYS.includes(s.color) ? s.color : DEFAULT_STROKE.color,
|
|
677
|
+
customColor: typeof s.customColor === "string" && HEX_RE.test(s.customColor) ? s.customColor : DEFAULT_STROKE.customColor
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
/** Move a possibly-partial strokes map into the full shape the UI reads. */
|
|
681
|
+
function strokesFrom(raw) {
|
|
682
|
+
const s = raw ?? {};
|
|
683
|
+
const out = {};
|
|
684
|
+
for (const k of STROKE_GROUPS) out[k] = adoptStroke(s[k]);
|
|
685
|
+
return out;
|
|
686
|
+
}
|
|
687
|
+
function rStrokes() {
|
|
688
|
+
return strokesFrom(cfg.strokes);
|
|
689
|
+
}
|
|
543
690
|
function rWop() {
|
|
544
691
|
return clamp01(cfg.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity);
|
|
545
692
|
}
|
|
@@ -611,6 +758,7 @@ window.__ModuleLoader__.load({
|
|
|
611
758
|
color: cfg.color,
|
|
612
759
|
opacities: { ...rOps() },
|
|
613
760
|
blurs: { ...rBlurs() },
|
|
761
|
+
strokes: rStrokes(),
|
|
614
762
|
settingsOpacity: rSop(),
|
|
615
763
|
wallpaperOpacity: rWop(),
|
|
616
764
|
blur: rBl(),
|
|
@@ -631,6 +779,7 @@ window.__ModuleLoader__.load({
|
|
|
631
779
|
...DEFAULT_CONFIG.blurs,
|
|
632
780
|
...ap.blurs ?? {}
|
|
633
781
|
};
|
|
782
|
+
cfg.strokes = strokesFrom(ap.strokes);
|
|
634
783
|
cfg.settingsOpacity = clamp01(ap.settingsOpacity, DEFAULT_CONFIG.settingsOpacity);
|
|
635
784
|
cfg.wallpaperOpacity = clamp01(ap.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity);
|
|
636
785
|
cfg.blur = typeof ap.blur === "number" ? Math.min(60, Math.max(0, ap.blur)) : DEFAULT_CONFIG.blur;
|
|
@@ -664,6 +813,7 @@ window.__ModuleLoader__.load({
|
|
|
664
813
|
...DEFAULT_CONFIG.blurs,
|
|
665
814
|
...ac.blurs ?? {}
|
|
666
815
|
},
|
|
816
|
+
strokes: strokesFrom(ac.strokes),
|
|
667
817
|
settingsOpacity: clamp01(ac.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
|
|
668
818
|
wallpaperOpacity: clamp01(ac.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity),
|
|
669
819
|
blur: num(ac.blur, DEFAULT_CONFIG.blur),
|
|
@@ -761,6 +911,7 @@ window.__ModuleLoader__.load({
|
|
|
761
911
|
input: num(ops.input, DEFAULT_CONFIG.opacities.input)
|
|
762
912
|
},
|
|
763
913
|
blurs,
|
|
914
|
+
strokes: strokesFrom(c.strokes),
|
|
764
915
|
settingsOpacity: num(c.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
|
|
765
916
|
wallpaperOpacity: num(c.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity),
|
|
766
917
|
blur: num(c.blur, DEFAULT_CONFIG.blur),
|
|
@@ -769,6 +920,8 @@ window.__ModuleLoader__.load({
|
|
|
769
920
|
backgroundType: bgType,
|
|
770
921
|
bgMode,
|
|
771
922
|
videoMime: typeof c.videoMime === "string" ? c.videoMime : null,
|
|
923
|
+
fontMime: typeof c.fontMime === "string" ? c.fontMime : null,
|
|
924
|
+
fontEnabled: typeof c.fontEnabled === "boolean" ? c.fontEnabled : DEFAULT_CONFIG.fontEnabled,
|
|
772
925
|
generatedBg: generatedBg ? normalizeGeneratedBg(generatedBg) : null,
|
|
773
926
|
regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
|
|
774
927
|
chatTextOpacity: clamp01(c.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
|
|
@@ -828,9 +981,15 @@ window.__ModuleLoader__.load({
|
|
|
828
981
|
const WALLPAPER_UPLOAD_URL = "/dsh-any-background/wallpaper/upload";
|
|
829
982
|
/** HTTP route new videos are POSTed to as raw bytes (see uploadVideo). */
|
|
830
983
|
const VIDEO_UPLOAD_URL = "/dsh-any-background/video/upload";
|
|
984
|
+
/** Same-origin serve URL of the persisted custom font (enough for @font-face). */
|
|
985
|
+
const FONT_SERVE_URL = "/dsh-any-background/font";
|
|
986
|
+
/** HTTP route custom fonts are POSTed to as raw bytes (see uploadFont). */
|
|
987
|
+
const FONT_UPLOAD_URL = "/dsh-any-background/font/upload";
|
|
831
988
|
const RPC_NS = "dshAnyBackground";
|
|
832
989
|
const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
|
|
833
990
|
let rpcCallFn = null;
|
|
991
|
+
/** Serve URL of the persisted custom font, filled by loadPersisted. */
|
|
992
|
+
let fontServeUrl = null;
|
|
834
993
|
function initRpc(call) {
|
|
835
994
|
rpcCallFn = call;
|
|
836
995
|
}
|
|
@@ -868,7 +1027,10 @@ window.__ModuleLoader__.load({
|
|
|
868
1027
|
/** Load the persisted theme (config + wallpaper URL + video URL) from the node
|
|
869
1028
|
* half. Image and video both travel as serve URLs — never bytes — so this RPC
|
|
870
1029
|
* stays tiny. Resolves true when the server advanced a due wallpaper rotation
|
|
871
|
-
* during the read — the restored wallpaper is then already the new pick.
|
|
1030
|
+
* during the read — the restored wallpaper is then already the new pick.
|
|
1031
|
+
* `firstRun` means the server had no theme-config.json and just materialized
|
|
1032
|
+
* its defaults: the caller should persist the browser side's own defaults and
|
|
1033
|
+
* re-read once so every slider starts from a value that is really on disk. */
|
|
872
1034
|
async function loadPersisted() {
|
|
873
1035
|
const data = await rpcCall("read", {});
|
|
874
1036
|
if (data && typeof data === "object") {
|
|
@@ -878,10 +1040,18 @@ window.__ModuleLoader__.load({
|
|
|
878
1040
|
else if (d.wallpaperUrl === null) setWpImageUrl(null);
|
|
879
1041
|
if (typeof d.videoUrl === "string") setWpVideoUrl(d.videoUrl, cfg.videoMime);
|
|
880
1042
|
else if (d.videoUrl === null) setWpVideoUrl(null, null);
|
|
1043
|
+
if (typeof d.fontUrl === "string") fontServeUrl = d.fontUrl;
|
|
1044
|
+
else if (d.fontUrl === null) fontServeUrl = null;
|
|
881
1045
|
if (cfg.backgroundType === "image") setWpUrl(rWpImage());
|
|
882
|
-
return
|
|
1046
|
+
return {
|
|
1047
|
+
rotated: d.rotated === true,
|
|
1048
|
+
firstRun: d.firstRun === true
|
|
1049
|
+
};
|
|
883
1050
|
}
|
|
884
|
-
return
|
|
1051
|
+
return {
|
|
1052
|
+
rotated: false,
|
|
1053
|
+
firstRun: false
|
|
1054
|
+
};
|
|
885
1055
|
}
|
|
886
1056
|
/** Persist a wallpaper (null removes it); one-shot, no debounce. */
|
|
887
1057
|
function persistWallpaper(dataUrl) {
|
|
@@ -959,34 +1129,96 @@ window.__ModuleLoader__.load({
|
|
|
959
1129
|
error: v?.error ?? "failed"
|
|
960
1130
|
};
|
|
961
1131
|
}
|
|
1132
|
+
/** Map a refused upload response onto an outcome. The body has to be read: the
|
|
1133
|
+
* node half names the reason there, and an oversized transfer answers
|
|
1134
|
+
* `413 { ok:false, error:'too large', limit:'100 MB' }`. Checking `res.ok`
|
|
1135
|
+
* alone threw that away, so an oversized upload surfaced as a bare failure —
|
|
1136
|
+
* or, on the wallpaper path, as nothing at all. */
|
|
1137
|
+
async function refusedUpload(res) {
|
|
1138
|
+
const body = await res.json().catch(() => null);
|
|
1139
|
+
if (body?.error === "too large") return {
|
|
1140
|
+
ok: false,
|
|
1141
|
+
refusal: {
|
|
1142
|
+
kind: "too-large",
|
|
1143
|
+
status: res.status,
|
|
1144
|
+
limit: typeof body.limit === "string" ? body.limit : void 0
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
return {
|
|
1148
|
+
ok: false,
|
|
1149
|
+
refusal: {
|
|
1150
|
+
kind: "http",
|
|
1151
|
+
status: res.status
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
/** Localized description of a refusal for the panel's toast. `fallbackKey` is
|
|
1156
|
+
* the caller's own "…upload failed" string, used for anything that is not a
|
|
1157
|
+
* size refusal. */
|
|
1158
|
+
function uploadRefusalText(o, t, fallbackKey) {
|
|
1159
|
+
if (o.refusal?.kind === "too-large") {
|
|
1160
|
+
const limit = o.refusal.limit ?? "";
|
|
1161
|
+
return t("uploadTooLarge").split("{limit}").join(limit);
|
|
1162
|
+
}
|
|
1163
|
+
if (o.refusal !== void 0) return `${t(fallbackKey)} (http ${o.refusal.status})`;
|
|
1164
|
+
return t(fallbackKey);
|
|
1165
|
+
}
|
|
962
1166
|
/** Upload a video's raw bytes over HTTP (MIME in Content-Type, body untouched
|
|
963
1167
|
* — no base64 inflation that would blow the RPC body limit on large clips). */
|
|
964
1168
|
async function uploadVideo(blob, mime) {
|
|
965
1169
|
try {
|
|
966
|
-
|
|
1170
|
+
const res = await fetch(VIDEO_UPLOAD_URL, {
|
|
967
1171
|
method: "POST",
|
|
968
1172
|
headers: { "Content-Type": mime || "application/octet-stream" },
|
|
969
1173
|
body: blob
|
|
970
|
-
})
|
|
1174
|
+
});
|
|
1175
|
+
return res.ok ? { ok: true } : await refusedUpload(res);
|
|
971
1176
|
} catch (e) {
|
|
972
1177
|
console.warn("dsh-any-background: video upload failed", e);
|
|
973
|
-
return false;
|
|
1178
|
+
return { ok: false };
|
|
974
1179
|
}
|
|
975
1180
|
}
|
|
976
1181
|
/** Upload a wallpaper's raw bytes over HTTP — same streaming model as videos,
|
|
977
1182
|
* original pixels preserved, zero base64 round-trips. */
|
|
978
1183
|
async function uploadWallpaper(blob) {
|
|
979
1184
|
try {
|
|
980
|
-
|
|
1185
|
+
const res = await fetch(WALLPAPER_UPLOAD_URL, {
|
|
981
1186
|
method: "POST",
|
|
982
1187
|
headers: { "Content-Type": blob.type || "image/jpeg" },
|
|
983
1188
|
body: blob
|
|
984
|
-
})
|
|
1189
|
+
});
|
|
1190
|
+
return res.ok ? { ok: true } : await refusedUpload(res);
|
|
985
1191
|
} catch (e) {
|
|
986
1192
|
console.warn("dsh-any-background: wallpaper upload failed", e);
|
|
987
|
-
return false;
|
|
1193
|
+
return { ok: false };
|
|
988
1194
|
}
|
|
989
1195
|
}
|
|
1196
|
+
/** Upload a custom font's raw bytes over HTTP; resolves the sniffed MIME on
|
|
1197
|
+
* success, or a refusal the caller can report (an oversized font used to be
|
|
1198
|
+
* indistinguishable from a corrupt one). */
|
|
1199
|
+
async function uploadFont(blob) {
|
|
1200
|
+
try {
|
|
1201
|
+
const res = await fetch(FONT_UPLOAD_URL, {
|
|
1202
|
+
method: "POST",
|
|
1203
|
+
headers: { "Content-Type": blob.type || "application/octet-stream" },
|
|
1204
|
+
body: blob
|
|
1205
|
+
});
|
|
1206
|
+
if (!res.ok) return await refusedUpload(res);
|
|
1207
|
+
const data = await res.json().catch(() => null);
|
|
1208
|
+
return {
|
|
1209
|
+
ok: true,
|
|
1210
|
+
mime: typeof data?.mime === "string" ? data.mime : "font/ttf"
|
|
1211
|
+
};
|
|
1212
|
+
} catch (e) {
|
|
1213
|
+
console.warn("dsh-any-background: font upload failed", e);
|
|
1214
|
+
return { ok: false };
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/** Remove the stored custom font (server deletes every variant + clears the
|
|
1218
|
+
* recorded MIME); resolves true once the slot is empty. */
|
|
1219
|
+
async function removeFont() {
|
|
1220
|
+
return await rpcCall("removeFont", {}) === true;
|
|
1221
|
+
}
|
|
990
1222
|
/** Add an image (data URL + small thumbnail) to the server-side rotation pool. */
|
|
991
1223
|
async function rotationAdd(dataUrl, thumb) {
|
|
992
1224
|
if (!rpcCallFn) return {
|
|
@@ -1160,6 +1392,12 @@ window.__ModuleLoader__.load({
|
|
|
1160
1392
|
v
|
|
1161
1393
|
];
|
|
1162
1394
|
}
|
|
1395
|
+
/** Format an RGB triple as a lowercase hex string. Channels are rounded: some
|
|
1396
|
+
* callers feed float channels and a fractional byte would produce invalid
|
|
1397
|
+
* hex. */
|
|
1398
|
+
function rgbToHex(rgb) {
|
|
1399
|
+
return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
|
|
1400
|
+
}
|
|
1163
1401
|
let tokensCacheKey = "";
|
|
1164
1402
|
let tokensCache = null;
|
|
1165
1403
|
/**
|
|
@@ -2303,9 +2541,15 @@ window.__ModuleLoader__.load({
|
|
|
2303
2541
|
wpController = null;
|
|
2304
2542
|
}
|
|
2305
2543
|
/** Register a callback fired once a generated snapshot is ready (so the caller
|
|
2306
|
-
* can re-sync the settings preview / store).
|
|
2544
|
+
* can re-sync the settings preview / store). Returns an unsubscribe: HMR
|
|
2545
|
+
* re-runs apply and would otherwise leave the previous apply's closure as the
|
|
2546
|
+
* live listener (a stale this-session callback invoked by a lingering
|
|
2547
|
+
* controller). */
|
|
2307
2548
|
function onGeneratedSnapshot(cb) {
|
|
2308
2549
|
snapshotListener = cb;
|
|
2550
|
+
return () => {
|
|
2551
|
+
if (snapshotListener === cb) snapshotListener = null;
|
|
2552
|
+
};
|
|
2309
2553
|
}
|
|
2310
2554
|
function ensureTokenStyle() {
|
|
2311
2555
|
if (tokenStyleEl?.isConnected) return tokenStyleEl;
|
|
@@ -2400,14 +2644,34 @@ window.__ModuleLoader__.load({
|
|
|
2400
2644
|
if (rSchemeOverride() !== "auto") return genTokens(220, .04, scheme === "dark" ? .14 : .92, scheme).tokens;
|
|
2401
2645
|
return null;
|
|
2402
2646
|
}
|
|
2647
|
+
/** Surface colors the opacity sliders fade when the plugin has NO palette of
|
|
2648
|
+
* its own (no picked color, no forced scheme), read live from the host's
|
|
2649
|
+
* resolved tokens on `:root` — the same trick applyPanelOverrides uses for the
|
|
2650
|
+
* workbench panel. Without this source the per-part alpha is never emitted at
|
|
2651
|
+
* all and every opacity slider looks inert until the user drags it once.
|
|
2652
|
+
* The sliders only ever supply the alpha: the host still decides the colors,
|
|
2653
|
+
* so a custom host skin survives. Returns null when the host exposes none. */
|
|
2654
|
+
function readHostOpacityTokens() {
|
|
2655
|
+
if (typeof getComputedStyle === "undefined") return null;
|
|
2656
|
+
const cs = getComputedStyle(document.documentElement);
|
|
2657
|
+
const out = {};
|
|
2658
|
+
let found = false;
|
|
2659
|
+
for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) {
|
|
2660
|
+
const v = cs.getPropertyValue(name).trim();
|
|
2661
|
+
if (v === "") continue;
|
|
2662
|
+
out[name] = v;
|
|
2663
|
+
found = true;
|
|
2664
|
+
}
|
|
2665
|
+
return found ? out : null;
|
|
2666
|
+
}
|
|
2403
2667
|
function applyCustomTokensNow(ops) {
|
|
2404
2668
|
const hasColor = rHasColor();
|
|
2405
2669
|
const override = rSchemeOverride();
|
|
2406
2670
|
const [h, s, l] = rColor();
|
|
2407
2671
|
const scheme = rScheme();
|
|
2408
2672
|
const verdict = rBgDark();
|
|
2409
|
-
const
|
|
2410
|
-
const tokens = { ...
|
|
2673
|
+
const surfaces = paletteTokens() ?? readHostOpacityTokens();
|
|
2674
|
+
const tokens = { ...surfaces };
|
|
2411
2675
|
if (override === "auto") {
|
|
2412
2676
|
const fontDark = hasColor ? rColorScheme() === "dark" : verdict;
|
|
2413
2677
|
if (fontDark !== null && fontDark !== void 0) {
|
|
@@ -2431,10 +2695,17 @@ window.__ModuleLoader__.load({
|
|
|
2431
2695
|
for (const name of appliedTokenNames) document.body.style.removeProperty(name);
|
|
2432
2696
|
appliedTokenNames = Object.keys(tokens);
|
|
2433
2697
|
}
|
|
2434
|
-
if (
|
|
2698
|
+
if (surfaces === null) {
|
|
2699
|
+
if (!hasColor && override === "auto" && verdict === null) clearThemeTokens();
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2435
2702
|
const root = document.documentElement;
|
|
2436
|
-
for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names)
|
|
2437
|
-
|
|
2703
|
+
for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) {
|
|
2704
|
+
if (surfaces[name] === void 0) continue;
|
|
2705
|
+
root.style.setProperty(OPACITY_VARS[name], toRgba(surfaces[name], ops[g.part]));
|
|
2706
|
+
}
|
|
2707
|
+
const menu = surfaces["--dsw-specific-menu"];
|
|
2708
|
+
if (menu !== void 0) root.style.setProperty("--dsh-any-op-menu-cordis", toRgba(menu, ops.input));
|
|
2438
2709
|
const bgKey = `${baseTokenKey}|${ops.bg}`;
|
|
2439
2710
|
if (bgKey !== lastBgKey) {
|
|
2440
2711
|
lastBgKey = bgKey;
|
|
@@ -2461,6 +2732,149 @@ window.__ModuleLoader__.load({
|
|
|
2461
2732
|
const PANEL_TOKEN_RULE = "[data-dsh-bottom-panel],[data-sidebar-right-panel]{--dsw-alias-bg-base:var(--dsh-any-panel-bg-base);--dsw-alias-bg-layer-1:var(--dsh-any-panel-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-panel-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-panel-layer-3);position:fixed!important;z-index:26!important}[data-sidebar-right-panel][data-sidebar-right-panel=\"fullscreen\"]{z-index:40!important}";
|
|
2462
2733
|
const PANEL_BLUR_RULE = "[data-dsh-bottom-panel],[data-sidebar-right-panel]{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}";
|
|
2463
2734
|
const PRODUCED_RULE = "[data-code-block-content] pre,[data-code-block-banner],[data-composer-chip],:not(pre)>code{-webkit-backdrop-filter:var(--dsh-any-blur-prod,none);backdrop-filter:var(--dsh-any-blur-prod,none)}@supports (background:color-mix(in srgb,red 50%,transparent)){.md-code-block{background:transparent!important}.md-code-block>div{background-color:transparent!important}.md-code-block [data-code-block-content] pre{background-color:color-mix(in srgb,var(--dsl-code-block-background,var(--dsw-alias-markdown-code-block,transparent)) var(--dsh-any-prod-pct,100%),transparent)!important}.md-code-block [data-code-block-banner]{background-color:color-mix(in srgb,var(--dsl-code-block-banner-background-color,var(--dsw-alias-markdown-code-block-banner,transparent)) var(--dsh-any-prod-pct,100%),transparent)!important}[data-code-block-content]{--shiki-background:color-mix(in srgb,var(--dsw-alias-markdown-code-block,var(--dsl-code-block-background,transparent)) var(--dsh-any-prod-pct,100%),transparent)}:not(pre)>code{background-color:color-mix(in srgb,var(--dsw-alias-markdown-inline-code,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-composer-chip]>*{background-color:color-mix(in srgb,var(--dsw-alias-interactive-bg-hover,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}}";
|
|
2735
|
+
const STROKE_RULE = [
|
|
2736
|
+
`${SETTINGS_PANEL_SEL}{-webkit-text-stroke:var(--dsh-any-stroke-settings-w,0px) var(--dsh-any-stroke-settings-c,transparent);paint-order:stroke fill}`,
|
|
2737
|
+
"[role=\"menu\"]:not([data-dockkit-tab-menu]),[role=\"listbox\"],body>[role=\"tree\"],body>[role=\"dialog\"]:not([aria-modal=\"true\"]){-webkit-text-stroke:var(--dsh-any-stroke-card-w,0px) var(--dsh-any-stroke-card-c,transparent);paint-order:stroke fill}",
|
|
2738
|
+
"[data-composer-card],[data-cordis-panel],[data-composer-card] textarea,[data-composer-card] input,[data-composer-card] [contenteditable],[data-cordis-panel] input,[data-cordis-panel] textarea{-webkit-text-stroke:var(--dsh-any-stroke-input-w,0px) var(--dsh-any-stroke-input-c,transparent);paint-order:stroke fill}",
|
|
2739
|
+
`[data-chat-flow]{-webkit-text-stroke:var(--dsh-any-stroke-chat-w,0px) var(--dsh-any-stroke-chat-c,transparent);paint-order:stroke fill}`,
|
|
2740
|
+
`[data-conversation-composer-overlay]{-webkit-text-stroke:var(--dsh-any-stroke-trajectory-w,0px) var(--dsh-any-stroke-trajectory-c,transparent);paint-order:stroke fill}`,
|
|
2741
|
+
"[data-code-block-content] pre,[data-code-block-content],[data-code-block-banner],[data-composer-chip],:not(pre)>code{-webkit-text-stroke:var(--dsh-any-stroke-produced-w,0px) var(--dsh-any-stroke-produced-c,transparent);paint-order:stroke fill}",
|
|
2742
|
+
`[data-dsh-bottom-panel],[data-sidebar-right-panel]{-webkit-text-stroke:var(--dsh-any-stroke-panel-w,0px) var(--dsh-any-stroke-panel-c,transparent);paint-order:stroke fill}`,
|
|
2743
|
+
`svg{-webkit-text-stroke-width:0!important;paint-order:normal!important}`,
|
|
2744
|
+
`::placeholder{-webkit-text-stroke-width:0!important}`
|
|
2745
|
+
].join("");
|
|
2746
|
+
/** Groups whose stroke lands on the structurally-discovered columns (inline),
|
|
2747
|
+
* versus every group served by the static STROKE_RULE selectors. */
|
|
2748
|
+
const STROKE_INLINE_GROUPS = ["bg", "sidebar"];
|
|
2749
|
+
const STROKE_VAR_GROUPS = [
|
|
2750
|
+
"card",
|
|
2751
|
+
"settings",
|
|
2752
|
+
"chat",
|
|
2753
|
+
"trajectory",
|
|
2754
|
+
"input",
|
|
2755
|
+
"panel",
|
|
2756
|
+
"produced"
|
|
2757
|
+
];
|
|
2758
|
+
/** Resolve one group's stroke color key into a concrete CSS color.
|
|
2759
|
+
* 'auto' contrasts the FONT direction (white fonts → black stroke and vice
|
|
2760
|
+
* versa — mirrors the label flip in applyCustomTokensNow); 'theme' follows
|
|
2761
|
+
* the live palette's brand primary (picked color → generated palette, else
|
|
2762
|
+
* the host's own resolved token). */
|
|
2763
|
+
function strokeColor(s) {
|
|
2764
|
+
switch (s.color) {
|
|
2765
|
+
case "gray": return "#808080";
|
|
2766
|
+
case "black": return "#000";
|
|
2767
|
+
case "white": return "#fff";
|
|
2768
|
+
case "custom": return s.customColor;
|
|
2769
|
+
case "theme":
|
|
2770
|
+
if (rHasColor() || rSchemeOverride() !== "auto") {
|
|
2771
|
+
const [h, sa, l] = rColor();
|
|
2772
|
+
return genTokens(h, sa, l, rColorScheme()).tokens["--dsw-alias-brand-primary"] ?? "#808080";
|
|
2773
|
+
}
|
|
2774
|
+
if (typeof getComputedStyle !== "undefined") {
|
|
2775
|
+
const v = getComputedStyle(document.documentElement).getPropertyValue("--dsw-alias-brand-primary").trim();
|
|
2776
|
+
if (v !== "") return v;
|
|
2777
|
+
}
|
|
2778
|
+
return "#808080";
|
|
2779
|
+
default: {
|
|
2780
|
+
let fontDark;
|
|
2781
|
+
if (rHasColor()) fontDark = rColorScheme() === "dark";
|
|
2782
|
+
else if (rSchemeOverride() !== "auto") fontDark = rScheme() === "dark";
|
|
2783
|
+
else if (rBgDark() !== null) fontDark = rBgDark() === true;
|
|
2784
|
+
else fontDark = false;
|
|
2785
|
+
return fontDark ? "#000" : "#fff";
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
/** Write every group's stroke width/color variables + the two column strokes.
|
|
2790
|
+
* Called from applyWp so palette/verdict changes re-derive 'auto'/'theme'. */
|
|
2791
|
+
/** Write (or clear) one column's inline text stroke. The stroke rides inline
|
|
2792
|
+
* styles and is fully removed at width 0 so nothing lingers after the slider
|
|
2793
|
+
* resets. */
|
|
2794
|
+
function applyInlineStroke(el, s) {
|
|
2795
|
+
if (s.width > 0) {
|
|
2796
|
+
el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
|
|
2797
|
+
el.style.setProperty("paint-order", "stroke fill");
|
|
2798
|
+
} else {
|
|
2799
|
+
el.style.removeProperty("-webkit-text-stroke");
|
|
2800
|
+
el.style.removeProperty("paint-order");
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
function applyStrokes() {
|
|
2804
|
+
const strokes = rStrokes();
|
|
2805
|
+
const root = document.documentElement;
|
|
2806
|
+
for (const g of STROKE_VAR_GROUPS) {
|
|
2807
|
+
const s = strokes[g];
|
|
2808
|
+
root.style.setProperty(`--dsh-any-stroke-${g}-w`, `${s.width}px`);
|
|
2809
|
+
root.style.setProperty(`--dsh-any-stroke-${g}-c`, strokeColor(s));
|
|
2810
|
+
}
|
|
2811
|
+
discoverParts();
|
|
2812
|
+
for (const g of STROKE_INLINE_GROUPS) {
|
|
2813
|
+
const el = g === "bg" ? centerEl : sidebarEl;
|
|
2814
|
+
if (el !== null) applyInlineStroke(el, strokes[g]);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
/** Live per-group stroke update during slider drag (no full re-apply). */
|
|
2818
|
+
function setPartStroke(part, s) {
|
|
2819
|
+
if (STROKE_VAR_GROUPS.includes(part)) {
|
|
2820
|
+
document.documentElement.style.setProperty(`--dsh-any-stroke-${part}-w`, `${s.width}px`);
|
|
2821
|
+
document.documentElement.style.setProperty(`--dsh-any-stroke-${part}-c`, strokeColor(s));
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
discoverParts();
|
|
2825
|
+
const el = part === "bg" ? centerEl : sidebarEl;
|
|
2826
|
+
if (el === null) return;
|
|
2827
|
+
applyInlineStroke(el, s);
|
|
2828
|
+
}
|
|
2829
|
+
/** Teardown only: drop every stroke variable and column inline stroke. */
|
|
2830
|
+
function removeStrokes() {
|
|
2831
|
+
const root = document.documentElement;
|
|
2832
|
+
for (const g of [...STROKE_VAR_GROUPS, ...STROKE_INLINE_GROUPS]) {
|
|
2833
|
+
root.style.removeProperty(`--dsh-any-stroke-${g}-w`);
|
|
2834
|
+
root.style.removeProperty(`--dsh-any-stroke-${g}-c`);
|
|
2835
|
+
}
|
|
2836
|
+
for (const el of [centerEl, sidebarEl]) {
|
|
2837
|
+
if (el === null) continue;
|
|
2838
|
+
el.style.removeProperty("-webkit-text-stroke");
|
|
2839
|
+
el.style.removeProperty("paint-order");
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
const FONT_FAMILY = "DAnyFont";
|
|
2843
|
+
let fontStyleEl = null;
|
|
2844
|
+
function fontFormatForMime(mime) {
|
|
2845
|
+
switch (mime) {
|
|
2846
|
+
case "font/woff2": return "woff2";
|
|
2847
|
+
case "font/woff": return "woff";
|
|
2848
|
+
case "font/otf": return "opentype";
|
|
2849
|
+
default: return "truetype";
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
/** Apply or clear the custom interface font. `url` is the serve URL (null =
|
|
2853
|
+
* nothing stored); `enabled` gates the token override without deleting the
|
|
2854
|
+
* file. The original host stack is re-read on every apply so a host skin
|
|
2855
|
+
* change is picked up, and stays as the fallback after 'DAnyFont'. */
|
|
2856
|
+
function applyFontFace(url, enabled, mime) {
|
|
2857
|
+
if (url === null || !enabled) {
|
|
2858
|
+
fontStyleEl?.remove();
|
|
2859
|
+
fontStyleEl = null;
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
if (fontStyleEl === null || !fontStyleEl.isConnected) {
|
|
2863
|
+
fontStyleEl = document.createElement("style");
|
|
2864
|
+
fontStyleEl.dataset.plugin = "dsh-any-background-font";
|
|
2865
|
+
document.head.appendChild(fontStyleEl);
|
|
2866
|
+
}
|
|
2867
|
+
let stack = "";
|
|
2868
|
+
if (typeof getComputedStyle !== "undefined") stack = getComputedStyle(document.documentElement).getPropertyValue("--dsw-font-family").trim();
|
|
2869
|
+
if (stack === "") stack = "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif";
|
|
2870
|
+
const fmt = fontFormatForMime(mime);
|
|
2871
|
+
fontStyleEl.textContent = `@font-face{font-family:'${FONT_FAMILY}';src:url('${url}') format('${fmt}');font-display:swap}body{--dsw-font-family:'${FONT_FAMILY}',${stack}}input,textarea,select,button{font-family:var(--dsw-font-family)}`;
|
|
2872
|
+
}
|
|
2873
|
+
/** Teardown only: drop the @font-face + token override. */
|
|
2874
|
+
function removeFontFace() {
|
|
2875
|
+
fontStyleEl?.remove();
|
|
2876
|
+
fontStyleEl = null;
|
|
2877
|
+
}
|
|
2464
2878
|
/** The host's own default colors for the panel layer tokens, used when the
|
|
2465
2879
|
* plugin has no palette (no picked color, no wallpaper verdict, no forced
|
|
2466
2880
|
* scheme). Without these the panel's re-scope would resolve to invalid vars
|
|
@@ -2515,27 +2929,35 @@ window.__ModuleLoader__.load({
|
|
|
2515
2929
|
if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-panel", `blur(${px}px)`);
|
|
2516
2930
|
else document.documentElement.style.removeProperty("--dsh-any-blur-panel");
|
|
2517
2931
|
}
|
|
2518
|
-
|
|
2932
|
+
/** Re-scope one surface group's layer tokens onto plugin-owned variables so a
|
|
2933
|
+
* dedicated slider owns its alpha (the host has no per-surface opacity). The
|
|
2934
|
+
* settings group additionally retints its dialog surface variable. Always
|
|
2935
|
+
* written explicitly — including at 100% — so removing a slider's effect means
|
|
2936
|
+
* writing 1, not deleting the variable (the style rules below have no fallback
|
|
2937
|
+
* and would otherwise resolve the body tokens that applyCustomTokens rewrote
|
|
2938
|
+
* with the homepage card alpha). */
|
|
2939
|
+
function applyLayerOverrides(prefix, op, surface) {
|
|
2519
2940
|
const [h, s, l] = rColor();
|
|
2520
2941
|
const tokens = genTokens(h, s, l, rColorScheme()).tokens;
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2942
|
+
if (surface) {
|
|
2943
|
+
const surfaceColor = tokens["--dsw-alias-bg-layer-2"];
|
|
2944
|
+
if (surfaceColor !== void 0) document.documentElement.style.setProperty(`${prefix}-surface`, toRgba(surfaceColor, op));
|
|
2945
|
+
}
|
|
2946
|
+
for (const layer of [
|
|
2947
|
+
1,
|
|
2948
|
+
2,
|
|
2949
|
+
3
|
|
2950
|
+
]) {
|
|
2951
|
+
const c = tokens[`--dsw-alias-bg-layer-${layer}`];
|
|
2952
|
+
if (c !== void 0) document.documentElement.style.setProperty(`${prefix}-layer-${layer}`, toRgba(c, op));
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
function applySettingsOverrides(op) {
|
|
2956
|
+
applyLayerOverrides("--dsh-any-bg-settings", op, true);
|
|
2528
2957
|
}
|
|
2529
2958
|
const TRAJECTORY_STYLE_RULE = "[data-conversation-composer-overlay]{--dsw-alias-bg-layer-1:var(--dsh-any-traj-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-traj-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-traj-layer-3)}";
|
|
2530
2959
|
function applyTrajectoryOverrides(op) {
|
|
2531
|
-
|
|
2532
|
-
const tokens = genTokens(h, s, l, rColorScheme()).tokens;
|
|
2533
|
-
const layer1 = tokens["--dsw-alias-bg-layer-1"];
|
|
2534
|
-
const layer2 = tokens["--dsw-alias-bg-layer-2"];
|
|
2535
|
-
const layer3 = tokens["--dsw-alias-bg-layer-3"];
|
|
2536
|
-
if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-1", toRgba(layer1, op));
|
|
2537
|
-
if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-2", toRgba(layer2, op));
|
|
2538
|
-
if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-3", toRgba(layer3, op));
|
|
2960
|
+
applyLayerOverrides("--dsh-any-traj", op, false);
|
|
2539
2961
|
}
|
|
2540
2962
|
/** Register a callback fired when a wallpaper-extracted theme color is
|
|
2541
2963
|
* adopted by the auto-adaptation path, so the section can re-register the
|
|
@@ -2544,6 +2966,9 @@ window.__ModuleLoader__.load({
|
|
|
2544
2966
|
let colorAdoptedListener = null;
|
|
2545
2967
|
function onColorAdopted(cb) {
|
|
2546
2968
|
colorAdoptedListener = cb;
|
|
2969
|
+
return () => {
|
|
2970
|
+
if (colorAdoptedListener === cb) colorAdoptedListener = null;
|
|
2971
|
+
};
|
|
2547
2972
|
}
|
|
2548
2973
|
/** Apply the theme color: use the saved pick directly, or fall back to
|
|
2549
2974
|
* extracting a dominant color from the current wallpaper. */
|
|
@@ -2559,8 +2984,8 @@ window.__ModuleLoader__.load({
|
|
|
2559
2984
|
if (hsl && rWp() === url) {
|
|
2560
2985
|
cfg.color = hsl;
|
|
2561
2986
|
colorAdoptedListener?.(hsl);
|
|
2987
|
+
applyWp();
|
|
2562
2988
|
}
|
|
2563
|
-
applyWp();
|
|
2564
2989
|
});
|
|
2565
2990
|
} else applyWp();
|
|
2566
2991
|
}
|
|
@@ -2592,7 +3017,7 @@ window.__ModuleLoader__.load({
|
|
|
2592
3017
|
* preserving the user's scale/intensity/speed/density/preset choices. */
|
|
2593
3018
|
function regenerateGeneratedBg() {
|
|
2594
3019
|
const params = cfg.generatedBg;
|
|
2595
|
-
if (!params || cfg.backgroundType === "image") return;
|
|
3020
|
+
if (!params || cfg.backgroundType === "image" || cfg.backgroundType === "video") return;
|
|
2596
3021
|
cfg.generatedBg = {
|
|
2597
3022
|
...params,
|
|
2598
3023
|
seed: randomSeed()
|
|
@@ -2615,9 +3040,10 @@ window.__ModuleLoader__.load({
|
|
|
2615
3040
|
wpEl.appendChild(wpController.canvas);
|
|
2616
3041
|
}
|
|
2617
3042
|
requestAnimationFrame(() => {
|
|
2618
|
-
if (!wpController) return;
|
|
2619
3043
|
const controller = wpController;
|
|
3044
|
+
if (controller === null) return;
|
|
2620
3045
|
const frame = controller.snapshot();
|
|
3046
|
+
if (wpController !== controller) return;
|
|
2621
3047
|
setWpUrl(frame);
|
|
2622
3048
|
applyWp();
|
|
2623
3049
|
snapshotListener?.();
|
|
@@ -2644,35 +3070,36 @@ window.__ModuleLoader__.load({
|
|
|
2644
3070
|
partBlurStyleEl.textContent = PART_BLUR_RULE;
|
|
2645
3071
|
document.head.appendChild(partBlurStyleEl);
|
|
2646
3072
|
}
|
|
2647
|
-
/** Find one direct child of `frame` whose `[data-rightbar-col]` marker matches.
|
|
2648
|
-
* The rightbar is its own grid track, NOT a "details" twin of the center
|
|
2649
|
-
* column, so it must never receive the main-bg tint or blur underlay. */
|
|
2650
|
-
function findRightbarInFrame(frame) {
|
|
2651
|
-
for (let i = 0; i < frame.children.length; i++) {
|
|
2652
|
-
const child = frame.children[i];
|
|
2653
|
-
if (child instanceof HTMLElement && child.dataset.rightbarCol !== void 0) return child;
|
|
2654
|
-
}
|
|
2655
|
-
return null;
|
|
2656
|
-
}
|
|
2657
3073
|
function discoverParts() {
|
|
2658
3074
|
const overlay = document.querySelector("[data-shell-overlay]");
|
|
2659
3075
|
if (overlay === null) return;
|
|
2660
3076
|
const frame = overlay.parentElement;
|
|
2661
3077
|
if (frame === null) return;
|
|
2662
3078
|
frameEl = frame;
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
3079
|
+
let overlayIdx = -1;
|
|
3080
|
+
let rightIdx = -1;
|
|
3081
|
+
let right = null;
|
|
3082
|
+
const children = frame.children;
|
|
3083
|
+
for (let i = 0; i < children.length; i++) {
|
|
3084
|
+
const child = children[i];
|
|
3085
|
+
if (child === overlay) overlayIdx = i;
|
|
3086
|
+
else if (child instanceof HTMLElement && child.dataset.rightbarCol !== void 0) {
|
|
3087
|
+
right = child;
|
|
3088
|
+
rightIdx = i;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
rightEl = right;
|
|
3092
|
+
if (right !== null) {
|
|
3093
|
+
sidebarEl = children[rightIdx - 2] ?? null;
|
|
3094
|
+
centerEl = children[rightIdx - 1] ?? null;
|
|
2669
3095
|
} else {
|
|
2670
|
-
sidebarEl =
|
|
2671
|
-
centerEl =
|
|
3096
|
+
sidebarEl = children[overlayIdx - 3] ?? null;
|
|
3097
|
+
centerEl = children[overlayIdx - 2] ?? null;
|
|
2672
3098
|
}
|
|
2673
3099
|
}
|
|
2674
3100
|
function setBlur(el, px) {
|
|
2675
3101
|
if (el === null) return;
|
|
3102
|
+
if (px === 0 && !el.classList.contains(PART_BLUR_CLASS) && el.getAttribute("data-dab-pos-patched") !== "1") return;
|
|
2676
3103
|
const underlay = el.querySelector(`:scope > .${PART_UNDERLAY_CLASS}`);
|
|
2677
3104
|
if (px > 0) {
|
|
2678
3105
|
ensurePartBlurStyle();
|
|
@@ -2806,6 +3233,34 @@ window.__ModuleLoader__.load({
|
|
|
2806
3233
|
plain: true
|
|
2807
3234
|
}];
|
|
2808
3235
|
const viewTargets = VIEW_CARDS.map(() => null);
|
|
3236
|
+
/** View specs whose real host marker has been seen at least once. Until a
|
|
3237
|
+
* marker is seen, its absence is ambiguous — "that view is not mounted" or
|
|
3238
|
+
* "this build has no such marker, keep running the generic fallback" — and the
|
|
3239
|
+
* pass has to keep probing. Once seen, absence can only mean the former, so the
|
|
3240
|
+
* pass may be skipped.
|
|
3241
|
+
*
|
|
3242
|
+
* This matters because the two specs are two different MAIN VIEWS and only one
|
|
3243
|
+
* of them is mounted at a time (the host renders the main slot for the active
|
|
3244
|
+
* panel id, keyed by `entryKey` — see AppFrame's MainPanel). So "every surface
|
|
3245
|
+
* present" is never true during a normal session, and an all-or-nothing gate on
|
|
3246
|
+
* it silently never engages: that is exactly how the previous version of this
|
|
3247
|
+
* observer ended up running the full pass on every animation frame while
|
|
3248
|
+
* tokens streamed. */
|
|
3249
|
+
const markerSeen = /* @__PURE__ */ new Set();
|
|
3250
|
+
/** Resolve just the stable host markers for the view specs — one querySelector
|
|
3251
|
+
* each, no fallback heuristics. Cheap enough to run on every coalesced burst,
|
|
3252
|
+
* and enough to notice a view mounting/unmounting. */
|
|
3253
|
+
function probeViewMarkers() {
|
|
3254
|
+
const center = centerEl;
|
|
3255
|
+
if (center === null || !document.body.contains(center)) return VIEW_CARDS.map(() => null);
|
|
3256
|
+
return VIEW_CARDS.map((spec) => center.querySelector(spec.sel));
|
|
3257
|
+
}
|
|
3258
|
+
/** Whether the pass must run regardless of the value/identity signature. */
|
|
3259
|
+
function needsPartsPass(markers) {
|
|
3260
|
+
if (!columnsPresent()) return true;
|
|
3261
|
+
if (markers.some((el, i) => el !== null && el !== viewTargets[i])) return true;
|
|
3262
|
+
return VIEW_CARDS.some((spec, i) => spec.fallback === true && markers[i] === null && !markerSeen.has(i));
|
|
3263
|
+
}
|
|
2809
3264
|
function isScrollableY(el) {
|
|
2810
3265
|
const oy = getComputedStyle(el).overflowY;
|
|
2811
3266
|
return oy === "auto" || oy === "scroll" || oy === "overlay";
|
|
@@ -2850,6 +3305,7 @@ window.__ModuleLoader__.load({
|
|
|
2850
3305
|
const marked = centerEl.querySelector(spec.sel);
|
|
2851
3306
|
const cached = viewTargets[idx];
|
|
2852
3307
|
if (marked !== null) {
|
|
3308
|
+
markerSeen.add(idx);
|
|
2853
3309
|
if (cached !== null && cached !== marked) {
|
|
2854
3310
|
setBlur(cached, 0);
|
|
2855
3311
|
restoreCardHost(cached, spec.mark, spec.prev, spec.plain === true);
|
|
@@ -2888,6 +3344,7 @@ window.__ModuleLoader__.load({
|
|
|
2888
3344
|
const ds = el.dataset;
|
|
2889
3345
|
ds[prev + "Bg"] = el.style.getPropertyValue("background");
|
|
2890
3346
|
if (plain) return;
|
|
3347
|
+
ds[prev + "BoxSizing"] = el.style.getPropertyValue("box-sizing");
|
|
2891
3348
|
ds[prev + "Border"] = el.style.getPropertyValue("border");
|
|
2892
3349
|
ds[prev + "Radius"] = el.style.getPropertyValue("border-radius");
|
|
2893
3350
|
ds[prev + "Padding"] = el.style.getPropertyValue("padding");
|
|
@@ -2902,9 +3359,11 @@ window.__ModuleLoader__.load({
|
|
|
2902
3359
|
};
|
|
2903
3360
|
restore("background", ds[prev + "Bg"]);
|
|
2904
3361
|
if (!plain) {
|
|
3362
|
+
restore("box-sizing", ds[prev + "BoxSizing"]);
|
|
2905
3363
|
restore("border", ds[prev + "Border"]);
|
|
2906
3364
|
restore("border-radius", ds[prev + "Radius"]);
|
|
2907
3365
|
restore("padding", ds[prev + "Padding"]);
|
|
3366
|
+
delete ds[prev + "BoxSizing"];
|
|
2908
3367
|
delete ds[prev + "Border"];
|
|
2909
3368
|
delete ds[prev + "Radius"];
|
|
2910
3369
|
delete ds[prev + "Padding"];
|
|
@@ -2974,6 +3433,7 @@ window.__ModuleLoader__.load({
|
|
|
2974
3433
|
target.style.border = surface !== void 0 ? `1px solid ${toRgba(surface, borderAlpha)}` : "1px solid transparent";
|
|
2975
3434
|
target.style.borderRadius = "16px";
|
|
2976
3435
|
target.style.padding = "18px";
|
|
3436
|
+
target.style.boxSizing = "border-box";
|
|
2977
3437
|
}
|
|
2978
3438
|
target.setAttribute(spec.mark, "1");
|
|
2979
3439
|
setBlur(target, blurPx);
|
|
@@ -2981,15 +3441,48 @@ window.__ModuleLoader__.load({
|
|
|
2981
3441
|
syncTableFix();
|
|
2982
3442
|
}
|
|
2983
3443
|
let partsObserver = null;
|
|
3444
|
+
let partsApplyRaf = 0;
|
|
3445
|
+
/** Signature of the last blur+opacity pass we actually wrote to the DOM. */
|
|
3446
|
+
let appliedPartsKey = "";
|
|
3447
|
+
/** The surfaces the last pass styled, compared BY IDENTITY: the host can swap
|
|
3448
|
+
* a view element for a fresh one while the slider values stay identical, and
|
|
3449
|
+
* the value signature alone would then skip the pass and leave the new view
|
|
3450
|
+
* unstyled until a slider moves. */
|
|
3451
|
+
let appliedTargets = [];
|
|
3452
|
+
/** The four frame columns are mounted and still attached. These are the frame's
|
|
3453
|
+
* own children (see the host's AppFrame: sidebar / center / rightbar carrying
|
|
3454
|
+
* `data-rightbar-col` / the overlay), so they exist as soon as the shell does —
|
|
3455
|
+
* unlike the per-view cards below, which come and go with the active view. */
|
|
3456
|
+
function columnsPresent() {
|
|
3457
|
+
return frameEl !== null && sidebarEl !== null && centerEl !== null && rightEl !== null && document.body.contains(frameEl) && document.body.contains(centerEl);
|
|
3458
|
+
}
|
|
3459
|
+
/** Snapshot of every surface the pass would style, for the identity check. */
|
|
3460
|
+
function targetsNow() {
|
|
3461
|
+
return [
|
|
3462
|
+
frameEl,
|
|
3463
|
+
sidebarEl,
|
|
3464
|
+
centerEl,
|
|
3465
|
+
rightEl,
|
|
3466
|
+
...viewTargets
|
|
3467
|
+
];
|
|
3468
|
+
}
|
|
2984
3469
|
/** Watch for the AppFrame mounting so persisted blurs land even when the shell
|
|
2985
|
-
* renders after this plugin's apply.
|
|
2986
|
-
* callback returns. */
|
|
3470
|
+
* renders after this plugin's apply. */
|
|
2987
3471
|
function watchParts() {
|
|
2988
3472
|
if (partsObserver !== null || typeof MutationObserver === "undefined") return;
|
|
2989
3473
|
partsObserver = new MutationObserver(() => {
|
|
2990
|
-
if (
|
|
2991
|
-
|
|
2992
|
-
|
|
3474
|
+
if (partsApplyRaf !== 0) return;
|
|
3475
|
+
partsApplyRaf = requestAnimationFrame(() => {
|
|
3476
|
+
partsApplyRaf = 0;
|
|
3477
|
+
const blurs = rBlurs();
|
|
3478
|
+
const ops = rOps();
|
|
3479
|
+
const key = JSON.stringify([blurs, ops]);
|
|
3480
|
+
if (!needsPartsPass(probeViewMarkers()) && key === appliedPartsKey && !targetsNow().some((el, i) => el !== appliedTargets[i])) return;
|
|
3481
|
+
applyPartBlurs(blurs);
|
|
3482
|
+
applyPartOpacities(ops);
|
|
3483
|
+
appliedPartsKey = key;
|
|
3484
|
+
appliedTargets = targetsNow();
|
|
3485
|
+
});
|
|
2993
3486
|
});
|
|
2994
3487
|
partsObserver.observe(document.body, {
|
|
2995
3488
|
childList: true,
|
|
@@ -2999,6 +3492,13 @@ window.__ModuleLoader__.load({
|
|
|
2999
3492
|
function stopWatchingParts() {
|
|
3000
3493
|
partsObserver?.disconnect();
|
|
3001
3494
|
partsObserver = null;
|
|
3495
|
+
if (partsApplyRaf !== 0) {
|
|
3496
|
+
cancelAnimationFrame(partsApplyRaf);
|
|
3497
|
+
partsApplyRaf = 0;
|
|
3498
|
+
}
|
|
3499
|
+
appliedPartsKey = "";
|
|
3500
|
+
appliedTargets = [];
|
|
3501
|
+
markerSeen.clear();
|
|
3002
3502
|
}
|
|
3003
3503
|
let themeObserver = null;
|
|
3004
3504
|
let themeRaf = 0;
|
|
@@ -3151,6 +3651,9 @@ window.__ModuleLoader__.load({
|
|
|
3151
3651
|
* be re-registered through the host theme service. */
|
|
3152
3652
|
function onVerdictApplied(cb) {
|
|
3153
3653
|
verdictListener = cb;
|
|
3654
|
+
return () => {
|
|
3655
|
+
if (verdictListener === cb) verdictListener = null;
|
|
3656
|
+
};
|
|
3154
3657
|
}
|
|
3155
3658
|
function applyVerdict(dark) {
|
|
3156
3659
|
if (rBgDark() === dark) return;
|
|
@@ -3278,6 +3781,8 @@ window.__ModuleLoader__.load({
|
|
|
3278
3781
|
clearVideoEl();
|
|
3279
3782
|
wpEl?.remove();
|
|
3280
3783
|
wpEl = null;
|
|
3784
|
+
wpVerdict = null;
|
|
3785
|
+
setBgDark(null);
|
|
3281
3786
|
}
|
|
3282
3787
|
} else if (cfg.backgroundType !== "image" && cfg.generatedBg) {
|
|
3283
3788
|
clearVideoEl();
|
|
@@ -3297,14 +3802,14 @@ window.__ModuleLoader__.load({
|
|
|
3297
3802
|
wpVerdict = null;
|
|
3298
3803
|
setBgDark(null);
|
|
3299
3804
|
}
|
|
3300
|
-
|
|
3301
|
-
else if (!url) clearThemeTokens();
|
|
3805
|
+
applyCustomTokens(rOps());
|
|
3302
3806
|
if (rHasColor()) {
|
|
3303
3807
|
applySettingsOverrides(rSop());
|
|
3304
3808
|
applyTrajectoryOverrides(rTrajectoryOpacity());
|
|
3305
3809
|
}
|
|
3306
3810
|
applyPanelOverrides(rPanelOpacity());
|
|
3307
3811
|
applyPartBlurs(rBlurs());
|
|
3812
|
+
applyStrokes();
|
|
3308
3813
|
}
|
|
3309
3814
|
function teardownWp() {
|
|
3310
3815
|
clearDynamicBg();
|
|
@@ -3349,6 +3854,14 @@ window.__ModuleLoader__.load({
|
|
|
3349
3854
|
pendingOps = null;
|
|
3350
3855
|
tableFixStyleEl?.remove();
|
|
3351
3856
|
tableFixStyleEl = null;
|
|
3857
|
+
partBlurStyleEl?.remove();
|
|
3858
|
+
partBlurStyleEl = null;
|
|
3859
|
+
removeStrokes();
|
|
3860
|
+
removeFontFace();
|
|
3861
|
+
imgNat = null;
|
|
3862
|
+
lowResUrl = null;
|
|
3863
|
+
lowResFor = "";
|
|
3864
|
+
dragLow = false;
|
|
3352
3865
|
setBlur(frameEl, 0);
|
|
3353
3866
|
setBlur(sidebarEl, 0);
|
|
3354
3867
|
setBlur(centerEl, 0);
|
|
@@ -3484,7 +3997,10 @@ window.__ModuleLoader__.load({
|
|
|
3484
3997
|
/* ── wheel card ──────────────────────────────────────────────────────────── */
|
|
3485
3998
|
.dab-wheel-card{position:relative;display:flex;align-items:center;justify-content:center;gap:28px;flex-wrap:wrap;padding:24px 18px}
|
|
3486
3999
|
.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}
|
|
3487
|
-
|
|
4000
|
+
/* flex:none is load bearing, not cosmetic: the pointer maths reads client
|
|
4001
|
+
* coordinates straight off this element's box, so any shrink below the 220px
|
|
4002
|
+
* drawing surface would desynchronise the ring from the cursor. */
|
|
4003
|
+
.dab-wheel{position:relative;flex:none;cursor:crosshair;border-radius:50%;box-shadow:0 12px 32px -14px rgba(0,0,0,.4)}
|
|
3488
4004
|
.dab-hint{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);padding:0 4px}
|
|
3489
4005
|
|
|
3490
4006
|
/* ── precise color inputs ────────────────────────────────────────────────── */
|
|
@@ -3538,7 +4054,7 @@ window.__ModuleLoader__.load({
|
|
|
3538
4054
|
.dab-slider::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:var(--dsw-alias-button-elevated-fill,#fff);border:2px solid var(--dsw-alias-brand-primary)}
|
|
3539
4055
|
|
|
3540
4056
|
/* ── interface part cards ────────────────────────────────────────────────── */
|
|
3541
|
-
.dab-grid-parts{display:grid;grid-template-columns:repeat(auto-fill,minmax(
|
|
4057
|
+
.dab-grid-parts{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(240px,100%),1fr));gap:13px}
|
|
3542
4058
|
.dab-part-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}
|
|
3543
4059
|
.dab-part-ico{width:32px;height:32px;flex:none;border-radius:10px;display:grid;place-items:center;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)}
|
|
3544
4060
|
.dab-part-name{font-size:13.5px;font-weight:600}
|
|
@@ -3556,6 +4072,29 @@ window.__ModuleLoader__.load({
|
|
|
3556
4072
|
.dab-toggle.is-on{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
|
|
3557
4073
|
.dab-toggle.is-on .dab-toggle-knob{transform:translateX(18px);background:#fff}
|
|
3558
4074
|
|
|
4075
|
+
/* ── custom font card ────────────────────────────────────────────────────── */
|
|
4076
|
+
.dab-font-title{display:flex;align-items:center;gap:11px}
|
|
4077
|
+
.dab-font-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:14px}
|
|
4078
|
+
.dab-font-name{font-family:var(--dab-mono);font-size:13.5px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));background:var(--dsw-alias-bg-layer-2);border-radius:99px;padding:5px 11px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4079
|
+
.dab-font-name.is-empty{color:var(--dsw-alias-label-tertiary)}
|
|
4080
|
+
.dab-font-actions{display:flex;align-items:center;gap:8px;margin-left:auto}
|
|
4081
|
+
/* The font card's labels read one step larger than the rest of the panel —
|
|
4082
|
+
they carry most of the explanation on this page. Scoped to the card so the
|
|
4083
|
+
same .dab-hint / .dab-btn elsewhere keep their size. */
|
|
4084
|
+
.dab-font-actions .dab-btn{font-size:14.5px}
|
|
4085
|
+
.dab-font-card .dab-hint{font-size:13.5px}
|
|
4086
|
+
|
|
4087
|
+
/* ── text-stroke color dots ──────────────────────────────────────────────── */
|
|
4088
|
+
.dab-stroke-dots{display:flex;align-items:center;gap:9px;margin-top:14px}
|
|
4089
|
+
.dab-stroke-dot-wrap{position:relative;width:18px;height:18px;flex:none}
|
|
4090
|
+
.dab-stroke-dot{width:18px;height:18px;padding:0;border-radius:50%;border:1.5px solid var(--dsw-alias-border-l2);background-clip:padding-box;cursor:pointer;transition:transform .18s cubic-bezier(.34,1.56,.64,1),box-shadow .18s ease}
|
|
4091
|
+
.dab-stroke-dot:hover{transform:scale(1.16)}
|
|
4092
|
+
/* 'auto' has no fixed value — half light / half dark reads as "derived". */
|
|
4093
|
+
.dab-stroke-dot-auto{background:conic-gradient(from -90deg,#fff 0 50%,#000 50% 100%)}
|
|
4094
|
+
.dab-stroke-dot.is-active{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1),0 0 0 3.5px var(--dsw-alias-brand-primary);border-color:transparent}
|
|
4095
|
+
/* The native color picker sits invisible on top of the custom swatch. */
|
|
4096
|
+
.dab-stroke-dot-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;border-radius:50%;background:none;opacity:0;cursor:pointer}
|
|
4097
|
+
|
|
3559
4098
|
/* ── background preview hero ─────────────────────────────────────────────── */
|
|
3560
4099
|
.dab-hero{position:relative;border-radius:16px;overflow:hidden;aspect-ratio:16/9;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}
|
|
3561
4100
|
.dab-hero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:transform .5s cubic-bezier(.22,1,.36,1)}
|
|
@@ -3592,7 +4131,7 @@ window.__ModuleLoader__.load({
|
|
|
3592
4131
|
.dab-spin{animation:dab-rotate .7s cubic-bezier(.3,.7,.3,1) 1}
|
|
3593
4132
|
|
|
3594
4133
|
/* ── profile page ────────────────────────────────────────────────────────── */
|
|
3595
|
-
.dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(
|
|
4134
|
+
.dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:13px}
|
|
3596
4135
|
.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)}
|
|
3597
4136
|
.dab-profile-title{font-size:14px;font-weight:650}
|
|
3598
4137
|
.dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
|
|
@@ -3601,7 +4140,7 @@ window.__ModuleLoader__.load({
|
|
|
3601
4140
|
|
|
3602
4141
|
/* ── presets / profiles / schedule / rotation ────────────────────────────── */
|
|
3603
4142
|
.dab-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
3604
|
-
.dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
|
|
4143
|
+
.dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(150px,100%),1fr));gap:10px}
|
|
3605
4144
|
.dab-preset{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:12px;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:13px;cursor:pointer;font:inherit;text-align:left;transition:border-color .25s,transform .25s,box-shadow .25s;animation:dab-row-in .34s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--i,0) * 45ms)}
|
|
3606
4145
|
.dab-preset:hover{transform:translateY(-2px);border-color:var(--dsw-alias-brand-primary);box-shadow:0 8px 22px -12px rgba(0,0,0,.3)}
|
|
3607
4146
|
.dab-preset-dot{width:22px;height:22px;border-radius:50%;margin-bottom:4px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.12),0 4px 10px -4px rgba(0,0,0,.35);background:var(--dsw-alias-bg-layer-2);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary)}
|
|
@@ -3628,7 +4167,7 @@ window.__ModuleLoader__.load({
|
|
|
3628
4167
|
.dab-schedule-wrap.is-open{grid-template-rows:1fr;visibility:visible;transition:grid-template-rows .42s cubic-bezier(.22,1,.36,1)}
|
|
3629
4168
|
.dab-schedule-clip{overflow:hidden;min-height:0;opacity:0;transition:opacity .28s ease}
|
|
3630
4169
|
.dab-schedule-wrap.is-open .dab-schedule-clip{opacity:1;transition:opacity .32s ease .1s}
|
|
3631
|
-
.dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;padding-top:12px}
|
|
4170
|
+
.dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:16px;padding-top:12px}
|
|
3632
4171
|
.dab-schedule-cell{min-width:0;animation:dab-row-in .34s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--i,0) * 60ms)}
|
|
3633
4172
|
.dab-time-row{display:flex;flex-direction:column;gap:9px}
|
|
3634
4173
|
.dab-time-label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
@@ -3664,6 +4203,9 @@ window.__ModuleLoader__.load({
|
|
|
3664
4203
|
.dab-crash{display:flex;flex-direction:column;gap:10px;align-items:center;padding:28px 18px;border:1px solid var(--dsw-alias-border-l2);border-radius:16px}
|
|
3665
4204
|
.dab-crash-title{font-size:15px;font-weight:650}
|
|
3666
4205
|
.dab-crash-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.5}
|
|
4206
|
+
/* The crash cause: readable, scrollable, and clipped to a few lines so a long
|
|
4207
|
+
* stack cannot push the reset button off the panel. */
|
|
4208
|
+
.dab-crash-detail{margin:0;max-width:min(520px,80vw);max-height:132px;overflow:auto;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);font-family:var(--dab-mono);font-size:10.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));text-align:left;white-space:pre-wrap;word-break:break-word}
|
|
3667
4209
|
|
|
3668
4210
|
/* ── keyframes ───────────────────────────────────────────────────────────── */
|
|
3669
4211
|
@keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
|
|
@@ -3679,6 +4221,72 @@ window.__ModuleLoader__.load({
|
|
|
3679
4221
|
@keyframes dab-zoom-in{from{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:none}}
|
|
3680
4222
|
@keyframes dab-flow{to{background-position:300% 50%}}
|
|
3681
4223
|
|
|
4224
|
+
/* ── sidebar surface (a dsh-better-sidebar page) ─────────────────────────────
|
|
4225
|
+
* Same shell, hosted in a panel that is handed a full-height column (the host's
|
|
4226
|
+
* native tab body is height:100%) and whose width the user drags freely. So only
|
|
4227
|
+
* THREE things are fixed here, all of them structural:
|
|
4228
|
+
* 1. the shell fills the height and the page body owns the scroll
|
|
4229
|
+
* (flex:1 + min-height:0), so the panel stays one scrollable column instead
|
|
4230
|
+
* of pushing the whole tab into a scrollbar;
|
|
4231
|
+
* 2. the nav is a rail ABOVE the page rather than beside it — a side rail would
|
|
4232
|
+
* spend 150px of a panel that is narrow by nature, and this way the page
|
|
4233
|
+
* keeps the full width for its own grids;
|
|
4234
|
+
* 3. long tokens (paths, hex, URLs) may wrap instead of widening the panel.
|
|
4235
|
+
*
|
|
4236
|
+
* Everything else — grid columns, paddings, type scale, whether the cards lift on
|
|
4237
|
+
* hover — is width-driven through the container queries below, which measure this
|
|
4238
|
+
* shell (the .dab-root container, which declares container-type:inline-size) and
|
|
4239
|
+
* not the viewport. A narrow sidebar compacts; a wide one falls back to the
|
|
4240
|
+
* dialog's own spacing and multi-column grids, because at that point it *is* a
|
|
4241
|
+
* dialog-sized surface. */
|
|
4242
|
+
[data-dab-surface="sidebar"].dab-root{height:100%;min-height:0;padding:12px 12px 0}
|
|
4243
|
+
[data-dab-surface="sidebar"] .dab-shell{display:flex;flex-direction:column;align-items:stretch;gap:11px;height:100%;min-height:0;max-width:none;padding-bottom:0}
|
|
4244
|
+
[data-dab-surface="sidebar"] .dab-nav{gap:0}
|
|
4245
|
+
[data-dab-surface="sidebar"] .dab-nav-list{flex-direction:row;gap:2px;overflow-x:auto;overflow-y:hidden;padding-bottom:1px}
|
|
4246
|
+
[data-dab-surface="sidebar"] .dab-nav-list::-webkit-scrollbar{display:none}
|
|
4247
|
+
[data-dab-surface="sidebar"] .dab-nav-ind{display:none}
|
|
4248
|
+
[data-dab-surface="sidebar"] .dab-nav-item{flex:none;height:32px;gap:7px;padding:0 10px;font-size:12px}
|
|
4249
|
+
[data-dab-surface="sidebar"] .dab-nav-item.is-active{background:var(--dsw-alias-bg-layer-2)}
|
|
4250
|
+
[data-dab-surface="sidebar"] .dab-page{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding-bottom:16px;scrollbar-width:thin}
|
|
4251
|
+
/* Last-resort safety for text that cannot wrap (paths, hex, URLs). */
|
|
4252
|
+
[data-dab-surface="sidebar"] .dab-hint,[data-dab-surface="sidebar"] .dab-part-name{overflow-wrap:anywhere}
|
|
4253
|
+
/* The panel's width is whatever the user dragged it to, so let the part cards
|
|
4254
|
+
* fill as many columns as actually fit instead of hard-coding one. 170px is the
|
|
4255
|
+
* narrowest a part card stays readable once its header can wrap (see the head
|
|
4256
|
+
* rules below), which puts the second column at roughly a 380px panel — the
|
|
4257
|
+
* default right-sidebar width. */
|
|
4258
|
+
[data-dab-surface="sidebar"] .dab-grid-parts{grid-template-columns:repeat(auto-fill,minmax(min(170px,100%),1fr));gap:12px}
|
|
4259
|
+
[data-dab-surface="sidebar"] .dab-types{grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr))}
|
|
4260
|
+
[data-dab-surface="sidebar"] .dab-card{padding:12px}
|
|
4261
|
+
[data-dab-surface="sidebar"] .dab-part-head{gap:8px;margin-bottom:10px}
|
|
4262
|
+
[data-dab-surface="sidebar"] .dab-part-ico{width:26px;height:26px;border-radius:8px}
|
|
4263
|
+
[data-dab-surface="sidebar"] .dab-part-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px}
|
|
4264
|
+
[data-dab-surface="sidebar"] .dab-part-badge{padding:2px 7px;font-size:10px}
|
|
4265
|
+
|
|
4266
|
+
/* Narrow panel: tighten the chrome so the content still has room. */
|
|
4267
|
+
@container (max-width:539px){
|
|
4268
|
+
[data-dab-surface="sidebar"] .dab-page{gap:11px}
|
|
4269
|
+
[data-dab-surface="sidebar"] .dab-h1{font-size:17px}
|
|
4270
|
+
[data-dab-surface="sidebar"] .dab-desc{font-size:12px}
|
|
4271
|
+
[data-dab-surface="sidebar"] .dab-orb-wrap{width:96px;height:96px}
|
|
4272
|
+
[data-dab-surface="sidebar"] .dab-wheel-card{padding:14px 10px;gap:16px}
|
|
4273
|
+
[data-dab-surface="sidebar"] .dab-wheel-glow{width:170px;height:170px}
|
|
4274
|
+
/* Lift-on-hover is a dialog affordance; in a cramped scrolling panel it only
|
|
4275
|
+
* adds jitter under the cursor. */
|
|
4276
|
+
[data-dab-surface="sidebar"] .dab-card-hover:hover{transform:none;box-shadow:none}
|
|
4277
|
+
}
|
|
4278
|
+
/* Roomy panel: behave like the dialog again — its spacing, its type scale, and
|
|
4279
|
+
* multi-column grids (a smaller track minimum than the dialog, because the page
|
|
4280
|
+
* here is a few hundred px narrower than a dialog's content column). */
|
|
4281
|
+
@container (min-width:540px){
|
|
4282
|
+
[data-dab-surface="sidebar"] .dab-shell{gap:16px}
|
|
4283
|
+
[data-dab-surface="sidebar"] .dab-page{gap:13px}
|
|
4284
|
+
[data-dab-surface="sidebar"] .dab-nav-item{height:34px;font-size:12.5px;padding:0 12px}
|
|
4285
|
+
/* The shared ≤620px rule flattens the type cards to one column, which is right
|
|
4286
|
+
* for a narrow phone-width dialog but wasteful in a roomy panel. */
|
|
4287
|
+
[data-dab-surface="sidebar"] .dab-types{grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
|
|
4288
|
+
}
|
|
4289
|
+
|
|
3682
4290
|
/* ── responsive & motion preferences ─────────────────────────────────────── */
|
|
3683
4291
|
@container (max-width:620px){
|
|
3684
4292
|
.dab-shell{grid-template-columns:1fr;gap:14px}
|
|
@@ -3693,6 +4301,30 @@ window.__ModuleLoader__.load({
|
|
|
3693
4301
|
@container (min-width:621px){
|
|
3694
4302
|
.dab-shell{grid-template-columns:158px minmax(0,1fr);gap:26px}
|
|
3695
4303
|
}
|
|
4304
|
+
/* Phone: on a handset the settings dialog's content column shrinks to roughly
|
|
4305
|
+
* the viewport, and the dialog-sized chrome (18px cards, 21px headings, a 118px
|
|
4306
|
+
* orb, 24px paddings) stops fitting — cards end up wider than the column or the
|
|
4307
|
+
* color row wraps into three cramped pieces. These are the same tightenings the
|
|
4308
|
+
* sidebar surface applies at its own narrow widths, so a phone settings dialog
|
|
4309
|
+
* and a narrow sidebar panel end up looking the same. */
|
|
4310
|
+
@container (max-width:480px){
|
|
4311
|
+
.dab-shell{gap:10px}
|
|
4312
|
+
.dab-page{gap:10px}
|
|
4313
|
+
.dab-head{margin:0 0 2px}
|
|
4314
|
+
.dab-h1{font-size:18px}
|
|
4315
|
+
.dab-desc{font-size:12px}
|
|
4316
|
+
.dab-card{padding:14px;border-radius:14px}
|
|
4317
|
+
.dab-hero-accent{gap:16px}
|
|
4318
|
+
.dab-orb-wrap{width:92px;height:92px}
|
|
4319
|
+
.dab-wheel-card{padding:14px 10px;gap:16px}
|
|
4320
|
+
.dab-wheel-glow{width:180px;height:180px}
|
|
4321
|
+
/* Three fixed type-card columns need ~400px; two adaptive ones fit 320px. */
|
|
4322
|
+
.dab-types{grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr))}
|
|
4323
|
+
.dab-grid-parts{gap:11px}
|
|
4324
|
+
.dab-part-head{margin-bottom:10px}
|
|
4325
|
+
.dab-slider-block + .dab-slider-block{margin-top:10px}
|
|
4326
|
+
.dab-footer{padding-top:10px}
|
|
4327
|
+
}
|
|
3696
4328
|
@media (prefers-reduced-motion:reduce){
|
|
3697
4329
|
.dab-root *,.dab-root *::before,.dab-root *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}
|
|
3698
4330
|
}
|
|
@@ -3985,6 +4617,12 @@ window.__ModuleLoader__.load({
|
|
|
3985
4617
|
className,
|
|
3986
4618
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
|
|
3987
4619
|
});
|
|
4620
|
+
/** Letterform glyph: a stroked capital A (the font/type page + nav entry). */
|
|
4621
|
+
const FontIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
|
|
4622
|
+
size,
|
|
4623
|
+
className,
|
|
4624
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 12.2 8 3.8l4 8.4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.6 9.6h4.8" })]
|
|
4625
|
+
});
|
|
3988
4626
|
const PlayIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
|
|
3989
4627
|
size,
|
|
3990
4628
|
className,
|
|
@@ -4017,25 +4655,32 @@ window.__ModuleLoader__.load({
|
|
|
4017
4655
|
this.props.onReset?.();
|
|
4018
4656
|
};
|
|
4019
4657
|
render() {
|
|
4020
|
-
if (this.state.error)
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4658
|
+
if (this.state.error) {
|
|
4659
|
+
const detail = this.state.error.stack ?? `${this.state.error.name}: ${this.state.error.message}`;
|
|
4660
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4661
|
+
className: "dab-crash",
|
|
4662
|
+
children: [
|
|
4663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4664
|
+
className: "dab-crash-title",
|
|
4665
|
+
children: this.props.t("crashTitle")
|
|
4666
|
+
}),
|
|
4667
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4668
|
+
className: "dab-crash-desc",
|
|
4669
|
+
children: this.props.t("crashDesc")
|
|
4670
|
+
}),
|
|
4671
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
4672
|
+
className: "dab-crash-detail",
|
|
4673
|
+
children: detail
|
|
4674
|
+
}),
|
|
4675
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4676
|
+
type: "button",
|
|
4677
|
+
className: "dab-btn",
|
|
4678
|
+
onClick: this.reset,
|
|
4679
|
+
children: this.props.t("crashReset")
|
|
4680
|
+
})
|
|
4681
|
+
]
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4039
4684
|
return this.props.children;
|
|
4040
4685
|
}
|
|
4041
4686
|
};
|
|
@@ -4184,6 +4829,10 @@ window.__ModuleLoader__.load({
|
|
|
4184
4829
|
}, [col]);
|
|
4185
4830
|
const pendingRef = (0, react.useRef)(null);
|
|
4186
4831
|
const rafRef = (0, react.useRef)(null);
|
|
4832
|
+
/** Unmount-time cleanup for a drag that never saw its mouseup (panel closed
|
|
4833
|
+
* mid-drag): without it the document listeners stay attached forever and the
|
|
4834
|
+
* pending rAF keeps calling setState on a dead component. */
|
|
4835
|
+
const dragCleanupRef = (0, react.useRef)(null);
|
|
4187
4836
|
const flushPending = (0, react.useCallback)(() => {
|
|
4188
4837
|
rafRef.current = null;
|
|
4189
4838
|
const p = pendingRef.current;
|
|
@@ -4225,13 +4874,25 @@ window.__ModuleLoader__.load({
|
|
|
4225
4874
|
schedule(colRef.current.hue, s, l);
|
|
4226
4875
|
}
|
|
4227
4876
|
};
|
|
4228
|
-
const
|
|
4877
|
+
const stopDrag = () => {
|
|
4229
4878
|
document.removeEventListener("mousemove", onMove);
|
|
4230
|
-
document.removeEventListener("mouseup",
|
|
4879
|
+
document.removeEventListener("mouseup", stopDrag);
|
|
4880
|
+
dragCleanupRef.current = null;
|
|
4231
4881
|
};
|
|
4882
|
+
dragCleanupRef.current = stopDrag;
|
|
4232
4883
|
document.addEventListener("mousemove", onMove);
|
|
4233
|
-
document.addEventListener("mouseup",
|
|
4884
|
+
document.addEventListener("mouseup", stopDrag);
|
|
4234
4885
|
}, [schedule]);
|
|
4886
|
+
(0, react.useEffect)(() => {
|
|
4887
|
+
return () => {
|
|
4888
|
+
if (rafRef.current !== null) {
|
|
4889
|
+
cancelAnimationFrame(rafRef.current);
|
|
4890
|
+
rafRef.current = null;
|
|
4891
|
+
}
|
|
4892
|
+
pendingRef.current = null;
|
|
4893
|
+
dragCleanupRef.current?.();
|
|
4894
|
+
};
|
|
4895
|
+
}, []);
|
|
4235
4896
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
4236
4897
|
ref: cvsRef,
|
|
4237
4898
|
width: WHEEL_SIZE,
|
|
@@ -4280,6 +4941,7 @@ window.__ModuleLoader__.load({
|
|
|
4280
4941
|
},
|
|
4281
4942
|
onChange: (e) => {
|
|
4282
4943
|
setText(e.target.value);
|
|
4944
|
+
if (e.target.value === "") return;
|
|
4283
4945
|
const v = Number(e.target.value);
|
|
4284
4946
|
if (Number.isFinite(v)) onChange(clamp(v, min, max));
|
|
4285
4947
|
}
|
|
@@ -4396,9 +5058,6 @@ window.__ModuleLoader__.load({
|
|
|
4396
5058
|
//#region src/client/components/ColorPicker.tsx
|
|
4397
5059
|
const MAG_SIZE = 96;
|
|
4398
5060
|
const MAG_ZOOM = 8;
|
|
4399
|
-
function toHex$1(rgb) {
|
|
4400
|
-
return "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
4401
|
-
}
|
|
4402
5061
|
/**
|
|
4403
5062
|
* Eyedropper modal: shows the wallpaper full-bleed (no drag/zoom) and lets the
|
|
4404
5063
|
* user click any pixel to adopt it as the theme color. A magnifier circle
|
|
@@ -4428,7 +5087,7 @@ window.__ModuleLoader__.load({
|
|
|
4428
5087
|
canvas.height = imgRef.current.naturalHeight;
|
|
4429
5088
|
const ctx = canvas.getContext("2d");
|
|
4430
5089
|
if (ctx) ctx.drawImage(imgRef.current, 0, 0);
|
|
4431
|
-
}, [ready]);
|
|
5090
|
+
}, [ready, url]);
|
|
4432
5091
|
const sampleAt = (0, react.useCallback)((clientX, clientY) => {
|
|
4433
5092
|
const canvas = canvasRef.current;
|
|
4434
5093
|
if (!canvas) return null;
|
|
@@ -4543,14 +5202,14 @@ window.__ModuleLoader__.load({
|
|
|
4543
5202
|
height: 28,
|
|
4544
5203
|
borderRadius: 8,
|
|
4545
5204
|
border: "1px solid rgba(255,255,255,0.4)",
|
|
4546
|
-
background: hover ?
|
|
5205
|
+
background: hover ? rgbToHex(hover.rgb) : "transparent"
|
|
4547
5206
|
} }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4548
5207
|
style: {
|
|
4549
5208
|
color: "#fff",
|
|
4550
5209
|
fontSize: 13,
|
|
4551
5210
|
fontFamily: "var(--dab-mono, monospace)"
|
|
4552
5211
|
},
|
|
4553
|
-
children: hover ? `${
|
|
5212
|
+
children: hover ? `${rgbToHex(hover.rgb)} · rgb(${hover.rgb.join(", ")})` : "—"
|
|
4554
5213
|
})]
|
|
4555
5214
|
}),
|
|
4556
5215
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -4633,9 +5292,6 @@ window.__ModuleLoader__.load({
|
|
|
4633
5292
|
.58
|
|
4634
5293
|
]
|
|
4635
5294
|
];
|
|
4636
|
-
function toHex(rgb) {
|
|
4637
|
-
return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
|
|
4638
|
-
}
|
|
4639
5295
|
function ColorPage({ p, notify }) {
|
|
4640
5296
|
const { t, hue, sat, lit, setColor, extractColor, setSchemeOverride, useStore } = p;
|
|
4641
5297
|
const storeUrl = useStore((s) => s.url);
|
|
@@ -4650,7 +5306,7 @@ window.__ModuleLoader__.load({
|
|
|
4650
5306
|
];
|
|
4651
5307
|
const [h, s, l] = hsvToHsl(wheel[0], wheel[1], wheel[2]);
|
|
4652
5308
|
const [r, g, b] = hslToRgb(h, s, l);
|
|
4653
|
-
const hex =
|
|
5309
|
+
const hex = rgbToHex([
|
|
4654
5310
|
r,
|
|
4655
5311
|
g,
|
|
4656
5312
|
b
|
|
@@ -4780,7 +5436,7 @@ window.__ModuleLoader__.load({
|
|
|
4780
5436
|
type: "button",
|
|
4781
5437
|
className: `dab-swatch${on ? " is-on" : ""}`,
|
|
4782
5438
|
style: { background: `hsl(${sh} ${Math.round(ss * 100)}% ${Math.round(sl * 100)}%)` },
|
|
4783
|
-
title:
|
|
5439
|
+
title: rgbToHex(hslToRgb(sh, ss, sl)).toUpperCase(),
|
|
4784
5440
|
onClick: () => setColor(...hslToHsv(sh, ss, sl))
|
|
4785
5441
|
}, i);
|
|
4786
5442
|
})
|
|
@@ -4855,15 +5511,16 @@ window.__ModuleLoader__.load({
|
|
|
4855
5511
|
const paint = (el, v) => {
|
|
4856
5512
|
el.style.setProperty("--pct", String((v - min) / (max - min) * 100));
|
|
4857
5513
|
};
|
|
5514
|
+
const fmtRef = (0, react.useRef)(fmt);
|
|
5515
|
+
fmtRef.current = fmt;
|
|
4858
5516
|
(0, react.useEffect)(() => {
|
|
4859
5517
|
if (inputRef.current) {
|
|
4860
5518
|
inputRef.current.value = String(def);
|
|
4861
5519
|
paint(inputRef.current, def);
|
|
4862
5520
|
}
|
|
4863
|
-
if (valRef.current) valRef.current.textContent =
|
|
5521
|
+
if (valRef.current) valRef.current.textContent = fmtRef.current(def);
|
|
4864
5522
|
}, [
|
|
4865
5523
|
def,
|
|
4866
|
-
fmt,
|
|
4867
5524
|
min,
|
|
4868
5525
|
max
|
|
4869
5526
|
]);
|
|
@@ -4975,7 +5632,7 @@ window.__ModuleLoader__.load({
|
|
|
4975
5632
|
}
|
|
4976
5633
|
//#endregion
|
|
4977
5634
|
//#region src/client/components/pages/InterfacePage.tsx
|
|
4978
|
-
const PARTS = [
|
|
5635
|
+
const PARTS$1 = [
|
|
4979
5636
|
{
|
|
4980
5637
|
opKey: "bg",
|
|
4981
5638
|
labelKey: "uiOpacityBg",
|
|
@@ -5023,9 +5680,10 @@ window.__ModuleLoader__.load({
|
|
|
5023
5680
|
}
|
|
5024
5681
|
];
|
|
5025
5682
|
function InterfacePage({ p }) {
|
|
5026
|
-
const { t, setOps, setBlurs, setSop, setPanelOp } = p;
|
|
5683
|
+
const { t, setOps, setBlurs, setSop, setPanelOp, useStore } = p;
|
|
5684
|
+
useStore((s) => s.metaRev);
|
|
5027
5685
|
const hasBetterSidebar = useBetterSidebar();
|
|
5028
|
-
const parts = PARTS.filter((part) => !(part.isPanel && !hasBetterSidebar));
|
|
5686
|
+
const parts = PARTS$1.filter((part) => !(part.isPanel && !hasBetterSidebar));
|
|
5029
5687
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
5030
5688
|
className: "dab-head dab-rise",
|
|
5031
5689
|
style: { "--d": 0 },
|
|
@@ -5152,10 +5810,336 @@ window.__ModuleLoader__.load({
|
|
|
5152
5810
|
})] });
|
|
5153
5811
|
}
|
|
5154
5812
|
//#endregion
|
|
5813
|
+
//#region src/client/components/pages/FontPage.tsx
|
|
5814
|
+
const PARTS = [
|
|
5815
|
+
{
|
|
5816
|
+
key: "bg",
|
|
5817
|
+
labelKey: "uiOpacityBg",
|
|
5818
|
+
Icon: CanvasIcon
|
|
5819
|
+
},
|
|
5820
|
+
{
|
|
5821
|
+
key: "sidebar",
|
|
5822
|
+
labelKey: "uiOpacitySide",
|
|
5823
|
+
Icon: SidebarIcon
|
|
5824
|
+
},
|
|
5825
|
+
{
|
|
5826
|
+
key: "card",
|
|
5827
|
+
labelKey: "uiOpacityCard",
|
|
5828
|
+
Icon: ChatIcon
|
|
5829
|
+
},
|
|
5830
|
+
{
|
|
5831
|
+
key: "input",
|
|
5832
|
+
labelKey: "uiOpacityInput",
|
|
5833
|
+
Icon: InputIcon
|
|
5834
|
+
},
|
|
5835
|
+
{
|
|
5836
|
+
key: "settings",
|
|
5837
|
+
labelKey: "uiSop",
|
|
5838
|
+
Icon: GearIcon
|
|
5839
|
+
},
|
|
5840
|
+
{
|
|
5841
|
+
key: "chat",
|
|
5842
|
+
labelKey: "uiChatRegion",
|
|
5843
|
+
Icon: TextIcon
|
|
5844
|
+
},
|
|
5845
|
+
{
|
|
5846
|
+
key: "trajectory",
|
|
5847
|
+
labelKey: "uiTrajectory",
|
|
5848
|
+
Icon: TrajectoryIcon
|
|
5849
|
+
},
|
|
5850
|
+
{
|
|
5851
|
+
key: "produced",
|
|
5852
|
+
labelKey: "uiProduced",
|
|
5853
|
+
Icon: TextIcon
|
|
5854
|
+
},
|
|
5855
|
+
{
|
|
5856
|
+
key: "panel",
|
|
5857
|
+
labelKey: "uiPanelRegion",
|
|
5858
|
+
Icon: PanelIcon,
|
|
5859
|
+
needsSidebar: true
|
|
5860
|
+
}
|
|
5861
|
+
];
|
|
5862
|
+
const COLOR_KEYS = [
|
|
5863
|
+
"auto",
|
|
5864
|
+
"gray",
|
|
5865
|
+
"black",
|
|
5866
|
+
"white",
|
|
5867
|
+
"theme",
|
|
5868
|
+
"custom"
|
|
5869
|
+
];
|
|
5870
|
+
const COLOR_LABEL_KEYS = {
|
|
5871
|
+
auto: "strokeAuto",
|
|
5872
|
+
gray: "strokeGray",
|
|
5873
|
+
black: "strokeBlack",
|
|
5874
|
+
white: "strokeWhite",
|
|
5875
|
+
theme: "strokeTheme",
|
|
5876
|
+
custom: "strokeCustom"
|
|
5877
|
+
};
|
|
5878
|
+
/** Swatch fill for one preset key. 'auto' gets its half-black/half-white
|
|
5879
|
+
* conic gradient from CSS, and 'custom' shows the stored hex. */
|
|
5880
|
+
function dotStyle(key, s) {
|
|
5881
|
+
switch (key) {
|
|
5882
|
+
case "gray": return { background: "#808080" };
|
|
5883
|
+
case "black": return { background: "#000" };
|
|
5884
|
+
case "white": return { background: "#fff" };
|
|
5885
|
+
case "theme": return { background: "var(--dsw-alias-brand-primary)" };
|
|
5886
|
+
case "custom": return { background: s.customColor };
|
|
5887
|
+
default: return {};
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
function FontPage({ p, notify }) {
|
|
5891
|
+
const { t, setFont, removeFont, setFontEnabled, setStrokes, useStore } = p;
|
|
5892
|
+
const hasBetterSidebar = useBetterSidebar();
|
|
5893
|
+
const fileRef = (0, react.useRef)(null);
|
|
5894
|
+
const [strokes, setStrokesState] = (0, react.useState)(() => rStrokes());
|
|
5895
|
+
const [fontInfo, setFontInfo] = (0, react.useState)(() => ({
|
|
5896
|
+
mime: cfg.fontMime,
|
|
5897
|
+
enabled: cfg.fontEnabled,
|
|
5898
|
+
name: null
|
|
5899
|
+
}));
|
|
5900
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
5901
|
+
const metaRev = useStore((s) => s.metaRev);
|
|
5902
|
+
(0, react.useEffect)(() => {
|
|
5903
|
+
setStrokesState(rStrokes());
|
|
5904
|
+
setFontInfo((prev) => prev.mime === cfg.fontMime && prev.enabled === cfg.fontEnabled ? prev : {
|
|
5905
|
+
mime: cfg.fontMime,
|
|
5906
|
+
enabled: cfg.fontEnabled,
|
|
5907
|
+
name: null
|
|
5908
|
+
});
|
|
5909
|
+
}, [metaRev]);
|
|
5910
|
+
const hasFont = fontInfo.mime !== null;
|
|
5911
|
+
const fontLabel = fontInfo.name ?? (fontInfo.mime !== null ? fontInfo.mime.replace("font/", "").toUpperCase() : t("fontNone"));
|
|
5912
|
+
/** Write one group's stroke: cfg + live DOM now, disk on commit. `live`
|
|
5913
|
+
* marks slider drags (debounced save, no full re-apply). */
|
|
5914
|
+
const patch = (key, next, live) => {
|
|
5915
|
+
const map = {
|
|
5916
|
+
...strokes,
|
|
5917
|
+
[key]: {
|
|
5918
|
+
...strokes[key],
|
|
5919
|
+
...next
|
|
5920
|
+
}
|
|
5921
|
+
};
|
|
5922
|
+
setStrokesState(map);
|
|
5923
|
+
cfg.strokes = map;
|
|
5924
|
+
setPartStroke(key, map[key]);
|
|
5925
|
+
if (live) saveConfig();
|
|
5926
|
+
else setStrokes(map);
|
|
5927
|
+
};
|
|
5928
|
+
const pickFont = async (file) => {
|
|
5929
|
+
setBusy(true);
|
|
5930
|
+
setFontInfo({
|
|
5931
|
+
...fontInfo,
|
|
5932
|
+
name: file.name
|
|
5933
|
+
});
|
|
5934
|
+
const outcome = await setFont(file);
|
|
5935
|
+
setBusy(false);
|
|
5936
|
+
if (outcome.ok) setFontInfo({
|
|
5937
|
+
mime: cfg.fontMime,
|
|
5938
|
+
enabled: cfg.fontEnabled,
|
|
5939
|
+
name: file.name
|
|
5940
|
+
});
|
|
5941
|
+
else {
|
|
5942
|
+
setFontInfo({
|
|
5943
|
+
mime: cfg.fontMime,
|
|
5944
|
+
enabled: cfg.fontEnabled,
|
|
5945
|
+
name: null
|
|
5946
|
+
});
|
|
5947
|
+
notify(uploadRefusalText(outcome, t, "fontFail"), false);
|
|
5948
|
+
}
|
|
5949
|
+
};
|
|
5950
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5951
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
5952
|
+
className: "dab-head dab-rise",
|
|
5953
|
+
style: { "--d": 0 },
|
|
5954
|
+
children: [
|
|
5955
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5956
|
+
className: "dab-overline",
|
|
5957
|
+
children: "Typography"
|
|
5958
|
+
}),
|
|
5959
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
5960
|
+
className: "dab-h1",
|
|
5961
|
+
children: t("fontPageTitle")
|
|
5962
|
+
}),
|
|
5963
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5964
|
+
className: "dab-desc",
|
|
5965
|
+
children: t("descFont")
|
|
5966
|
+
})
|
|
5967
|
+
]
|
|
5968
|
+
}),
|
|
5969
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
5970
|
+
className: "dab-card dab-rise dab-font-card",
|
|
5971
|
+
style: { "--d": 1 },
|
|
5972
|
+
children: [
|
|
5973
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5974
|
+
className: "dab-row-head",
|
|
5975
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5976
|
+
className: "dab-font-title",
|
|
5977
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5978
|
+
className: "dab-part-ico",
|
|
5979
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextIcon, { size: 16 })
|
|
5980
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5981
|
+
className: "dab-part-name",
|
|
5982
|
+
children: t("fontTitle")
|
|
5983
|
+
})]
|
|
5984
|
+
}), hasFont ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5985
|
+
type: "button",
|
|
5986
|
+
className: `dab-toggle${fontInfo.enabled ? " is-on" : ""}`,
|
|
5987
|
+
role: "switch",
|
|
5988
|
+
"aria-checked": fontInfo.enabled,
|
|
5989
|
+
"aria-label": t("fontEnabled"),
|
|
5990
|
+
onClick: () => {
|
|
5991
|
+
const v = !fontInfo.enabled;
|
|
5992
|
+
setFontEnabled(v);
|
|
5993
|
+
setFontInfo({
|
|
5994
|
+
...fontInfo,
|
|
5995
|
+
enabled: v
|
|
5996
|
+
});
|
|
5997
|
+
},
|
|
5998
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5999
|
+
}) : null]
|
|
6000
|
+
}),
|
|
6001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6002
|
+
className: "dab-font-row",
|
|
6003
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6004
|
+
className: `dab-font-name${hasFont ? "" : " is-empty"}`,
|
|
6005
|
+
title: fontLabel,
|
|
6006
|
+
children: fontLabel
|
|
6007
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6008
|
+
className: "dab-font-actions",
|
|
6009
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6010
|
+
type: "button",
|
|
6011
|
+
className: "dab-btn dab-btn-primary",
|
|
6012
|
+
disabled: busy,
|
|
6013
|
+
onClick: () => fileRef.current?.click(),
|
|
6014
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 14 }), busy ? t("fontUploading") : t("fontUpload")]
|
|
6015
|
+
}), hasFont ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6016
|
+
type: "button",
|
|
6017
|
+
className: "dab-btn dab-btn-danger",
|
|
6018
|
+
onClick: () => {
|
|
6019
|
+
removeFont();
|
|
6020
|
+
setFontInfo({
|
|
6021
|
+
mime: null,
|
|
6022
|
+
enabled: cfg.fontEnabled,
|
|
6023
|
+
name: null
|
|
6024
|
+
});
|
|
6025
|
+
},
|
|
6026
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("fontRemove")]
|
|
6027
|
+
}) : null]
|
|
6028
|
+
})]
|
|
6029
|
+
}),
|
|
6030
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6031
|
+
className: "dab-hint",
|
|
6032
|
+
style: { marginTop: 10 },
|
|
6033
|
+
children: t("fontHint")
|
|
6034
|
+
}),
|
|
6035
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6036
|
+
ref: fileRef,
|
|
6037
|
+
type: "file",
|
|
6038
|
+
accept: ".ttf,.otf,.woff,.woff2,font/*",
|
|
6039
|
+
style: { display: "none" },
|
|
6040
|
+
onChange: (e) => {
|
|
6041
|
+
const f = e.target.files?.[0];
|
|
6042
|
+
e.target.value = "";
|
|
6043
|
+
if (f) pickFont(f);
|
|
6044
|
+
}
|
|
6045
|
+
})
|
|
6046
|
+
]
|
|
6047
|
+
}),
|
|
6048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6049
|
+
className: "dab-card dab-rise",
|
|
6050
|
+
style: { "--d": 2 },
|
|
6051
|
+
children: [
|
|
6052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6053
|
+
className: "dab-part-head",
|
|
6054
|
+
style: { marginBottom: 10 },
|
|
6055
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6056
|
+
className: "dab-part-name",
|
|
6057
|
+
children: t("strokeTitle")
|
|
6058
|
+
})
|
|
6059
|
+
}),
|
|
6060
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6061
|
+
className: "dab-hint",
|
|
6062
|
+
style: { marginBottom: 14 },
|
|
6063
|
+
children: t("strokeHint")
|
|
6064
|
+
}),
|
|
6065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6066
|
+
className: "dab-grid-parts",
|
|
6067
|
+
children: PARTS.filter((part) => !(part.needsSidebar && !hasBetterSidebar)).map((part, i) => {
|
|
6068
|
+
const s = strokes[part.key];
|
|
6069
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6070
|
+
className: "dab-card dab-card-hover dab-rise",
|
|
6071
|
+
style: { "--d": i + 3 },
|
|
6072
|
+
children: [
|
|
6073
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6074
|
+
className: "dab-part-head",
|
|
6075
|
+
children: [
|
|
6076
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6077
|
+
className: "dab-part-ico",
|
|
6078
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(part.Icon, { size: 16 })
|
|
6079
|
+
}),
|
|
6080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6081
|
+
className: "dab-part-name",
|
|
6082
|
+
children: t(part.labelKey)
|
|
6083
|
+
}),
|
|
6084
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6085
|
+
className: "dab-part-badge",
|
|
6086
|
+
children: [s.width, "px"]
|
|
6087
|
+
})
|
|
6088
|
+
]
|
|
6089
|
+
}),
|
|
6090
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
6091
|
+
label: t("strokeWidth"),
|
|
6092
|
+
min: 0,
|
|
6093
|
+
max: 4,
|
|
6094
|
+
step: .5,
|
|
6095
|
+
def: s.width,
|
|
6096
|
+
fmt: (v) => `${v}px`,
|
|
6097
|
+
onInput: (v) => patch(part.key, { width: v }, true),
|
|
6098
|
+
onChange: (v) => patch(part.key, { width: v }, false)
|
|
6099
|
+
}),
|
|
6100
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6101
|
+
className: "dab-stroke-dots",
|
|
6102
|
+
children: COLOR_KEYS.map((key) => {
|
|
6103
|
+
const active = s.color === key;
|
|
6104
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6105
|
+
className: "dab-stroke-dot-wrap",
|
|
6106
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6107
|
+
type: "button",
|
|
6108
|
+
className: `dab-stroke-dot${key === "auto" ? " dab-stroke-dot-auto" : ""}${active ? " is-active" : ""}`,
|
|
6109
|
+
style: dotStyle(key, s),
|
|
6110
|
+
title: t(COLOR_LABEL_KEYS[key]),
|
|
6111
|
+
"aria-label": t(COLOR_LABEL_KEYS[key]),
|
|
6112
|
+
"aria-pressed": active,
|
|
6113
|
+
onClick: () => patch(part.key, { color: key }, false)
|
|
6114
|
+
}), key === "custom" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6115
|
+
type: "color",
|
|
6116
|
+
className: "dab-stroke-dot-input",
|
|
6117
|
+
value: s.customColor,
|
|
6118
|
+
"aria-label": t("strokeCustom"),
|
|
6119
|
+
onChange: (e) => patch(part.key, {
|
|
6120
|
+
color: "custom",
|
|
6121
|
+
customColor: e.target.value
|
|
6122
|
+
}, false)
|
|
6123
|
+
}) : null]
|
|
6124
|
+
}, key);
|
|
6125
|
+
})
|
|
6126
|
+
})
|
|
6127
|
+
]
|
|
6128
|
+
}, part.key);
|
|
6129
|
+
})
|
|
6130
|
+
})
|
|
6131
|
+
]
|
|
6132
|
+
})
|
|
6133
|
+
] });
|
|
6134
|
+
}
|
|
6135
|
+
//#endregion
|
|
5155
6136
|
//#region src/client/components/BgEditor.tsx
|
|
5156
6137
|
/** Placement slot for the active background type: video framing lives in its
|
|
5157
6138
|
* own state so image and video edits never overwrite each other. */
|
|
5158
6139
|
const activeState = () => cfg.backgroundType === "video" ? cfg.videoBgState : cfg.bgState;
|
|
6140
|
+
/** Zoom is shared by every input path (wheel + pinch) so all pictures animate
|
|
6141
|
+
* between the same bounds. */
|
|
6142
|
+
const clampZoom = (z) => Math.max(.1, Math.min(10, z));
|
|
5159
6143
|
function BgEditor({ url, t, onClose, onCommit }) {
|
|
5160
6144
|
const pw = Math.min(window.innerWidth * .75, 860);
|
|
5161
6145
|
const ph = Math.round(pw * window.innerHeight / window.innerWidth);
|
|
@@ -5174,6 +6158,19 @@ window.__ModuleLoader__.load({
|
|
|
5174
6158
|
});
|
|
5175
6159
|
const containerRef = (0, react.useRef)(null);
|
|
5176
6160
|
const imgRef = (0, react.useRef)(null);
|
|
6161
|
+
/**
|
|
6162
|
+
* The overlay node is held in STATE rather than a plain ref, so the listener
|
|
6163
|
+
* effect below re-runs the moment it actually exists.
|
|
6164
|
+
*
|
|
6165
|
+
* This is not a style choice. Everything here renders through <Portal>, which
|
|
6166
|
+
* returns null on its first render — its own effect has to create the host
|
|
6167
|
+
* node under <html> first. A plain ref is therefore still null when a
|
|
6168
|
+
* mount-only effect runs, and an effect whose deps never change again attaches
|
|
6169
|
+
* nothing, ever. That is exactly how the touch handlers below ended up dead on
|
|
6170
|
+
* real phones: the wheel handler only survived because its deps churn on every
|
|
6171
|
+
* zoom/pan, so the wallpaper's img.onload re-ran it once and wired it up.
|
|
6172
|
+
*/
|
|
6173
|
+
const [overlayEl, setOverlayEl] = (0, react.useState)(null);
|
|
5177
6174
|
const dragRef = (0, react.useRef)({
|
|
5178
6175
|
active: false,
|
|
5179
6176
|
sx: 0,
|
|
@@ -5181,6 +6178,29 @@ window.__ModuleLoader__.load({
|
|
|
5181
6178
|
spx: 0,
|
|
5182
6179
|
spy: 0
|
|
5183
6180
|
});
|
|
6181
|
+
/** Unmount-time cleanup for a drag that never saw its mouseup (editor closed
|
|
6182
|
+
* mid-drag): without it the document listeners stay attached forever and
|
|
6183
|
+
* keep calling setPos on a dead component. */
|
|
6184
|
+
const dragStopRef = (0, react.useRef)(null);
|
|
6185
|
+
/** Set once a gesture actually moved, so the click that follows a drag is not
|
|
6186
|
+
* mistaken for a tap on the backdrop (mousedown and mouseup in different
|
|
6187
|
+
* subtrees make the click land on the overlay, i.e. "outside"). Reset at the
|
|
6188
|
+
* start of every gesture — touchstart / mousedown / wheel — so it can only
|
|
6189
|
+
* ever describe the gesture that just ended, never an earlier one. */
|
|
6190
|
+
const movedRef = (0, react.useRef)(false);
|
|
6191
|
+
const viewRef = (0, react.useRef)({
|
|
6192
|
+
zoom,
|
|
6193
|
+
pos
|
|
6194
|
+
});
|
|
6195
|
+
viewRef.current = {
|
|
6196
|
+
zoom,
|
|
6197
|
+
pos
|
|
6198
|
+
};
|
|
6199
|
+
/** Pinch gesture baseline (two fingers down): finger spread + midpoint and
|
|
6200
|
+
* the transform that was live at that moment. */
|
|
6201
|
+
const pinchRef = (0, react.useRef)(null);
|
|
6202
|
+
/** Single-finger pan baseline. */
|
|
6203
|
+
const touchDragRef = (0, react.useRef)(null);
|
|
5184
6204
|
(0, react.useEffect)(() => {
|
|
5185
6205
|
const img = new Image();
|
|
5186
6206
|
img.onload = () => {
|
|
@@ -5213,6 +6233,7 @@ window.__ModuleLoader__.load({
|
|
|
5213
6233
|
]);
|
|
5214
6234
|
const onDown = (0, react.useCallback)((e) => {
|
|
5215
6235
|
e.preventDefault();
|
|
6236
|
+
movedRef.current = false;
|
|
5216
6237
|
dragRef.current = {
|
|
5217
6238
|
active: true,
|
|
5218
6239
|
sx: e.clientX,
|
|
@@ -5222,41 +6243,151 @@ window.__ModuleLoader__.load({
|
|
|
5222
6243
|
};
|
|
5223
6244
|
const onMove = (ev) => {
|
|
5224
6245
|
if (!dragRef.current.active) return;
|
|
6246
|
+
if (Math.abs(ev.clientX - dragRef.current.sx) > 4 || Math.abs(ev.clientY - dragRef.current.sy) > 4) movedRef.current = true;
|
|
5225
6247
|
setPos({
|
|
5226
6248
|
x: dragRef.current.spx + ev.clientX - dragRef.current.sx,
|
|
5227
6249
|
y: dragRef.current.spy + ev.clientY - dragRef.current.sy
|
|
5228
6250
|
});
|
|
5229
6251
|
};
|
|
5230
|
-
const
|
|
6252
|
+
const stopDrag = () => {
|
|
5231
6253
|
dragRef.current.active = false;
|
|
5232
6254
|
document.removeEventListener("mousemove", onMove);
|
|
5233
|
-
document.removeEventListener("mouseup",
|
|
6255
|
+
document.removeEventListener("mouseup", stopDrag);
|
|
6256
|
+
dragStopRef.current = null;
|
|
5234
6257
|
};
|
|
6258
|
+
dragStopRef.current = stopDrag;
|
|
5235
6259
|
document.addEventListener("mousemove", onMove);
|
|
5236
|
-
document.addEventListener("mouseup",
|
|
6260
|
+
document.addEventListener("mouseup", stopDrag);
|
|
5237
6261
|
}, [pos]);
|
|
6262
|
+
(0, react.useEffect)(() => () => {
|
|
6263
|
+
dragStopRef.current?.();
|
|
6264
|
+
}, []);
|
|
5238
6265
|
const onWheelCb = (0, react.useCallback)((e) => {
|
|
5239
6266
|
e.preventDefault();
|
|
6267
|
+
movedRef.current = false;
|
|
5240
6268
|
const el = containerRef.current;
|
|
5241
6269
|
if (!el) return;
|
|
5242
6270
|
const rect = el.getBoundingClientRect();
|
|
5243
6271
|
const mx = rect.width / 2, my = rect.height / 2;
|
|
5244
6272
|
const factor = e.deltaY > 0 ? .97 : 1.03;
|
|
5245
|
-
const
|
|
5246
|
-
const
|
|
5247
|
-
const
|
|
6273
|
+
const { zoom: z, pos: p } = viewRef.current;
|
|
6274
|
+
const nz = clampZoom(z * factor);
|
|
6275
|
+
const nx = mx - (mx - p.x) * (nz / z);
|
|
6276
|
+
const ny = my - (my - p.y) * (nz / z);
|
|
5248
6277
|
setZoom(nz);
|
|
5249
6278
|
setPos({
|
|
5250
6279
|
x: nx,
|
|
5251
6280
|
y: ny
|
|
5252
6281
|
});
|
|
5253
|
-
}, [
|
|
6282
|
+
}, []);
|
|
6283
|
+
const onTouchStartCb = (0, react.useCallback)((e) => {
|
|
6284
|
+
const card = containerRef.current;
|
|
6285
|
+
if (!card) return;
|
|
6286
|
+
movedRef.current = false;
|
|
6287
|
+
if (e.touches.length >= 2) {
|
|
6288
|
+
touchDragRef.current = null;
|
|
6289
|
+
const rect = card.getBoundingClientRect();
|
|
6290
|
+
const a = e.touches[0], b = e.touches[1];
|
|
6291
|
+
pinchRef.current = {
|
|
6292
|
+
dist: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY),
|
|
6293
|
+
zoom: viewRef.current.zoom,
|
|
6294
|
+
cx: (a.clientX + b.clientX) / 2 - rect.left,
|
|
6295
|
+
cy: (a.clientY + b.clientY) / 2 - rect.top,
|
|
6296
|
+
px: viewRef.current.pos.x,
|
|
6297
|
+
py: viewRef.current.pos.y
|
|
6298
|
+
};
|
|
6299
|
+
return;
|
|
6300
|
+
}
|
|
6301
|
+
if (e.touches.length === 1) {
|
|
6302
|
+
pinchRef.current = null;
|
|
6303
|
+
if (!card.contains(e.target)) {
|
|
6304
|
+
touchDragRef.current = null;
|
|
6305
|
+
return;
|
|
6306
|
+
}
|
|
6307
|
+
const tp = e.touches[0];
|
|
6308
|
+
touchDragRef.current = {
|
|
6309
|
+
sx: tp.clientX,
|
|
6310
|
+
sy: tp.clientY,
|
|
6311
|
+
spx: viewRef.current.pos.x,
|
|
6312
|
+
spy: viewRef.current.pos.y
|
|
6313
|
+
};
|
|
6314
|
+
}
|
|
6315
|
+
}, []);
|
|
6316
|
+
const onTouchMoveCb = (0, react.useCallback)((e) => {
|
|
6317
|
+
const card = containerRef.current;
|
|
6318
|
+
if (!card) return;
|
|
6319
|
+
if (e.touches.length >= 2 && pinchRef.current !== null) {
|
|
6320
|
+
e.preventDefault();
|
|
6321
|
+
movedRef.current = true;
|
|
6322
|
+
const p = pinchRef.current;
|
|
6323
|
+
if (p.dist <= 0) return;
|
|
6324
|
+
const rect = card.getBoundingClientRect();
|
|
6325
|
+
const a = e.touches[0], b = e.touches[1];
|
|
6326
|
+
const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
6327
|
+
const cx = (a.clientX + b.clientX) / 2 - rect.left;
|
|
6328
|
+
const cy = (a.clientY + b.clientY) / 2 - rect.top;
|
|
6329
|
+
const nz = clampZoom(p.zoom * (dist / p.dist));
|
|
6330
|
+
setZoom(nz);
|
|
6331
|
+
setPos({
|
|
6332
|
+
x: cx - (p.cx - p.px) / p.zoom * nz,
|
|
6333
|
+
y: cy - (p.cy - p.py) / p.zoom * nz
|
|
6334
|
+
});
|
|
6335
|
+
return;
|
|
6336
|
+
}
|
|
6337
|
+
if (e.touches.length === 1 && touchDragRef.current !== null) {
|
|
6338
|
+
e.preventDefault();
|
|
6339
|
+
const d = touchDragRef.current;
|
|
6340
|
+
const tp = e.touches[0];
|
|
6341
|
+
if (Math.abs(tp.clientX - d.sx) > 4 || Math.abs(tp.clientY - d.sy) > 4) movedRef.current = true;
|
|
6342
|
+
setPos({
|
|
6343
|
+
x: d.spx + tp.clientX - d.sx,
|
|
6344
|
+
y: d.spy + tp.clientY - d.sy
|
|
6345
|
+
});
|
|
6346
|
+
}
|
|
6347
|
+
}, []);
|
|
6348
|
+
const onTouchEndCb = (0, react.useCallback)((e) => {
|
|
6349
|
+
if (e.touches.length === 1) {
|
|
6350
|
+
const tp = e.touches[0];
|
|
6351
|
+
touchDragRef.current = {
|
|
6352
|
+
sx: tp.clientX,
|
|
6353
|
+
sy: tp.clientY,
|
|
6354
|
+
spx: viewRef.current.pos.x,
|
|
6355
|
+
spy: viewRef.current.pos.y
|
|
6356
|
+
};
|
|
6357
|
+
pinchRef.current = null;
|
|
6358
|
+
} else if (e.touches.length === 0) {
|
|
6359
|
+
touchDragRef.current = null;
|
|
6360
|
+
pinchRef.current = null;
|
|
6361
|
+
}
|
|
6362
|
+
}, []);
|
|
5254
6363
|
(0, react.useEffect)(() => {
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
6364
|
+
if (!overlayEl) return;
|
|
6365
|
+
const noPageGesture = (e) => e.preventDefault();
|
|
6366
|
+
overlayEl.addEventListener("wheel", onWheelCb, { passive: false });
|
|
6367
|
+
overlayEl.addEventListener("touchstart", onTouchStartCb, { passive: false });
|
|
6368
|
+
overlayEl.addEventListener("touchmove", onTouchMoveCb, { passive: false });
|
|
6369
|
+
overlayEl.addEventListener("touchend", onTouchEndCb, { passive: false });
|
|
6370
|
+
overlayEl.addEventListener("touchcancel", onTouchEndCb, { passive: false });
|
|
6371
|
+
overlayEl.addEventListener("gesturestart", noPageGesture, { passive: false });
|
|
6372
|
+
overlayEl.addEventListener("gesturechange", noPageGesture, { passive: false });
|
|
6373
|
+
overlayEl.addEventListener("gestureend", noPageGesture, { passive: false });
|
|
6374
|
+
return () => {
|
|
6375
|
+
overlayEl.removeEventListener("wheel", onWheelCb);
|
|
6376
|
+
overlayEl.removeEventListener("touchstart", onTouchStartCb);
|
|
6377
|
+
overlayEl.removeEventListener("touchmove", onTouchMoveCb);
|
|
6378
|
+
overlayEl.removeEventListener("touchend", onTouchEndCb);
|
|
6379
|
+
overlayEl.removeEventListener("touchcancel", onTouchEndCb);
|
|
6380
|
+
overlayEl.removeEventListener("gesturestart", noPageGesture);
|
|
6381
|
+
overlayEl.removeEventListener("gesturechange", noPageGesture);
|
|
6382
|
+
overlayEl.removeEventListener("gestureend", noPageGesture);
|
|
6383
|
+
};
|
|
6384
|
+
}, [
|
|
6385
|
+
overlayEl,
|
|
6386
|
+
onWheelCb,
|
|
6387
|
+
onTouchStartCb,
|
|
6388
|
+
onTouchMoveCb,
|
|
6389
|
+
onTouchEndCb
|
|
6390
|
+
]);
|
|
5260
6391
|
const resetView = (0, react.useCallback)(() => {
|
|
5261
6392
|
if (imgSize.w === 0) return;
|
|
5262
6393
|
setZoom(1);
|
|
@@ -5270,10 +6401,22 @@ window.__ModuleLoader__.load({
|
|
|
5270
6401
|
imgSize
|
|
5271
6402
|
]);
|
|
5272
6403
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6404
|
+
ref: setOverlayEl,
|
|
5273
6405
|
className: "dab-overlay",
|
|
6406
|
+
style: {
|
|
6407
|
+
touchAction: "none",
|
|
6408
|
+
overscrollBehavior: "contain"
|
|
6409
|
+
},
|
|
5274
6410
|
onClick: (e) => {
|
|
6411
|
+
if (movedRef.current) {
|
|
6412
|
+
movedRef.current = false;
|
|
6413
|
+
return;
|
|
6414
|
+
}
|
|
5275
6415
|
if (e.target === e.currentTarget) onClose();
|
|
5276
6416
|
},
|
|
6417
|
+
onMouseDown: () => {
|
|
6418
|
+
movedRef.current = false;
|
|
6419
|
+
},
|
|
5277
6420
|
children: [
|
|
5278
6421
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5279
6422
|
className: "dab-overlay-title",
|
|
@@ -5290,7 +6433,10 @@ window.__ModuleLoader__.load({
|
|
|
5290
6433
|
background: "#000",
|
|
5291
6434
|
cursor: "grab",
|
|
5292
6435
|
width: pw,
|
|
5293
|
-
height: ph
|
|
6436
|
+
height: ph,
|
|
6437
|
+
touchAction: "none",
|
|
6438
|
+
WebkitUserSelect: "none",
|
|
6439
|
+
userSelect: "none"
|
|
5294
6440
|
},
|
|
5295
6441
|
onMouseDown: onDown,
|
|
5296
6442
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
@@ -5366,7 +6512,7 @@ window.__ModuleLoader__.load({
|
|
|
5366
6512
|
labelKey: "bgModeCenter"
|
|
5367
6513
|
}
|
|
5368
6514
|
];
|
|
5369
|
-
function BackgroundPage({ p }) {
|
|
6515
|
+
function BackgroundPage({ p, notify }) {
|
|
5370
6516
|
const { t, setWpFromServer, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, setRotation, addRotationItems, removeRotationItem, rotateNow, setVideoFromUrl, useStore } = p;
|
|
5371
6517
|
const storeUrl = useStore((s) => s.url);
|
|
5372
6518
|
const backgroundType = useStore((s) => s.backgroundType);
|
|
@@ -5383,7 +6529,11 @@ window.__ModuleLoader__.load({
|
|
|
5383
6529
|
const [urlVal, setUrlVal] = (0, react.useState)("");
|
|
5384
6530
|
const [urlBusy, setUrlBusy] = (0, react.useState)(false);
|
|
5385
6531
|
const [urlErr, setUrlErr] = (0, react.useState)(null);
|
|
6532
|
+
const bgRev = useStore((s) => s.bgRev);
|
|
5386
6533
|
const [mode, setModeState] = (0, react.useState)(rBgMode());
|
|
6534
|
+
(0, react.useEffect)(() => {
|
|
6535
|
+
setModeState(rBgMode());
|
|
6536
|
+
}, [bgRev]);
|
|
5387
6537
|
const isVideo = backgroundType === "video";
|
|
5388
6538
|
const isStatic = backgroundType === "image" || isVideo;
|
|
5389
6539
|
const isGenerated = !isStatic;
|
|
@@ -5397,14 +6547,15 @@ window.__ModuleLoader__.load({
|
|
|
5397
6547
|
genPreset,
|
|
5398
6548
|
generatedBg?.seed
|
|
5399
6549
|
]);
|
|
5400
|
-
const onFileSelect = (f) => {
|
|
6550
|
+
const onFileSelect = async (f) => {
|
|
5401
6551
|
if (f.type.startsWith("video/")) {
|
|
5402
|
-
setVideo(f, f.type);
|
|
6552
|
+
const outcome = await setVideo(f, f.type);
|
|
6553
|
+
if (!outcome.ok) notify(uploadRefusalText(outcome, t, "videoUploadFail"), false);
|
|
5403
6554
|
return;
|
|
5404
6555
|
}
|
|
5405
|
-
uploadWallpaper(f)
|
|
5406
|
-
|
|
5407
|
-
|
|
6556
|
+
const outcome = await uploadWallpaper(f);
|
|
6557
|
+
if (outcome.ok) setWpFromServer(WALLPAPER_SERVE_URL);
|
|
6558
|
+
else notify(uploadRefusalText(outcome, t, "bgUploadFail"), false);
|
|
5408
6559
|
};
|
|
5409
6560
|
const onDrop = (e) => {
|
|
5410
6561
|
e.preventDefault();
|
|
@@ -6075,6 +7226,27 @@ window.__ModuleLoader__.load({
|
|
|
6075
7226
|
panel: 0,
|
|
6076
7227
|
produced: 0
|
|
6077
7228
|
};
|
|
7229
|
+
/** Presets ship with strokes off — text outlines are an opt-in look. Fresh
|
|
7230
|
+
* objects per call so a profile restore can never alias another preset's
|
|
7231
|
+
* stroke config (applyAppearance copies, but mutation-proofing is free). */
|
|
7232
|
+
const zeroStrokes = () => {
|
|
7233
|
+
const off = () => ({
|
|
7234
|
+
width: 0,
|
|
7235
|
+
color: "auto",
|
|
7236
|
+
customColor: "#808080"
|
|
7237
|
+
});
|
|
7238
|
+
return {
|
|
7239
|
+
bg: off(),
|
|
7240
|
+
sidebar: off(),
|
|
7241
|
+
card: off(),
|
|
7242
|
+
settings: off(),
|
|
7243
|
+
chat: off(),
|
|
7244
|
+
trajectory: off(),
|
|
7245
|
+
input: off(),
|
|
7246
|
+
panel: off(),
|
|
7247
|
+
produced: off()
|
|
7248
|
+
};
|
|
7249
|
+
};
|
|
6078
7250
|
const BUILTIN_PRESETS = [
|
|
6079
7251
|
{
|
|
6080
7252
|
key: "default",
|
|
@@ -6087,6 +7259,7 @@ window.__ModuleLoader__.load({
|
|
|
6087
7259
|
input: 1
|
|
6088
7260
|
},
|
|
6089
7261
|
blurs: { ...zeroBlurs },
|
|
7262
|
+
strokes: zeroStrokes(),
|
|
6090
7263
|
settingsOpacity: 1,
|
|
6091
7264
|
wallpaperOpacity: 1,
|
|
6092
7265
|
blur: 0,
|
|
@@ -6120,6 +7293,7 @@ window.__ModuleLoader__.load({
|
|
|
6120
7293
|
input: 14,
|
|
6121
7294
|
panel: 12
|
|
6122
7295
|
},
|
|
7296
|
+
strokes: zeroStrokes(),
|
|
6123
7297
|
settingsOpacity: .88,
|
|
6124
7298
|
wallpaperOpacity: 1,
|
|
6125
7299
|
blur: 0,
|
|
@@ -6140,6 +7314,7 @@ window.__ModuleLoader__.load({
|
|
|
6140
7314
|
input: 1
|
|
6141
7315
|
},
|
|
6142
7316
|
blurs: { ...zeroBlurs },
|
|
7317
|
+
strokes: zeroStrokes(),
|
|
6143
7318
|
settingsOpacity: 1,
|
|
6144
7319
|
wallpaperOpacity: 1,
|
|
6145
7320
|
blur: 0,
|
|
@@ -6173,6 +7348,7 @@ window.__ModuleLoader__.load({
|
|
|
6173
7348
|
input: 16,
|
|
6174
7349
|
panel: 16
|
|
6175
7350
|
},
|
|
7351
|
+
strokes: zeroStrokes(),
|
|
6176
7352
|
settingsOpacity: .85,
|
|
6177
7353
|
wallpaperOpacity: .92,
|
|
6178
7354
|
blur: 2,
|
|
@@ -6206,6 +7382,7 @@ window.__ModuleLoader__.load({
|
|
|
6206
7382
|
input: 20,
|
|
6207
7383
|
panel: 18
|
|
6208
7384
|
},
|
|
7385
|
+
strokes: zeroStrokes(),
|
|
6209
7386
|
settingsOpacity: .8,
|
|
6210
7387
|
wallpaperOpacity: 1,
|
|
6211
7388
|
blur: 0,
|
|
@@ -6230,6 +7407,7 @@ window.__ModuleLoader__.load({
|
|
|
6230
7407
|
input: 1
|
|
6231
7408
|
},
|
|
6232
7409
|
blurs: { ...zeroBlurs },
|
|
7410
|
+
strokes: zeroStrokes(),
|
|
6233
7411
|
settingsOpacity: 1,
|
|
6234
7412
|
wallpaperOpacity: 1,
|
|
6235
7413
|
blur: 0,
|
|
@@ -6651,31 +7829,37 @@ window.__ModuleLoader__.load({
|
|
|
6651
7829
|
//#endregion
|
|
6652
7830
|
//#region src/client/components/ThemeSection.tsx
|
|
6653
7831
|
/**
|
|
6654
|
-
* dsh-any-background —
|
|
7832
|
+
* dsh-any-background — appearance shell for the five pages.
|
|
6655
7833
|
*
|
|
6656
|
-
*
|
|
6657
|
-
*
|
|
6658
|
-
*
|
|
6659
|
-
*
|
|
6660
|
-
*
|
|
6661
|
-
*
|
|
7834
|
+
* Two surfaces render this shell, and only the chrome differs:
|
|
7835
|
+
* · `settings` — injected into the host settings dialog's content column,
|
|
7836
|
+
* which supplies the modal frame (nav rail on the left, brand block, page
|
|
7837
|
+
* max-width, the dialog's own scrolling);
|
|
7838
|
+
* · `sidebar` — a page inside dsh-better-sidebar, where the panel is much
|
|
7839
|
+
* narrower, is handed a full-height column, and has to own its own scroll
|
|
7840
|
+
* region. The brand block is dropped (the sidebar's tab bar already names
|
|
7841
|
+
* the page) and the rail turns into a compact row.
|
|
7842
|
+
* The five pages themselves never branch on the surface: width-driven
|
|
7843
|
+
* adaptation lives in the stylesheet's container queries, which measure the
|
|
7844
|
+
* shell itself rather than assuming either surface's width.
|
|
6662
7845
|
*/
|
|
6663
7846
|
function ThemeSection(props) {
|
|
6664
7847
|
ensureUiCss();
|
|
6665
7848
|
const { t } = props;
|
|
7849
|
+
const surface = props.surface ?? "settings";
|
|
6666
7850
|
const [page, setPage] = (0, react.useState)(0);
|
|
6667
7851
|
const [toast, setToast] = (0, react.useState)(null);
|
|
6668
7852
|
const toastTimer = (0, react.useRef)(void 0);
|
|
6669
7853
|
(0, react.useEffect)(() => () => window.clearTimeout(toastTimer.current), []);
|
|
6670
|
-
const notify = (msg, ok = true) => {
|
|
7854
|
+
const notify = (0, react.useCallback)((msg, ok = true) => {
|
|
6671
7855
|
setToast({
|
|
6672
7856
|
msg,
|
|
6673
7857
|
ok
|
|
6674
7858
|
});
|
|
6675
7859
|
window.clearTimeout(toastTimer.current);
|
|
6676
7860
|
toastTimer.current = window.setTimeout(() => setToast(null), 2600);
|
|
6677
|
-
};
|
|
6678
|
-
const pages = [
|
|
7861
|
+
}, []);
|
|
7862
|
+
const pages = (0, react.useMemo)(() => [
|
|
6679
7863
|
{
|
|
6680
7864
|
label: t("pageColor"),
|
|
6681
7865
|
Icon: DropletIcon,
|
|
@@ -6689,10 +7873,21 @@ window.__ModuleLoader__.load({
|
|
|
6689
7873
|
Icon: LayersIcon,
|
|
6690
7874
|
node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InterfacePage, { p: props })
|
|
6691
7875
|
},
|
|
7876
|
+
{
|
|
7877
|
+
label: t("pageFont"),
|
|
7878
|
+
Icon: FontIcon,
|
|
7879
|
+
node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FontPage, {
|
|
7880
|
+
p: props,
|
|
7881
|
+
notify
|
|
7882
|
+
})
|
|
7883
|
+
},
|
|
6692
7884
|
{
|
|
6693
7885
|
label: t("pageBackground"),
|
|
6694
7886
|
Icon: PhotoIcon,
|
|
6695
|
-
node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackgroundPage, {
|
|
7887
|
+
node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackgroundPage, {
|
|
7888
|
+
p: props,
|
|
7889
|
+
notify
|
|
7890
|
+
})
|
|
6696
7891
|
},
|
|
6697
7892
|
{
|
|
6698
7893
|
label: t("pageProfile"),
|
|
@@ -6702,16 +7897,21 @@ window.__ModuleLoader__.load({
|
|
|
6702
7897
|
notify
|
|
6703
7898
|
})
|
|
6704
7899
|
}
|
|
6705
|
-
]
|
|
7900
|
+
], [
|
|
7901
|
+
t,
|
|
7902
|
+
props,
|
|
7903
|
+
notify
|
|
7904
|
+
]);
|
|
6706
7905
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ErrorBoundary, {
|
|
6707
7906
|
t,
|
|
6708
7907
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6709
7908
|
className: "dab-root",
|
|
7909
|
+
"data-dab-surface": surface,
|
|
6710
7910
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6711
7911
|
className: "dab-shell",
|
|
6712
7912
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
|
|
6713
7913
|
className: "dab-nav",
|
|
6714
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7914
|
+
children: [surface === "sidebar" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6715
7915
|
className: "dab-brand",
|
|
6716
7916
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6717
7917
|
className: "dab-brand-tile",
|
|
@@ -6751,6 +7951,138 @@ window.__ModuleLoader__.load({
|
|
|
6751
7951
|
});
|
|
6752
7952
|
}
|
|
6753
7953
|
//#endregion
|
|
7954
|
+
//#region src/client/sidebar/store-hook.ts
|
|
7955
|
+
/**
|
|
7956
|
+
* dsh-any-background — selector hook over a host store instance.
|
|
7957
|
+
*
|
|
7958
|
+
* The settings slot kit hands the pages a `useStore(selector)` hook that is
|
|
7959
|
+
* bound to this plugin's store instance by the host renderer. A better-sidebar
|
|
7960
|
+
* page gets no such kit — its tab props carry `ctx`/`store`/`scope` for
|
|
7961
|
+
* better-sidebar's own store — so the same contract is rebuilt here from the
|
|
7962
|
+
* store instance itself, which already IS an observable source
|
|
7963
|
+
* (`getSnapshot`/`subscribe`, see `defineStore` in the host's client store).
|
|
7964
|
+
*
|
|
7965
|
+
* Semantics match the host binding on purpose: reduce the published snapshot
|
|
7966
|
+
* through the selector and compare with `Object.is`, so a page subscribing to
|
|
7967
|
+
* one field does not re-render on unrelated writes.
|
|
7968
|
+
*/
|
|
7969
|
+
/**
|
|
7970
|
+
* Bind a store instance to a `useStore(selector)` hook. Call once per store and
|
|
7971
|
+
* share the result: the returned hook keeps one subscription identity for its
|
|
7972
|
+
* whole lifetime.
|
|
7973
|
+
* @param store - observable store instance (`defineStore` handle).
|
|
7974
|
+
* @returns a selector hook reading that instance.
|
|
7975
|
+
*/
|
|
7976
|
+
function createStoreHook(store) {
|
|
7977
|
+
const subscribe = (onChange) => store.subscribe(onChange);
|
|
7978
|
+
const { getSnapshot } = store;
|
|
7979
|
+
return function useStoreSelector(selector) {
|
|
7980
|
+
const cache = (0, react.useRef)(null);
|
|
7981
|
+
const read = (0, react.useCallback)(() => {
|
|
7982
|
+
const snapshot = getSnapshot();
|
|
7983
|
+
const hit = cache.current;
|
|
7984
|
+
if (hit !== null && hit.snapshot === snapshot) return hit.value;
|
|
7985
|
+
const value = selector(snapshot);
|
|
7986
|
+
cache.current = {
|
|
7987
|
+
snapshot,
|
|
7988
|
+
value
|
|
7989
|
+
};
|
|
7990
|
+
return value;
|
|
7991
|
+
}, [selector]);
|
|
7992
|
+
return (0, react.useSyncExternalStore)(subscribe, read, read);
|
|
7993
|
+
};
|
|
7994
|
+
}
|
|
7995
|
+
//#endregion
|
|
7996
|
+
//#region src/client/sidebar/tab.tsx
|
|
7997
|
+
/**
|
|
7998
|
+
* dsh-any-background — dsh-better-sidebar integration.
|
|
7999
|
+
*
|
|
8000
|
+
* Publishes the plugin's appearance controls as a sidebar page ("主题") in
|
|
8001
|
+
* better-sidebar's tab registry. The page renders the SAME five-page tree as
|
|
8002
|
+
* the settings section — `ThemeSection` takes a `surface` variant — so there is
|
|
8003
|
+
* one UI to maintain, not two forks of it.
|
|
8004
|
+
*
|
|
8005
|
+
* The dependency is optional at every level, which is why nothing here imports
|
|
8006
|
+
* the package:
|
|
8007
|
+
* · the service is read off the client context at runtime, so a DSH install
|
|
8008
|
+
* without better-sidebar silently registers nothing, and a better-sidebar
|
|
8009
|
+
* that mounts *after* this plugin still gets the page (`ctx.inject` fires
|
|
8010
|
+
* whenever the service shows up, never when it never does);
|
|
8011
|
+
* · the plugin's own types are restated in the minimal local contract below
|
|
8012
|
+
* instead of imported — a value import would be rejected by the host's
|
|
8013
|
+
* client-bundle purity gate, and even a type-only import would make the
|
|
8014
|
+
* package a build-time dependency for everyone who does not run it.
|
|
8015
|
+
*/
|
|
8016
|
+
/**
|
|
8017
|
+
* The active locale id, used as a re-render signal: page labels are resolved by
|
|
8018
|
+
* calling `t(...)` at render time, so the props object (and with it the memoized
|
|
8019
|
+
* page tree) has to be rebuilt when the language flips — otherwise the page
|
|
8020
|
+
* would keep painting the previous language until some unrelated store write
|
|
8021
|
+
* happened to re-render it.
|
|
8022
|
+
* @param locale - host locale service.
|
|
8023
|
+
* @returns the active locale id (a stable string, safe for `Object.is`).
|
|
8024
|
+
*/
|
|
8025
|
+
function useActiveLocale(locale) {
|
|
8026
|
+
const subscribe = (0, react.useCallback)((onChange) => locale.subscribe(onChange), [locale]);
|
|
8027
|
+
const read = (0, react.useCallback)(() => locale.getSnapshot().active, [locale]);
|
|
8028
|
+
return (0, react.useSyncExternalStore)(subscribe, read, read);
|
|
8029
|
+
}
|
|
8030
|
+
function ThemeTab({ face, useStore, locale }) {
|
|
8031
|
+
const active = useActiveLocale(locale);
|
|
8032
|
+
const props = (0, react.useMemo)(() => ({
|
|
8033
|
+
...face,
|
|
8034
|
+
useStore
|
|
8035
|
+
}), [
|
|
8036
|
+
face,
|
|
8037
|
+
useStore,
|
|
8038
|
+
active
|
|
8039
|
+
]);
|
|
8040
|
+
return (0, react.createElement)(ThemeSection, {
|
|
8041
|
+
...props,
|
|
8042
|
+
surface: "sidebar"
|
|
8043
|
+
});
|
|
8044
|
+
}
|
|
8045
|
+
/**
|
|
8046
|
+
* Register the appearance page with better-sidebar, if it is installed.
|
|
8047
|
+
*
|
|
8048
|
+
* Call from the client `apply`. The registration is owned by the injected
|
|
8049
|
+
* scope's effect, so unloading the plugin (or HMR) withdraws it — without that,
|
|
8050
|
+
* the next activation would throw on the duplicate id.
|
|
8051
|
+
* @param ctx - client context.
|
|
8052
|
+
* @param opts.face - lazy accessor for the shared business face.
|
|
8053
|
+
* @param opts.store - the plugin's store instance, or null on a host where the
|
|
8054
|
+
* store module never resolved (in that case there is nothing to bind and no
|
|
8055
|
+
* page is registered).
|
|
8056
|
+
*/
|
|
8057
|
+
function registerThemeSidebarTab(ctx, opts) {
|
|
8058
|
+
const { store } = opts;
|
|
8059
|
+
if (store === null || typeof store.getSnapshot !== "function" || typeof store.subscribe !== "function") {
|
|
8060
|
+
console.warn("dsh-any-background: no observable store instance on this host; the sidebar page stays unregistered");
|
|
8061
|
+
return;
|
|
8062
|
+
}
|
|
8063
|
+
const locale = ctx.locale;
|
|
8064
|
+
const t = locale.bind(NS);
|
|
8065
|
+
const useStore = createStoreHook(store);
|
|
8066
|
+
const ORDER = 55;
|
|
8067
|
+
ctx.inject?.(["betterSidebar"], (scope) => {
|
|
8068
|
+
const service = scope.betterSidebar;
|
|
8069
|
+
if (service === void 0) return;
|
|
8070
|
+
const face = opts.face();
|
|
8071
|
+
scope.effect(() => service.registerTab({
|
|
8072
|
+
id: "dsh-any-background:theme",
|
|
8073
|
+
title: () => t("nav"),
|
|
8074
|
+
icon: (size) => (0, react.createElement)(SunIcon, { size }),
|
|
8075
|
+
order: ORDER,
|
|
8076
|
+
single: true,
|
|
8077
|
+
component: () => (0, react.createElement)(ThemeTab, {
|
|
8078
|
+
face,
|
|
8079
|
+
useStore,
|
|
8080
|
+
locale
|
|
8081
|
+
})
|
|
8082
|
+
}));
|
|
8083
|
+
});
|
|
8084
|
+
}
|
|
8085
|
+
//#endregion
|
|
6754
8086
|
//#region src/client/index.tsx
|
|
6755
8087
|
/**
|
|
6756
8088
|
* dsh-any-background — browser half entry.
|
|
@@ -6771,6 +8103,7 @@ window.__ModuleLoader__.load({
|
|
|
6771
8103
|
initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
|
|
6772
8104
|
const [initH, initS, initL] = rColor();
|
|
6773
8105
|
let customDispose = null;
|
|
8106
|
+
let registerFailed = false;
|
|
6774
8107
|
const registerCustom = (h, s, l) => {
|
|
6775
8108
|
customDispose?.();
|
|
6776
8109
|
try {
|
|
@@ -6803,30 +8136,54 @@ window.__ModuleLoader__.load({
|
|
|
6803
8136
|
colorScheme,
|
|
6804
8137
|
tokens
|
|
6805
8138
|
});
|
|
6806
|
-
} catch {
|
|
8139
|
+
} catch (e) {
|
|
8140
|
+
if (!(() => {
|
|
8141
|
+
try {
|
|
8142
|
+
return ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
|
|
8143
|
+
} catch {
|
|
8144
|
+
return false;
|
|
8145
|
+
}
|
|
8146
|
+
})()) {
|
|
8147
|
+
if (!registerFailed) {
|
|
8148
|
+
registerFailed = true;
|
|
8149
|
+
console.error("dsh-any-background: host theme register failed; the custom skin will stay inactive", e);
|
|
8150
|
+
}
|
|
8151
|
+
customDispose = null;
|
|
8152
|
+
return false;
|
|
8153
|
+
}
|
|
6807
8154
|
customDispose = null;
|
|
6808
8155
|
}
|
|
6809
|
-
const present =
|
|
8156
|
+
const present = (() => {
|
|
8157
|
+
try {
|
|
8158
|
+
return ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
|
|
8159
|
+
} catch {
|
|
8160
|
+
return false;
|
|
8161
|
+
}
|
|
8162
|
+
})();
|
|
6810
8163
|
if (present) ctx.theme.setTheme(CUSTOM_ID);
|
|
6811
8164
|
return present;
|
|
6812
8165
|
};
|
|
6813
8166
|
if (rHasColor()) registerCustom(initH, initS, initL);
|
|
6814
|
-
onVerdictApplied(() => {
|
|
8167
|
+
const disposeVerdict = onVerdictApplied(() => {
|
|
6815
8168
|
if (!rHasColor()) registerCustom();
|
|
6816
8169
|
});
|
|
6817
|
-
onColorAdopted((hsl) => {
|
|
8170
|
+
const disposeColorAdopted = onColorAdopted((hsl) => {
|
|
6818
8171
|
registerCustom(hsl[0], hsl[1], hsl[2]);
|
|
6819
8172
|
saveConfig();
|
|
6820
8173
|
colorRev++;
|
|
6821
8174
|
bound?.syncColor(hslToHsv(hsl[0], hsl[1], hsl[2]), colorRev);
|
|
6822
8175
|
});
|
|
8176
|
+
ctx.effect(() => () => {
|
|
8177
|
+
disposeVerdict();
|
|
8178
|
+
disposeColorAdopted();
|
|
8179
|
+
}, "dsh-any-background: verdict/color listeners");
|
|
6823
8180
|
ctx.effect(() => () => {
|
|
6824
8181
|
customDispose?.();
|
|
6825
8182
|
if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
|
|
6826
8183
|
}, "dsh-any-background: skin dispose");
|
|
6827
8184
|
const styleEl = document.createElement("style");
|
|
6828
8185
|
styleEl.dataset.plugin = "dsh-any-background";
|
|
6829
|
-
styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${POPOVER_BLUR_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}${MOBILE_HEADER_RULE}${PANEL_TOKEN_RULE}${PANEL_BLUR_RULE}${PRODUCED_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
|
|
8186
|
+
styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${POPOVER_BLUR_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}${MOBILE_HEADER_RULE}${PANEL_TOKEN_RULE}${PANEL_BLUR_RULE}${PRODUCED_RULE}${STROKE_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
|
|
6830
8187
|
document.head.appendChild(styleEl);
|
|
6831
8188
|
ctx.effect(() => () => {
|
|
6832
8189
|
styleEl?.parentNode?.removeChild(styleEl);
|
|
@@ -6837,7 +8194,7 @@ window.__ModuleLoader__.load({
|
|
|
6837
8194
|
let colorRev = 0;
|
|
6838
8195
|
let bgRev = 0;
|
|
6839
8196
|
const colorTimerRef = { current: null };
|
|
6840
|
-
const
|
|
8197
|
+
const storeSpec = defineStore === null ? null : defineStore({
|
|
6841
8198
|
init: () => ({
|
|
6842
8199
|
url: null,
|
|
6843
8200
|
rev: -1,
|
|
@@ -6888,13 +8245,44 @@ window.__ModuleLoader__.load({
|
|
|
6888
8245
|
}
|
|
6889
8246
|
}
|
|
6890
8247
|
});
|
|
8248
|
+
/** One shared instance, whatever the host build handed back.
|
|
8249
|
+
*
|
|
8250
|
+
* The plugin publishes a single appearance state that BOTH surfaces read —
|
|
8251
|
+
* the settings section (through the renderer's `useStore`) and the
|
|
8252
|
+
* better-sidebar page (through the hook in `sidebar/store-hook`). Handing the
|
|
8253
|
+
* host a declaration whose `create` always returns this very instance is what
|
|
8254
|
+
* keeps them from drifting: letting the renderer mint its own would give the
|
|
8255
|
+
* sidebar a second, permanently empty copy of the state. */
|
|
8256
|
+
const storeInstance = storeSpec === null ? null : typeof storeSpec.getSnapshot === "function" ? storeSpec : storeSpec.create();
|
|
8257
|
+
/** What gets registered: the instance on newer builds, a single-instance
|
|
8258
|
+
* declaration on the ones that expect a declaration. */
|
|
8259
|
+
const store = storeSpec === null || storeInstance === storeSpec ? storeSpec : {
|
|
8260
|
+
spec: storeSpec.spec ?? {},
|
|
8261
|
+
create: () => storeInstance
|
|
8262
|
+
};
|
|
6891
8263
|
let bound = null;
|
|
8264
|
+
if (storeInstance !== null) bound = storeInstance.actions;
|
|
6892
8265
|
const syncBg = () => {
|
|
6893
8266
|
rev++;
|
|
6894
8267
|
bgRev++;
|
|
6895
8268
|
bound?.syncBg(rWp(), rev, cfg.backgroundType, cfg.generatedBg, bgRev, cfg.regenerateOnReload);
|
|
6896
8269
|
};
|
|
6897
|
-
onGeneratedSnapshot(syncBg);
|
|
8270
|
+
const disposeSnapshot = onGeneratedSnapshot(syncBg);
|
|
8271
|
+
ctx.effect(() => () => disposeSnapshot(), "dsh-any-background: snapshot listener");
|
|
8272
|
+
/** Capture the first frame of the video now applied and finish its
|
|
8273
|
+
* adaptation: the frame stands in for every still-image API (previews,
|
|
8274
|
+
* color extraction) and the brightness verdict follows it. The stale guard
|
|
8275
|
+
* skips a capture whose video was swapped out from under it (another
|
|
8276
|
+
* upload or a rotation switch landing mid-decode). */
|
|
8277
|
+
const captureAndApplyVideo = (url) => {
|
|
8278
|
+
if (url === null) return;
|
|
8279
|
+
captureVideoSnapshot(url).then((snap) => {
|
|
8280
|
+
if (rWpVideo() !== url) return;
|
|
8281
|
+
setWpVideoSnapshot(snap);
|
|
8282
|
+
applyThemeColor();
|
|
8283
|
+
syncBg();
|
|
8284
|
+
});
|
|
8285
|
+
};
|
|
6898
8286
|
let metaRev = 0;
|
|
6899
8287
|
const syncMetaNow = () => {
|
|
6900
8288
|
metaRev++;
|
|
@@ -7015,15 +8403,21 @@ window.__ModuleLoader__.load({
|
|
|
7015
8403
|
const scheduleTick = () => {
|
|
7016
8404
|
const sc = rSchedule();
|
|
7017
8405
|
if (!sc.enabled) return;
|
|
7018
|
-
const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)")
|
|
8406
|
+
const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ?? false : isNightNow(sc)) ? sc.nightProfile : sc.dayProfile;
|
|
7019
8407
|
if (!want || want === cfg.activeProfile) return;
|
|
7020
8408
|
applyProfileById(want);
|
|
7021
8409
|
};
|
|
7022
8410
|
applyWp();
|
|
7023
8411
|
syncBg();
|
|
7024
8412
|
watchParts();
|
|
7025
|
-
loadPersisted().then(async (serverRotated) => {
|
|
8413
|
+
loadPersisted().then(async ({ rotated: serverRotated, firstRun }) => {
|
|
8414
|
+
if (firstRun) {
|
|
8415
|
+
persistConfig();
|
|
8416
|
+
await loadPersisted();
|
|
8417
|
+
applyWp();
|
|
8418
|
+
}
|
|
7026
8419
|
syncMetaNow();
|
|
8420
|
+
applyFontFace(fontServeUrl, cfg.fontEnabled, cfg.fontMime);
|
|
7027
8421
|
if (rHasColor()) {
|
|
7028
8422
|
const [h, s, l] = rColor();
|
|
7029
8423
|
registerCustom(h, s, l);
|
|
@@ -7037,12 +8431,7 @@ window.__ModuleLoader__.load({
|
|
|
7037
8431
|
if (cfg.backgroundType === "video") {
|
|
7038
8432
|
const v = rWpVideo();
|
|
7039
8433
|
if (v) {
|
|
7040
|
-
|
|
7041
|
-
if (rWpVideo() !== v) return;
|
|
7042
|
-
setWpVideoSnapshot(snap);
|
|
7043
|
-
applyThemeColor();
|
|
7044
|
-
syncBg();
|
|
7045
|
-
});
|
|
8434
|
+
captureAndApplyVideo(v);
|
|
7046
8435
|
applyWp();
|
|
7047
8436
|
} else {
|
|
7048
8437
|
cfg.backgroundType = "image";
|
|
@@ -7092,19 +8481,25 @@ window.__ModuleLoader__.load({
|
|
|
7092
8481
|
document.body.append(sentinel);
|
|
7093
8482
|
const viewportObserver = new ResizeObserver(applySoon);
|
|
7094
8483
|
viewportObserver.observe(sentinel);
|
|
7095
|
-
const dprQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
7096
|
-
dprQuery
|
|
8484
|
+
const dprQuery = window.matchMedia?.(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
8485
|
+
dprQuery?.addEventListener?.("change", applySoon);
|
|
7097
8486
|
ctx.effect(() => () => {
|
|
7098
8487
|
viewportObserver.disconnect();
|
|
7099
|
-
dprQuery
|
|
8488
|
+
dprQuery?.removeEventListener?.("change", applySoon);
|
|
7100
8489
|
sentinel.remove();
|
|
7101
8490
|
}, "dsh-any-background: viewport watch");
|
|
7102
8491
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
7103
8492
|
zh,
|
|
7104
8493
|
en
|
|
7105
8494
|
}), "dsh-any-background: i18n");
|
|
7106
|
-
|
|
7107
|
-
|
|
8495
|
+
/** The business face, built once on first use and shared by both surfaces
|
|
8496
|
+
* (the settings section and the better-sidebar page). Memoizing is not just
|
|
8497
|
+
* about cost: the face closes over per-surface state — the font preview blob
|
|
8498
|
+
* URL behind releaseFontPreview — so a second build would give the sidebar
|
|
8499
|
+
* its own preview slot and leak whichever copy never gets released. */
|
|
8500
|
+
let themeFace = null;
|
|
8501
|
+
const buildFace = () => {
|
|
8502
|
+
if (themeFace !== null) return themeFace;
|
|
7108
8503
|
syncBg();
|
|
7109
8504
|
syncMetaNow();
|
|
7110
8505
|
const playVideoFromBlob = (blob, mime) => {
|
|
@@ -7115,20 +8510,37 @@ window.__ModuleLoader__.load({
|
|
|
7115
8510
|
setWpVideoUrl(localUrl, mime ?? blob.type ?? "video/mp4");
|
|
7116
8511
|
applyWp();
|
|
7117
8512
|
syncBg();
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
if (
|
|
7121
|
-
|
|
7122
|
-
applyThemeColor();
|
|
7123
|
-
syncBg();
|
|
7124
|
-
});
|
|
7125
|
-
uploadVideo(blob, mime ?? blob.type ?? "video/mp4").then((ok) => {
|
|
7126
|
-
if (ok) persistConfig();
|
|
8513
|
+
captureAndApplyVideo(rWpVideo());
|
|
8514
|
+
return uploadVideo(blob, mime ?? blob.type ?? "video/mp4").then((outcome) => {
|
|
8515
|
+
if (outcome.ok) persistConfig();
|
|
8516
|
+
return outcome;
|
|
7127
8517
|
});
|
|
7128
8518
|
};
|
|
8519
|
+
const FONT_EXT_MIME = {
|
|
8520
|
+
woff2: "font/woff2",
|
|
8521
|
+
woff: "font/woff",
|
|
8522
|
+
otf: "font/otf",
|
|
8523
|
+
ttf: "font/ttf"
|
|
8524
|
+
};
|
|
8525
|
+
const fontMimeFromName = (name) => {
|
|
8526
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
8527
|
+
return FONT_EXT_MIME[ext] ?? "font/ttf";
|
|
8528
|
+
};
|
|
8529
|
+
let fontPreviewUrl = null;
|
|
8530
|
+
const releaseFontPreview = (delay) => {
|
|
8531
|
+
if (fontPreviewUrl === null) return;
|
|
8532
|
+
const url = fontPreviewUrl;
|
|
8533
|
+
fontPreviewUrl = null;
|
|
8534
|
+
window.setTimeout(() => URL.revokeObjectURL(url), delay);
|
|
8535
|
+
};
|
|
8536
|
+
/** Point the @font-face at whatever is authoritative right now: the
|
|
8537
|
+
* persisted slot when one exists, otherwise nothing. */
|
|
8538
|
+
const applyStoredFont = () => {
|
|
8539
|
+
applyFontFace(cfg.fontMime !== null ? FONT_SERVE_URL : null, cfg.fontEnabled, cfg.fontMime);
|
|
8540
|
+
};
|
|
7129
8541
|
const [wh, ws, wl] = rColor();
|
|
7130
8542
|
const [dh, ds, dv] = hslToHsv(wh, ws, wl);
|
|
7131
|
-
|
|
8543
|
+
const built = {
|
|
7132
8544
|
t: ctx.locale.bind(NS),
|
|
7133
8545
|
hue: dh,
|
|
7134
8546
|
sat: ds,
|
|
@@ -7178,13 +8590,11 @@ window.__ModuleLoader__.load({
|
|
|
7178
8590
|
applyThemeColor();
|
|
7179
8591
|
syncBg();
|
|
7180
8592
|
saveConfig();
|
|
7181
|
-
return;
|
|
7182
|
-
}
|
|
7183
|
-
if (typeof u !== "string") {
|
|
7184
|
-
playVideoFromBlob(u, mime);
|
|
7185
|
-
return;
|
|
8593
|
+
return { ok: true };
|
|
7186
8594
|
}
|
|
7187
|
-
|
|
8595
|
+
if (typeof u !== "string") return playVideoFromBlob(u, mime);
|
|
8596
|
+
const ok = await persistVideo(u);
|
|
8597
|
+
const live = ok ? VIDEO_SERVE_URL : u;
|
|
7188
8598
|
cfg.backgroundType = "video";
|
|
7189
8599
|
setWpUrl(null);
|
|
7190
8600
|
cfg.videoBgState = { ...DEFAULT_CONFIG.bgState };
|
|
@@ -7192,13 +8602,8 @@ window.__ModuleLoader__.load({
|
|
|
7192
8602
|
applyWp();
|
|
7193
8603
|
saveConfig();
|
|
7194
8604
|
syncBg();
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
if (rWpVideo() !== applied) return;
|
|
7198
|
-
setWpVideoSnapshot(snap);
|
|
7199
|
-
applyThemeColor();
|
|
7200
|
-
syncBg();
|
|
7201
|
-
});
|
|
8605
|
+
captureAndApplyVideo(rWpVideo());
|
|
8606
|
+
return { ok };
|
|
7202
8607
|
},
|
|
7203
8608
|
setBgType: (type) => {
|
|
7204
8609
|
setBackgroundType(type);
|
|
@@ -7232,6 +8637,44 @@ window.__ModuleLoader__.load({
|
|
|
7232
8637
|
syncBg();
|
|
7233
8638
|
saveConfig();
|
|
7234
8639
|
},
|
|
8640
|
+
setStrokes: (strokes) => {
|
|
8641
|
+
cfg.strokes = strokes;
|
|
8642
|
+
applyStrokes();
|
|
8643
|
+
saveConfig();
|
|
8644
|
+
},
|
|
8645
|
+
setFont: async (file) => {
|
|
8646
|
+
releaseFontPreview(0);
|
|
8647
|
+
const localUrl = URL.createObjectURL(file);
|
|
8648
|
+
fontPreviewUrl = localUrl;
|
|
8649
|
+
applyFontFace(localUrl, true, fontMimeFromName(file.name));
|
|
8650
|
+
const outcome = await uploadFont(file);
|
|
8651
|
+
if (!outcome.ok || outcome.mime === void 0) {
|
|
8652
|
+
applyStoredFont();
|
|
8653
|
+
releaseFontPreview(4e3);
|
|
8654
|
+
return {
|
|
8655
|
+
ok: false,
|
|
8656
|
+
refusal: outcome.refusal
|
|
8657
|
+
};
|
|
8658
|
+
}
|
|
8659
|
+
cfg.fontMime = outcome.mime;
|
|
8660
|
+
cfg.fontEnabled = true;
|
|
8661
|
+
applyFontFace(FONT_SERVE_URL, true, outcome.mime);
|
|
8662
|
+
releaseFontPreview(4e3);
|
|
8663
|
+
persistConfig();
|
|
8664
|
+
return { ok: true };
|
|
8665
|
+
},
|
|
8666
|
+
removeFont: () => {
|
|
8667
|
+
releaseFontPreview(0);
|
|
8668
|
+
cfg.fontMime = null;
|
|
8669
|
+
applyFontFace(null, false, null);
|
|
8670
|
+
removeFont();
|
|
8671
|
+
persistConfig();
|
|
8672
|
+
},
|
|
8673
|
+
setFontEnabled: (v) => {
|
|
8674
|
+
cfg.fontEnabled = v;
|
|
8675
|
+
applyStoredFont();
|
|
8676
|
+
persistConfig();
|
|
8677
|
+
},
|
|
7235
8678
|
setWop: (v) => {
|
|
7236
8679
|
cfg.wallpaperOpacity = v;
|
|
7237
8680
|
applyWp();
|
|
@@ -7304,7 +8747,7 @@ window.__ModuleLoader__.load({
|
|
|
7304
8747
|
a.href = url;
|
|
7305
8748
|
a.download = "dsh-any-theme.json";
|
|
7306
8749
|
a.click();
|
|
7307
|
-
URL.revokeObjectURL(url);
|
|
8750
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 4e3);
|
|
7308
8751
|
},
|
|
7309
8752
|
importTheme: async (file) => {
|
|
7310
8753
|
try {
|
|
@@ -7324,16 +8767,9 @@ window.__ModuleLoader__.load({
|
|
|
7324
8767
|
}
|
|
7325
8768
|
if (blob !== null) playVideoFromBlob(blob, cfg.videoMime);
|
|
7326
8769
|
else {
|
|
7327
|
-
|
|
7328
|
-
setWpVideoUrl(live, cfg.videoMime);
|
|
8770
|
+
setWpVideoUrl(await persistVideo(video) ? VIDEO_SERVE_URL : video, cfg.videoMime);
|
|
7329
8771
|
applyWp();
|
|
7330
|
-
|
|
7331
|
-
captureVideoSnapshot(live).then((snap) => {
|
|
7332
|
-
if (rWpVideo() !== applied) return;
|
|
7333
|
-
setWpVideoSnapshot(snap);
|
|
7334
|
-
applyThemeColor();
|
|
7335
|
-
syncBg();
|
|
7336
|
-
});
|
|
8772
|
+
captureAndApplyVideo(rWpVideo());
|
|
7337
8773
|
}
|
|
7338
8774
|
} else {
|
|
7339
8775
|
setWpVideoUrl(null, null);
|
|
@@ -7492,18 +8928,19 @@ window.__ModuleLoader__.load({
|
|
|
7492
8928
|
applyWp();
|
|
7493
8929
|
saveConfig();
|
|
7494
8930
|
syncBg();
|
|
7495
|
-
|
|
7496
|
-
captureVideoSnapshot(applied).then((snap) => {
|
|
7497
|
-
if (rWpVideo() !== applied) return;
|
|
7498
|
-
setWpVideoSnapshot(snap);
|
|
7499
|
-
applyThemeColor();
|
|
7500
|
-
syncBg();
|
|
7501
|
-
});
|
|
8931
|
+
captureAndApplyVideo(rWpVideo());
|
|
7502
8932
|
return true;
|
|
7503
8933
|
}
|
|
7504
8934
|
};
|
|
8935
|
+
themeFace = built;
|
|
8936
|
+
return built;
|
|
8937
|
+
};
|
|
8938
|
+
const sectionInject = (actions) => {
|
|
8939
|
+
bound = actions;
|
|
8940
|
+
return buildFace();
|
|
7505
8941
|
};
|
|
7506
|
-
|
|
8942
|
+
if (store === null) console.error("dsh-any-background: settings panel disabled on this host (no store module)");
|
|
8943
|
+
else ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
7507
8944
|
name: "settings.section",
|
|
7508
8945
|
id: "dsh-any-background",
|
|
7509
8946
|
order: 35,
|
|
@@ -7512,6 +8949,10 @@ window.__ModuleLoader__.load({
|
|
|
7512
8949
|
store,
|
|
7513
8950
|
inject: sectionInject
|
|
7514
8951
|
}, ThemeSection));
|
|
8952
|
+
registerThemeSidebarTab(ctx, {
|
|
8953
|
+
face: buildFace,
|
|
8954
|
+
store: storeInstance
|
|
8955
|
+
});
|
|
7515
8956
|
const navLabel = () => ctx.locale.bind(NS)("nav");
|
|
7516
8957
|
const applyNavIcon = () => {
|
|
7517
8958
|
const nav = document.querySelector("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]")?.querySelector("nav");
|