dsh-background-by-model 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/CHANGELOG.zh.md +23 -1
- package/README.md +62 -35
- package/README.zh.md +62 -35
- package/lib/client.js +1393 -556
- package/lib/index.js +483 -37
- package/package.json +6 -6
- package/example_img/rule-editor.webp +0 -0
- package/example_img/wallpaper-deepseek.webp +0 -0
- package/example_img/wallpaper-kimi.webp +0 -0
- package/holiday/mid-autumn.webp +0 -0
- package/holiday/national-day.webp +0 -0
- package/lib/client.js.map +0 -1
package/lib/client.js
CHANGED
|
@@ -33,6 +33,7 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
uiOpacityBg: "主背景",
|
|
34
34
|
uiOpacitySide: "左侧栏",
|
|
35
35
|
uiOpacityCard: "卡片与菜单面板",
|
|
36
|
+
uiOpacityCode: "代码块",
|
|
36
37
|
uiOpacityInput: "输入框与控件",
|
|
37
38
|
uiSop: "设置界面透明度",
|
|
38
39
|
uiChatRegion: "对话文本框",
|
|
@@ -54,12 +55,21 @@ window.__ModuleLoader__.load({
|
|
|
54
55
|
tryoutEmpty: "输入模型名即可试跑",
|
|
55
56
|
autoExtract: "选图后自动取主题色",
|
|
56
57
|
ruleAdd: "新增规则",
|
|
58
|
+
ruleNoneHint: "还没有规则,点上面的「新增规则」新建一条",
|
|
57
59
|
ruleFallbackBadge: "兜底",
|
|
58
60
|
ruleMatchPlaceholder: "如 flash / glm(忽略大小写,留空则不参与匹配)",
|
|
59
61
|
ruleEnabled: "启用",
|
|
60
62
|
ruleUp: "上移",
|
|
61
63
|
ruleDown: "下移",
|
|
62
64
|
ruleRemove: "删除规则",
|
|
65
|
+
tabListLabel: "规则",
|
|
66
|
+
tabPick: "选择规则",
|
|
67
|
+
tabMore: "更多规则",
|
|
68
|
+
tabUnnamed: "未命名规则",
|
|
69
|
+
tabLive: "正在使用",
|
|
70
|
+
tabRotating: "轮换中",
|
|
71
|
+
tabOff: "已停用",
|
|
72
|
+
ruleRemoveConfirm: "再点一次删除",
|
|
63
73
|
ruleNoImage: "尚未选择图片",
|
|
64
74
|
ruleFromUrl: "从网址",
|
|
65
75
|
ruleUrlPlaceholder: "粘贴图片网址 https://…",
|
|
@@ -83,6 +93,7 @@ window.__ModuleLoader__.load({
|
|
|
83
93
|
ruleColorTargetImage: "作用于选中的图片:",
|
|
84
94
|
ruleColorTargetRule: "作用于本规则(它还没有图片)",
|
|
85
95
|
ruleLayout: "布局模式",
|
|
96
|
+
ruleLayoutNoImage: "添加图片后可分别设置每张图的布局模式",
|
|
86
97
|
ruleFramingEdit: "编辑位置",
|
|
87
98
|
ruleOpacity: "背景透明度",
|
|
88
99
|
ruleBlur: "背景模糊",
|
|
@@ -110,7 +121,6 @@ window.__ModuleLoader__.load({
|
|
|
110
121
|
rotEvery1m: "1 分钟",
|
|
111
122
|
rotEvery5m: "5 分钟",
|
|
112
123
|
rotEvery30m: "30 分钟",
|
|
113
|
-
rotEvery1h: "1 小时",
|
|
114
124
|
bgModeFit: "适应",
|
|
115
125
|
bgModeFill: "填充",
|
|
116
126
|
bgModeStretch: "拉伸",
|
|
@@ -140,7 +150,26 @@ window.__ModuleLoader__.load({
|
|
|
140
150
|
toastExportDone: "配置文件已开始下载",
|
|
141
151
|
importDone: "已导入配置",
|
|
142
152
|
importFail: "导入失败,文件格式不正确",
|
|
153
|
+
recCardTitle: "推荐配置",
|
|
154
|
+
recCardDesc: "从项目素材库下载作者维护的一整套配置(规则、壁纸与接口设置),一键替换当前的全部内容",
|
|
155
|
+
recAdopt: "采用推荐配置",
|
|
156
|
+
recBusy: "正在下载",
|
|
157
|
+
recConfirmTitle: "替换当前的全部配置?",
|
|
158
|
+
recConfirmBody: "会先删除你现在的全部规则与壁纸,再写入这份推荐配置。删除的文件无法恢复。",
|
|
159
|
+
recStatRules: "条规则",
|
|
160
|
+
recStatImages: "张壁纸",
|
|
161
|
+
recConfirmOk: "删除并替换",
|
|
162
|
+
recConfirmCancel: "取消",
|
|
163
|
+
recDone: "已采用推荐配置",
|
|
164
|
+
recFailNetwork: "推荐配置下载失败,请检查网络后重试",
|
|
165
|
+
recFailNotProfile: "素材库返回的内容不是配置,请稍后重试",
|
|
166
|
+
recFailNewer: "这份推荐配置需要更新的插件版本,未做任何改动",
|
|
167
|
+
recFailTooMany: "这份推荐配置列出的图片数量超出限制,未做任何改动",
|
|
168
|
+
recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
|
|
169
|
+
recFailImage: "这张壁纸没拿到(素材库里缺这个文件):",
|
|
170
|
+
recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
|
|
143
171
|
footerTag: "按模型换背景插件",
|
|
172
|
+
repoLink: "在 GitHub 上打开仓库",
|
|
144
173
|
holidayTitle: "节日特殊背景",
|
|
145
174
|
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)",
|
|
146
175
|
trTitle: "切换效果",
|
|
@@ -174,6 +203,7 @@ window.__ModuleLoader__.load({
|
|
|
174
203
|
uiOpacityBg: "Main background",
|
|
175
204
|
uiOpacitySide: "Left panel",
|
|
176
205
|
uiOpacityCard: "Cards & menus",
|
|
206
|
+
uiOpacityCode: "Code blocks",
|
|
177
207
|
uiOpacityInput: "Input & controls",
|
|
178
208
|
uiSop: "Settings interface opacity",
|
|
179
209
|
uiChatRegion: "Conversation text frame",
|
|
@@ -195,12 +225,21 @@ window.__ModuleLoader__.load({
|
|
|
195
225
|
tryoutEmpty: "Type a model name to test the list",
|
|
196
226
|
autoExtract: "Extract the theme color when an image is chosen",
|
|
197
227
|
ruleAdd: "Add rule",
|
|
228
|
+
ruleNoneHint: "No rules yet — use “Add rule” in the tab strip above",
|
|
198
229
|
ruleFallbackBadge: "fallback",
|
|
199
230
|
ruleMatchPlaceholder: "e.g. flash / glm (case-insensitive, empty never matches)",
|
|
200
231
|
ruleEnabled: "Enabled",
|
|
201
232
|
ruleUp: "Move up",
|
|
202
233
|
ruleDown: "Move down",
|
|
203
234
|
ruleRemove: "Remove rule",
|
|
235
|
+
tabListLabel: "Rules",
|
|
236
|
+
tabPick: "Pick a rule",
|
|
237
|
+
tabMore: "More rules",
|
|
238
|
+
tabUnnamed: "Unnamed rule",
|
|
239
|
+
tabLive: "In use",
|
|
240
|
+
tabRotating: "Rotating",
|
|
241
|
+
tabOff: "Disabled",
|
|
242
|
+
ruleRemoveConfirm: "Click again to delete",
|
|
204
243
|
ruleNoImage: "No image yet",
|
|
205
244
|
ruleFromUrl: "From URL",
|
|
206
245
|
ruleUrlPlaceholder: "Paste an image URL https://…",
|
|
@@ -224,6 +263,7 @@ window.__ModuleLoader__.load({
|
|
|
224
263
|
ruleColorTargetRule: "Applies to this rule (it has no image yet)",
|
|
225
264
|
ruleColorExtract: "Extract from image",
|
|
226
265
|
ruleLayout: "Layout mode",
|
|
266
|
+
ruleLayoutNoImage: "Add an image to give each one its own layout mode",
|
|
227
267
|
ruleFramingEdit: "Edit position",
|
|
228
268
|
ruleOpacity: "Background opacity",
|
|
229
269
|
ruleBlur: "Background blur",
|
|
@@ -251,7 +291,6 @@ window.__ModuleLoader__.load({
|
|
|
251
291
|
rotEvery1m: "1 min",
|
|
252
292
|
rotEvery5m: "5 min",
|
|
253
293
|
rotEvery30m: "30 min",
|
|
254
|
-
rotEvery1h: "1 h",
|
|
255
294
|
bgModeFit: "Fit",
|
|
256
295
|
bgModeFill: "Fill",
|
|
257
296
|
bgModeStretch: "Stretch",
|
|
@@ -281,7 +320,26 @@ window.__ModuleLoader__.load({
|
|
|
281
320
|
toastExportDone: "Export started",
|
|
282
321
|
importDone: "Profile imported",
|
|
283
322
|
importFail: "Import failed — invalid file",
|
|
323
|
+
recCardTitle: "Recommended profile",
|
|
324
|
+
recCardDesc: "Download the author's own full setup — rules, wallpapers and interface settings — and replace everything with it",
|
|
325
|
+
recAdopt: "Use recommended profile",
|
|
326
|
+
recBusy: "Downloading",
|
|
327
|
+
recConfirmTitle: "Replace your entire profile?",
|
|
328
|
+
recConfirmBody: "Every rule and wallpaper you have is deleted first, then this profile is written in its place. Deleted files cannot be recovered.",
|
|
329
|
+
recStatRules: "rules",
|
|
330
|
+
recStatImages: "wallpapers",
|
|
331
|
+
recConfirmOk: "Delete and replace",
|
|
332
|
+
recConfirmCancel: "Cancel",
|
|
333
|
+
recDone: "Recommended profile applied",
|
|
334
|
+
recFailNetwork: "Could not download the recommended profile — check your connection and retry",
|
|
335
|
+
recFailNotProfile: "The asset host answered with something that is not a profile — try again later",
|
|
336
|
+
recFailNewer: "This profile needs a newer plugin version. Nothing was changed.",
|
|
337
|
+
recFailTooMany: "The recommended profile names more images than the plugin will fetch. Nothing was changed.",
|
|
338
|
+
recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
|
|
339
|
+
recFailImage: "Could not fetch this wallpaper — the asset host has no such file:",
|
|
340
|
+
recFailApply: "Could not write the recommended profile — your own is untouched",
|
|
284
341
|
footerTag: "Per-model wallpaper plugin",
|
|
342
|
+
repoLink: "Open the repository on GitHub",
|
|
285
343
|
holidayTitle: "Holiday backgrounds",
|
|
286
344
|
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)",
|
|
287
345
|
trTitle: "Switch effect",
|
|
@@ -578,18 +636,22 @@ window.__ModuleLoader__.load({
|
|
|
578
636
|
* `sidebar` sidebar (`--dsw-specific-sidebar-fill`)
|
|
579
637
|
* `card` cards/panels (`--dsw-alias-bg-layer-1/2/3`, `--dsw-specific-menu`
|
|
580
638
|
* and its 0.1.7 alias `--dsw-menu-surface-fill`)
|
|
639
|
+
* `code` code blocks, banner included (`--dsw-alias-markdown-code-block`,
|
|
640
|
+
* `--dsw-alias-markdown-code-block-banner`)
|
|
581
641
|
* `input` input/control surfaces (`--dsw-specific-input-major`)
|
|
582
642
|
*/
|
|
583
643
|
const PART_OPACITY_KEYS = [
|
|
584
644
|
"bg",
|
|
585
645
|
"sidebar",
|
|
586
646
|
"card",
|
|
647
|
+
"code",
|
|
587
648
|
"input"
|
|
588
649
|
];
|
|
589
650
|
const DEFAULT_PART_OPACITIES = {
|
|
590
651
|
bg: .85,
|
|
591
652
|
sidebar: .93,
|
|
592
653
|
card: 1,
|
|
654
|
+
code: 1,
|
|
593
655
|
input: 1
|
|
594
656
|
};
|
|
595
657
|
/**
|
|
@@ -722,7 +784,9 @@ window.__ModuleLoader__.load({
|
|
|
722
784
|
*
|
|
723
785
|
* Holidays default to `fill` rather than a rule's `fit`: these are full-bleed
|
|
724
786
|
* festival wallpapers, and `fit` would letterbox them on every aspect ratio the
|
|
725
|
-
* art was not cut for.
|
|
787
|
+
* art was not cut for. The mode now lives on the IMAGE (see `BgImage.bgMode`), so
|
|
788
|
+
* that default is written there — and `fill` is what a holiday's read-back
|
|
789
|
+
* sanitizer refuses to lose (see `normalizeHolidayRule`).
|
|
726
790
|
*/
|
|
727
791
|
function defaultHolidayRule(def) {
|
|
728
792
|
const color = hexToHsl(def.color);
|
|
@@ -730,13 +794,13 @@ window.__ModuleLoader__.load({
|
|
|
730
794
|
id: def.id,
|
|
731
795
|
images: [{
|
|
732
796
|
slot: def.slot,
|
|
797
|
+
bgMode: "fill",
|
|
733
798
|
bgState: { ...DEFAULT_BG_STATE },
|
|
734
799
|
color
|
|
735
800
|
}],
|
|
736
801
|
match: "",
|
|
737
802
|
enabled: true,
|
|
738
803
|
color,
|
|
739
|
-
bgMode: "fill",
|
|
740
804
|
wallpaperOpacity: 1,
|
|
741
805
|
blur: 0,
|
|
742
806
|
rotate: defaultRotation()
|
|
@@ -867,12 +931,21 @@ window.__ModuleLoader__.load({
|
|
|
867
931
|
* same as absent: an entry with `color: null` is an image the user explicitly set
|
|
868
932
|
* to "follow the system theme", and re-inheriting the rule's color over that
|
|
869
933
|
* would undo a deliberate choice on every load.
|
|
934
|
+
*
|
|
935
|
+
* `inheritMode` is the same one-shot lift for the layout mode, and it is the
|
|
936
|
+
* caller's to resolve because the two callers answer differently: a user rule
|
|
937
|
+
* passes the mode its config carried at the RULE level (a schema-5 config, where
|
|
938
|
+
* 适应/填充 was one choice for the whole rotation), a holiday passes `fill` (its
|
|
939
|
+
* own default, which is not up for the lift to change). An entry that HAS a
|
|
940
|
+
* `bgMode` key keeps it — including one that has just been set to a value equal
|
|
941
|
+
* to the default, which is a decision and not an absence.
|
|
870
942
|
*/
|
|
871
|
-
function normalizeImage(raw, inherit = null) {
|
|
943
|
+
function normalizeImage(raw, inherit = null, inheritMode = "fit") {
|
|
872
944
|
const i = raw ?? {};
|
|
873
945
|
if (typeof i.slot !== "string" || !SLOT_RE.test(i.slot)) return null;
|
|
874
946
|
return {
|
|
875
947
|
slot: i.slot,
|
|
948
|
+
bgMode: BG_MODES$1.includes(i.bgMode) ? i.bgMode : inheritMode,
|
|
876
949
|
bgState: normalizeBgState(i.bgState),
|
|
877
950
|
color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit
|
|
878
951
|
};
|
|
@@ -892,6 +965,14 @@ window.__ModuleLoader__.load({
|
|
|
892
965
|
* it as well: it is not a default for its images at paint time any more, but it
|
|
893
966
|
* is still what the rule paints once its last picture is gone.
|
|
894
967
|
*
|
|
968
|
+
* The layout mode makes the same trip in the other direction, and this is the
|
|
969
|
+
* last release that has to: a config written before 0.7.5 keeps `bgMode` at the RULE
|
|
970
|
+
* level, where one value covered every picture of a rotation, so it is read here
|
|
971
|
+
* once and handed to each image as its inherited mode. What the user chose
|
|
972
|
+
* therefore survives the move the same way it survived the color's — the mode is
|
|
973
|
+
* not reset to the default on upgrade, and the field it used to live on is simply
|
|
974
|
+
* not written back.
|
|
975
|
+
*
|
|
895
976
|
* An EMPTY list is a legitimate rule, not a broken one: removing a rule's last
|
|
896
977
|
* picture has to be expressible, and forcing an entry back in is what produced a
|
|
897
978
|
* phantom blank image that took slot 1 and could never be deleted again (the next
|
|
@@ -906,11 +987,12 @@ window.__ModuleLoader__.load({
|
|
|
906
987
|
const id = typeof r.id === "string" && r.id !== "" ? r.id : null;
|
|
907
988
|
if (id === null) return null;
|
|
908
989
|
const color = normalizeHsl(r.color);
|
|
990
|
+
const mode = BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit";
|
|
909
991
|
const images = [];
|
|
910
992
|
const seen = /* @__PURE__ */ new Set();
|
|
911
993
|
const hadList = Array.isArray(r.images);
|
|
912
994
|
if (hadList) for (const entry of r.images) {
|
|
913
|
-
const image = normalizeImage(entry, color);
|
|
995
|
+
const image = normalizeImage(entry, color, mode);
|
|
914
996
|
if (image === null || seen.has(image.slot)) continue;
|
|
915
997
|
seen.add(image.slot);
|
|
916
998
|
images.push(image);
|
|
@@ -918,21 +1000,22 @@ window.__ModuleLoader__.load({
|
|
|
918
1000
|
const legacySlot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
|
|
919
1001
|
if (images.length === 0 && legacySlot !== null) images.push({
|
|
920
1002
|
slot: legacySlot,
|
|
1003
|
+
bgMode: mode,
|
|
921
1004
|
bgState: normalizeBgState(r.bgState),
|
|
922
1005
|
color
|
|
923
1006
|
});
|
|
924
1007
|
if (images.length === 0 && !hadList) return null;
|
|
925
|
-
const
|
|
1008
|
+
const rotate = normalizeRotation(r.rotate);
|
|
1009
|
+
if (images.length < 2) rotate.enabled = false;
|
|
926
1010
|
return {
|
|
927
1011
|
id,
|
|
928
1012
|
images,
|
|
929
1013
|
match: typeof r.match === "string" ? r.match : "",
|
|
930
1014
|
enabled: r.enabled !== false,
|
|
931
1015
|
color,
|
|
932
|
-
bgMode: mode,
|
|
933
1016
|
wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
|
|
934
1017
|
blur: clamp$1(r.blur, 0, 60, 0),
|
|
935
|
-
rotate
|
|
1018
|
+
rotate
|
|
936
1019
|
};
|
|
937
1020
|
}
|
|
938
1021
|
/**
|
|
@@ -943,9 +1026,12 @@ window.__ModuleLoader__.load({
|
|
|
943
1026
|
* its bytes, and its theme color is a fixed part of what that holiday looks like —
|
|
944
1027
|
* so a stale or hand-edited value would either orphan the wallpaper, point the
|
|
945
1028
|
* holiday at a rule's image, or paint a festival in a color it does not have.
|
|
946
|
-
* Everything else is sanitized exactly like a rule's field — except
|
|
947
|
-
*
|
|
948
|
-
*
|
|
1029
|
+
* Everything else is sanitized exactly like a rule's field — except three things
|
|
1030
|
+
* that stay the definition's own: the image list and the rotation (so a festival
|
|
1031
|
+
* can never be swapped or cycled), and the layout mode, which is the definition's
|
|
1032
|
+
* `fill` for the same reason the color is its own — full-bleed art letterboxed by
|
|
1033
|
+
* a stale `fit` is the one thing this feature must not do, and the mode is not
|
|
1034
|
+
* editable for a holiday anywhere in the panel.
|
|
949
1035
|
*/
|
|
950
1036
|
function normalizeHolidayRule(def, raw) {
|
|
951
1037
|
const r = raw ?? {};
|
|
@@ -953,11 +1039,11 @@ window.__ModuleLoader__.load({
|
|
|
953
1039
|
return {
|
|
954
1040
|
...base,
|
|
955
1041
|
enabled: r.enabled !== false,
|
|
956
|
-
bgMode: BG_MODES$1.includes(r.bgMode) ? r.bgMode : base.bgMode,
|
|
957
1042
|
wallpaperOpacity: clamp01(r.wallpaperOpacity, base.wallpaperOpacity),
|
|
958
1043
|
blur: clamp$1(r.blur, 0, 60, base.blur),
|
|
959
1044
|
images: base.images.map((image) => ({
|
|
960
1045
|
slot: image.slot,
|
|
1046
|
+
bgMode: image.bgMode,
|
|
961
1047
|
bgState: normalizeBgState(r.images?.[0]?.bgState ?? r.bgState),
|
|
962
1048
|
color: image.color
|
|
963
1049
|
}))
|
|
@@ -999,50 +1085,6 @@ window.__ModuleLoader__.load({
|
|
|
999
1085
|
holidays: normalizeHolidays(r.holidays)
|
|
1000
1086
|
};
|
|
1001
1087
|
}
|
|
1002
|
-
//#endregion
|
|
1003
|
-
//#region src/client/state.ts
|
|
1004
|
-
const PALETTE = [
|
|
1005
|
-
[
|
|
1006
|
-
356,
|
|
1007
|
-
.72,
|
|
1008
|
-
.55
|
|
1009
|
-
],
|
|
1010
|
-
[
|
|
1011
|
-
24,
|
|
1012
|
-
.78,
|
|
1013
|
-
.55
|
|
1014
|
-
],
|
|
1015
|
-
[
|
|
1016
|
-
44,
|
|
1017
|
-
.8,
|
|
1018
|
-
.55
|
|
1019
|
-
],
|
|
1020
|
-
[
|
|
1021
|
-
152,
|
|
1022
|
-
.62,
|
|
1023
|
-
.5
|
|
1024
|
-
],
|
|
1025
|
-
[
|
|
1026
|
-
174,
|
|
1027
|
-
.68,
|
|
1028
|
-
.48
|
|
1029
|
-
],
|
|
1030
|
-
[
|
|
1031
|
-
208,
|
|
1032
|
-
.72,
|
|
1033
|
-
.55
|
|
1034
|
-
],
|
|
1035
|
-
[
|
|
1036
|
-
252,
|
|
1037
|
-
.68,
|
|
1038
|
-
.6
|
|
1039
|
-
],
|
|
1040
|
-
[
|
|
1041
|
-
300,
|
|
1042
|
-
.64,
|
|
1043
|
-
.58
|
|
1044
|
-
]
|
|
1045
|
-
];
|
|
1046
1088
|
freshThemeConfig();
|
|
1047
1089
|
let cfg = freshThemeConfig();
|
|
1048
1090
|
const images = /* @__PURE__ */ new Map();
|
|
@@ -1094,6 +1136,10 @@ window.__ModuleLoader__.load({
|
|
|
1094
1136
|
return null;
|
|
1095
1137
|
}
|
|
1096
1138
|
}
|
|
1139
|
+
function clearImages() {
|
|
1140
|
+
for (const slot of Array.from(displayUrls.keys())) revokeDisplay(slot);
|
|
1141
|
+
images.clear();
|
|
1142
|
+
}
|
|
1097
1143
|
let activeRuleId = null;
|
|
1098
1144
|
let activeMatched = false;
|
|
1099
1145
|
let modelLabel = "";
|
|
@@ -1194,7 +1240,6 @@ window.__ModuleLoader__.load({
|
|
|
1194
1240
|
match: "",
|
|
1195
1241
|
enabled: true,
|
|
1196
1242
|
color: null,
|
|
1197
|
-
bgMode: "fit",
|
|
1198
1243
|
wallpaperOpacity: 1,
|
|
1199
1244
|
blur: 0,
|
|
1200
1245
|
rotate: defaultRotation()
|
|
@@ -1255,8 +1300,17 @@ window.__ModuleLoader__.load({
|
|
|
1255
1300
|
.25
|
|
1256
1301
|
];
|
|
1257
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Layout mode of the CURRENT image (per image, see BgImage).
|
|
1305
|
+
*
|
|
1306
|
+
* Read from the image and never from the rule, which no longer has a mode at all:
|
|
1307
|
+
* a rotation through a photo and a screenshot has to be able to letterbox one and
|
|
1308
|
+
* fill the other, and a rule-level answer would be the wrong one for half the
|
|
1309
|
+
* list. A rule with no image has nothing to place, so it answers with the
|
|
1310
|
+
* default — the renderer's null case, painted as "no wallpaper".
|
|
1311
|
+
*/
|
|
1258
1312
|
function rBgMode() {
|
|
1259
|
-
return
|
|
1313
|
+
return activeImage()?.bgMode ?? "fit";
|
|
1260
1314
|
}
|
|
1261
1315
|
function rWop() {
|
|
1262
1316
|
return clamp01(activeRule()?.wallpaperOpacity, 1);
|
|
@@ -1329,6 +1383,14 @@ window.__ModuleLoader__.load({
|
|
|
1329
1383
|
}
|
|
1330
1384
|
rotIndex = 0;
|
|
1331
1385
|
}
|
|
1386
|
+
function resetConfig() {
|
|
1387
|
+
cfg = freshThemeConfig();
|
|
1388
|
+
clearImages();
|
|
1389
|
+
activeRuleId = null;
|
|
1390
|
+
activeMatched = false;
|
|
1391
|
+
modelLabel = "";
|
|
1392
|
+
rotIndex = 0;
|
|
1393
|
+
}
|
|
1332
1394
|
//#endregion
|
|
1333
1395
|
//#region src/client/rotation.ts
|
|
1334
1396
|
/** Dwell-time presets the settings UI offers, in ms. */
|
|
@@ -1352,10 +1414,6 @@ window.__ModuleLoader__.load({
|
|
|
1352
1414
|
{
|
|
1353
1415
|
ms: 18e5,
|
|
1354
1416
|
key: "rotEvery30m"
|
|
1355
|
-
},
|
|
1356
|
-
{
|
|
1357
|
-
ms: 36e5,
|
|
1358
|
-
key: "rotEvery1h"
|
|
1359
1417
|
}
|
|
1360
1418
|
];
|
|
1361
1419
|
/**
|
|
@@ -1510,7 +1568,7 @@ window.__ModuleLoader__.load({
|
|
|
1510
1568
|
const d = data;
|
|
1511
1569
|
const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
|
|
1512
1570
|
const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
|
|
1513
|
-
hostShape = schema === null ? "legacy" : schema >=
|
|
1571
|
+
hostShape = schema === null ? "legacy" : schema >= 6 ? "current" : "legacy";
|
|
1514
1572
|
if (hostShape === "legacy") holdWrite();
|
|
1515
1573
|
return {
|
|
1516
1574
|
config: d.config,
|
|
@@ -1559,6 +1617,52 @@ window.__ModuleLoader__.load({
|
|
|
1559
1617
|
error: v?.error ?? "failed"
|
|
1560
1618
|
};
|
|
1561
1619
|
}
|
|
1620
|
+
/**
|
|
1621
|
+
* The profile's manifest and config, in one answer. Reads nothing, writes nothing.
|
|
1622
|
+
*
|
|
1623
|
+
* Raw rather than `rpcCall` for the same reason `fetchImageUrl` is: the node half
|
|
1624
|
+
* answers this one with a value that CARRIES its own failure, and the `error`
|
|
1625
|
+
* inside it is the only thing worth showing the user — the generic "rpc failed"
|
|
1626
|
+
* log would throw that distinction away.
|
|
1627
|
+
*/
|
|
1628
|
+
async function fetchPresetConfig() {
|
|
1629
|
+
return unwrapPreset(await rpcRaw("fetchPresetConfig", {}));
|
|
1630
|
+
}
|
|
1631
|
+
/** One of the profile's wallpapers, addressed by the slot the config names. */
|
|
1632
|
+
async function fetchPresetImage(slot) {
|
|
1633
|
+
return unwrapPreset(await rpcRaw("fetchPresetImage", { slot }));
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* The envelope both profile calls share: a transport failure and a value-level
|
|
1637
|
+
* failure have to come out the same shape, because the page renders one message
|
|
1638
|
+
* either way and must not have to know which layer said no.
|
|
1639
|
+
*/
|
|
1640
|
+
function unwrapPreset(res) {
|
|
1641
|
+
if (!res) return {
|
|
1642
|
+
ok: false,
|
|
1643
|
+
error: "no response"
|
|
1644
|
+
};
|
|
1645
|
+
if (res.ok !== true) return {
|
|
1646
|
+
ok: false,
|
|
1647
|
+
error: res.error?.message ?? "request failed"
|
|
1648
|
+
};
|
|
1649
|
+
const v = res.value;
|
|
1650
|
+
if (v?.ok !== true) return {
|
|
1651
|
+
ok: false,
|
|
1652
|
+
error: typeof v?.error === "string" ? v.error : "failed"
|
|
1653
|
+
};
|
|
1654
|
+
return v;
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Empty the store: the config and every rule image.
|
|
1658
|
+
*
|
|
1659
|
+
* Only the recommended profile uses this, and only after its download has
|
|
1660
|
+
* already succeeded — see `applyRecommended`. Nothing else in the plugin deletes
|
|
1661
|
+
* the user's configuration wholesale.
|
|
1662
|
+
*/
|
|
1663
|
+
async function resetStore() {
|
|
1664
|
+
return await rpcCall("resetStore", {}) === true;
|
|
1665
|
+
}
|
|
1562
1666
|
/** Host default model — used only when the per-session services are absent. */
|
|
1563
1667
|
async function readDefaultModel() {
|
|
1564
1668
|
const data = await rpcCall("defaultModel", {});
|
|
@@ -1603,10 +1707,11 @@ window.__ModuleLoader__.load({
|
|
|
1603
1707
|
if (names.length === 0) return "";
|
|
1604
1708
|
const css = registrations(names, input.initial);
|
|
1605
1709
|
if (!(input.durationMs > 0)) return css;
|
|
1710
|
+
const written = new Set(input.written);
|
|
1606
1711
|
const timing = (n) => `${n} ${input.durationMs}ms ${input.easing}`;
|
|
1607
1712
|
return `${css}${input.scopes.map((s) => ({
|
|
1608
1713
|
selector: s.selector,
|
|
1609
|
-
transitions: [...s.tokens.filter((n) => names.includes(n)), ...s.properties ?? []]
|
|
1714
|
+
transitions: [...s.tokens.filter((n) => names.includes(n) && written.has(n)), ...s.properties ?? []]
|
|
1610
1715
|
})).filter((s) => s.transitions.length > 0).map((s) => `${s.selector}{transition:${s.transitions.map(timing).join(",")}}`).join("")}`;
|
|
1611
1716
|
}
|
|
1612
1717
|
/**
|
|
@@ -1777,6 +1882,7 @@ window.__ModuleLoader__.load({
|
|
|
1777
1882
|
"--dsw-alias-interactive-bg-hover": rgba(h(0), s(0), l(-.3), .08),
|
|
1778
1883
|
"--dsw-alias-interactive-bg-active": rgba(h(0), s(0), l(-.3), .14),
|
|
1779
1884
|
"--dsw-alias-markdown-code-block": hsl(h(0), s(-.1), l(-.03)),
|
|
1885
|
+
"--dsw-alias-markdown-code-block-banner": hsl(h(0), s(-.1), l(-.06)),
|
|
1780
1886
|
"--dsw-alias-markdown-inline-code": hsl(h(0), s(-.08), l(.04)),
|
|
1781
1887
|
"--dsw-specific-sidebar-fill": hsl(h(0), s(-.1), l(-.03)),
|
|
1782
1888
|
"--dsw-specific-sidebar-nav-item-active": hsl(h(0), s(-.08), l(.05)),
|
|
@@ -1998,11 +2104,43 @@ window.__ModuleLoader__.load({
|
|
|
1998
2104
|
.dab-root{position:relative;color:var(--dsw-alias-label-primary);animation:dab-fade-in .35s ease both;container-type:inline-size;display:flex;flex-direction:column;align-items:center;width:100%;min-width:0;--dab-mono:ui-monospace,"Cascadia Mono","SF Mono",Consolas,"Courier New",monospace}
|
|
1999
2105
|
.dab-root *,.dab-root *::before,.dab-root *::after{box-sizing:border-box}
|
|
2000
2106
|
.dab-root button{font-family:inherit}
|
|
2107
|
+
/* ── scrollbars: the same bar as the settings panel we sit inside ────────────
|
|
2108
|
+
* The host styles scrollbars globally off its --dsh-scrollbar-* tokens, but those
|
|
2109
|
+
* declarations describe the app shell and its own scroll containers; a strip
|
|
2110
|
+
* nested this deep got the platform default instead — a thick light bar with
|
|
2111
|
+
* arrows, which is what the tab strip used to show beside the panel's own slim
|
|
2112
|
+
* vertical one.
|
|
2113
|
+
*
|
|
2114
|
+
* So the plugin declares the block for its OWN scroll containers. The values are
|
|
2115
|
+
* the host theme package's, for the layer-2 surface this panel is painted in:
|
|
2116
|
+
* 5px, rounded, layer-2 tint, transparent track. Two notes for whoever edits it:
|
|
2117
|
+
*
|
|
2118
|
+
* - the tokens go ON THE SCROLLER (the selector that carries the overflow), not
|
|
2119
|
+
* on .dab-root. A container that declares scrollbar-width:thin (as the nav
|
|
2120
|
+
* rail does on narrow panels) paints its own standard bar regardless of the
|
|
2121
|
+
* webkit block, and the host's standard-property fallback reads the thumb
|
|
2122
|
+
* token from that same element.
|
|
2123
|
+
* - every selector here is scoped to a dab- class: the stylesheet is injected
|
|
2124
|
+
* into the host page once, so a bare webkit-scrollbar selector would restyle
|
|
2125
|
+
* the whole harness. */
|
|
2126
|
+
.dab-tabs,.dab-pick-menu{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);--dsh-scrollbar-width:5px}
|
|
2127
|
+
.dab-tabs::-webkit-scrollbar,.dab-pick-menu::-webkit-scrollbar{width:var(--dsh-scrollbar-width,5px);height:var(--dsh-scrollbar-width,5px)}
|
|
2128
|
+
.dab-tabs::-webkit-scrollbar-track,.dab-pick-menu::-webkit-scrollbar-track{background:transparent}
|
|
2129
|
+
.dab-tabs::-webkit-scrollbar-thumb,.dab-pick-menu::-webkit-scrollbar-thumb{background:var(--dsh-scrollbar-thumb);background-clip:content-box;border-radius:999px}
|
|
2130
|
+
.dab-tabs::-webkit-scrollbar-thumb:hover,.dab-pick-menu::-webkit-scrollbar-thumb:hover{background:var(--dsh-scrollbar-thumb-hover)}
|
|
2131
|
+
.dab-tabs::-webkit-scrollbar-corner,.dab-pick-menu::-webkit-scrollbar-corner{background:transparent}
|
|
2132
|
+
/* The host hides the platform's stepper arrows; without this the strip keeps the
|
|
2133
|
+
little os arrows at both ends of the track while the panel's bar has none. */
|
|
2134
|
+
.dab-tabs::-webkit-scrollbar-button,.dab-pick-menu::-webkit-scrollbar-button{display:none}
|
|
2001
2135
|
|
|
2002
2136
|
/* ── shell: nav rail + page body ─────────────────────────────────────────── */
|
|
2003
2137
|
.dab-shell{display:grid;grid-template-columns:158px minmax(0,1fr);gap:26px;align-items:start;padding-bottom:8px;width:100%;max-width:980px;margin:0 auto}
|
|
2004
2138
|
.dab-nav{position:sticky;top:0;display:flex;flex-direction:column;gap:18px}
|
|
2005
|
-
|
|
2139
|
+
/* The brand is an <a> (it opens the repository): an anchor brings its own colour
|
|
2140
|
+
and underline, so both are reset here rather than left to the host's link
|
|
2141
|
+
styles, which would repaint the tile's label. */
|
|
2142
|
+
.dab-brand{display:flex;align-items:center;gap:10px;padding:2px 6px;color:inherit;text-decoration:none}
|
|
2143
|
+
.dab-brand:hover .dab-brand-name{color:var(--dsw-alias-brand-primary)}
|
|
2006
2144
|
.dab-brand-tile{width:30px;height:30px;flex:none;border-radius:9px;display:grid;place-items:center;color:var(--dsw-alias-label-primary-inverted);background:var(--dsw-alias-brand-primary);box-shadow:0 4px 14px -4px var(--dsw-alias-brand-primary)}
|
|
2007
2145
|
.dab-brand-name{font-size:13px;font-weight:650;letter-spacing:.01em;line-height:1.25}
|
|
2008
2146
|
.dab-brand-tag{font-size:9px;letter-spacing:.16em;font-weight:600;color:var(--dsw-alias-label-quaternary,var(--dsw-alias-label-tertiary));text-transform:uppercase}
|
|
@@ -2019,7 +2157,10 @@ window.__ModuleLoader__.load({
|
|
|
2019
2157
|
.dab-head{margin:2px 0 5px}
|
|
2020
2158
|
.dab-overline{font-size:10.5px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--dsw-alias-brand-primary);opacity:.9}
|
|
2021
2159
|
.dab-h1{margin:4px 0 0;font-size:21px;font-weight:700;letter-spacing:-.01em}
|
|
2022
|
-
|
|
2160
|
+
/* No max-width on the page description: it is a single line of context under the
|
|
2161
|
+
title, and capping it at a prose measure left it wrapping early with dead space
|
|
2162
|
+
to its right while the cards below ran the full width of the panel. */
|
|
2163
|
+
.dab-desc{margin:6px 0 0;font-size:12.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary)}
|
|
2023
2164
|
.dab-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;padding:18px}
|
|
2024
2165
|
.dab-card-hover{transition:transform .28s ease,box-shadow .28s ease,border-color .28s ease}
|
|
2025
2166
|
.dab-card-hover:hover{transform:translateY(-2px);box-shadow:0 10px 28px -12px rgba(0,0,0,.28)}
|
|
@@ -2036,10 +2177,6 @@ window.__ModuleLoader__.load({
|
|
|
2036
2177
|
|
|
2037
2178
|
/* ── swatches ────────────────────────────────────────────────────────────── */
|
|
2038
2179
|
.dab-swatch-title{font-size:12px;font-weight:600;margin-bottom:10px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
2039
|
-
.dab-swatches{display:flex;flex-wrap:wrap;gap:9px}
|
|
2040
|
-
.dab-swatch{width:25px;height:25px;border-radius:50%;border:0;padding:0;cursor:pointer;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1);transition:transform .22s cubic-bezier(.34,1.56,.64,1),box-shadow .22s ease}
|
|
2041
|
-
.dab-swatch:hover{transform:scale(1.2)}
|
|
2042
|
-
.dab-swatch.is-on{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1),0 0 0 4px var(--dsw-alias-brand-primary)}
|
|
2043
2180
|
|
|
2044
2181
|
/* ── wheel card ──────────────────────────────────────────────────────────── */
|
|
2045
2182
|
.dab-wheel-card{position:relative;display:flex;align-items:center;justify-content:center;gap:28px;flex-wrap:wrap;padding:24px 18px}
|
|
@@ -2089,10 +2226,15 @@ window.__ModuleLoader__.load({
|
|
|
2089
2226
|
.dab-btn-danger{color:var(--dsw-alias-state-error-primary)}
|
|
2090
2227
|
.dab-btn-ghost{background:transparent;border-color:transparent;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
2091
2228
|
.dab-btn-ghost:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);box-shadow:none}
|
|
2092
|
-
.dab-btn:focus-visible,.dab-nav-item:focus-visible,.dab-seg-item:focus-visible
|
|
2229
|
+
.dab-btn:focus-visible,.dab-nav-item:focus-visible,.dab-seg-item:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
2093
2230
|
.dab-chip-row{display:flex;flex-wrap:wrap;gap:8px}
|
|
2094
2231
|
.dab-chip{height:30px;padding:0 14px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .22s ease}
|
|
2095
2232
|
.dab-chip:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
|
|
2233
|
+
/* A mode chip on a rule with no picture: there is nothing to lay out. It reads
|
|
2234
|
+
as unavailable rather than as an active choice, and the hover that would
|
|
2235
|
+
promise otherwise is dropped with it. */
|
|
2236
|
+
.dab-chip:disabled{opacity:.45;cursor:not-allowed}
|
|
2237
|
+
.dab-chip:disabled:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
2096
2238
|
.dab-chip.is-active{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted);border-color:transparent}
|
|
2097
2239
|
|
|
2098
2240
|
/* ── sliders ─────────────────────────────────────────────────────────────── */
|
|
@@ -2151,6 +2293,10 @@ window.__ModuleLoader__.load({
|
|
|
2151
2293
|
.dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
|
|
2152
2294
|
.dab-footer{margin-top:6px;padding:14px 4px 0;border-top:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
2153
2295
|
.dab-footer-mono{font-family:var(--dab-mono);letter-spacing:.02em}
|
|
2296
|
+
/* Same reset as the brand: the package name links to the repository, and the
|
|
2297
|
+
underline appears on hover where it reads as an affordance instead of chrome. */
|
|
2298
|
+
.dab-footer a{color:inherit;text-decoration:none}
|
|
2299
|
+
.dab-footer a:hover{color:var(--dsw-alias-brand-primary);text-decoration:underline}
|
|
2154
2300
|
|
|
2155
2301
|
/* ── toast ───────────────────────────────────────────────────────────────── */
|
|
2156
2302
|
.dab-toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%);display:flex;align-items:center;gap:8px;height:38px;padding:0 16px;border-radius:99px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2);box-shadow:0 10px 30px -8px rgba(0,0,0,.38);font-size:12.5px;z-index:10001;animation:dab-toast-in .32s cubic-bezier(.22,1,.36,1) both}
|
|
@@ -2168,12 +2314,83 @@ window.__ModuleLoader__.load({
|
|
|
2168
2314
|
.dab-modal-card{animation:dab-zoom-in .3s cubic-bezier(.22,1,.36,1) both;max-width:calc(100vw - 40px);max-height:calc(100vh - 120px);overflow:auto}
|
|
2169
2315
|
.dab-overlay .dab-btn{background:rgba(255,255,255,.94);color:#14161a;border-color:transparent}
|
|
2170
2316
|
.dab-overlay .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted)}
|
|
2317
|
+
/* The destructive choice has to survive the white-on-dark treatment above: a
|
|
2318
|
+
confirmation whose "delete everything" button looks exactly like its "cancel"
|
|
2319
|
+
is the one place where losing the distinction actually costs something. */
|
|
2320
|
+
.dab-overlay .dab-btn-danger{background:#dc2626;color:#fff;border-color:transparent}
|
|
2171
2321
|
.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}
|
|
2172
2322
|
.dab-crash-title{font-size:15px;font-weight:650}
|
|
2173
2323
|
.dab-crash-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.5}
|
|
2174
2324
|
|
|
2175
2325
|
/* ── model rules ─────────────────────────────────────────────────────────── */
|
|
2176
|
-
|
|
2326
|
+
/* ── rule tab strip ──────────────────────────────────────────────────────────
|
|
2327
|
+
* The rules are edited ONE at a time: every rule is a tab in a single sideways
|
|
2328
|
+
* scrolling row, and the page renders exactly one editor under it. The height of
|
|
2329
|
+
* this panel is therefore a function of one card, not of the rule count — which
|
|
2330
|
+
* is what the stacked accordion could not promise.
|
|
2331
|
+
*
|
|
2332
|
+
* The strip scrolls instead of wrapping for the same reason: a wrapped strip
|
|
2333
|
+
* would grow a row taller with every rule and hand the page height straight back
|
|
2334
|
+
* to the list. Tabs are fixed-height and the match label elides, so switching
|
|
2335
|
+
* rules can never reflow the row either. */
|
|
2336
|
+
/* Deliberately NOT sticky: the horizontal scroller above the tabs clips on BOTH
|
|
2337
|
+
axes, so the "which rule" menu — an absolutely positioned child — would be cut
|
|
2338
|
+
off at the strip's own bottom edge instead of floating over the page.
|
|
2339
|
+
Switching rules stays a click near the top of the panel, which is one screen
|
|
2340
|
+
away at every realistic rule count. */
|
|
2341
|
+
/* The tiles are aligned by their TOP edge, not by centering: the strip's box is
|
|
2342
|
+
taller than a tab (its height includes the horizontal scrollbar, which no other
|
|
2343
|
+
tile in the row has), so centering against the row would put the two icon
|
|
2344
|
+
buttons a few pixels below the tabs they sit beside. Top alignment makes the
|
|
2345
|
+
strip's top and the buttons' top the same line; the strip's own 3px top padding
|
|
2346
|
+
is mirrored on the buttons, and everything below the bar comes from the strip's
|
|
2347
|
+
bottom padding — so the bar keeps the same distance from the TAB bottoms and
|
|
2348
|
+
from the icon tiles' bottoms. */
|
|
2349
|
+
.dab-tabs-row{display:flex;align-items:flex-start;gap:8px;padding-bottom:0;margin-bottom:6px}
|
|
2350
|
+
/* The strip is the scroller the scrollbar block above is written for. Its bottom
|
|
2351
|
+
padding is the whole gap under the tiles: the bar is painted at the bottom of
|
|
2352
|
+
this box, so this is what keeps it off the tabs (and, because the row adds
|
|
2353
|
+
nothing here, off the icon buttons beside them too). */
|
|
2354
|
+
.dab-tabs{flex:1;min-width:0;display:flex;align-items:center;gap:6px;overflow-x:auto;overscroll-behavior-x:contain;padding:3px 2px 8px}
|
|
2355
|
+
.dab-tab{height:34px;flex:none;display:flex;align-items:center;gap:7px;padding:0 11px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-size:12.5px;cursor:pointer;max-width:196px;transition:color .2s,border-color .2s,background .2s,opacity .2s}
|
|
2356
|
+
.dab-tab:hover{color:var(--dsw-alias-label-primary);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,var(--dsw-alias-border-l2))}
|
|
2357
|
+
.dab-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
2358
|
+
/* The selected tab reads like the nav rail's own indicator: a brand tint, a
|
|
2359
|
+
brand border, and the label lifted to full contrast. */
|
|
2360
|
+
.dab-tab.is-active{color:var(--dsw-alias-brand-primary);font-weight:600;border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent)}
|
|
2361
|
+
.dab-tab.is-off{opacity:.5}
|
|
2362
|
+
.dab-tab:not(.is-active).is-off:hover{opacity:.75}
|
|
2363
|
+
.dab-tab-thumb{width:20px;height:20px;flex:none;border-radius:5px;object-fit:cover;border:1px solid var(--dsw-alias-border-l2)}
|
|
2364
|
+
.dab-tab-num{font-family:var(--dab-mono);font-size:10.5px;font-weight:700;opacity:.8}
|
|
2365
|
+
.dab-tab-match{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2366
|
+
.dab-tab-live{width:6px;height:6px;flex:none;border-radius:50%;background:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,transparent);animation:dab-pulse 1.8s ease-in-out infinite}
|
|
2367
|
+
.dab-tab-rot{flex:none;font-size:11px;line-height:1;opacity:.75}
|
|
2368
|
+
/* The strip's two actions — add a rule, open the rule list — are square icon
|
|
2369
|
+
tiles beside the scroller rather than tiles inside it: inside, a long rule list
|
|
2370
|
+
pushed them off the end, so the one control that ADDS a rule could only be
|
|
2371
|
+
reached by scrolling to the bottom of the rules. They keep the tab's box so the
|
|
2372
|
+
row stays one height, and the icon is the whole label (title + aria-label). */
|
|
2373
|
+
/* The top margin mirrors the strip's own padding-top, so the icon inside the tile
|
|
2374
|
+
sits on the same line as the text inside the tabs. */
|
|
2375
|
+
.dab-tab-icon{width:34px;margin-top:3px;padding:0;justify-content:center;color:var(--dsw-alias-label-tertiary);max-width:none}
|
|
2376
|
+
.dab-tab-icon:hover{color:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
|
|
2377
|
+
|
|
2378
|
+
/* ── "which rule" picker (the strip's overflow menu) ───────────────────────── */
|
|
2379
|
+
.dab-pick{position:relative;flex:none}
|
|
2380
|
+
.dab-pick-scrim{position:fixed;inset:0;z-index:40;background:transparent}
|
|
2381
|
+
.dab-pick-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:41;min-width:220px;max-height:284px;overflow-y:auto;display:flex;flex-direction:column;gap:2px;padding:5px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);box-shadow:0 18px 44px -14px rgba(0,0,0,.45);animation:dab-rise-in .18s ease both}
|
|
2382
|
+
.dab-pick-item{display:flex;align-items:center;gap:9px;padding:7px 9px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));font-size:12.5px;text-align:left;cursor:pointer}
|
|
2383
|
+
.dab-pick-item:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
2384
|
+
.dab-pick-item.is-active{color:var(--dsw-alias-brand-primary);font-weight:600;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
2385
|
+
.dab-pick-match{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2386
|
+
.dab-pick-n{flex:none;font-family:var(--dsw-mono);font-size:10.5px;opacity:.7}
|
|
2387
|
+
|
|
2388
|
+
/* ── one rule editor (the tab panel's content) ─────────────────────────────── */
|
|
2389
|
+
.dab-rule-panel{min-height:0}
|
|
2390
|
+
/* The armed delete button stops being a 28px icon box and says what the second
|
|
2391
|
+
click will do — a worded state, not only a colour one. */
|
|
2392
|
+
.dab-icon-btn.is-armed{width:auto;padding:0 9px;color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}
|
|
2393
|
+
.dab-confirm-txt{font-size:11px;font-weight:600;white-space:nowrap}
|
|
2177
2394
|
.dab-rule{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;padding:14px;transition:border-color .25s,box-shadow .25s,opacity .25s}
|
|
2178
2395
|
.dab-rule.is-active{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 16%,transparent)}
|
|
2179
2396
|
.dab-rule.is-off{opacity:.55}
|
|
@@ -2425,6 +2642,10 @@ window.__ModuleLoader__.load({
|
|
|
2425
2642
|
"--dsw-menu-surface-fill"
|
|
2426
2643
|
]
|
|
2427
2644
|
},
|
|
2645
|
+
{
|
|
2646
|
+
part: "code",
|
|
2647
|
+
names: ["--dsw-alias-markdown-code-block", "--dsw-alias-markdown-code-block-banner"]
|
|
2648
|
+
},
|
|
2428
2649
|
{
|
|
2429
2650
|
part: "input",
|
|
2430
2651
|
names: ["--dsw-specific-input-major"]
|
|
@@ -2438,7 +2659,9 @@ window.__ModuleLoader__.load({
|
|
|
2438
2659
|
"--dsw-alias-bg-layer-3": "--dsh-any-op-card-3",
|
|
2439
2660
|
"--dsw-specific-input-major": "--dsh-any-op-input",
|
|
2440
2661
|
"--dsw-specific-menu": "--dsh-any-op-menu",
|
|
2441
|
-
"--dsw-menu-surface-fill": "--dsh-any-op-menu"
|
|
2662
|
+
"--dsw-menu-surface-fill": "--dsh-any-op-menu",
|
|
2663
|
+
"--dsw-alias-markdown-code-block": "--dsh-any-op-code",
|
|
2664
|
+
"--dsw-alias-markdown-code-block-banner": "--dsh-any-op-code-banner"
|
|
2442
2665
|
};
|
|
2443
2666
|
let baseTokenKey = "";
|
|
2444
2667
|
const HOST_SURFACE_TOKENS = Object.keys(OPACITY_VARS);
|
|
@@ -2663,7 +2886,7 @@ window.__ModuleLoader__.load({
|
|
|
2663
2886
|
const scheme = own ? forceDark ? "dark" : "light" : resolveHostDark() ? "dark" : "light";
|
|
2664
2887
|
const fadeable = paletteChanged && paletteFadeAllowed(paletteScheme, scheme);
|
|
2665
2888
|
lastFadeable = fadeable;
|
|
2666
|
-
applyPaletteFade(fadeable);
|
|
2889
|
+
applyPaletteFade(fadeable, Object.keys(tokens));
|
|
2667
2890
|
if (paletteChanged) {
|
|
2668
2891
|
paletteScheme = scheme;
|
|
2669
2892
|
if (own) {
|
|
@@ -2675,7 +2898,7 @@ window.__ModuleLoader__.load({
|
|
|
2675
2898
|
const opVar = OPACITY_VARS[name];
|
|
2676
2899
|
decls.push(`${name}:${opVar !== void 0 ? `var(${opVar})` : value}!important`);
|
|
2677
2900
|
}
|
|
2678
|
-
ensureTokenStyle().textContent =
|
|
2901
|
+
ensureTokenStyle().textContent = `:root,body{${decls.join(";")}}`;
|
|
2679
2902
|
if (own) {
|
|
2680
2903
|
for (const name of appliedTokenNames) document.body.style.removeProperty(name);
|
|
2681
2904
|
appliedTokenNames = Object.keys(tokens);
|
|
@@ -2745,14 +2968,26 @@ window.__ModuleLoader__.load({
|
|
|
2745
2968
|
"--dsw-alias-bg-layer-2",
|
|
2746
2969
|
"--dsw-alias-bg-layer-3"
|
|
2747
2970
|
];
|
|
2748
|
-
/** The token names
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
2971
|
+
/** The token names a palette CAN carry — deliberately the widest branch.
|
|
2972
|
+
*
|
|
2973
|
+
* `genTokens(220, 0.55, 0.25)` resolves to the DARK branch (l = 0.25), and that
|
|
2974
|
+
* branch is the superset: the light one emits roughly thirty fewer aliases (no
|
|
2975
|
+
* bubble, selector, tip, tooltip/toast, toolbar and markdown state tokens). This
|
|
2976
|
+
* list is therefore what the REGISTRATIONS are emitted for — a later light→dark
|
|
2977
|
+
* change has to find the dark names already registered — and NOT the transition
|
|
2978
|
+
* list, which is narrowed per apply to the palette in force (`written` in
|
|
2979
|
+
* ./palette-fade). Assuming the two branches carry the same set is exactly the
|
|
2980
|
+
* mistake that let the fade animate host-owned tokens: a name only the dark
|
|
2981
|
+
* branch declares is, under a light palette, the HOST's value.
|
|
2982
|
+
*
|
|
2983
|
+
* An unknown name is only registered when the host can answer for it (see
|
|
2984
|
+
* `fadeInitials`). */
|
|
2751
2985
|
function fadeTokenNames() {
|
|
2752
2986
|
return Object.keys(genTokens(220, .55, .25).tokens);
|
|
2753
2987
|
}
|
|
2754
2988
|
/** One `transition` list per element that DECLARES palette tokens, each carrying
|
|
2755
|
-
* only the tokens that element actually declares
|
|
2989
|
+
* only the tokens that element actually declares — and, at emission, only those
|
|
2990
|
+
* the palette in force writes (see `written`).
|
|
2756
2991
|
*
|
|
2757
2992
|
* Two of the entries carry a STANDARD property instead, because those surfaces
|
|
2758
2993
|
* are not painted from a registered token: the settings dialog's plate and the
|
|
@@ -2764,6 +2999,10 @@ window.__ModuleLoader__.load({
|
|
|
2764
2999
|
* re-declares IS one, naming the token is what reaches it. */
|
|
2765
3000
|
function fadeScopes() {
|
|
2766
3001
|
return [
|
|
3002
|
+
{
|
|
3003
|
+
selector: ":root",
|
|
3004
|
+
tokens: fadeTokenNames()
|
|
3005
|
+
},
|
|
2767
3006
|
{
|
|
2768
3007
|
selector: "body",
|
|
2769
3008
|
tokens: fadeTokenNames()
|
|
@@ -2851,12 +3090,21 @@ window.__ModuleLoader__.load({
|
|
|
2851
3090
|
*
|
|
2852
3091
|
* `animate` is false on every pass that only moves an alpha (a slider drag): the
|
|
2853
3092
|
* tokens are rewritten there too, and a live transition would make the surface
|
|
2854
|
-
* lag behind the pointer.
|
|
2855
|
-
|
|
3093
|
+
* lag behind the pointer.
|
|
3094
|
+
*
|
|
3095
|
+
* `written` is the token set of the palette IN FORCE — `Object.keys(tokens)` of
|
|
3096
|
+
* the very block this pass is about to write — and only those may carry a
|
|
3097
|
+
* transition: a name the palette does not write still belongs to the host, and a
|
|
3098
|
+
* transition on it animates the host's own scheme change at this plugin's
|
|
3099
|
+
* duration (see ./palette-fade for the report that shape produced). The
|
|
3100
|
+
* registrations stay on the full set, so a later palette of the other branch
|
|
3101
|
+
* still has them in place. */
|
|
3102
|
+
function applyPaletteFade(animate, written) {
|
|
2856
3103
|
const initials = fadeInitialsOf(fadeTokenNames());
|
|
2857
3104
|
const css = paletteFadeCss({
|
|
2858
3105
|
scopes: fadeScopes(),
|
|
2859
3106
|
initial: initials,
|
|
3107
|
+
written,
|
|
2860
3108
|
durationMs: animate ? paletteFadeMs() : 0,
|
|
2861
3109
|
easing: rTransition().easing
|
|
2862
3110
|
});
|
|
@@ -4069,6 +4317,24 @@ window.__ModuleLoader__.load({
|
|
|
4069
4317
|
rx: "2"
|
|
4070
4318
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.6v2M7 8h2" })]
|
|
4071
4319
|
});
|
|
4320
|
+
/** Angle brackets: the code-block surface on the Interface page. */
|
|
4321
|
+
const CodeIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
|
|
4322
|
+
size,
|
|
4323
|
+
className,
|
|
4324
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.7 4.3 2.4 8l3.3 3.7M10.3 4.3 13.6 8l-3.3 3.7" })
|
|
4325
|
+
});
|
|
4326
|
+
/** Appends a rule — the tab strip's own action, beside the strip it adds to. */
|
|
4327
|
+
const PlusIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
|
|
4328
|
+
size,
|
|
4329
|
+
className,
|
|
4330
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 3.4v9.2M3.4 8h9.2" })
|
|
4331
|
+
});
|
|
4332
|
+
/** Opens the "which rule" menu: down means a list drops out under the button. */
|
|
4333
|
+
const ChevronDownIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
|
|
4334
|
+
size,
|
|
4335
|
+
className,
|
|
4336
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.3 6.4 8 10.1l3.7-3.7" })
|
|
4337
|
+
});
|
|
4072
4338
|
//#endregion
|
|
4073
4339
|
//#region src/client/components/ErrorBoundary.tsx
|
|
4074
4340
|
/**
|
|
@@ -4139,6 +4405,19 @@ window.__ModuleLoader__.load({
|
|
|
4139
4405
|
return target ? (0, react_dom.createPortal)(children, target) : null;
|
|
4140
4406
|
}
|
|
4141
4407
|
//#endregion
|
|
4408
|
+
//#region src/client/repo.ts
|
|
4409
|
+
/**
|
|
4410
|
+
* The project's public repository — the ONE address the shell links to.
|
|
4411
|
+
*
|
|
4412
|
+
* Both links the interface ships (the brand in the header, the package name in
|
|
4413
|
+
* the profile footer) read this constant: two literals would drift the day the
|
|
4414
|
+
* repository moves, and a link that points at the wrong place is indistinguishable
|
|
4415
|
+
* from a link that works until someone clicks it. `scripts/ui-strings-check.ts`
|
|
4416
|
+
* holds it against `package.json`'s `repository`, which is what npm and the
|
|
4417
|
+
* plugin market read.
|
|
4418
|
+
*/
|
|
4419
|
+
const REPO_URL = "https://github.com/HarmlessFunny/dsh-background-by-model";
|
|
4420
|
+
//#endregion
|
|
4142
4421
|
//#region src/client/components/LiveSlider.tsx
|
|
4143
4422
|
/**
|
|
4144
4423
|
* Zero-lag slider: the thumb and value label update through DOM refs while
|
|
@@ -4235,6 +4514,12 @@ window.__ModuleLoader__.load({
|
|
|
4235
4514
|
labelKey: "uiOpacityCard",
|
|
4236
4515
|
Icon: ChatIcon
|
|
4237
4516
|
},
|
|
4517
|
+
{
|
|
4518
|
+
opKey: "code",
|
|
4519
|
+
labelKey: "uiOpacityCode",
|
|
4520
|
+
Icon: CodeIcon,
|
|
4521
|
+
noBlur: true
|
|
4522
|
+
},
|
|
4238
4523
|
{
|
|
4239
4524
|
opKey: "input",
|
|
4240
4525
|
labelKey: "uiOpacityInput",
|
|
@@ -4278,9 +4563,9 @@ window.__ModuleLoader__.load({
|
|
|
4278
4563
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4279
4564
|
className: "dab-grid-parts",
|
|
4280
4565
|
children: PARTS.map((part, i) => {
|
|
4281
|
-
const { labelKey, Icon, isSettings, isChat, isTrajectory, isRightbar } = part;
|
|
4566
|
+
const { labelKey, Icon, isSettings, isChat, isTrajectory, isRightbar, noBlur } = part;
|
|
4282
4567
|
const opKey = part.opKey;
|
|
4283
|
-
const blurKey = isChat ? "chat" : isTrajectory ? "trajectory" : isRightbar ? "rightbar" : isSettings ? "settings" : opKey;
|
|
4568
|
+
const blurKey = isChat ? "chat" : isTrajectory ? "trajectory" : isRightbar ? "rightbar" : isSettings ? "settings" : noBlur ? null : opKey;
|
|
4284
4569
|
const opacity = isChat ? rChatTextOpacity() : isTrajectory ? rTrajectoryOpacity() : isRightbar ? rRightbarOpacity() : isSettings ? rSop() : rOps()[opKey];
|
|
4285
4570
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4286
4571
|
className: "dab-card dab-card-hover dab-rise",
|
|
@@ -4351,7 +4636,7 @@ window.__ModuleLoader__.load({
|
|
|
4351
4636
|
}
|
|
4352
4637
|
}
|
|
4353
4638
|
}),
|
|
4354
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
4639
|
+
blurKey !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
4355
4640
|
label: t("uiBlur"),
|
|
4356
4641
|
min: 0,
|
|
4357
4642
|
max: 60,
|
|
@@ -4372,11 +4657,47 @@ window.__ModuleLoader__.load({
|
|
|
4372
4657
|
}
|
|
4373
4658
|
})
|
|
4374
4659
|
]
|
|
4375
|
-
}, blurKey);
|
|
4660
|
+
}, blurKey ?? opKey);
|
|
4376
4661
|
})
|
|
4377
4662
|
})] });
|
|
4378
4663
|
}
|
|
4379
4664
|
//#endregion
|
|
4665
|
+
//#region src/client/rules-view.ts
|
|
4666
|
+
/**
|
|
4667
|
+
* The rule the editor should show.
|
|
4668
|
+
*
|
|
4669
|
+
* `sel` wins whenever it is still in the list: the selection is the user's, and
|
|
4670
|
+
* a model switch, a reorder or an image edit must not move it. The fallbacks run
|
|
4671
|
+
* only when there is no selection at all, or when the rule it named is gone
|
|
4672
|
+
* (deleted here, or replaced wholesale by an import or by the persisted config
|
|
4673
|
+
* landing after the first render — whose ids are not the ones we started with).
|
|
4674
|
+
* The active rule is preferred over the first one so that opening the panel
|
|
4675
|
+
* lands on the rule the current model is using.
|
|
4676
|
+
*/
|
|
4677
|
+
function resolveTab(rules, sel, activeId) {
|
|
4678
|
+
if (rules.length === 0) return null;
|
|
4679
|
+
if (sel !== null) {
|
|
4680
|
+
const held = rules.find((r) => r.id === sel);
|
|
4681
|
+
if (held !== void 0) return held;
|
|
4682
|
+
}
|
|
4683
|
+
if (activeId !== null) {
|
|
4684
|
+
const live = rules.find((r) => r.id === activeId);
|
|
4685
|
+
if (live !== void 0) return live;
|
|
4686
|
+
}
|
|
4687
|
+
return rules[0] ?? null;
|
|
4688
|
+
}
|
|
4689
|
+
/**
|
|
4690
|
+
* The rule to show once `id` has been removed: the next one, or the previous one
|
|
4691
|
+
* when the removed rule was last. Deliberately NOT the first rule — the user is
|
|
4692
|
+
* working down a list, and jumping to the top loses their place — and `null`
|
|
4693
|
+
* when it was the only rule, which the panel renders as "no rules yet".
|
|
4694
|
+
*/
|
|
4695
|
+
function neighbourAfter(rules, id) {
|
|
4696
|
+
const i = rules.findIndex((r) => r.id === id);
|
|
4697
|
+
if (i < 0) return null;
|
|
4698
|
+
return rules[i + 1] ?? rules[i - 1] ?? null;
|
|
4699
|
+
}
|
|
4700
|
+
//#endregion
|
|
4380
4701
|
//#region src/client/utils/image.ts
|
|
4381
4702
|
/**
|
|
4382
4703
|
* Read a chosen image file as a data URL WITHOUT re-encoding: the original
|
|
@@ -5153,16 +5474,6 @@ window.__ModuleLoader__.load({
|
|
|
5153
5474
|
.55,
|
|
5154
5475
|
.25
|
|
5155
5476
|
];
|
|
5156
|
-
/**
|
|
5157
|
-
* Failing-hop codes the model watcher can report, mapped to their copy. Only
|
|
5158
|
-
* these are rendered: `waiting` and `fallback` already have their own hints.
|
|
5159
|
-
*/
|
|
5160
|
-
const MODEL_NOTE_KEYS = {
|
|
5161
|
-
"no-service": "statusNoteNoService",
|
|
5162
|
-
"no-session": "statusNoteNoSession",
|
|
5163
|
-
"no-projection": "statusNoteNoProjection",
|
|
5164
|
-
"empty-selection": "statusNoteEmptySelection"
|
|
5165
|
-
};
|
|
5166
5477
|
function toHex(rgb) {
|
|
5167
5478
|
return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
|
|
5168
5479
|
}
|
|
@@ -5170,171 +5481,307 @@ window.__ModuleLoader__.load({
|
|
|
5170
5481
|
const { t, useStore } = p;
|
|
5171
5482
|
const store = useStore((s) => s);
|
|
5172
5483
|
const rules = cfg.rules;
|
|
5173
|
-
const [
|
|
5174
|
-
|
|
5484
|
+
const [selId, setSelId] = (0, react.useState)(() => resolveTab(rules, null, store.activeRuleId)?.id ?? null);
|
|
5485
|
+
/** True once the user has picked a tab themselves, which stops the follow above. */
|
|
5486
|
+
const pickedRef = (0, react.useRef)(false);
|
|
5175
5487
|
(0, react.useEffect)(() => {
|
|
5176
|
-
if (
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
children:
|
|
5250
|
-
}),
|
|
5251
|
-
store.model === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5252
|
-
className: "dab-hint",
|
|
5253
|
-
style: { marginTop: 6 },
|
|
5254
|
-
children: t("statusUnknownHint")
|
|
5255
|
-
}) : null,
|
|
5256
|
-
store.model !== "" && store.modelSource === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5257
|
-
className: "dab-hint",
|
|
5258
|
-
style: { marginTop: 6 },
|
|
5259
|
-
children: t("statusSourceDefaultHint")
|
|
5260
|
-
}) : null,
|
|
5261
|
-
MODEL_NOTE_KEYS[store.modelNote] !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5488
|
+
if (pickedRef.current) return;
|
|
5489
|
+
const held = resolveTab(rules, selId, store.activeRuleId);
|
|
5490
|
+
if (held !== null && held.id !== selId) setSelId(held.id);
|
|
5491
|
+
}, [
|
|
5492
|
+
store.rulesRev,
|
|
5493
|
+
store.activeRuleId,
|
|
5494
|
+
selId,
|
|
5495
|
+
rules
|
|
5496
|
+
]);
|
|
5497
|
+
const active = resolveTab(rules, selId, store.activeRuleId)?.id ?? null;
|
|
5498
|
+
const [selImage, setSelImage] = (0, react.useState)({});
|
|
5499
|
+
const selIdxOf = (id) => selImage[id] ?? 0;
|
|
5500
|
+
const setSelIdxOf = (id, i) => setSelImage((m) => m[id] === i ? m : {
|
|
5501
|
+
...m,
|
|
5502
|
+
[id]: i
|
|
5503
|
+
});
|
|
5504
|
+
/** Select a rule as the edited one; keyboard stepping shares this. */
|
|
5505
|
+
const selectRule = (id) => {
|
|
5506
|
+
pickedRef.current = true;
|
|
5507
|
+
setSelId(id);
|
|
5508
|
+
};
|
|
5509
|
+
/** Delete a rule and land on its neighbour — never on the first rule. */
|
|
5510
|
+
const removeRule = (id) => {
|
|
5511
|
+
if (id === active) {
|
|
5512
|
+
const next = neighbourAfter(rules, id);
|
|
5513
|
+
pickedRef.current = true;
|
|
5514
|
+
setSelId(next?.id ?? null);
|
|
5515
|
+
}
|
|
5516
|
+
setSelImage((m) => {
|
|
5517
|
+
const n = { ...m };
|
|
5518
|
+
delete n[id];
|
|
5519
|
+
return n;
|
|
5520
|
+
});
|
|
5521
|
+
p.removeRule(id);
|
|
5522
|
+
};
|
|
5523
|
+
const addRule = () => {
|
|
5524
|
+
pickedRef.current = true;
|
|
5525
|
+
setSelId(p.addRule());
|
|
5526
|
+
};
|
|
5527
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
5528
|
+
className: "dab-head dab-rise",
|
|
5529
|
+
style: { "--d": 0 },
|
|
5530
|
+
children: [
|
|
5531
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5532
|
+
className: "dab-overline",
|
|
5533
|
+
children: "Rules"
|
|
5534
|
+
}),
|
|
5535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
5536
|
+
className: "dab-h1",
|
|
5537
|
+
children: t("pageModelBg")
|
|
5538
|
+
}),
|
|
5539
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5540
|
+
className: "dab-desc",
|
|
5541
|
+
children: t("descModelBg")
|
|
5542
|
+
})
|
|
5543
|
+
]
|
|
5544
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
5545
|
+
className: "dab-rise",
|
|
5546
|
+
style: { "--d": 1 },
|
|
5547
|
+
children: [
|
|
5548
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5549
|
+
className: "dab-chip-row",
|
|
5550
|
+
style: {
|
|
5551
|
+
marginBottom: 12,
|
|
5552
|
+
alignItems: "center"
|
|
5553
|
+
},
|
|
5554
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5555
|
+
type: "button",
|
|
5556
|
+
className: `dab-toggle${cfg.autoExtract ? " is-on" : ""}`,
|
|
5557
|
+
role: "switch",
|
|
5558
|
+
"aria-checked": cfg.autoExtract,
|
|
5559
|
+
title: t("autoExtract"),
|
|
5560
|
+
onClick: () => p.setAutoExtract(!cfg.autoExtract),
|
|
5561
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5562
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5262
5563
|
className: "dab-hint",
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
title: t("autoExtract"),
|
|
5284
|
-
onClick: () => p.setAutoExtract(!cfg.autoExtract),
|
|
5285
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5286
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5564
|
+
children: t("autoExtract")
|
|
5565
|
+
})]
|
|
5566
|
+
}),
|
|
5567
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RulesTabs, {
|
|
5568
|
+
rules,
|
|
5569
|
+
activeId: active,
|
|
5570
|
+
liveId: store.activeRuleId,
|
|
5571
|
+
p,
|
|
5572
|
+
onSelect: selectRule,
|
|
5573
|
+
onAdd: addRule
|
|
5574
|
+
}),
|
|
5575
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5576
|
+
className: "dab-rule-panel",
|
|
5577
|
+
role: "tabpanel",
|
|
5578
|
+
id: active === null ? void 0 : `dab-rule-panel-${active}`,
|
|
5579
|
+
"aria-labelledby": active === null ? void 0 : `dab-rule-tab-${active}`,
|
|
5580
|
+
children: (() => {
|
|
5581
|
+
const idx = rules.findIndex((r) => r.id === active);
|
|
5582
|
+
const rule = idx < 0 ? void 0 : rules[idx];
|
|
5583
|
+
if (rule === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5287
5584
|
className: "dab-hint",
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
className: "dab-rules",
|
|
5293
|
-
children: rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleCard, {
|
|
5585
|
+
style: { padding: "4px 2px" },
|
|
5586
|
+
children: t("ruleNoneHint")
|
|
5587
|
+
});
|
|
5588
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleCard, {
|
|
5294
5589
|
p,
|
|
5295
5590
|
rule,
|
|
5296
|
-
index:
|
|
5591
|
+
index: idx,
|
|
5297
5592
|
total: rules.length,
|
|
5298
5593
|
liveIndex: rule.id === store.activeRuleId ? store.rotIndex : -1,
|
|
5299
5594
|
active: rule.id === store.activeRuleId,
|
|
5300
|
-
notify
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
},
|
|
5310
|
-
children: ["+ ", t("ruleAdd")]
|
|
5311
|
-
})
|
|
5312
|
-
]
|
|
5313
|
-
})
|
|
5314
|
-
] });
|
|
5595
|
+
notify,
|
|
5596
|
+
sel: selIdxOf(rule.id),
|
|
5597
|
+
onSel: (i) => setSelIdxOf(rule.id, i),
|
|
5598
|
+
onRemove: () => removeRule(rule.id)
|
|
5599
|
+
}, rule.id);
|
|
5600
|
+
})()
|
|
5601
|
+
})
|
|
5602
|
+
]
|
|
5603
|
+
})] });
|
|
5315
5604
|
}
|
|
5316
|
-
|
|
5605
|
+
/**
|
|
5606
|
+
* The rule tab strip: every rule is one tab, and the tab at the end appends a
|
|
5607
|
+
* new rule — so what the user browses and what they add to live in one row.
|
|
5608
|
+
*
|
|
5609
|
+
* A tab carries what the card head used to: its number, a thumbnail of the first
|
|
5610
|
+
* image, the match string, and two states that are worth knowing WITHOUT opening
|
|
5611
|
+
* it — whether the rule is the one in use, and whether it rotates or is off. The
|
|
5612
|
+
* strip scrolls sideways rather than wrapping, because a wrapped strip would put
|
|
5613
|
+
* the page height back in the hands of the rule count, which is the whole thing
|
|
5614
|
+
* this layout exists to prevent.
|
|
5615
|
+
*/
|
|
5616
|
+
function RulesTabs({ rules, activeId, liveId, p, onSelect, onAdd }) {
|
|
5317
5617
|
const { t } = p;
|
|
5318
|
-
const
|
|
5319
|
-
const [
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
const
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5618
|
+
const items = (0, react.useRef)([]);
|
|
5619
|
+
const [pick, setPick] = (0, react.useState)(false);
|
|
5620
|
+
/** Select a rule and bring its tab into view — the strip is wider than the page. */
|
|
5621
|
+
const go = (i) => {
|
|
5622
|
+
const rule = rules[i];
|
|
5623
|
+
if (rule === void 0) return;
|
|
5624
|
+
onSelect(rule.id);
|
|
5625
|
+
items.current[i]?.scrollIntoView({
|
|
5626
|
+
block: "nearest",
|
|
5627
|
+
inline: "nearest"
|
|
5628
|
+
});
|
|
5629
|
+
};
|
|
5630
|
+
const onKeys = (e) => {
|
|
5631
|
+
const i = rules.findIndex((r) => r.id === activeId);
|
|
5632
|
+
if (i < 0) return;
|
|
5633
|
+
if (e.key === "ArrowRight") {
|
|
5634
|
+
e.preventDefault();
|
|
5635
|
+
go(Math.min(i + 1, rules.length - 1));
|
|
5636
|
+
} else if (e.key === "ArrowLeft") {
|
|
5637
|
+
e.preventDefault();
|
|
5638
|
+
go(Math.max(i - 1, 0));
|
|
5639
|
+
} else if (e.key === "Home") {
|
|
5640
|
+
e.preventDefault();
|
|
5641
|
+
go(0);
|
|
5642
|
+
} else if (e.key === "End") {
|
|
5643
|
+
e.preventDefault();
|
|
5644
|
+
go(rules.length - 1);
|
|
5645
|
+
}
|
|
5646
|
+
};
|
|
5647
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5648
|
+
className: "dab-tabs-row",
|
|
5649
|
+
children: [
|
|
5650
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5651
|
+
className: "dab-tabs",
|
|
5652
|
+
role: "tablist",
|
|
5653
|
+
"aria-label": t("tabListLabel"),
|
|
5654
|
+
onKeyDown: onKeys,
|
|
5655
|
+
children: rules.map((rule, i) => {
|
|
5656
|
+
const on = rule.id === activeId;
|
|
5657
|
+
const live = rule.id === liveId;
|
|
5658
|
+
const thumb = rule.images[0] === void 0 ? null : p.imageOf(rule.images[0].slot);
|
|
5659
|
+
const match = rule.match.trim();
|
|
5660
|
+
const title = [
|
|
5661
|
+
match === "" ? t("tabUnnamed") : match,
|
|
5662
|
+
`${rule.images.length}`,
|
|
5663
|
+
live ? t("tabLive") : null,
|
|
5664
|
+
rule.images.length >= 2 && rule.rotate.enabled ? t("tabRotating") : null,
|
|
5665
|
+
rule.enabled ? null : t("tabOff")
|
|
5666
|
+
].filter((x) => x !== null).join(" · ");
|
|
5667
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5668
|
+
type: "button",
|
|
5669
|
+
role: "tab",
|
|
5670
|
+
"aria-selected": on,
|
|
5671
|
+
id: `dab-rule-tab-${rule.id}`,
|
|
5672
|
+
"aria-controls": `dab-rule-panel-${rule.id}`,
|
|
5673
|
+
tabIndex: on ? 0 : -1,
|
|
5674
|
+
ref: (el) => {
|
|
5675
|
+
items.current[i] = el;
|
|
5676
|
+
},
|
|
5677
|
+
className: `dab-tab${on ? " is-active" : ""}${rule.enabled ? "" : " is-off"}${live ? " is-live" : ""}`,
|
|
5678
|
+
title,
|
|
5679
|
+
onClick: () => go(i),
|
|
5680
|
+
children: [
|
|
5681
|
+
thumb === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
5682
|
+
className: "dab-tab-thumb",
|
|
5683
|
+
src: thumb,
|
|
5684
|
+
alt: "",
|
|
5685
|
+
draggable: false
|
|
5686
|
+
}),
|
|
5687
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5688
|
+
className: "dab-tab-num",
|
|
5689
|
+
children: i + 1
|
|
5690
|
+
}),
|
|
5691
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5692
|
+
className: "dab-tab-match",
|
|
5693
|
+
children: match === "" ? t("tabUnnamed") : match
|
|
5694
|
+
}),
|
|
5695
|
+
rule.enabled && live ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5696
|
+
className: "dab-tab-live",
|
|
5697
|
+
title: t("tabLive")
|
|
5698
|
+
}) : null,
|
|
5699
|
+
rule.enabled && rule.images.length >= 2 && rule.rotate.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5700
|
+
className: "dab-tab-rot",
|
|
5701
|
+
title: t("tabRotating"),
|
|
5702
|
+
children: "⟳"
|
|
5703
|
+
}) : null
|
|
5704
|
+
]
|
|
5705
|
+
}, rule.id);
|
|
5706
|
+
})
|
|
5707
|
+
}),
|
|
5708
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5709
|
+
type: "button",
|
|
5710
|
+
className: "dab-tab dab-tab-icon",
|
|
5711
|
+
onClick: onAdd,
|
|
5712
|
+
title: t("ruleAdd"),
|
|
5713
|
+
"aria-label": t("ruleAdd"),
|
|
5714
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, { size: 16 })
|
|
5715
|
+
}),
|
|
5716
|
+
rules.length > 3 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5717
|
+
className: "dab-pick",
|
|
5718
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5719
|
+
type: "button",
|
|
5720
|
+
className: "dab-tab dab-tab-icon",
|
|
5721
|
+
"aria-haspopup": "listbox",
|
|
5722
|
+
"aria-expanded": pick,
|
|
5723
|
+
title: t("tabPick"),
|
|
5724
|
+
"aria-label": t("tabPick"),
|
|
5725
|
+
onClick: () => setPick((o) => !o),
|
|
5726
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { size: 16 })
|
|
5727
|
+
}), pick ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5728
|
+
className: "dab-pick-scrim",
|
|
5729
|
+
onClick: () => setPick(false)
|
|
5730
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5731
|
+
className: "dab-pick-menu",
|
|
5732
|
+
role: "listbox",
|
|
5733
|
+
"aria-label": t("tabMore"),
|
|
5734
|
+
children: rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5735
|
+
type: "button",
|
|
5736
|
+
role: "option",
|
|
5737
|
+
"aria-selected": rule.id === activeId,
|
|
5738
|
+
className: `dab-pick-item${rule.id === activeId ? " is-active" : ""}`,
|
|
5739
|
+
onClick: () => {
|
|
5740
|
+
setPick(false);
|
|
5741
|
+
go(i);
|
|
5742
|
+
},
|
|
5743
|
+
children: [
|
|
5744
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5745
|
+
className: "dab-tab-num",
|
|
5746
|
+
children: i + 1
|
|
5747
|
+
}),
|
|
5748
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5749
|
+
className: "dab-pick-match",
|
|
5750
|
+
children: rule.match.trim() === "" ? t("tabUnnamed") : rule.match
|
|
5751
|
+
}),
|
|
5752
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5753
|
+
className: "dab-pick-n",
|
|
5754
|
+
children: rule.images.length
|
|
5755
|
+
})
|
|
5756
|
+
]
|
|
5757
|
+
}, rule.id))
|
|
5758
|
+
})] }) : null]
|
|
5759
|
+
}) : null
|
|
5760
|
+
]
|
|
5761
|
+
});
|
|
5762
|
+
}
|
|
5763
|
+
function RuleCard({ p, rule, index, total, active, liveIndex, notify, sel, onSel, onRemove }) {
|
|
5764
|
+
const { t } = p;
|
|
5765
|
+
const [dragOver, setDragOver] = (0, react.useState)(false);
|
|
5766
|
+
const pendingDrag = (0, react.useRef)(null);
|
|
5767
|
+
/** Insertion slot of the drag in flight (index of the tile it goes BEFORE). */
|
|
5768
|
+
const dragAt = (0, react.useRef)(null);
|
|
5769
|
+
const [drop, setDrop] = (0, react.useState)(null);
|
|
5770
|
+
/** Set while a drag is real, so the click that ends it is not also a selection. */
|
|
5771
|
+
const draggedRef = (0, react.useRef)(false);
|
|
5772
|
+
const [urlOpen, setUrlOpen] = (0, react.useState)(false);
|
|
5773
|
+
const [urlVal, setUrlVal] = (0, react.useState)("");
|
|
5774
|
+
const [urlBusy, setUrlBusy] = (0, react.useState)(false);
|
|
5775
|
+
const [urlErr, setUrlErr] = (0, react.useState)(null);
|
|
5776
|
+
const [editorOpen, setEditorOpen] = (0, react.useState)(false);
|
|
5777
|
+
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
5778
|
+
const [extracting, setExtracting] = (0, react.useState)(false);
|
|
5779
|
+
const [secDraft, setSecDraft] = (0, react.useState)(null);
|
|
5780
|
+
/** Two-step delete: this card is the only place the rule can be removed from. */
|
|
5781
|
+
const [confirmRemove, setConfirmRemove] = (0, react.useState)(false);
|
|
5334
5782
|
/** Whether the next file picked replaces the selected image or is added. */
|
|
5335
5783
|
const [replace, setReplace] = (0, react.useState)(false);
|
|
5336
5784
|
const fileRef = (0, react.useRef)(null);
|
|
5337
|
-
const [sel, setSel] = (0, react.useState)(0);
|
|
5338
5785
|
const selIdx = rule.images.length === 0 ? 0 : Math.min(sel, rule.images.length - 1);
|
|
5339
5786
|
const image = rule.images[selIdx];
|
|
5340
5787
|
const url = image === void 0 ? null : p.imageOf(image.slot);
|
|
@@ -5346,9 +5793,8 @@ window.__ModuleLoader__.load({
|
|
|
5346
5793
|
const [h, s, l] = ownColor ?? SEED_COLOR;
|
|
5347
5794
|
const wheel = hslToHsv(h, s, l);
|
|
5348
5795
|
(0, react.useEffect)(() => {
|
|
5349
|
-
|
|
5796
|
+
p.loadRuleImages(rule.id);
|
|
5350
5797
|
}, [
|
|
5351
|
-
open,
|
|
5352
5798
|
rule.id,
|
|
5353
5799
|
rule.images.length,
|
|
5354
5800
|
p
|
|
@@ -5380,7 +5826,7 @@ window.__ModuleLoader__.load({
|
|
|
5380
5826
|
pending--;
|
|
5381
5827
|
if (pending === 0) {
|
|
5382
5828
|
p.addRuleImages(rule.id, out.filter((x) => typeof x === "string"));
|
|
5383
|
-
|
|
5829
|
+
onSel(first);
|
|
5384
5830
|
}
|
|
5385
5831
|
});
|
|
5386
5832
|
});
|
|
@@ -5415,7 +5861,7 @@ window.__ModuleLoader__.load({
|
|
|
5415
5861
|
setUrlBusy(false);
|
|
5416
5862
|
setUrlOpen(false);
|
|
5417
5863
|
setUrlVal("");
|
|
5418
|
-
|
|
5864
|
+
onSel(first);
|
|
5419
5865
|
};
|
|
5420
5866
|
const onExtract = async () => {
|
|
5421
5867
|
if (url === null || extracting || image === void 0) return;
|
|
@@ -5450,6 +5896,7 @@ window.__ModuleLoader__.load({
|
|
|
5450
5896
|
const rotatable = rule.images.length >= 2;
|
|
5451
5897
|
const preset = ROTATE_PRESETS.find((x) => x.ms === rule.rotate.intervalMs);
|
|
5452
5898
|
const selSlot = image?.slot ?? null;
|
|
5899
|
+
const bgMode = image?.bgMode ?? "fit";
|
|
5453
5900
|
/** How far the pointer must travel before a click turns into a drag. */
|
|
5454
5901
|
const DRAG_SLOP = 5;
|
|
5455
5902
|
const tileDown = (i, slot) => (e) => {
|
|
@@ -5504,13 +5951,13 @@ window.__ModuleLoader__.load({
|
|
|
5504
5951
|
const to = end.at > pen.from ? end.at - 1 : end.at;
|
|
5505
5952
|
if (to === pen.from) return;
|
|
5506
5953
|
p.moveRuleImageTo(rule.id, pen.slot, to);
|
|
5507
|
-
|
|
5954
|
+
onSel(to);
|
|
5508
5955
|
};
|
|
5509
5956
|
const removeSel = () => {
|
|
5510
5957
|
if (selSlot === null) return;
|
|
5511
5958
|
const at = selIdx;
|
|
5512
5959
|
p.removeRuleImage(rule.id, selSlot);
|
|
5513
|
-
|
|
5960
|
+
onSel(Math.max(0, Math.min(at, rule.images.length - 2)));
|
|
5514
5961
|
};
|
|
5515
5962
|
/** Commit the custom dwell time the moment it is typed (seconds → ms). */
|
|
5516
5963
|
const commitSeconds = () => {
|
|
@@ -5526,14 +5973,6 @@ window.__ModuleLoader__.load({
|
|
|
5526
5973
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5527
5974
|
className: "dab-rule-head",
|
|
5528
5975
|
children: [
|
|
5529
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5530
|
-
type: "button",
|
|
5531
|
-
className: "dab-icon-btn",
|
|
5532
|
-
onClick: () => setOpen((o) => !o),
|
|
5533
|
-
title: open ? "Collapse" : "Expand",
|
|
5534
|
-
"aria-expanded": open,
|
|
5535
|
-
children: open ? "▾" : "▸"
|
|
5536
|
-
}),
|
|
5537
5976
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5538
5977
|
className: "dab-rule-num",
|
|
5539
5978
|
children: index + 1
|
|
@@ -5575,14 +6014,25 @@ window.__ModuleLoader__.load({
|
|
|
5575
6014
|
}),
|
|
5576
6015
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5577
6016
|
type: "button",
|
|
5578
|
-
className:
|
|
5579
|
-
title: t("ruleRemove"),
|
|
5580
|
-
|
|
5581
|
-
|
|
6017
|
+
className: `dab-icon-btn dab-icon-btn-danger${confirmRemove ? " is-armed" : ""}`,
|
|
6018
|
+
title: confirmRemove ? t("ruleRemoveConfirm") : t("ruleRemove"),
|
|
6019
|
+
onBlur: () => setConfirmRemove(false),
|
|
6020
|
+
onClick: () => {
|
|
6021
|
+
if (!confirmRemove) {
|
|
6022
|
+
setConfirmRemove(true);
|
|
6023
|
+
return;
|
|
6024
|
+
}
|
|
6025
|
+
setConfirmRemove(false);
|
|
6026
|
+
onRemove();
|
|
6027
|
+
},
|
|
6028
|
+
children: confirmRemove ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6029
|
+
className: "dab-confirm-txt",
|
|
6030
|
+
children: t("ruleRemoveConfirm")
|
|
6031
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 })
|
|
5582
6032
|
})
|
|
5583
6033
|
]
|
|
5584
6034
|
}),
|
|
5585
|
-
|
|
6035
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5586
6036
|
className: "dab-rule-body",
|
|
5587
6037
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5588
6038
|
className: "dab-rule-cols",
|
|
@@ -5642,7 +6092,7 @@ window.__ModuleLoader__.load({
|
|
|
5642
6092
|
draggedRef.current = false;
|
|
5643
6093
|
return;
|
|
5644
6094
|
}
|
|
5645
|
-
|
|
6095
|
+
onSel(i);
|
|
5646
6096
|
},
|
|
5647
6097
|
onDoubleClick: () => {
|
|
5648
6098
|
if (!active) {
|
|
@@ -5791,140 +6241,6 @@ window.__ModuleLoader__.load({
|
|
|
5791
6241
|
style: { marginTop: 8 },
|
|
5792
6242
|
children: t("ruleImagePendingHint")
|
|
5793
6243
|
}) : null,
|
|
5794
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5795
|
-
style: { marginTop: 14 },
|
|
5796
|
-
children: [
|
|
5797
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5798
|
-
className: "dab-rule-section-title",
|
|
5799
|
-
children: t("rotTitle")
|
|
5800
|
-
}),
|
|
5801
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5802
|
-
className: "dab-chip-row",
|
|
5803
|
-
style: { alignItems: "center" },
|
|
5804
|
-
children: [
|
|
5805
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5806
|
-
type: "button",
|
|
5807
|
-
className: `dab-toggle${rule.rotate.enabled ? " is-on" : ""}`,
|
|
5808
|
-
role: "switch",
|
|
5809
|
-
"aria-checked": rule.rotate.enabled,
|
|
5810
|
-
title: t("rotEnable"),
|
|
5811
|
-
disabled: !rotatable,
|
|
5812
|
-
onClick: () => p.setRuleRotation(rule.id, { enabled: !rule.rotate.enabled }),
|
|
5813
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5814
|
-
}),
|
|
5815
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5816
|
-
className: "dab-hint",
|
|
5817
|
-
style: { padding: 0 },
|
|
5818
|
-
children: t("rotEnable")
|
|
5819
|
-
}),
|
|
5820
|
-
!rotatable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5821
|
-
className: "dab-hint",
|
|
5822
|
-
style: { padding: 0 },
|
|
5823
|
-
children: t("rotNeedTwo")
|
|
5824
|
-
}) : null
|
|
5825
|
-
]
|
|
5826
|
-
}),
|
|
5827
|
-
rotatable && rule.rotate.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5828
|
-
className: "dab-rot-panel",
|
|
5829
|
-
children: [
|
|
5830
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5831
|
-
className: "dab-rule-section-title",
|
|
5832
|
-
children: t("rotEvery")
|
|
5833
|
-
}),
|
|
5834
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5835
|
-
className: "dab-chip-row",
|
|
5836
|
-
children: [
|
|
5837
|
-
ROTATE_PRESETS.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5838
|
-
type: "button",
|
|
5839
|
-
className: `dab-chip${item.ms === rule.rotate.intervalMs ? " is-active" : ""}`,
|
|
5840
|
-
onClick: () => p.setRuleRotation(rule.id, { intervalMs: item.ms }),
|
|
5841
|
-
children: t(item.key)
|
|
5842
|
-
}, item.ms)),
|
|
5843
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5844
|
-
type: "number",
|
|
5845
|
-
className: "dab-num dab-num-sec",
|
|
5846
|
-
min: 5,
|
|
5847
|
-
max: 86400,
|
|
5848
|
-
step: 5,
|
|
5849
|
-
value: secDraft ?? (preset === void 0 ? Math.round(rule.rotate.intervalMs / 1e3) : ""),
|
|
5850
|
-
placeholder: t("rotCustom"),
|
|
5851
|
-
onChange: (e) => setSecDraft(e.target.value),
|
|
5852
|
-
onBlur: commitSeconds,
|
|
5853
|
-
onKeyDown: (e) => {
|
|
5854
|
-
if (e.key === "Enter") {
|
|
5855
|
-
e.preventDefault();
|
|
5856
|
-
commitSeconds();
|
|
5857
|
-
}
|
|
5858
|
-
}
|
|
5859
|
-
}),
|
|
5860
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5861
|
-
className: "dab-hint",
|
|
5862
|
-
style: { padding: 0 },
|
|
5863
|
-
children: t("rotSeconds")
|
|
5864
|
-
})
|
|
5865
|
-
]
|
|
5866
|
-
}),
|
|
5867
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5868
|
-
className: "dab-rule-section-title",
|
|
5869
|
-
style: { marginTop: 12 },
|
|
5870
|
-
children: t("rotOrder")
|
|
5871
|
-
}),
|
|
5872
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5873
|
-
className: "dab-chip-row",
|
|
5874
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5875
|
-
type: "button",
|
|
5876
|
-
className: `dab-chip${rule.rotate.order === "order" ? " is-active" : ""}`,
|
|
5877
|
-
onClick: () => p.setRuleRotation(rule.id, { order: "order" }),
|
|
5878
|
-
children: t("rotOrderSeq")
|
|
5879
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5880
|
-
type: "button",
|
|
5881
|
-
className: `dab-chip${rule.rotate.order === "shuffle" ? " is-active" : ""}`,
|
|
5882
|
-
onClick: () => p.setRuleRotation(rule.id, { order: "shuffle" }),
|
|
5883
|
-
children: t("rotOrderShuffle")
|
|
5884
|
-
})]
|
|
5885
|
-
}),
|
|
5886
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5887
|
-
className: "dab-chip-row",
|
|
5888
|
-
style: {
|
|
5889
|
-
marginTop: 12,
|
|
5890
|
-
alignItems: "center"
|
|
5891
|
-
},
|
|
5892
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5893
|
-
type: "button",
|
|
5894
|
-
className: `dab-toggle${rule.rotate.advanceOnSwitch ? " is-on" : ""}`,
|
|
5895
|
-
role: "switch",
|
|
5896
|
-
"aria-checked": rule.rotate.advanceOnSwitch,
|
|
5897
|
-
title: t("rotOnSwitch"),
|
|
5898
|
-
onClick: () => p.setRuleRotation(rule.id, { advanceOnSwitch: !rule.rotate.advanceOnSwitch }),
|
|
5899
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5900
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5901
|
-
className: "dab-hint",
|
|
5902
|
-
style: { padding: 0 },
|
|
5903
|
-
children: t("rotOnSwitch")
|
|
5904
|
-
})]
|
|
5905
|
-
}),
|
|
5906
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5907
|
-
className: "dab-chip-row",
|
|
5908
|
-
style: {
|
|
5909
|
-
marginTop: 12,
|
|
5910
|
-
alignItems: "center"
|
|
5911
|
-
},
|
|
5912
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5913
|
-
type: "button",
|
|
5914
|
-
className: "dab-btn",
|
|
5915
|
-
disabled: !active,
|
|
5916
|
-
onClick: () => p.rotateNow(rule.id),
|
|
5917
|
-
children: t("rotNext")
|
|
5918
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5919
|
-
className: "dab-hint",
|
|
5920
|
-
style: { padding: 0 },
|
|
5921
|
-
children: active ? `${t("rotShowing")} ${rule.images.length === 0 ? 0 : Math.min(liveIndex + 1, rule.images.length)} / ${rule.images.length}` : t("rotNextInactive")
|
|
5922
|
-
})]
|
|
5923
|
-
})
|
|
5924
|
-
]
|
|
5925
|
-
}) : null
|
|
5926
|
-
]
|
|
5927
|
-
}),
|
|
5928
6244
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5929
6245
|
style: { marginTop: 14 },
|
|
5930
6246
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -5932,12 +6248,17 @@ window.__ModuleLoader__.load({
|
|
|
5932
6248
|
children: t("ruleLayout")
|
|
5933
6249
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5934
6250
|
className: "dab-chip-row",
|
|
6251
|
+
style: { marginTop: 8 },
|
|
5935
6252
|
children: [BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5936
6253
|
type: "button",
|
|
5937
|
-
className: `dab-chip${
|
|
5938
|
-
|
|
6254
|
+
className: `dab-chip${bgMode === m.mode ? " is-active" : ""}`,
|
|
6255
|
+
disabled: image === void 0,
|
|
6256
|
+
title: image === void 0 ? t("ruleLayoutNoImage") : void 0,
|
|
6257
|
+
onClick: () => {
|
|
6258
|
+
if (image !== void 0) p.setImageMode(rule.id, image.slot, m.mode);
|
|
6259
|
+
},
|
|
5939
6260
|
children: t(m.key)
|
|
5940
|
-
}, m.mode)), url !== null &&
|
|
6261
|
+
}, m.mode)), url !== null && bgMode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5941
6262
|
type: "button",
|
|
5942
6263
|
className: "dab-btn",
|
|
5943
6264
|
onClick: () => setEditorOpen(true),
|
|
@@ -5990,71 +6311,53 @@ window.__ModuleLoader__.load({
|
|
|
5990
6311
|
style: { marginTop: 6 },
|
|
5991
6312
|
children: t("ruleColorPendingImageHint")
|
|
5992
6313
|
})
|
|
5993
|
-
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6314
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6315
|
+
style: {
|
|
6316
|
+
display: "flex",
|
|
6317
|
+
gap: 16,
|
|
6318
|
+
alignItems: "center",
|
|
6319
|
+
flexWrap: "wrap"
|
|
6320
|
+
},
|
|
6321
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorWheel, {
|
|
6322
|
+
hue: wheel[0],
|
|
6323
|
+
sat: wheel[1],
|
|
6324
|
+
lit: wheel[2],
|
|
6325
|
+
onChange: onColor
|
|
6326
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6327
|
+
className: "dab-inputs",
|
|
6328
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorInputs, {
|
|
6002
6329
|
hue: wheel[0],
|
|
6003
6330
|
sat: wheel[1],
|
|
6004
6331
|
lit: wheel[2],
|
|
6005
6332
|
onChange: onColor
|
|
6006
|
-
})
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
})
|
|
6014
|
-
})]
|
|
6015
|
-
}),
|
|
6016
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6017
|
-
className: "dab-chip-row",
|
|
6018
|
-
style: { marginTop: 12 },
|
|
6019
|
-
children: [
|
|
6020
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6021
|
-
type: "button",
|
|
6022
|
-
className: "dab-btn",
|
|
6023
|
-
disabled: url === null || extracting,
|
|
6024
|
-
onClick: () => void onExtract(),
|
|
6025
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 13 }), extracting ? t("extracting") : t("ruleColorExtract")]
|
|
6026
|
-
}),
|
|
6027
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6028
|
-
type: "button",
|
|
6029
|
-
className: "dab-btn",
|
|
6030
|
-
disabled: url === null,
|
|
6031
|
-
onClick: () => setPickerOpen(true),
|
|
6032
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PipetteIcon, { size: 13 }), t("eyedropper")]
|
|
6033
|
-
}),
|
|
6034
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6035
|
-
type: "button",
|
|
6036
|
-
className: "dab-btn dab-btn-ghost",
|
|
6037
|
-
onClick: () => setColor(null),
|
|
6038
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }), t("ruleColorNone")]
|
|
6039
|
-
})
|
|
6040
|
-
]
|
|
6041
|
-
}),
|
|
6042
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6043
|
-
className: "dab-swatches",
|
|
6044
|
-
style: { marginTop: 12 },
|
|
6045
|
-
children: PALETTE.map(([sh, ss, sl], i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6333
|
+
})
|
|
6334
|
+
})]
|
|
6335
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6336
|
+
className: "dab-chip-row",
|
|
6337
|
+
style: { marginTop: 12 },
|
|
6338
|
+
children: [
|
|
6339
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6046
6340
|
type: "button",
|
|
6047
|
-
className: "dab-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6341
|
+
className: "dab-btn",
|
|
6342
|
+
disabled: url === null || extracting,
|
|
6343
|
+
onClick: () => void onExtract(),
|
|
6344
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 13 }), extracting ? t("extracting") : t("ruleColorExtract")]
|
|
6345
|
+
}),
|
|
6346
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6347
|
+
type: "button",
|
|
6348
|
+
className: "dab-btn",
|
|
6349
|
+
disabled: url === null,
|
|
6350
|
+
onClick: () => setPickerOpen(true),
|
|
6351
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PipetteIcon, { size: 13 }), t("eyedropper")]
|
|
6352
|
+
}),
|
|
6353
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6354
|
+
type: "button",
|
|
6355
|
+
className: "dab-btn dab-btn-ghost",
|
|
6356
|
+
onClick: () => setColor(null),
|
|
6357
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }), t("ruleColorNone")]
|
|
6358
|
+
})
|
|
6359
|
+
]
|
|
6360
|
+
})] }),
|
|
6058
6361
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6059
6362
|
style: { marginTop: 16 },
|
|
6060
6363
|
children: [
|
|
@@ -6082,10 +6385,136 @@ window.__ModuleLoader__.load({
|
|
|
6082
6385
|
children: t("ruleBlurHint")
|
|
6083
6386
|
})
|
|
6084
6387
|
]
|
|
6388
|
+
}),
|
|
6389
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6390
|
+
style: { marginTop: 14 },
|
|
6391
|
+
children: [
|
|
6392
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6393
|
+
className: "dab-rule-section-title",
|
|
6394
|
+
children: t("rotTitle")
|
|
6395
|
+
}),
|
|
6396
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6397
|
+
className: "dab-chip-row",
|
|
6398
|
+
style: { alignItems: "center" },
|
|
6399
|
+
children: [
|
|
6400
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6401
|
+
type: "button",
|
|
6402
|
+
className: `dab-toggle${rule.rotate.enabled ? " is-on" : ""}`,
|
|
6403
|
+
role: "switch",
|
|
6404
|
+
"aria-checked": rule.rotate.enabled,
|
|
6405
|
+
title: t("rotEnable"),
|
|
6406
|
+
disabled: !rotatable,
|
|
6407
|
+
onClick: () => p.setRuleRotation(rule.id, { enabled: !rule.rotate.enabled }),
|
|
6408
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
6409
|
+
}),
|
|
6410
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6411
|
+
className: "dab-hint",
|
|
6412
|
+
style: { padding: 0 },
|
|
6413
|
+
children: t("rotEnable")
|
|
6414
|
+
}),
|
|
6415
|
+
!rotatable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6416
|
+
className: "dab-hint",
|
|
6417
|
+
style: { padding: 0 },
|
|
6418
|
+
children: t("rotNeedTwo")
|
|
6419
|
+
}) : null,
|
|
6420
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6421
|
+
type: "button",
|
|
6422
|
+
className: "dab-btn",
|
|
6423
|
+
disabled: !active || !rotatable,
|
|
6424
|
+
onClick: () => p.rotateNow(rule.id),
|
|
6425
|
+
children: t("rotNext")
|
|
6426
|
+
}),
|
|
6427
|
+
rotatable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6428
|
+
className: "dab-hint",
|
|
6429
|
+
style: { padding: 0 },
|
|
6430
|
+
children: active ? `${t("rotShowing")} ${Math.min(liveIndex + 1, rule.images.length)} / ${rule.images.length}` : t("rotNextInactive")
|
|
6431
|
+
}) : null
|
|
6432
|
+
]
|
|
6433
|
+
}),
|
|
6434
|
+
rotatable && rule.rotate.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6435
|
+
className: "dab-rot-panel",
|
|
6436
|
+
children: [
|
|
6437
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6438
|
+
className: "dab-rule-section-title",
|
|
6439
|
+
children: t("rotEvery")
|
|
6440
|
+
}),
|
|
6441
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6442
|
+
className: "dab-chip-row",
|
|
6443
|
+
children: [
|
|
6444
|
+
ROTATE_PRESETS.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6445
|
+
type: "button",
|
|
6446
|
+
className: `dab-chip${item.ms === rule.rotate.intervalMs ? " is-active" : ""}`,
|
|
6447
|
+
onClick: () => p.setRuleRotation(rule.id, { intervalMs: item.ms }),
|
|
6448
|
+
children: t(item.key)
|
|
6449
|
+
}, item.ms)),
|
|
6450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6451
|
+
type: "number",
|
|
6452
|
+
className: "dab-num dab-num-sec",
|
|
6453
|
+
min: 5,
|
|
6454
|
+
max: 86400,
|
|
6455
|
+
step: 5,
|
|
6456
|
+
value: secDraft ?? (preset === void 0 ? Math.round(rule.rotate.intervalMs / 1e3) : ""),
|
|
6457
|
+
placeholder: t("rotCustom"),
|
|
6458
|
+
onChange: (e) => setSecDraft(e.target.value),
|
|
6459
|
+
onBlur: commitSeconds,
|
|
6460
|
+
onKeyDown: (e) => {
|
|
6461
|
+
if (e.key === "Enter") {
|
|
6462
|
+
e.preventDefault();
|
|
6463
|
+
commitSeconds();
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6466
|
+
}),
|
|
6467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6468
|
+
className: "dab-hint",
|
|
6469
|
+
style: { padding: 0 },
|
|
6470
|
+
children: t("rotSeconds")
|
|
6471
|
+
})
|
|
6472
|
+
]
|
|
6473
|
+
}),
|
|
6474
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6475
|
+
className: "dab-rule-section-title",
|
|
6476
|
+
style: { marginTop: 12 },
|
|
6477
|
+
children: t("rotOrder")
|
|
6478
|
+
}),
|
|
6479
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6480
|
+
className: "dab-chip-row",
|
|
6481
|
+
style: { alignItems: "center" },
|
|
6482
|
+
children: [
|
|
6483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6484
|
+
type: "button",
|
|
6485
|
+
className: `dab-chip${rule.rotate.order === "order" ? " is-active" : ""}`,
|
|
6486
|
+
onClick: () => p.setRuleRotation(rule.id, { order: "order" }),
|
|
6487
|
+
children: t("rotOrderSeq")
|
|
6488
|
+
}),
|
|
6489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6490
|
+
type: "button",
|
|
6491
|
+
className: `dab-chip${rule.rotate.order === "shuffle" ? " is-active" : ""}`,
|
|
6492
|
+
onClick: () => p.setRuleRotation(rule.id, { order: "shuffle" }),
|
|
6493
|
+
children: t("rotOrderShuffle")
|
|
6494
|
+
}),
|
|
6495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6496
|
+
type: "button",
|
|
6497
|
+
className: `dab-toggle${rule.rotate.advanceOnSwitch ? " is-on" : ""}`,
|
|
6498
|
+
role: "switch",
|
|
6499
|
+
"aria-checked": rule.rotate.advanceOnSwitch,
|
|
6500
|
+
title: t("rotOnSwitch"),
|
|
6501
|
+
onClick: () => p.setRuleRotation(rule.id, { advanceOnSwitch: !rule.rotate.advanceOnSwitch }),
|
|
6502
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
6503
|
+
}),
|
|
6504
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6505
|
+
className: "dab-hint",
|
|
6506
|
+
style: { padding: 0 },
|
|
6507
|
+
children: t("rotOnSwitch")
|
|
6508
|
+
})
|
|
6509
|
+
]
|
|
6510
|
+
})
|
|
6511
|
+
]
|
|
6512
|
+
}) : null
|
|
6513
|
+
]
|
|
6085
6514
|
})
|
|
6086
6515
|
] })]
|
|
6087
6516
|
})
|
|
6088
|
-
})
|
|
6517
|
+
}),
|
|
6089
6518
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6090
6519
|
ref: fileRef,
|
|
6091
6520
|
type: "file",
|
|
@@ -6144,12 +6573,57 @@ window.__ModuleLoader__.load({
|
|
|
6144
6573
|
"ease-out": "trEasingOut",
|
|
6145
6574
|
"ease-in-out": "trEasingInOut"
|
|
6146
6575
|
};
|
|
6576
|
+
/**
|
|
6577
|
+
* Failing-hop codes the model watcher can report, mapped to their copy. Only
|
|
6578
|
+
* these are rendered: `waiting` and `fallback` already have their own hints.
|
|
6579
|
+
*/
|
|
6580
|
+
const MODEL_NOTE_KEYS = {
|
|
6581
|
+
"no-service": "statusNoteNoService",
|
|
6582
|
+
"no-session": "statusNoteNoSession",
|
|
6583
|
+
"no-projection": "statusNoteNoProjection",
|
|
6584
|
+
"empty-selection": "statusNoteEmptySelection"
|
|
6585
|
+
};
|
|
6586
|
+
/**
|
|
6587
|
+
* The ways the recommended-profile download can fail, mapped to their copy.
|
|
6588
|
+
*
|
|
6589
|
+
* They are worth telling apart because they ask different things of the user and
|
|
6590
|
+
* of whoever maintains the assets repository: nothing answering is a network to
|
|
6591
|
+
* retry, a mirror that answered with something that is not a profile is not
|
|
6592
|
+
* going to start working on a retry, a profile written for a newer plugin is a
|
|
6593
|
+
* "update this build", and too many images is a profile nobody should install.
|
|
6594
|
+
* A failure that names a SLOT is a fifth case and is rendered separately — that
|
|
6595
|
+
* one is the author's file naming, and only the slot tells them which.
|
|
6596
|
+
*/
|
|
6597
|
+
const REC_ERROR_KEYS = {
|
|
6598
|
+
"download failed": "recFailNetwork",
|
|
6599
|
+
"not a profile": "recFailNotProfile",
|
|
6600
|
+
"newer version": "recFailNewer",
|
|
6601
|
+
"too many images": "recFailTooMany",
|
|
6602
|
+
"too large": "recFailTooLarge"
|
|
6603
|
+
};
|
|
6604
|
+
/** Size of a download, at the precision a person reads it at. */
|
|
6605
|
+
const fmtBytes = (n) => n >= 1048576 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
6147
6606
|
function ProfilePage({ p, notify }) {
|
|
6148
6607
|
const { t, exportTheme, importTheme, setTransition } = p;
|
|
6149
6608
|
const importRef = (0, react.useRef)(null);
|
|
6150
6609
|
p.useStore((s) => s.rev + s.rulesRev);
|
|
6151
6610
|
const holidays = cfg.holidays;
|
|
6152
6611
|
const tr = cfg.transition;
|
|
6612
|
+
const store = p.useStore((s) => s);
|
|
6613
|
+
const [text, setText] = (0, react.useState)("");
|
|
6614
|
+
const [touched, setTouched] = (0, react.useState)(false);
|
|
6615
|
+
(0, react.useEffect)(() => {
|
|
6616
|
+
if (!touched) setText(store.model);
|
|
6617
|
+
}, [store.model, touched]);
|
|
6618
|
+
const probe = text.trim();
|
|
6619
|
+
const result = (() => {
|
|
6620
|
+
if (probe === "") return null;
|
|
6621
|
+
const hit = matchRule(cfg.rules, probe);
|
|
6622
|
+
const rule = hit.rule;
|
|
6623
|
+
if (rule === null) return null;
|
|
6624
|
+
const n = cfg.rules.findIndex((r) => r.id === rule.id) + 1;
|
|
6625
|
+
return `${hit.matched ? t("tryoutHit") : t("tryoutFallback")} ${n}`;
|
|
6626
|
+
})();
|
|
6153
6627
|
const previewRef = (0, react.useRef)(null);
|
|
6154
6628
|
/** The OS preference, read at render: it vetoes the real switch (see
|
|
6155
6629
|
* `transitionPlan`), so the card has to say so rather than show a preview of
|
|
@@ -6190,6 +6664,47 @@ window.__ModuleLoader__.load({
|
|
|
6190
6664
|
notify(t("importFail"), false);
|
|
6191
6665
|
}
|
|
6192
6666
|
};
|
|
6667
|
+
const [pending, setPending] = (0, react.useState)(null);
|
|
6668
|
+
const [recBusy, setRecBusy] = (0, react.useState)(false);
|
|
6669
|
+
const [recDone, setRecDone] = (0, react.useState)(0);
|
|
6670
|
+
const [recTotal, setRecTotal] = (0, react.useState)(0);
|
|
6671
|
+
/** What the confirmation describes: countable facts about the download. */
|
|
6672
|
+
const recStats = (() => {
|
|
6673
|
+
if (pending === null) return null;
|
|
6674
|
+
const rules = pending.config?.rules;
|
|
6675
|
+
const images = pending.images ?? {};
|
|
6676
|
+
let bytes = 0;
|
|
6677
|
+
for (const url of Object.values(images)) bytes += url.length;
|
|
6678
|
+
return {
|
|
6679
|
+
rules: Array.isArray(rules) ? rules.length : 0,
|
|
6680
|
+
images: Object.keys(images).length,
|
|
6681
|
+
bytes
|
|
6682
|
+
};
|
|
6683
|
+
})();
|
|
6684
|
+
const onRecommended = async () => {
|
|
6685
|
+
setRecBusy(true);
|
|
6686
|
+
setRecDone(0);
|
|
6687
|
+
setRecTotal(0);
|
|
6688
|
+
const res = await p.fetchRecommended((done, total) => {
|
|
6689
|
+
setRecDone(done);
|
|
6690
|
+
setRecTotal(total);
|
|
6691
|
+
});
|
|
6692
|
+
setRecBusy(false);
|
|
6693
|
+
if (!res.ok) {
|
|
6694
|
+
notify(res.slot === void 0 ? t(REC_ERROR_KEYS[res.error] ?? "recFailNetwork") : `${t("recFailImage")} ${res.slot}`, false);
|
|
6695
|
+
return;
|
|
6696
|
+
}
|
|
6697
|
+
setPending(res.preset);
|
|
6698
|
+
};
|
|
6699
|
+
const onConfirmRecommended = async () => {
|
|
6700
|
+
const preset = pending;
|
|
6701
|
+
if (preset === null) return;
|
|
6702
|
+
setRecBusy(true);
|
|
6703
|
+
const ok = await p.applyRecommended(preset);
|
|
6704
|
+
setRecBusy(false);
|
|
6705
|
+
setPending(null);
|
|
6706
|
+
notify(ok ? t("recDone") : t("recFailApply"), ok);
|
|
6707
|
+
};
|
|
6193
6708
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
6194
6709
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
6195
6710
|
className: "dab-head dab-rise",
|
|
@@ -6270,12 +6785,7 @@ window.__ModuleLoader__.load({
|
|
|
6270
6785
|
}, easing))
|
|
6271
6786
|
})] }),
|
|
6272
6787
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6273
|
-
className: "dab-swatch-title",
|
|
6274
6788
|
style: { marginTop: 12 },
|
|
6275
|
-
children: t("trDuration")
|
|
6276
|
-
}),
|
|
6277
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6278
|
-
style: { marginTop: 2 },
|
|
6279
6789
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
6280
6790
|
label: t("trDuration"),
|
|
6281
6791
|
min: 0,
|
|
@@ -6326,75 +6836,219 @@ window.__ModuleLoader__.load({
|
|
|
6326
6836
|
}) : null
|
|
6327
6837
|
]
|
|
6328
6838
|
}),
|
|
6839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6840
|
+
className: "dab-card dab-rise",
|
|
6841
|
+
style: { "--d": 2 },
|
|
6842
|
+
children: [
|
|
6843
|
+
store.hostStale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6844
|
+
className: "dab-hint dab-stale",
|
|
6845
|
+
style: { marginBottom: 10 },
|
|
6846
|
+
children: t("hostStaleHint")
|
|
6847
|
+
}) : null,
|
|
6848
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6849
|
+
className: "dab-swatch-title",
|
|
6850
|
+
children: t("tryoutTitle")
|
|
6851
|
+
}),
|
|
6852
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6853
|
+
style: {
|
|
6854
|
+
marginTop: 10,
|
|
6855
|
+
display: "flex",
|
|
6856
|
+
flexWrap: "wrap",
|
|
6857
|
+
gap: 8,
|
|
6858
|
+
alignItems: "center"
|
|
6859
|
+
},
|
|
6860
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6861
|
+
type: "text",
|
|
6862
|
+
className: "dab-urlinput",
|
|
6863
|
+
value: text,
|
|
6864
|
+
placeholder: t("tryoutPlaceholder"),
|
|
6865
|
+
onChange: (e) => {
|
|
6866
|
+
setTouched(true);
|
|
6867
|
+
setText(e.target.value);
|
|
6868
|
+
}
|
|
6869
|
+
}), store.model !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6870
|
+
type: "button",
|
|
6871
|
+
className: "dab-btn",
|
|
6872
|
+
onClick: () => {
|
|
6873
|
+
setTouched(true);
|
|
6874
|
+
setText(store.model);
|
|
6875
|
+
},
|
|
6876
|
+
children: t("tryoutUseCurrent")
|
|
6877
|
+
}) : null]
|
|
6878
|
+
}),
|
|
6879
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6880
|
+
className: "dab-hint",
|
|
6881
|
+
style: { marginTop: 9 },
|
|
6882
|
+
children: probe === "" ? t("tryoutEmpty") : result === null ? t("tryoutNone") : result
|
|
6883
|
+
}),
|
|
6884
|
+
store.model === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6885
|
+
className: "dab-hint",
|
|
6886
|
+
style: { marginTop: 6 },
|
|
6887
|
+
children: t("statusUnknownHint")
|
|
6888
|
+
}) : null,
|
|
6889
|
+
store.model !== "" && store.modelSource === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6890
|
+
className: "dab-hint",
|
|
6891
|
+
style: { marginTop: 6 },
|
|
6892
|
+
children: t("statusSourceDefaultHint")
|
|
6893
|
+
}) : null,
|
|
6894
|
+
MODEL_NOTE_KEYS[store.modelNote] !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6895
|
+
className: "dab-hint",
|
|
6896
|
+
style: { marginTop: 6 },
|
|
6897
|
+
children: t(MODEL_NOTE_KEYS[store.modelNote])
|
|
6898
|
+
}) : null
|
|
6899
|
+
]
|
|
6900
|
+
}),
|
|
6329
6901
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6330
6902
|
className: "dab-profile-grid dab-rise",
|
|
6331
6903
|
style: { "--d": 3 },
|
|
6332
|
-
children: [
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6904
|
+
children: [
|
|
6905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6906
|
+
className: "dab-card dab-card-hover",
|
|
6907
|
+
children: [
|
|
6908
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6909
|
+
className: "dab-profile-ico",
|
|
6910
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 17 })
|
|
6911
|
+
}),
|
|
6912
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6913
|
+
className: "dab-profile-title",
|
|
6914
|
+
children: t("recCardTitle")
|
|
6915
|
+
}),
|
|
6916
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6917
|
+
className: "dab-profile-desc",
|
|
6918
|
+
children: t("recCardDesc")
|
|
6919
|
+
}),
|
|
6920
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6921
|
+
type: "button",
|
|
6922
|
+
className: "dab-btn dab-btn-primary",
|
|
6923
|
+
disabled: recBusy || store.hostStale,
|
|
6924
|
+
title: store.hostStale ? t("hostStaleHint") : t("recAdopt"),
|
|
6925
|
+
onClick: () => void onRecommended(),
|
|
6926
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), !recBusy ? t("recAdopt") : recTotal > 0 ? `${t("recBusy")} ${recDone}/${recTotal}` : t("recBusy")]
|
|
6927
|
+
})
|
|
6928
|
+
]
|
|
6929
|
+
}),
|
|
6930
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6931
|
+
className: "dab-card dab-card-hover",
|
|
6932
|
+
children: [
|
|
6933
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6934
|
+
className: "dab-profile-ico",
|
|
6935
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, { size: 17 })
|
|
6936
|
+
}),
|
|
6937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6938
|
+
className: "dab-profile-title",
|
|
6939
|
+
children: t("exportCardTitle")
|
|
6940
|
+
}),
|
|
6941
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6942
|
+
className: "dab-profile-desc",
|
|
6943
|
+
children: t("exportCardDesc")
|
|
6944
|
+
}),
|
|
6945
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6946
|
+
type: "button",
|
|
6947
|
+
className: "dab-btn dab-btn-primary",
|
|
6948
|
+
onClick: () => {
|
|
6949
|
+
exportTheme();
|
|
6950
|
+
notify(t("toastExportDone"));
|
|
6951
|
+
},
|
|
6952
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, { size: 14 }), t("exportTheme")]
|
|
6953
|
+
})
|
|
6954
|
+
]
|
|
6955
|
+
}),
|
|
6956
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6957
|
+
className: "dab-card dab-card-hover",
|
|
6958
|
+
children: [
|
|
6959
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6960
|
+
className: "dab-profile-ico",
|
|
6961
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 17 })
|
|
6962
|
+
}),
|
|
6963
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6964
|
+
className: "dab-profile-title",
|
|
6965
|
+
children: t("importCardTitle")
|
|
6966
|
+
}),
|
|
6967
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6968
|
+
className: "dab-profile-desc",
|
|
6969
|
+
children: t("importCardDesc")
|
|
6970
|
+
}),
|
|
6971
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6972
|
+
type: "button",
|
|
6973
|
+
className: "dab-btn",
|
|
6974
|
+
onClick: () => importRef.current?.click(),
|
|
6975
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 14 }), t("importTheme")]
|
|
6976
|
+
}),
|
|
6977
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6978
|
+
ref: importRef,
|
|
6979
|
+
type: "file",
|
|
6980
|
+
accept: "application/json,.json",
|
|
6981
|
+
style: { display: "none" },
|
|
6982
|
+
onChange: (e) => {
|
|
6983
|
+
const f = e.target.files?.[0];
|
|
6984
|
+
if (!f) return;
|
|
6985
|
+
onImport(f);
|
|
6986
|
+
e.target.value = "";
|
|
6987
|
+
}
|
|
6988
|
+
})
|
|
6989
|
+
]
|
|
6990
|
+
})
|
|
6991
|
+
]
|
|
6992
|
+
}),
|
|
6993
|
+
pending !== null && recStats !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6994
|
+
className: "dab-overlay",
|
|
6995
|
+
onClick: (e) => {
|
|
6996
|
+
if (e.target === e.currentTarget && !recBusy) setPending(null);
|
|
6997
|
+
},
|
|
6998
|
+
children: [
|
|
6999
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7000
|
+
className: "dab-overlay-title",
|
|
7001
|
+
children: t("recConfirmTitle")
|
|
7002
|
+
}),
|
|
7003
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7004
|
+
className: "dab-modal-card",
|
|
7005
|
+
style: {
|
|
7006
|
+
maxWidth: "min(92vw, 460px)",
|
|
7007
|
+
textAlign: "center"
|
|
7008
|
+
},
|
|
7009
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7010
|
+
className: "dab-overlay-hint",
|
|
7011
|
+
style: { lineHeight: 1.7 },
|
|
7012
|
+
children: t("recConfirmBody")
|
|
7013
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7014
|
+
className: "dab-overlay-hint",
|
|
7015
|
+
style: {
|
|
7016
|
+
marginTop: 10,
|
|
7017
|
+
fontFamily: "var(--dab-mono, monospace)"
|
|
6353
7018
|
},
|
|
6354
|
-
children:
|
|
6355
|
-
})
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
}),
|
|
6364
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6365
|
-
className: "dab-profile-title",
|
|
6366
|
-
children: t("importCardTitle")
|
|
6367
|
-
}),
|
|
6368
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6369
|
-
className: "dab-profile-desc",
|
|
6370
|
-
children: t("importCardDesc")
|
|
6371
|
-
}),
|
|
6372
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7019
|
+
children: `${recStats.rules} ${t("recStatRules")} · ${recStats.images} ${t("recStatImages")} · ${fmtBytes(recStats.bytes)}`
|
|
7020
|
+
})]
|
|
7021
|
+
}),
|
|
7022
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7023
|
+
style: {
|
|
7024
|
+
display: "flex",
|
|
7025
|
+
gap: 10
|
|
7026
|
+
},
|
|
7027
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6373
7028
|
type: "button",
|
|
6374
7029
|
className: "dab-btn",
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
}
|
|
6389
|
-
})
|
|
6390
|
-
]
|
|
6391
|
-
})]
|
|
6392
|
-
}),
|
|
7030
|
+
disabled: recBusy,
|
|
7031
|
+
onClick: () => setPending(null),
|
|
7032
|
+
children: t("recConfirmCancel")
|
|
7033
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7034
|
+
type: "button",
|
|
7035
|
+
className: "dab-btn dab-btn-danger",
|
|
7036
|
+
disabled: recBusy,
|
|
7037
|
+
onClick: () => void onConfirmRecommended(),
|
|
7038
|
+
children: t("recConfirmOk")
|
|
7039
|
+
})]
|
|
7040
|
+
})
|
|
7041
|
+
]
|
|
7042
|
+
}) }) : null,
|
|
6393
7043
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
6394
7044
|
className: "dab-footer dab-rise",
|
|
6395
7045
|
style: { "--d": 4 },
|
|
6396
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
7046
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
6397
7047
|
className: "dab-footer-mono",
|
|
7048
|
+
href: REPO_URL,
|
|
7049
|
+
target: "_blank",
|
|
7050
|
+
rel: "noreferrer noopener",
|
|
7051
|
+
title: t("repoLink"),
|
|
6398
7052
|
children: "dsh-background-by-model"
|
|
6399
7053
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("footerTag") })]
|
|
6400
7054
|
})
|
|
@@ -6458,8 +7112,12 @@ window.__ModuleLoader__.load({
|
|
|
6458
7112
|
className: "dab-shell",
|
|
6459
7113
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
|
|
6460
7114
|
className: "dab-nav",
|
|
6461
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
7115
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
6462
7116
|
className: "dab-brand",
|
|
7117
|
+
href: REPO_URL,
|
|
7118
|
+
target: "_blank",
|
|
7119
|
+
rel: "noreferrer noopener",
|
|
7120
|
+
title: t("repoLink"),
|
|
6463
7121
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6464
7122
|
className: "dab-brand-tile",
|
|
6465
7123
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 15 })
|
|
@@ -6515,14 +7173,52 @@ window.__ModuleLoader__.load({
|
|
|
6515
7173
|
"connection"
|
|
6516
7174
|
];
|
|
6517
7175
|
const CUSTOM_ID = "custom-color";
|
|
7176
|
+
/**
|
|
7177
|
+
* Ceiling on what one recommended profile may add up to, across every wallpaper.
|
|
7178
|
+
*
|
|
7179
|
+
* The node half caps each file and the number of files, but it answers one
|
|
7180
|
+
* independent call at a time and never sees a running total — the client is the
|
|
7181
|
+
* only side that does, so the total is bounded here. The real profile is 2.2 MB
|
|
7182
|
+
* of images across seven of them, so this is generous by design: it exists to
|
|
7183
|
+
* stop a config that names sixty-four eight-megabyte slots from turning one
|
|
7184
|
+
* button press into half a gigabyte, not to second-guess the author.
|
|
7185
|
+
*/
|
|
7186
|
+
const PRESET_TOTAL_MAX = 33554432;
|
|
7187
|
+
/**
|
|
7188
|
+
* How many wallpapers of a profile are in flight at once.
|
|
7189
|
+
*
|
|
7190
|
+
* Measured rather than guessed. The real seven-file profile, fetched against the
|
|
7191
|
+
* live asset host: 8.3 s one at a time, 4.8 s with four in flight (25 s when the
|
|
7192
|
+
* CDN edge was still cold). What is being paid is per-request latency, and the
|
|
7193
|
+
* host speaks HTTP/2 — the bytes are identical either way and the connection is
|
|
7194
|
+
* reused — so overlapping them buys wall-clock time and risks nothing but a
|
|
7195
|
+
* little contention.
|
|
7196
|
+
*
|
|
7197
|
+
* Four rather than all seven: a burst that wide competes for the same bandwidth
|
|
7198
|
+
* and makes the per-file timeout harder to reason about, and four already hides
|
|
7199
|
+
* most of the latency. Whichever worker finishes first takes the next file, so
|
|
7200
|
+
* the pool stays busy to the end rather than idling on the largest one.
|
|
7201
|
+
*/
|
|
7202
|
+
const PRESET_CONCURRENCY = 4;
|
|
6518
7203
|
function apply(ctx) {
|
|
6519
7204
|
initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
|
|
6520
7205
|
let customDispose = null;
|
|
6521
7206
|
let skinTimer = null;
|
|
7207
|
+
/** Colour scheme of the skin this plugin has registered, or null when none is. */
|
|
7208
|
+
let skinScheme = null;
|
|
7209
|
+
/** Scheme the host currently holds for our theme id; null when it is not there. */
|
|
7210
|
+
const registeredScheme = () => {
|
|
7211
|
+
const theme = ctx.theme.getTheme().themes.find((t) => t.id === CUSTOM_ID);
|
|
7212
|
+
return theme === void 0 ? null : theme.colorScheme;
|
|
7213
|
+
};
|
|
6522
7214
|
const registerCustom = (h, s, l) => {
|
|
7215
|
+
const { colorScheme, tokens } = genTokens(h, s, l);
|
|
7216
|
+
if (skinScheme === colorScheme && registeredScheme() !== null) {
|
|
7217
|
+
if (ctx.theme.getTheme().preference !== CUSTOM_ID) ctx.theme.setTheme(CUSTOM_ID);
|
|
7218
|
+
return;
|
|
7219
|
+
}
|
|
6523
7220
|
customDispose?.();
|
|
6524
7221
|
try {
|
|
6525
|
-
const { colorScheme, tokens } = genTokens(h, s, l);
|
|
6526
7222
|
customDispose = ctx.theme.register({
|
|
6527
7223
|
id: CUSTOM_ID,
|
|
6528
7224
|
colorScheme,
|
|
@@ -6531,12 +7227,14 @@ window.__ModuleLoader__.load({
|
|
|
6531
7227
|
} catch {
|
|
6532
7228
|
customDispose = null;
|
|
6533
7229
|
}
|
|
7230
|
+
skinScheme = registeredScheme() ?? colorScheme;
|
|
6534
7231
|
if (ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
|
|
6535
7232
|
};
|
|
6536
7233
|
/** A rule without a saved color means "follow the system theme". */
|
|
6537
7234
|
const dropCustom = () => {
|
|
6538
7235
|
customDispose?.();
|
|
6539
7236
|
customDispose = null;
|
|
7237
|
+
skinScheme = null;
|
|
6540
7238
|
try {
|
|
6541
7239
|
if (ctx.theme.getTheme().preference === CUSTOM_ID) ctx.theme.setTheme("system");
|
|
6542
7240
|
} catch {}
|
|
@@ -6847,10 +7545,12 @@ window.__ModuleLoader__.load({
|
|
|
6847
7545
|
/**
|
|
6848
7546
|
* The slot of the holiday that could paint TODAY, or null.
|
|
6849
7547
|
*
|
|
6850
|
-
* Gated on the CALENDAR, not merely on the switch
|
|
6851
|
-
*
|
|
6852
|
-
*
|
|
6853
|
-
*
|
|
7548
|
+
* Gated on the CALENDAR, not merely on the switch — and this gate is worth more
|
|
7549
|
+
* than it used to be. The art does not ship in the package any more: the first
|
|
7550
|
+
* read on the day sends the node half to the network, so a profile that is
|
|
7551
|
+
* simply not on a holiday must not merely avoid ~730 KB of base64 over this
|
|
7552
|
+
* channel, it must avoid starting a download at all. On every other day this
|
|
7553
|
+
* returns null and nothing is fetched, cached or paid for.
|
|
6854
7554
|
*/
|
|
6855
7555
|
const todayHolidaySlot = () => {
|
|
6856
7556
|
if (!cfg.holidays.enabled) return null;
|
|
@@ -7028,6 +7728,53 @@ window.__ModuleLoader__.load({
|
|
|
7028
7728
|
matched: activeMatched
|
|
7029
7729
|
};
|
|
7030
7730
|
};
|
|
7731
|
+
/**
|
|
7732
|
+
* Write a whole theme file into the store: its rules, its wallpapers, nothing
|
|
7733
|
+
* else. The ONE door both imports come through — the file the user picked and
|
|
7734
|
+
* the profile downloaded from the project's asset host — so a downloaded
|
|
7735
|
+
* profile cannot arrive by a laxer route than a hand-picked file does.
|
|
7736
|
+
*
|
|
7737
|
+
* `wipeFirst` is the entire difference between them, and it belongs to the
|
|
7738
|
+
* recommended profile alone. `nextSlot` hands slots out as `m1`, `m2`, … — a
|
|
7739
|
+
* counter, not a random token — so a downloaded profile and the store it lands
|
|
7740
|
+
* in collide on those names as a matter of course rather than by coincidence.
|
|
7741
|
+
* Emptying the store first is what makes "replace everything with this"
|
|
7742
|
+
* literally true, instead of overwriting whichever of the user's own pictures
|
|
7743
|
+
* happened to share a name. The file path keeps the older behaviour: it
|
|
7744
|
+
* replaces the rules but pre-emptively deletes nothing.
|
|
7745
|
+
*
|
|
7746
|
+
* The wipe happens HERE rather than before the download, and that ordering is
|
|
7747
|
+
* the one thing worth being pedantic about: an unreachable CDN must not cost
|
|
7748
|
+
* the user their configuration on the way to a profile that never arrived. By
|
|
7749
|
+
* the time this runs the bytes are already in hand, so the caller has nothing
|
|
7750
|
+
* left to fail at.
|
|
7751
|
+
*/
|
|
7752
|
+
const applyThemeFile = async (data, wipeFirst) => {
|
|
7753
|
+
if (typeof data.version === "number" && data.version > 6) return false;
|
|
7754
|
+
if (typeof data.config !== "object" || data.config === null) return false;
|
|
7755
|
+
if (wipeFirst) {
|
|
7756
|
+
await resetStore();
|
|
7757
|
+
resetConfig();
|
|
7758
|
+
}
|
|
7759
|
+
adoptConfig(data.config);
|
|
7760
|
+
const incoming = data.images ?? {};
|
|
7761
|
+
const keep = /* @__PURE__ */ new Set();
|
|
7762
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
7763
|
+
keep.add(slot);
|
|
7764
|
+
const raw = incoming[slot];
|
|
7765
|
+
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
7766
|
+
setImage(slot, raw);
|
|
7767
|
+
writeImage(slot, raw);
|
|
7768
|
+
} else {
|
|
7769
|
+
setImage(slot, null);
|
|
7770
|
+
deleteImage(slot);
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && /^[A-Za-z0-9_-]{1,32}$/.test(slot)) deleteImage(slot);
|
|
7774
|
+
persistConfig();
|
|
7775
|
+
applyActive();
|
|
7776
|
+
return true;
|
|
7777
|
+
};
|
|
7031
7778
|
const sectionInject = (actions) => {
|
|
7032
7779
|
bound = actions;
|
|
7033
7780
|
sync();
|
|
@@ -7091,6 +7838,7 @@ window.__ModuleLoader__.load({
|
|
|
7091
7838
|
const slot = nextSlot();
|
|
7092
7839
|
const image = {
|
|
7093
7840
|
slot,
|
|
7841
|
+
bgMode: "fit",
|
|
7094
7842
|
bgState: { ...DEFAULT_BG_STATE },
|
|
7095
7843
|
color: null
|
|
7096
7844
|
};
|
|
@@ -7111,6 +7859,7 @@ window.__ModuleLoader__.load({
|
|
|
7111
7859
|
if (idx < 0) return;
|
|
7112
7860
|
const winner = winnerId();
|
|
7113
7861
|
rule.images.splice(idx, 1);
|
|
7862
|
+
if (rule.images.length < 2 && rule.rotate.enabled) patchRotation(rule, { enabled: false });
|
|
7114
7863
|
if (id === activeRuleId) setRotIndex(0);
|
|
7115
7864
|
rulesRev++;
|
|
7116
7865
|
persistConfig();
|
|
@@ -7151,6 +7900,29 @@ window.__ModuleLoader__.load({
|
|
|
7151
7900
|
saveConfig();
|
|
7152
7901
|
repaintIfMoved(id, winner);
|
|
7153
7902
|
},
|
|
7903
|
+
/**
|
|
7904
|
+
* Store ONE image's layout mode. Addressed by slot like `setImageFraming`,
|
|
7905
|
+
* for the same reason: how a picture meets the viewport is a property of the
|
|
7906
|
+
* picture, and 适应 on a tall screenshot says nothing about the landscape
|
|
7907
|
+
* photo beside it in the rotation.
|
|
7908
|
+
*
|
|
7909
|
+
* A mode is a placement, not a paint: it changes nothing about WHICH image is
|
|
7910
|
+
* on screen, only how it is laid out. So the patch is written, sanitized, and
|
|
7911
|
+
* the layer repainted through the same `repaintIfMoved` the framing commit
|
|
7912
|
+
* uses — that call repaints the current image whether or not the winner
|
|
7913
|
+
* moved, which is what makes the chips feel live.
|
|
7914
|
+
*/
|
|
7915
|
+
setImageMode: (id, slot, mode) => {
|
|
7916
|
+
const rule = ruleById(id);
|
|
7917
|
+
const image = rule?.images.find((i) => i.slot === slot);
|
|
7918
|
+
if (rule === null || image === void 0) return;
|
|
7919
|
+
const winner = winnerId();
|
|
7920
|
+
image.bgMode = mode;
|
|
7921
|
+
normalizeRuleInPlace(rule);
|
|
7922
|
+
rulesRev++;
|
|
7923
|
+
saveConfig();
|
|
7924
|
+
repaintIfMoved(id, winner);
|
|
7925
|
+
},
|
|
7154
7926
|
setRuleRotation: (id, patch) => {
|
|
7155
7927
|
const rule = ruleById(id);
|
|
7156
7928
|
if (rule === null) return;
|
|
@@ -7205,6 +7977,7 @@ window.__ModuleLoader__.load({
|
|
|
7205
7977
|
const winner = winnerId();
|
|
7206
7978
|
rule.images.push({
|
|
7207
7979
|
slot,
|
|
7980
|
+
bgMode: "fit",
|
|
7208
7981
|
bgState: { ...DEFAULT_BG_STATE },
|
|
7209
7982
|
color: null
|
|
7210
7983
|
});
|
|
@@ -7329,29 +8102,93 @@ window.__ModuleLoader__.load({
|
|
|
7329
8102
|
try {
|
|
7330
8103
|
const data = JSON.parse(await file.text());
|
|
7331
8104
|
if (!data || typeof data !== "object") return false;
|
|
7332
|
-
|
|
7333
|
-
if (typeof d.config !== "object" || d.config === null) return false;
|
|
7334
|
-
adoptConfig(d.config);
|
|
7335
|
-
const incoming = d.images ?? {};
|
|
7336
|
-
const keep = /* @__PURE__ */ new Set();
|
|
7337
|
-
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
7338
|
-
keep.add(slot);
|
|
7339
|
-
const raw = incoming[slot];
|
|
7340
|
-
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
7341
|
-
setImage(slot, raw);
|
|
7342
|
-
writeImage(slot, raw);
|
|
7343
|
-
} else {
|
|
7344
|
-
setImage(slot, null);
|
|
7345
|
-
deleteImage(slot);
|
|
7346
|
-
}
|
|
7347
|
-
}
|
|
7348
|
-
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && /^[A-Za-z0-9_-]{1,32}$/.test(slot)) deleteImage(slot);
|
|
7349
|
-
persistConfig();
|
|
7350
|
-
applyActive();
|
|
7351
|
-
return true;
|
|
8105
|
+
return await applyThemeFile(data, false);
|
|
7352
8106
|
} catch {
|
|
7353
8107
|
return false;
|
|
7354
8108
|
}
|
|
8109
|
+
},
|
|
8110
|
+
/**
|
|
8111
|
+
* Fetch the project's recommended profile. Downloads only.
|
|
8112
|
+
*
|
|
8113
|
+
* Split from applying it on purpose: the page has to ask before anything is
|
|
8114
|
+
* destroyed, and a confirmation can only describe what is actually in hand
|
|
8115
|
+
* — "7 rules, 7 wallpapers, 2.9 MB" — which means the download comes first
|
|
8116
|
+
* and the question second.
|
|
8117
|
+
*
|
|
8118
|
+
* The profile is a DIRECTORY now, so this is a pool rather than one call:
|
|
8119
|
+
* the manifest and config say which slots exist, and each wallpaper is one
|
|
8120
|
+
* request, up to `PRESET_CONCURRENCY` of them at a time. What the fan-out
|
|
8121
|
+
* buys is wall-clock time — the real profile measured 8.3 s fetched one at
|
|
8122
|
+
* a time against 4.8 s with four in flight — while each file keeps its own
|
|
8123
|
+
* mirror fallback, its own validation, and its own slot name in a failure.
|
|
8124
|
+
*
|
|
8125
|
+
* STRICT, and the pool is what makes that a decision rather than a
|
|
8126
|
+
* formality: the first wallpaper that cannot be had stops the queue, so the
|
|
8127
|
+
* files nobody has asked for yet are never asked for. Whatever was already
|
|
8128
|
+
* in flight finishes and is thrown away. Nothing has been touched at this
|
|
8129
|
+
* point, so "the author renamed a file" arrives as an error naming the file
|
|
8130
|
+
* rather than as a profile that is quietly missing a picture — and that
|
|
8131
|
+
* loudness is the whole reason this feature is allowed to follow a branch.
|
|
8132
|
+
*/
|
|
8133
|
+
fetchRecommended: async (onProgress) => {
|
|
8134
|
+
const head = await fetchPresetConfig();
|
|
8135
|
+
if (!head.ok) return {
|
|
8136
|
+
ok: false,
|
|
8137
|
+
error: head.error
|
|
8138
|
+
};
|
|
8139
|
+
const slots = head.slots;
|
|
8140
|
+
const images = {};
|
|
8141
|
+
let next = 0;
|
|
8142
|
+
let done = 0;
|
|
8143
|
+
let bytes = 0;
|
|
8144
|
+
/** Latched by the first worker to fail, so the rest stop asking. */
|
|
8145
|
+
let stopped = false;
|
|
8146
|
+
onProgress?.(0, slots.length);
|
|
8147
|
+
const worker = async () => {
|
|
8148
|
+
for (;;) {
|
|
8149
|
+
if (stopped || next >= slots.length) return null;
|
|
8150
|
+
const slot = slots[next++];
|
|
8151
|
+
const got = await fetchPresetImage(slot);
|
|
8152
|
+
if (!got.ok) {
|
|
8153
|
+
stopped = true;
|
|
8154
|
+
return {
|
|
8155
|
+
error: got.error,
|
|
8156
|
+
slot
|
|
8157
|
+
};
|
|
8158
|
+
}
|
|
8159
|
+
images[slot] = got.dataUrl;
|
|
8160
|
+
bytes += got.dataUrl.length;
|
|
8161
|
+
if (bytes > PRESET_TOTAL_MAX) {
|
|
8162
|
+
stopped = true;
|
|
8163
|
+
return { error: "too large" };
|
|
8164
|
+
}
|
|
8165
|
+
onProgress?.(++done, slots.length);
|
|
8166
|
+
}
|
|
8167
|
+
};
|
|
8168
|
+
const failure = (await Promise.all(Array.from({ length: Math.min(PRESET_CONCURRENCY, slots.length) }, worker))).find((r) => r !== null) ?? null;
|
|
8169
|
+
if (failure !== null) return {
|
|
8170
|
+
ok: false,
|
|
8171
|
+
error: failure.error,
|
|
8172
|
+
slot: failure.slot
|
|
8173
|
+
};
|
|
8174
|
+
return {
|
|
8175
|
+
ok: true,
|
|
8176
|
+
preset: {
|
|
8177
|
+
version: head.version,
|
|
8178
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8179
|
+
config: head.config,
|
|
8180
|
+
images
|
|
8181
|
+
}
|
|
8182
|
+
};
|
|
8183
|
+
},
|
|
8184
|
+
/**
|
|
8185
|
+
* Replace EVERYTHING with a downloaded profile: empty the store, then write
|
|
8186
|
+
* the profile into it. Irreversible by design, which is why the page asks
|
|
8187
|
+
* first and why nothing here is offered as an undo.
|
|
8188
|
+
*/
|
|
8189
|
+
applyRecommended: async (preset) => {
|
|
8190
|
+
if (!canWriteConfig()) return false;
|
|
8191
|
+
return await applyThemeFile(preset, true);
|
|
7355
8192
|
}
|
|
7356
8193
|
};
|
|
7357
8194
|
};
|