dsh-background-by-model 0.7.5 → 0.7.7
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 +25 -1
- package/CHANGELOG.zh.md +25 -1
- package/README.md +58 -26
- package/README.zh.md +58 -26
- package/lib/client.js +1252 -559
- package/lib/index.js +472 -33
- package/package.json +4 -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
|
@@ -55,12 +55,21 @@ window.__ModuleLoader__.load({
|
|
|
55
55
|
tryoutEmpty: "输入模型名即可试跑",
|
|
56
56
|
autoExtract: "选图后自动取主题色",
|
|
57
57
|
ruleAdd: "新增规则",
|
|
58
|
+
ruleNoneHint: "还没有规则,点上面的「新增规则」新建一条",
|
|
58
59
|
ruleFallbackBadge: "兜底",
|
|
59
60
|
ruleMatchPlaceholder: "如 flash / glm(忽略大小写,留空则不参与匹配)",
|
|
60
61
|
ruleEnabled: "启用",
|
|
61
62
|
ruleUp: "上移",
|
|
62
63
|
ruleDown: "下移",
|
|
63
64
|
ruleRemove: "删除规则",
|
|
65
|
+
tabListLabel: "规则",
|
|
66
|
+
tabPick: "选择规则",
|
|
67
|
+
tabMore: "更多规则",
|
|
68
|
+
tabUnnamed: "未命名规则",
|
|
69
|
+
tabLive: "正在使用",
|
|
70
|
+
tabRotating: "轮换中",
|
|
71
|
+
tabOff: "已停用",
|
|
72
|
+
ruleRemoveConfirm: "再点一次删除",
|
|
64
73
|
ruleNoImage: "尚未选择图片",
|
|
65
74
|
ruleFromUrl: "从网址",
|
|
66
75
|
ruleUrlPlaceholder: "粘贴图片网址 https://…",
|
|
@@ -112,7 +121,6 @@ window.__ModuleLoader__.load({
|
|
|
112
121
|
rotEvery1m: "1 分钟",
|
|
113
122
|
rotEvery5m: "5 分钟",
|
|
114
123
|
rotEvery30m: "30 分钟",
|
|
115
|
-
rotEvery1h: "1 小时",
|
|
116
124
|
bgModeFit: "适应",
|
|
117
125
|
bgModeFill: "填充",
|
|
118
126
|
bgModeStretch: "拉伸",
|
|
@@ -142,6 +150,24 @@ window.__ModuleLoader__.load({
|
|
|
142
150
|
toastExportDone: "配置文件已开始下载",
|
|
143
151
|
importDone: "已导入配置",
|
|
144
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: "未能写入推荐配置,你原来的配置没有被改动",
|
|
145
171
|
footerTag: "按模型换背景插件",
|
|
146
172
|
repoLink: "在 GitHub 上打开仓库",
|
|
147
173
|
holidayTitle: "节日特殊背景",
|
|
@@ -199,12 +225,21 @@ window.__ModuleLoader__.load({
|
|
|
199
225
|
tryoutEmpty: "Type a model name to test the list",
|
|
200
226
|
autoExtract: "Extract the theme color when an image is chosen",
|
|
201
227
|
ruleAdd: "Add rule",
|
|
228
|
+
ruleNoneHint: "No rules yet — use “Add rule” in the tab strip above",
|
|
202
229
|
ruleFallbackBadge: "fallback",
|
|
203
230
|
ruleMatchPlaceholder: "e.g. flash / glm (case-insensitive, empty never matches)",
|
|
204
231
|
ruleEnabled: "Enabled",
|
|
205
232
|
ruleUp: "Move up",
|
|
206
233
|
ruleDown: "Move down",
|
|
207
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",
|
|
208
243
|
ruleNoImage: "No image yet",
|
|
209
244
|
ruleFromUrl: "From URL",
|
|
210
245
|
ruleUrlPlaceholder: "Paste an image URL https://…",
|
|
@@ -256,7 +291,6 @@ window.__ModuleLoader__.load({
|
|
|
256
291
|
rotEvery1m: "1 min",
|
|
257
292
|
rotEvery5m: "5 min",
|
|
258
293
|
rotEvery30m: "30 min",
|
|
259
|
-
rotEvery1h: "1 h",
|
|
260
294
|
bgModeFit: "Fit",
|
|
261
295
|
bgModeFill: "Fill",
|
|
262
296
|
bgModeStretch: "Stretch",
|
|
@@ -286,6 +320,24 @@ window.__ModuleLoader__.load({
|
|
|
286
320
|
toastExportDone: "Export started",
|
|
287
321
|
importDone: "Profile imported",
|
|
288
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",
|
|
289
341
|
footerTag: "Per-model wallpaper plugin",
|
|
290
342
|
repoLink: "Open the repository on GitHub",
|
|
291
343
|
holidayTitle: "Holiday backgrounds",
|
|
@@ -596,11 +648,11 @@ window.__ModuleLoader__.load({
|
|
|
596
648
|
"input"
|
|
597
649
|
];
|
|
598
650
|
const DEFAULT_PART_OPACITIES = {
|
|
599
|
-
bg:
|
|
600
|
-
sidebar: .
|
|
651
|
+
bg: 0,
|
|
652
|
+
sidebar: .1,
|
|
601
653
|
card: 1,
|
|
602
|
-
code:
|
|
603
|
-
input:
|
|
654
|
+
code: .81,
|
|
655
|
+
input: .88
|
|
604
656
|
};
|
|
605
657
|
/**
|
|
606
658
|
* Interface blur (px, 0..60), global (Interface page):
|
|
@@ -631,7 +683,7 @@ window.__ModuleLoader__.load({
|
|
|
631
683
|
chat: 0,
|
|
632
684
|
trajectory: 0,
|
|
633
685
|
rightbar: 0,
|
|
634
|
-
input:
|
|
686
|
+
input: 60
|
|
635
687
|
};
|
|
636
688
|
/** Longest allowed switch duration; 0 is a hard cut. */
|
|
637
689
|
const TRANSITION_MAX_MS = 3e3;
|
|
@@ -690,6 +742,9 @@ window.__ModuleLoader__.load({
|
|
|
690
742
|
function ruleCanPaint(rule) {
|
|
691
743
|
return rule.enabled && (ruleSlots(rule).length > 0 || rule.color !== null);
|
|
692
744
|
}
|
|
745
|
+
const DEFAULT_CHAT_TEXT_OPACITY = .35;
|
|
746
|
+
const DEFAULT_TRAJECTORY_OPACITY = .73;
|
|
747
|
+
const DEFAULT_RIGHTBAR_OPACITY = .45;
|
|
693
748
|
/**
|
|
694
749
|
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
695
750
|
* null when the string is not a hex color.
|
|
@@ -766,12 +821,12 @@ window.__ModuleLoader__.load({
|
|
|
766
821
|
blurs: { ...DEFAULT_PART_BLURS },
|
|
767
822
|
transition: defaultTransition(),
|
|
768
823
|
settingsOpacity: 1,
|
|
769
|
-
chatTextOpacity:
|
|
770
|
-
trajectoryOpacity:
|
|
771
|
-
rightbarOpacity:
|
|
824
|
+
chatTextOpacity: DEFAULT_CHAT_TEXT_OPACITY,
|
|
825
|
+
trajectoryOpacity: DEFAULT_TRAJECTORY_OPACITY,
|
|
826
|
+
rightbarOpacity: DEFAULT_RIGHTBAR_OPACITY,
|
|
772
827
|
autoExtract: true,
|
|
773
828
|
holidays: {
|
|
774
|
-
enabled:
|
|
829
|
+
enabled: false,
|
|
775
830
|
items: defaultHolidayRules()
|
|
776
831
|
}
|
|
777
832
|
};
|
|
@@ -914,7 +969,7 @@ window.__ModuleLoader__.load({
|
|
|
914
969
|
* is still what the rule paints once its last picture is gone.
|
|
915
970
|
*
|
|
916
971
|
* The layout mode makes the same trip in the other direction, and this is the
|
|
917
|
-
* last release that has to: a config written before 0.
|
|
972
|
+
* last release that has to: a config written before 0.7.5 keeps `bgMode` at the RULE
|
|
918
973
|
* level, where one value covered every picture of a rotation, so it is read here
|
|
919
974
|
* once and handed to each image as its inherited mode. What the user chose
|
|
920
975
|
* therefore survives the move the same way it survived the color's — the mode is
|
|
@@ -1006,10 +1061,32 @@ window.__ModuleLoader__.load({
|
|
|
1006
1061
|
if (typeof id === "string") stored.set(id, item);
|
|
1007
1062
|
}
|
|
1008
1063
|
return {
|
|
1009
|
-
enabled: r.enabled
|
|
1064
|
+
enabled: typeof r.enabled === "boolean" ? r.enabled : false,
|
|
1010
1065
|
items: HOLIDAYS.map((def) => normalizeHolidayRule(def, stored.get(def.id)))
|
|
1011
1066
|
};
|
|
1012
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The file-preview panel's own opacity.
|
|
1070
|
+
*
|
|
1071
|
+
* Three states, and the middle one is why this is a function rather than the
|
|
1072
|
+
* one-line fallback every other field gets: a real number is the card's own
|
|
1073
|
+
* value, `null` is "never owned, so follow `opacities.bg`", and a KEY that is not
|
|
1074
|
+
* there at all — a hand-written config, or one written before the option existed
|
|
1075
|
+
* — is the shipped profile's 45% (see DEFAULT_RIGHTBAR_OPACITY).
|
|
1076
|
+
*
|
|
1077
|
+
* `null` has to survive as ITSELF instead of falling through to the default:
|
|
1078
|
+
* every release so far has written it — a panel the user never dragged is
|
|
1079
|
+
* persisted as an explicit `null` — so treating it as "missing" would hand the
|
|
1080
|
+
* shipped 45% to every configuration whose preview panel was following the main
|
|
1081
|
+
* background, a silent change to a setup nobody asked to change. It is also
|
|
1082
|
+
* unreachable from the UI (no control returns to it); it exists for configs that
|
|
1083
|
+
* were written while the shipped default was `null`.
|
|
1084
|
+
*/
|
|
1085
|
+
function normalizeRightbarOpacity(v) {
|
|
1086
|
+
if (v === null) return null;
|
|
1087
|
+
if (typeof v === "number" && isFinite(v)) return clamp01(v, 1);
|
|
1088
|
+
return DEFAULT_RIGHTBAR_OPACITY;
|
|
1089
|
+
}
|
|
1013
1090
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
1014
1091
|
function normalizeConfig(raw) {
|
|
1015
1092
|
const r = raw ?? {};
|
|
@@ -1026,57 +1103,13 @@ window.__ModuleLoader__.load({
|
|
|
1026
1103
|
blurs,
|
|
1027
1104
|
transition: normalizeTransition(r.transition, legacyFadeMs(r.rules)),
|
|
1028
1105
|
settingsOpacity: clamp01(r.settingsOpacity, 1),
|
|
1029
|
-
chatTextOpacity: clamp01(r.chatTextOpacity,
|
|
1030
|
-
trajectoryOpacity: clamp01(r.trajectoryOpacity,
|
|
1031
|
-
rightbarOpacity:
|
|
1106
|
+
chatTextOpacity: clamp01(r.chatTextOpacity, DEFAULT_CHAT_TEXT_OPACITY),
|
|
1107
|
+
trajectoryOpacity: clamp01(r.trajectoryOpacity, DEFAULT_TRAJECTORY_OPACITY),
|
|
1108
|
+
rightbarOpacity: normalizeRightbarOpacity(r.rightbarOpacity),
|
|
1032
1109
|
autoExtract: r.autoExtract !== false,
|
|
1033
1110
|
holidays: normalizeHolidays(r.holidays)
|
|
1034
1111
|
};
|
|
1035
1112
|
}
|
|
1036
|
-
//#endregion
|
|
1037
|
-
//#region src/client/state.ts
|
|
1038
|
-
const PALETTE = [
|
|
1039
|
-
[
|
|
1040
|
-
356,
|
|
1041
|
-
.72,
|
|
1042
|
-
.55
|
|
1043
|
-
],
|
|
1044
|
-
[
|
|
1045
|
-
24,
|
|
1046
|
-
.78,
|
|
1047
|
-
.55
|
|
1048
|
-
],
|
|
1049
|
-
[
|
|
1050
|
-
44,
|
|
1051
|
-
.8,
|
|
1052
|
-
.55
|
|
1053
|
-
],
|
|
1054
|
-
[
|
|
1055
|
-
152,
|
|
1056
|
-
.62,
|
|
1057
|
-
.5
|
|
1058
|
-
],
|
|
1059
|
-
[
|
|
1060
|
-
174,
|
|
1061
|
-
.68,
|
|
1062
|
-
.48
|
|
1063
|
-
],
|
|
1064
|
-
[
|
|
1065
|
-
208,
|
|
1066
|
-
.72,
|
|
1067
|
-
.55
|
|
1068
|
-
],
|
|
1069
|
-
[
|
|
1070
|
-
252,
|
|
1071
|
-
.68,
|
|
1072
|
-
.6
|
|
1073
|
-
],
|
|
1074
|
-
[
|
|
1075
|
-
300,
|
|
1076
|
-
.64,
|
|
1077
|
-
.58
|
|
1078
|
-
]
|
|
1079
|
-
];
|
|
1080
1113
|
freshThemeConfig();
|
|
1081
1114
|
let cfg = freshThemeConfig();
|
|
1082
1115
|
const images = /* @__PURE__ */ new Map();
|
|
@@ -1128,6 +1161,10 @@ window.__ModuleLoader__.load({
|
|
|
1128
1161
|
return null;
|
|
1129
1162
|
}
|
|
1130
1163
|
}
|
|
1164
|
+
function clearImages() {
|
|
1165
|
+
for (const slot of Array.from(displayUrls.keys())) revokeDisplay(slot);
|
|
1166
|
+
images.clear();
|
|
1167
|
+
}
|
|
1131
1168
|
let activeRuleId = null;
|
|
1132
1169
|
let activeMatched = false;
|
|
1133
1170
|
let modelLabel = "";
|
|
@@ -1352,10 +1389,10 @@ window.__ModuleLoader__.load({
|
|
|
1352
1389
|
return clamp01(cfg.settingsOpacity, 1);
|
|
1353
1390
|
}
|
|
1354
1391
|
function rChatTextOpacity() {
|
|
1355
|
-
return clamp01(cfg.chatTextOpacity,
|
|
1392
|
+
return clamp01(cfg.chatTextOpacity, DEFAULT_CHAT_TEXT_OPACITY);
|
|
1356
1393
|
}
|
|
1357
1394
|
function rTrajectoryOpacity() {
|
|
1358
|
-
return clamp01(cfg.trajectoryOpacity,
|
|
1395
|
+
return clamp01(cfg.trajectoryOpacity, DEFAULT_TRAJECTORY_OPACITY);
|
|
1359
1396
|
}
|
|
1360
1397
|
/** Own opacity of the file-preview panel; while unowned it IS the main background's. */
|
|
1361
1398
|
function rRightbarOpacity() {
|
|
@@ -1371,6 +1408,14 @@ window.__ModuleLoader__.load({
|
|
|
1371
1408
|
}
|
|
1372
1409
|
rotIndex = 0;
|
|
1373
1410
|
}
|
|
1411
|
+
function resetConfig() {
|
|
1412
|
+
cfg = freshThemeConfig();
|
|
1413
|
+
clearImages();
|
|
1414
|
+
activeRuleId = null;
|
|
1415
|
+
activeMatched = false;
|
|
1416
|
+
modelLabel = "";
|
|
1417
|
+
rotIndex = 0;
|
|
1418
|
+
}
|
|
1374
1419
|
//#endregion
|
|
1375
1420
|
//#region src/client/rotation.ts
|
|
1376
1421
|
/** Dwell-time presets the settings UI offers, in ms. */
|
|
@@ -1394,10 +1439,6 @@ window.__ModuleLoader__.load({
|
|
|
1394
1439
|
{
|
|
1395
1440
|
ms: 18e5,
|
|
1396
1441
|
key: "rotEvery30m"
|
|
1397
|
-
},
|
|
1398
|
-
{
|
|
1399
|
-
ms: 36e5,
|
|
1400
|
-
key: "rotEvery1h"
|
|
1401
1442
|
}
|
|
1402
1443
|
];
|
|
1403
1444
|
/**
|
|
@@ -1601,6 +1642,52 @@ window.__ModuleLoader__.load({
|
|
|
1601
1642
|
error: v?.error ?? "failed"
|
|
1602
1643
|
};
|
|
1603
1644
|
}
|
|
1645
|
+
/**
|
|
1646
|
+
* The profile's manifest and config, in one answer. Reads nothing, writes nothing.
|
|
1647
|
+
*
|
|
1648
|
+
* Raw rather than `rpcCall` for the same reason `fetchImageUrl` is: the node half
|
|
1649
|
+
* answers this one with a value that CARRIES its own failure, and the `error`
|
|
1650
|
+
* inside it is the only thing worth showing the user — the generic "rpc failed"
|
|
1651
|
+
* log would throw that distinction away.
|
|
1652
|
+
*/
|
|
1653
|
+
async function fetchPresetConfig() {
|
|
1654
|
+
return unwrapPreset(await rpcRaw("fetchPresetConfig", {}));
|
|
1655
|
+
}
|
|
1656
|
+
/** One of the profile's wallpapers, addressed by the slot the config names. */
|
|
1657
|
+
async function fetchPresetImage(slot) {
|
|
1658
|
+
return unwrapPreset(await rpcRaw("fetchPresetImage", { slot }));
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* The envelope both profile calls share: a transport failure and a value-level
|
|
1662
|
+
* failure have to come out the same shape, because the page renders one message
|
|
1663
|
+
* either way and must not have to know which layer said no.
|
|
1664
|
+
*/
|
|
1665
|
+
function unwrapPreset(res) {
|
|
1666
|
+
if (!res) return {
|
|
1667
|
+
ok: false,
|
|
1668
|
+
error: "no response"
|
|
1669
|
+
};
|
|
1670
|
+
if (res.ok !== true) return {
|
|
1671
|
+
ok: false,
|
|
1672
|
+
error: res.error?.message ?? "request failed"
|
|
1673
|
+
};
|
|
1674
|
+
const v = res.value;
|
|
1675
|
+
if (v?.ok !== true) return {
|
|
1676
|
+
ok: false,
|
|
1677
|
+
error: typeof v?.error === "string" ? v.error : "failed"
|
|
1678
|
+
};
|
|
1679
|
+
return v;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Empty the store: the config and every rule image.
|
|
1683
|
+
*
|
|
1684
|
+
* Only the recommended profile uses this, and only after its download has
|
|
1685
|
+
* already succeeded — see `applyRecommended`. Nothing else in the plugin deletes
|
|
1686
|
+
* the user's configuration wholesale.
|
|
1687
|
+
*/
|
|
1688
|
+
async function resetStore() {
|
|
1689
|
+
return await rpcCall("resetStore", {}) === true;
|
|
1690
|
+
}
|
|
1604
1691
|
/** Host default model — used only when the per-session services are absent. */
|
|
1605
1692
|
async function readDefaultModel() {
|
|
1606
1693
|
const data = await rpcCall("defaultModel", {});
|
|
@@ -2042,6 +2129,34 @@ window.__ModuleLoader__.load({
|
|
|
2042
2129
|
.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}
|
|
2043
2130
|
.dab-root *,.dab-root *::before,.dab-root *::after{box-sizing:border-box}
|
|
2044
2131
|
.dab-root button{font-family:inherit}
|
|
2132
|
+
/* ── scrollbars: the same bar as the settings panel we sit inside ────────────
|
|
2133
|
+
* The host styles scrollbars globally off its --dsh-scrollbar-* tokens, but those
|
|
2134
|
+
* declarations describe the app shell and its own scroll containers; a strip
|
|
2135
|
+
* nested this deep got the platform default instead — a thick light bar with
|
|
2136
|
+
* arrows, which is what the tab strip used to show beside the panel's own slim
|
|
2137
|
+
* vertical one.
|
|
2138
|
+
*
|
|
2139
|
+
* So the plugin declares the block for its OWN scroll containers. The values are
|
|
2140
|
+
* the host theme package's, for the layer-2 surface this panel is painted in:
|
|
2141
|
+
* 5px, rounded, layer-2 tint, transparent track. Two notes for whoever edits it:
|
|
2142
|
+
*
|
|
2143
|
+
* - the tokens go ON THE SCROLLER (the selector that carries the overflow), not
|
|
2144
|
+
* on .dab-root. A container that declares scrollbar-width:thin (as the nav
|
|
2145
|
+
* rail does on narrow panels) paints its own standard bar regardless of the
|
|
2146
|
+
* webkit block, and the host's standard-property fallback reads the thumb
|
|
2147
|
+
* token from that same element.
|
|
2148
|
+
* - every selector here is scoped to a dab- class: the stylesheet is injected
|
|
2149
|
+
* into the host page once, so a bare webkit-scrollbar selector would restyle
|
|
2150
|
+
* the whole harness. */
|
|
2151
|
+
.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}
|
|
2152
|
+
.dab-tabs::-webkit-scrollbar,.dab-pick-menu::-webkit-scrollbar{width:var(--dsh-scrollbar-width,5px);height:var(--dsh-scrollbar-width,5px)}
|
|
2153
|
+
.dab-tabs::-webkit-scrollbar-track,.dab-pick-menu::-webkit-scrollbar-track{background:transparent}
|
|
2154
|
+
.dab-tabs::-webkit-scrollbar-thumb,.dab-pick-menu::-webkit-scrollbar-thumb{background:var(--dsh-scrollbar-thumb);background-clip:content-box;border-radius:999px}
|
|
2155
|
+
.dab-tabs::-webkit-scrollbar-thumb:hover,.dab-pick-menu::-webkit-scrollbar-thumb:hover{background:var(--dsh-scrollbar-thumb-hover)}
|
|
2156
|
+
.dab-tabs::-webkit-scrollbar-corner,.dab-pick-menu::-webkit-scrollbar-corner{background:transparent}
|
|
2157
|
+
/* The host hides the platform's stepper arrows; without this the strip keeps the
|
|
2158
|
+
little os arrows at both ends of the track while the panel's bar has none. */
|
|
2159
|
+
.dab-tabs::-webkit-scrollbar-button,.dab-pick-menu::-webkit-scrollbar-button{display:none}
|
|
2045
2160
|
|
|
2046
2161
|
/* ── shell: nav rail + page body ─────────────────────────────────────────── */
|
|
2047
2162
|
.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}
|
|
@@ -2067,7 +2182,10 @@ window.__ModuleLoader__.load({
|
|
|
2067
2182
|
.dab-head{margin:2px 0 5px}
|
|
2068
2183
|
.dab-overline{font-size:10.5px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--dsw-alias-brand-primary);opacity:.9}
|
|
2069
2184
|
.dab-h1{margin:4px 0 0;font-size:21px;font-weight:700;letter-spacing:-.01em}
|
|
2070
|
-
|
|
2185
|
+
/* No max-width on the page description: it is a single line of context under the
|
|
2186
|
+
title, and capping it at a prose measure left it wrapping early with dead space
|
|
2187
|
+
to its right while the cards below ran the full width of the panel. */
|
|
2188
|
+
.dab-desc{margin:6px 0 0;font-size:12.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary)}
|
|
2071
2189
|
.dab-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;padding:18px}
|
|
2072
2190
|
.dab-card-hover{transition:transform .28s ease,box-shadow .28s ease,border-color .28s ease}
|
|
2073
2191
|
.dab-card-hover:hover{transform:translateY(-2px);box-shadow:0 10px 28px -12px rgba(0,0,0,.28)}
|
|
@@ -2084,10 +2202,6 @@ window.__ModuleLoader__.load({
|
|
|
2084
2202
|
|
|
2085
2203
|
/* ── swatches ────────────────────────────────────────────────────────────── */
|
|
2086
2204
|
.dab-swatch-title{font-size:12px;font-weight:600;margin-bottom:10px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
2087
|
-
.dab-swatches{display:flex;flex-wrap:wrap;gap:9px}
|
|
2088
|
-
.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}
|
|
2089
|
-
.dab-swatch:hover{transform:scale(1.2)}
|
|
2090
|
-
.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)}
|
|
2091
2205
|
|
|
2092
2206
|
/* ── wheel card ──────────────────────────────────────────────────────────── */
|
|
2093
2207
|
.dab-wheel-card{position:relative;display:flex;align-items:center;justify-content:center;gap:28px;flex-wrap:wrap;padding:24px 18px}
|
|
@@ -2137,7 +2251,7 @@ window.__ModuleLoader__.load({
|
|
|
2137
2251
|
.dab-btn-danger{color:var(--dsw-alias-state-error-primary)}
|
|
2138
2252
|
.dab-btn-ghost{background:transparent;border-color:transparent;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
2139
2253
|
.dab-btn-ghost:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);box-shadow:none}
|
|
2140
|
-
.dab-btn:focus-visible,.dab-nav-item:focus-visible,.dab-seg-item:focus-visible
|
|
2254
|
+
.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}
|
|
2141
2255
|
.dab-chip-row{display:flex;flex-wrap:wrap;gap:8px}
|
|
2142
2256
|
.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}
|
|
2143
2257
|
.dab-chip:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
|
|
@@ -2165,7 +2279,6 @@ window.__ModuleLoader__.load({
|
|
|
2165
2279
|
.dab-part-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}
|
|
2166
2280
|
.dab-part-ico{width:32px;height:32px;flex:none;border-radius:10px;display:grid;place-items:center;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
2167
2281
|
.dab-part-name{font-size:13.5px;font-weight:600}
|
|
2168
|
-
.dab-part-badge{margin-left:auto;font-family:var(--dab-mono);font-size:11px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2);border-radius:99px;padding:3px 9px}
|
|
2169
2282
|
|
|
2170
2283
|
/* ── segmented control ───────────────────────────────────────────────────── */
|
|
2171
2284
|
.dab-seg{position:relative;display:inline-flex;padding:3px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:11px}
|
|
@@ -2225,12 +2338,83 @@ window.__ModuleLoader__.load({
|
|
|
2225
2338
|
.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}
|
|
2226
2339
|
.dab-overlay .dab-btn{background:rgba(255,255,255,.94);color:#14161a;border-color:transparent}
|
|
2227
2340
|
.dab-overlay .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted)}
|
|
2341
|
+
/* The destructive choice has to survive the white-on-dark treatment above: a
|
|
2342
|
+
confirmation whose "delete everything" button looks exactly like its "cancel"
|
|
2343
|
+
is the one place where losing the distinction actually costs something. */
|
|
2344
|
+
.dab-overlay .dab-btn-danger{background:#dc2626;color:#fff;border-color:transparent}
|
|
2228
2345
|
.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}
|
|
2229
2346
|
.dab-crash-title{font-size:15px;font-weight:650}
|
|
2230
2347
|
.dab-crash-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.5}
|
|
2231
2348
|
|
|
2232
2349
|
/* ── model rules ─────────────────────────────────────────────────────────── */
|
|
2233
|
-
|
|
2350
|
+
/* ── rule tab strip ──────────────────────────────────────────────────────────
|
|
2351
|
+
* The rules are edited ONE at a time: every rule is a tab in a single sideways
|
|
2352
|
+
* scrolling row, and the page renders exactly one editor under it. The height of
|
|
2353
|
+
* this panel is therefore a function of one card, not of the rule count — which
|
|
2354
|
+
* is what the stacked accordion could not promise.
|
|
2355
|
+
*
|
|
2356
|
+
* The strip scrolls instead of wrapping for the same reason: a wrapped strip
|
|
2357
|
+
* would grow a row taller with every rule and hand the page height straight back
|
|
2358
|
+
* to the list. Tabs are fixed-height and the match label elides, so switching
|
|
2359
|
+
* rules can never reflow the row either. */
|
|
2360
|
+
/* Deliberately NOT sticky: the horizontal scroller above the tabs clips on BOTH
|
|
2361
|
+
axes, so the "which rule" menu — an absolutely positioned child — would be cut
|
|
2362
|
+
off at the strip's own bottom edge instead of floating over the page.
|
|
2363
|
+
Switching rules stays a click near the top of the panel, which is one screen
|
|
2364
|
+
away at every realistic rule count. */
|
|
2365
|
+
/* The tiles are aligned by their TOP edge, not by centering: the strip's box is
|
|
2366
|
+
taller than a tab (its height includes the horizontal scrollbar, which no other
|
|
2367
|
+
tile in the row has), so centering against the row would put the two icon
|
|
2368
|
+
buttons a few pixels below the tabs they sit beside. Top alignment makes the
|
|
2369
|
+
strip's top and the buttons' top the same line; the strip's own 3px top padding
|
|
2370
|
+
is mirrored on the buttons, and everything below the bar comes from the strip's
|
|
2371
|
+
bottom padding — so the bar keeps the same distance from the TAB bottoms and
|
|
2372
|
+
from the icon tiles' bottoms. */
|
|
2373
|
+
.dab-tabs-row{display:flex;align-items:flex-start;gap:8px;padding-bottom:0;margin-bottom:6px}
|
|
2374
|
+
/* The strip is the scroller the scrollbar block above is written for. Its bottom
|
|
2375
|
+
padding is the whole gap under the tiles: the bar is painted at the bottom of
|
|
2376
|
+
this box, so this is what keeps it off the tabs (and, because the row adds
|
|
2377
|
+
nothing here, off the icon buttons beside them too). */
|
|
2378
|
+
.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}
|
|
2379
|
+
.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}
|
|
2380
|
+
.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))}
|
|
2381
|
+
.dab-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
2382
|
+
/* The selected tab reads like the nav rail's own indicator: a brand tint, a
|
|
2383
|
+
brand border, and the label lifted to full contrast. */
|
|
2384
|
+
.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)}
|
|
2385
|
+
.dab-tab.is-off{opacity:.5}
|
|
2386
|
+
.dab-tab:not(.is-active).is-off:hover{opacity:.75}
|
|
2387
|
+
.dab-tab-thumb{width:20px;height:20px;flex:none;border-radius:5px;object-fit:cover;border:1px solid var(--dsw-alias-border-l2)}
|
|
2388
|
+
.dab-tab-num{font-family:var(--dab-mono);font-size:10.5px;font-weight:700;opacity:.8}
|
|
2389
|
+
.dab-tab-match{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2390
|
+
.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}
|
|
2391
|
+
.dab-tab-rot{flex:none;font-size:11px;line-height:1;opacity:.75}
|
|
2392
|
+
/* The strip's two actions — add a rule, open the rule list — are square icon
|
|
2393
|
+
tiles beside the scroller rather than tiles inside it: inside, a long rule list
|
|
2394
|
+
pushed them off the end, so the one control that ADDS a rule could only be
|
|
2395
|
+
reached by scrolling to the bottom of the rules. They keep the tab's box so the
|
|
2396
|
+
row stays one height, and the icon is the whole label (title + aria-label). */
|
|
2397
|
+
/* The top margin mirrors the strip's own padding-top, so the icon inside the tile
|
|
2398
|
+
sits on the same line as the text inside the tabs. */
|
|
2399
|
+
.dab-tab-icon{width:34px;margin-top:3px;padding:0;justify-content:center;color:var(--dsw-alias-label-tertiary);max-width:none}
|
|
2400
|
+
.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)}
|
|
2401
|
+
|
|
2402
|
+
/* ── "which rule" picker (the strip's overflow menu) ───────────────────────── */
|
|
2403
|
+
.dab-pick{position:relative;flex:none}
|
|
2404
|
+
.dab-pick-scrim{position:fixed;inset:0;z-index:40;background:transparent}
|
|
2405
|
+
.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}
|
|
2406
|
+
.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}
|
|
2407
|
+
.dab-pick-item:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
2408
|
+
.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)}
|
|
2409
|
+
.dab-pick-match{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2410
|
+
.dab-pick-n{flex:none;font-family:var(--dsw-mono);font-size:10.5px;opacity:.7}
|
|
2411
|
+
|
|
2412
|
+
/* ── one rule editor (the tab panel's content) ─────────────────────────────── */
|
|
2413
|
+
.dab-rule-panel{min-height:0}
|
|
2414
|
+
/* The armed delete button stops being a 28px icon box and says what the second
|
|
2415
|
+
click will do — a worded state, not only a colour one. */
|
|
2416
|
+
.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)}
|
|
2417
|
+
.dab-confirm-txt{font-size:11px;font-weight:600;white-space:nowrap}
|
|
2234
2418
|
.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}
|
|
2235
2419
|
.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)}
|
|
2236
2420
|
.dab-rule.is-off{opacity:.55}
|
|
@@ -4163,6 +4347,18 @@ window.__ModuleLoader__.load({
|
|
|
4163
4347
|
className,
|
|
4164
4348
|
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" })
|
|
4165
4349
|
});
|
|
4350
|
+
/** Appends a rule — the tab strip's own action, beside the strip it adds to. */
|
|
4351
|
+
const PlusIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
|
|
4352
|
+
size,
|
|
4353
|
+
className,
|
|
4354
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 3.4v9.2M3.4 8h9.2" })
|
|
4355
|
+
});
|
|
4356
|
+
/** Opens the "which rule" menu: down means a list drops out under the button. */
|
|
4357
|
+
const ChevronDownIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
|
|
4358
|
+
size,
|
|
4359
|
+
className,
|
|
4360
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.3 6.4 8 10.1l3.7-3.7" })
|
|
4361
|
+
});
|
|
4166
4362
|
//#endregion
|
|
4167
4363
|
//#region src/client/components/ErrorBoundary.tsx
|
|
4168
4364
|
/**
|
|
@@ -4401,20 +4597,13 @@ window.__ModuleLoader__.load({
|
|
|
4401
4597
|
children: [
|
|
4402
4598
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4403
4599
|
className: "dab-part-head",
|
|
4404
|
-
children: [
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
children: t(labelKey)
|
|
4412
|
-
}),
|
|
4413
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4414
|
-
className: "dab-part-badge",
|
|
4415
|
-
children: [Math.round(opacity * 100), "%"]
|
|
4416
|
-
})
|
|
4417
|
-
]
|
|
4600
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4601
|
+
className: "dab-part-ico",
|
|
4602
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { size: 16 })
|
|
4603
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4604
|
+
className: "dab-part-name",
|
|
4605
|
+
children: t(labelKey)
|
|
4606
|
+
})]
|
|
4418
4607
|
}),
|
|
4419
4608
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
4420
4609
|
label: t("uiOpacity"),
|
|
@@ -4490,6 +4679,42 @@ window.__ModuleLoader__.load({
|
|
|
4490
4679
|
})] });
|
|
4491
4680
|
}
|
|
4492
4681
|
//#endregion
|
|
4682
|
+
//#region src/client/rules-view.ts
|
|
4683
|
+
/**
|
|
4684
|
+
* The rule the editor should show.
|
|
4685
|
+
*
|
|
4686
|
+
* `sel` wins whenever it is still in the list: the selection is the user's, and
|
|
4687
|
+
* a model switch, a reorder or an image edit must not move it. The fallbacks run
|
|
4688
|
+
* only when there is no selection at all, or when the rule it named is gone
|
|
4689
|
+
* (deleted here, or replaced wholesale by an import or by the persisted config
|
|
4690
|
+
* landing after the first render — whose ids are not the ones we started with).
|
|
4691
|
+
* The active rule is preferred over the first one so that opening the panel
|
|
4692
|
+
* lands on the rule the current model is using.
|
|
4693
|
+
*/
|
|
4694
|
+
function resolveTab(rules, sel, activeId) {
|
|
4695
|
+
if (rules.length === 0) return null;
|
|
4696
|
+
if (sel !== null) {
|
|
4697
|
+
const held = rules.find((r) => r.id === sel);
|
|
4698
|
+
if (held !== void 0) return held;
|
|
4699
|
+
}
|
|
4700
|
+
if (activeId !== null) {
|
|
4701
|
+
const live = rules.find((r) => r.id === activeId);
|
|
4702
|
+
if (live !== void 0) return live;
|
|
4703
|
+
}
|
|
4704
|
+
return rules[0] ?? null;
|
|
4705
|
+
}
|
|
4706
|
+
/**
|
|
4707
|
+
* The rule to show once `id` has been removed: the next one, or the previous one
|
|
4708
|
+
* when the removed rule was last. Deliberately NOT the first rule — the user is
|
|
4709
|
+
* working down a list, and jumping to the top loses their place — and `null`
|
|
4710
|
+
* when it was the only rule, which the panel renders as "no rules yet".
|
|
4711
|
+
*/
|
|
4712
|
+
function neighbourAfter(rules, id) {
|
|
4713
|
+
const i = rules.findIndex((r) => r.id === id);
|
|
4714
|
+
if (i < 0) return null;
|
|
4715
|
+
return rules[i + 1] ?? rules[i - 1] ?? null;
|
|
4716
|
+
}
|
|
4717
|
+
//#endregion
|
|
4493
4718
|
//#region src/client/utils/image.ts
|
|
4494
4719
|
/**
|
|
4495
4720
|
* Read a chosen image file as a data URL WITHOUT re-encoding: the original
|
|
@@ -5266,16 +5491,6 @@ window.__ModuleLoader__.load({
|
|
|
5266
5491
|
.55,
|
|
5267
5492
|
.25
|
|
5268
5493
|
];
|
|
5269
|
-
/**
|
|
5270
|
-
* Failing-hop codes the model watcher can report, mapped to their copy. Only
|
|
5271
|
-
* these are rendered: `waiting` and `fallback` already have their own hints.
|
|
5272
|
-
*/
|
|
5273
|
-
const MODEL_NOTE_KEYS = {
|
|
5274
|
-
"no-service": "statusNoteNoService",
|
|
5275
|
-
"no-session": "statusNoteNoSession",
|
|
5276
|
-
"no-projection": "statusNoteNoProjection",
|
|
5277
|
-
"empty-selection": "statusNoteEmptySelection"
|
|
5278
|
-
};
|
|
5279
5494
|
function toHex(rgb) {
|
|
5280
5495
|
return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
|
|
5281
5496
|
}
|
|
@@ -5283,185 +5498,320 @@ window.__ModuleLoader__.load({
|
|
|
5283
5498
|
const { t, useStore } = p;
|
|
5284
5499
|
const store = useStore((s) => s);
|
|
5285
5500
|
const rules = cfg.rules;
|
|
5286
|
-
const [
|
|
5287
|
-
|
|
5501
|
+
const [selId, setSelId] = (0, react.useState)(() => resolveTab(rules, null, store.activeRuleId)?.id ?? null);
|
|
5502
|
+
/** True once the user has picked a tab themselves, which stops the follow above. */
|
|
5503
|
+
const pickedRef = (0, react.useRef)(false);
|
|
5288
5504
|
(0, react.useEffect)(() => {
|
|
5289
|
-
if (
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
children:
|
|
5363
|
-
}),
|
|
5364
|
-
store.model === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5365
|
-
className: "dab-hint",
|
|
5366
|
-
style: { marginTop: 6 },
|
|
5367
|
-
children: t("statusUnknownHint")
|
|
5368
|
-
}) : null,
|
|
5369
|
-
store.model !== "" && store.modelSource === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5370
|
-
className: "dab-hint",
|
|
5371
|
-
style: { marginTop: 6 },
|
|
5372
|
-
children: t("statusSourceDefaultHint")
|
|
5373
|
-
}) : null,
|
|
5374
|
-
MODEL_NOTE_KEYS[store.modelNote] !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5505
|
+
if (pickedRef.current) return;
|
|
5506
|
+
const held = resolveTab(rules, selId, store.activeRuleId);
|
|
5507
|
+
if (held !== null && held.id !== selId) setSelId(held.id);
|
|
5508
|
+
}, [
|
|
5509
|
+
store.rulesRev,
|
|
5510
|
+
store.activeRuleId,
|
|
5511
|
+
selId,
|
|
5512
|
+
rules
|
|
5513
|
+
]);
|
|
5514
|
+
const active = resolveTab(rules, selId, store.activeRuleId)?.id ?? null;
|
|
5515
|
+
const [selImage, setSelImage] = (0, react.useState)({});
|
|
5516
|
+
const selIdxOf = (id) => selImage[id] ?? 0;
|
|
5517
|
+
const setSelIdxOf = (id, i) => setSelImage((m) => m[id] === i ? m : {
|
|
5518
|
+
...m,
|
|
5519
|
+
[id]: i
|
|
5520
|
+
});
|
|
5521
|
+
/** Select a rule as the edited one; keyboard stepping shares this. */
|
|
5522
|
+
const selectRule = (id) => {
|
|
5523
|
+
pickedRef.current = true;
|
|
5524
|
+
setSelId(id);
|
|
5525
|
+
};
|
|
5526
|
+
/** Delete a rule and land on its neighbour — never on the first rule. */
|
|
5527
|
+
const removeRule = (id) => {
|
|
5528
|
+
if (id === active) {
|
|
5529
|
+
const next = neighbourAfter(rules, id);
|
|
5530
|
+
pickedRef.current = true;
|
|
5531
|
+
setSelId(next?.id ?? null);
|
|
5532
|
+
}
|
|
5533
|
+
setSelImage((m) => {
|
|
5534
|
+
const n = { ...m };
|
|
5535
|
+
delete n[id];
|
|
5536
|
+
return n;
|
|
5537
|
+
});
|
|
5538
|
+
p.removeRule(id);
|
|
5539
|
+
};
|
|
5540
|
+
const addRule = () => {
|
|
5541
|
+
pickedRef.current = true;
|
|
5542
|
+
setSelId(p.addRule());
|
|
5543
|
+
};
|
|
5544
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
5545
|
+
className: "dab-head dab-rise",
|
|
5546
|
+
style: { "--d": 0 },
|
|
5547
|
+
children: [
|
|
5548
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5549
|
+
className: "dab-overline",
|
|
5550
|
+
children: "Rules"
|
|
5551
|
+
}),
|
|
5552
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
5553
|
+
className: "dab-h1",
|
|
5554
|
+
children: t("pageModelBg")
|
|
5555
|
+
}),
|
|
5556
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5557
|
+
className: "dab-desc",
|
|
5558
|
+
children: t("descModelBg")
|
|
5559
|
+
})
|
|
5560
|
+
]
|
|
5561
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
5562
|
+
className: "dab-rise",
|
|
5563
|
+
style: { "--d": 1 },
|
|
5564
|
+
children: [
|
|
5565
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5566
|
+
className: "dab-chip-row",
|
|
5567
|
+
style: {
|
|
5568
|
+
marginBottom: 12,
|
|
5569
|
+
alignItems: "center"
|
|
5570
|
+
},
|
|
5571
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5572
|
+
type: "button",
|
|
5573
|
+
className: `dab-toggle${cfg.autoExtract ? " is-on" : ""}`,
|
|
5574
|
+
role: "switch",
|
|
5575
|
+
"aria-checked": cfg.autoExtract,
|
|
5576
|
+
title: t("autoExtract"),
|
|
5577
|
+
onClick: () => p.setAutoExtract(!cfg.autoExtract),
|
|
5578
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5579
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5375
5580
|
className: "dab-hint",
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
title: t("autoExtract"),
|
|
5397
|
-
onClick: () => p.setAutoExtract(!cfg.autoExtract),
|
|
5398
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5399
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5581
|
+
children: t("autoExtract")
|
|
5582
|
+
})]
|
|
5583
|
+
}),
|
|
5584
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RulesTabs, {
|
|
5585
|
+
rules,
|
|
5586
|
+
activeId: active,
|
|
5587
|
+
liveId: store.activeRuleId,
|
|
5588
|
+
p,
|
|
5589
|
+
onSelect: selectRule,
|
|
5590
|
+
onAdd: addRule
|
|
5591
|
+
}),
|
|
5592
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5593
|
+
className: "dab-rule-panel",
|
|
5594
|
+
role: "tabpanel",
|
|
5595
|
+
id: active === null ? void 0 : `dab-rule-panel-${active}`,
|
|
5596
|
+
"aria-labelledby": active === null ? void 0 : `dab-rule-tab-${active}`,
|
|
5597
|
+
children: (() => {
|
|
5598
|
+
const idx = rules.findIndex((r) => r.id === active);
|
|
5599
|
+
const rule = idx < 0 ? void 0 : rules[idx];
|
|
5600
|
+
if (rule === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5400
5601
|
className: "dab-hint",
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
className: "dab-rules",
|
|
5406
|
-
children: rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleCard, {
|
|
5602
|
+
style: { padding: "4px 2px" },
|
|
5603
|
+
children: t("ruleNoneHint")
|
|
5604
|
+
});
|
|
5605
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleCard, {
|
|
5407
5606
|
p,
|
|
5408
5607
|
rule,
|
|
5409
|
-
index:
|
|
5608
|
+
index: idx,
|
|
5410
5609
|
total: rules.length,
|
|
5411
5610
|
liveIndex: rule.id === store.activeRuleId ? store.rotIndex : -1,
|
|
5412
5611
|
active: rule.id === store.activeRuleId,
|
|
5413
|
-
notify
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
},
|
|
5423
|
-
children: ["+ ", t("ruleAdd")]
|
|
5424
|
-
})
|
|
5425
|
-
]
|
|
5426
|
-
})
|
|
5427
|
-
] });
|
|
5612
|
+
notify,
|
|
5613
|
+
sel: selIdxOf(rule.id),
|
|
5614
|
+
onSel: (i) => setSelIdxOf(rule.id, i),
|
|
5615
|
+
onRemove: () => removeRule(rule.id)
|
|
5616
|
+
}, rule.id);
|
|
5617
|
+
})()
|
|
5618
|
+
})
|
|
5619
|
+
]
|
|
5620
|
+
})] });
|
|
5428
5621
|
}
|
|
5429
|
-
|
|
5622
|
+
/**
|
|
5623
|
+
* The rule tab strip: every rule is one tab, and the tab at the end appends a
|
|
5624
|
+
* new rule — so what the user browses and what they add to live in one row.
|
|
5625
|
+
*
|
|
5626
|
+
* A tab carries what the card head used to: its number, a thumbnail of the first
|
|
5627
|
+
* image, the match string, and two states that are worth knowing WITHOUT opening
|
|
5628
|
+
* it — whether the rule is the one in use, and whether it rotates or is off. The
|
|
5629
|
+
* strip scrolls sideways rather than wrapping, because a wrapped strip would put
|
|
5630
|
+
* the page height back in the hands of the rule count, which is the whole thing
|
|
5631
|
+
* this layout exists to prevent.
|
|
5632
|
+
*/
|
|
5633
|
+
function RulesTabs({ rules, activeId, liveId, p, onSelect, onAdd }) {
|
|
5430
5634
|
const { t } = p;
|
|
5431
|
-
const
|
|
5432
|
-
const [
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
const [urlErr, setUrlErr] = (0, react.useState)(null);
|
|
5443
|
-
const [editorOpen, setEditorOpen] = (0, react.useState)(false);
|
|
5444
|
-
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
5445
|
-
const [extracting, setExtracting] = (0, react.useState)(false);
|
|
5446
|
-
const [secDraft, setSecDraft] = (0, react.useState)(null);
|
|
5447
|
-
/** Whether the next file picked replaces the selected image or is added. */
|
|
5448
|
-
const [replace, setReplace] = (0, react.useState)(false);
|
|
5449
|
-
const fileRef = (0, react.useRef)(null);
|
|
5450
|
-
const [sel, setSel] = (0, react.useState)(0);
|
|
5451
|
-
const selIdx = rule.images.length === 0 ? 0 : Math.min(sel, rule.images.length - 1);
|
|
5452
|
-
const image = rule.images[selIdx];
|
|
5453
|
-
const url = image === void 0 ? null : p.imageOf(image.slot);
|
|
5454
|
-
const ownColor = image === void 0 ? rule.color : image.color;
|
|
5455
|
-
const setColor = (color) => {
|
|
5456
|
-
if (image === void 0) p.setRule(rule.id, { color });
|
|
5457
|
-
else p.setImageColor(rule.id, image.slot, color);
|
|
5635
|
+
const items = (0, react.useRef)([]);
|
|
5636
|
+
const [pick, setPick] = (0, react.useState)(false);
|
|
5637
|
+
/** Select a rule and bring its tab into view — the strip is wider than the page. */
|
|
5638
|
+
const go = (i) => {
|
|
5639
|
+
const rule = rules[i];
|
|
5640
|
+
if (rule === void 0) return;
|
|
5641
|
+
onSelect(rule.id);
|
|
5642
|
+
items.current[i]?.scrollIntoView({
|
|
5643
|
+
block: "nearest",
|
|
5644
|
+
inline: "nearest"
|
|
5645
|
+
});
|
|
5458
5646
|
};
|
|
5459
|
-
const
|
|
5460
|
-
|
|
5647
|
+
const onKeys = (e) => {
|
|
5648
|
+
const i = rules.findIndex((r) => r.id === activeId);
|
|
5649
|
+
if (i < 0) return;
|
|
5650
|
+
if (e.key === "ArrowRight") {
|
|
5651
|
+
e.preventDefault();
|
|
5652
|
+
go(Math.min(i + 1, rules.length - 1));
|
|
5653
|
+
} else if (e.key === "ArrowLeft") {
|
|
5654
|
+
e.preventDefault();
|
|
5655
|
+
go(Math.max(i - 1, 0));
|
|
5656
|
+
} else if (e.key === "Home") {
|
|
5657
|
+
e.preventDefault();
|
|
5658
|
+
go(0);
|
|
5659
|
+
} else if (e.key === "End") {
|
|
5660
|
+
e.preventDefault();
|
|
5661
|
+
go(rules.length - 1);
|
|
5662
|
+
}
|
|
5663
|
+
};
|
|
5664
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5665
|
+
className: "dab-tabs-row",
|
|
5666
|
+
children: [
|
|
5667
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5668
|
+
className: "dab-tabs",
|
|
5669
|
+
role: "tablist",
|
|
5670
|
+
"aria-label": t("tabListLabel"),
|
|
5671
|
+
onKeyDown: onKeys,
|
|
5672
|
+
children: rules.map((rule, i) => {
|
|
5673
|
+
const on = rule.id === activeId;
|
|
5674
|
+
const live = rule.id === liveId;
|
|
5675
|
+
const thumb = rule.images[0] === void 0 ? null : p.imageOf(rule.images[0].slot);
|
|
5676
|
+
const match = rule.match.trim();
|
|
5677
|
+
const title = [
|
|
5678
|
+
match === "" ? t("tabUnnamed") : match,
|
|
5679
|
+
`${rule.images.length}`,
|
|
5680
|
+
live ? t("tabLive") : null,
|
|
5681
|
+
rule.images.length >= 2 && rule.rotate.enabled ? t("tabRotating") : null,
|
|
5682
|
+
rule.enabled ? null : t("tabOff")
|
|
5683
|
+
].filter((x) => x !== null).join(" · ");
|
|
5684
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5685
|
+
type: "button",
|
|
5686
|
+
role: "tab",
|
|
5687
|
+
"aria-selected": on,
|
|
5688
|
+
id: `dab-rule-tab-${rule.id}`,
|
|
5689
|
+
"aria-controls": `dab-rule-panel-${rule.id}`,
|
|
5690
|
+
tabIndex: on ? 0 : -1,
|
|
5691
|
+
ref: (el) => {
|
|
5692
|
+
items.current[i] = el;
|
|
5693
|
+
},
|
|
5694
|
+
className: `dab-tab${on ? " is-active" : ""}${rule.enabled ? "" : " is-off"}${live ? " is-live" : ""}`,
|
|
5695
|
+
title,
|
|
5696
|
+
onClick: () => go(i),
|
|
5697
|
+
children: [
|
|
5698
|
+
thumb === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
5699
|
+
className: "dab-tab-thumb",
|
|
5700
|
+
src: thumb,
|
|
5701
|
+
alt: "",
|
|
5702
|
+
draggable: false
|
|
5703
|
+
}),
|
|
5704
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5705
|
+
className: "dab-tab-num",
|
|
5706
|
+
children: i + 1
|
|
5707
|
+
}),
|
|
5708
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5709
|
+
className: "dab-tab-match",
|
|
5710
|
+
children: match === "" ? t("tabUnnamed") : match
|
|
5711
|
+
}),
|
|
5712
|
+
rule.enabled && live ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5713
|
+
className: "dab-tab-live",
|
|
5714
|
+
title: t("tabLive")
|
|
5715
|
+
}) : null,
|
|
5716
|
+
rule.enabled && rule.images.length >= 2 && rule.rotate.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5717
|
+
className: "dab-tab-rot",
|
|
5718
|
+
title: t("tabRotating"),
|
|
5719
|
+
children: "⟳"
|
|
5720
|
+
}) : null
|
|
5721
|
+
]
|
|
5722
|
+
}, rule.id);
|
|
5723
|
+
})
|
|
5724
|
+
}),
|
|
5725
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5726
|
+
type: "button",
|
|
5727
|
+
className: "dab-tab dab-tab-icon",
|
|
5728
|
+
onClick: onAdd,
|
|
5729
|
+
title: t("ruleAdd"),
|
|
5730
|
+
"aria-label": t("ruleAdd"),
|
|
5731
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, { size: 16 })
|
|
5732
|
+
}),
|
|
5733
|
+
rules.length > 3 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5734
|
+
className: "dab-pick",
|
|
5735
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5736
|
+
type: "button",
|
|
5737
|
+
className: "dab-tab dab-tab-icon",
|
|
5738
|
+
"aria-haspopup": "listbox",
|
|
5739
|
+
"aria-expanded": pick,
|
|
5740
|
+
title: t("tabPick"),
|
|
5741
|
+
"aria-label": t("tabPick"),
|
|
5742
|
+
onClick: () => setPick((o) => !o),
|
|
5743
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { size: 16 })
|
|
5744
|
+
}), pick ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5745
|
+
className: "dab-pick-scrim",
|
|
5746
|
+
onClick: () => setPick(false)
|
|
5747
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5748
|
+
className: "dab-pick-menu",
|
|
5749
|
+
role: "listbox",
|
|
5750
|
+
"aria-label": t("tabMore"),
|
|
5751
|
+
children: rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5752
|
+
type: "button",
|
|
5753
|
+
role: "option",
|
|
5754
|
+
"aria-selected": rule.id === activeId,
|
|
5755
|
+
className: `dab-pick-item${rule.id === activeId ? " is-active" : ""}`,
|
|
5756
|
+
onClick: () => {
|
|
5757
|
+
setPick(false);
|
|
5758
|
+
go(i);
|
|
5759
|
+
},
|
|
5760
|
+
children: [
|
|
5761
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5762
|
+
className: "dab-tab-num",
|
|
5763
|
+
children: i + 1
|
|
5764
|
+
}),
|
|
5765
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5766
|
+
className: "dab-pick-match",
|
|
5767
|
+
children: rule.match.trim() === "" ? t("tabUnnamed") : rule.match
|
|
5768
|
+
}),
|
|
5769
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5770
|
+
className: "dab-pick-n",
|
|
5771
|
+
children: rule.images.length
|
|
5772
|
+
})
|
|
5773
|
+
]
|
|
5774
|
+
}, rule.id))
|
|
5775
|
+
})] }) : null]
|
|
5776
|
+
}) : null
|
|
5777
|
+
]
|
|
5778
|
+
});
|
|
5779
|
+
}
|
|
5780
|
+
function RuleCard({ p, rule, index, total, active, liveIndex, notify, sel, onSel, onRemove }) {
|
|
5781
|
+
const { t } = p;
|
|
5782
|
+
const [dragOver, setDragOver] = (0, react.useState)(false);
|
|
5783
|
+
const pendingDrag = (0, react.useRef)(null);
|
|
5784
|
+
/** Insertion slot of the drag in flight (index of the tile it goes BEFORE). */
|
|
5785
|
+
const dragAt = (0, react.useRef)(null);
|
|
5786
|
+
const [drop, setDrop] = (0, react.useState)(null);
|
|
5787
|
+
/** Set while a drag is real, so the click that ends it is not also a selection. */
|
|
5788
|
+
const draggedRef = (0, react.useRef)(false);
|
|
5789
|
+
const [urlOpen, setUrlOpen] = (0, react.useState)(false);
|
|
5790
|
+
const [urlVal, setUrlVal] = (0, react.useState)("");
|
|
5791
|
+
const [urlBusy, setUrlBusy] = (0, react.useState)(false);
|
|
5792
|
+
const [urlErr, setUrlErr] = (0, react.useState)(null);
|
|
5793
|
+
const [editorOpen, setEditorOpen] = (0, react.useState)(false);
|
|
5794
|
+
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
5795
|
+
const [extracting, setExtracting] = (0, react.useState)(false);
|
|
5796
|
+
const [secDraft, setSecDraft] = (0, react.useState)(null);
|
|
5797
|
+
/** Two-step delete: this card is the only place the rule can be removed from. */
|
|
5798
|
+
const [confirmRemove, setConfirmRemove] = (0, react.useState)(false);
|
|
5799
|
+
/** Whether the next file picked replaces the selected image or is added. */
|
|
5800
|
+
const [replace, setReplace] = (0, react.useState)(false);
|
|
5801
|
+
const fileRef = (0, react.useRef)(null);
|
|
5802
|
+
const selIdx = rule.images.length === 0 ? 0 : Math.min(sel, rule.images.length - 1);
|
|
5803
|
+
const image = rule.images[selIdx];
|
|
5804
|
+
const url = image === void 0 ? null : p.imageOf(image.slot);
|
|
5805
|
+
const ownColor = image === void 0 ? rule.color : image.color;
|
|
5806
|
+
const setColor = (color) => {
|
|
5807
|
+
if (image === void 0) p.setRule(rule.id, { color });
|
|
5808
|
+
else p.setImageColor(rule.id, image.slot, color);
|
|
5809
|
+
};
|
|
5810
|
+
const [h, s, l] = ownColor ?? SEED_COLOR;
|
|
5811
|
+
const wheel = hslToHsv(h, s, l);
|
|
5461
5812
|
(0, react.useEffect)(() => {
|
|
5462
|
-
|
|
5813
|
+
p.loadRuleImages(rule.id);
|
|
5463
5814
|
}, [
|
|
5464
|
-
open,
|
|
5465
5815
|
rule.id,
|
|
5466
5816
|
rule.images.length,
|
|
5467
5817
|
p
|
|
@@ -5493,7 +5843,7 @@ window.__ModuleLoader__.load({
|
|
|
5493
5843
|
pending--;
|
|
5494
5844
|
if (pending === 0) {
|
|
5495
5845
|
p.addRuleImages(rule.id, out.filter((x) => typeof x === "string"));
|
|
5496
|
-
|
|
5846
|
+
onSel(first);
|
|
5497
5847
|
}
|
|
5498
5848
|
});
|
|
5499
5849
|
});
|
|
@@ -5528,7 +5878,7 @@ window.__ModuleLoader__.load({
|
|
|
5528
5878
|
setUrlBusy(false);
|
|
5529
5879
|
setUrlOpen(false);
|
|
5530
5880
|
setUrlVal("");
|
|
5531
|
-
|
|
5881
|
+
onSel(first);
|
|
5532
5882
|
};
|
|
5533
5883
|
const onExtract = async () => {
|
|
5534
5884
|
if (url === null || extracting || image === void 0) return;
|
|
@@ -5618,13 +5968,13 @@ window.__ModuleLoader__.load({
|
|
|
5618
5968
|
const to = end.at > pen.from ? end.at - 1 : end.at;
|
|
5619
5969
|
if (to === pen.from) return;
|
|
5620
5970
|
p.moveRuleImageTo(rule.id, pen.slot, to);
|
|
5621
|
-
|
|
5971
|
+
onSel(to);
|
|
5622
5972
|
};
|
|
5623
5973
|
const removeSel = () => {
|
|
5624
5974
|
if (selSlot === null) return;
|
|
5625
5975
|
const at = selIdx;
|
|
5626
5976
|
p.removeRuleImage(rule.id, selSlot);
|
|
5627
|
-
|
|
5977
|
+
onSel(Math.max(0, Math.min(at, rule.images.length - 2)));
|
|
5628
5978
|
};
|
|
5629
5979
|
/** Commit the custom dwell time the moment it is typed (seconds → ms). */
|
|
5630
5980
|
const commitSeconds = () => {
|
|
@@ -5640,14 +5990,6 @@ window.__ModuleLoader__.load({
|
|
|
5640
5990
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5641
5991
|
className: "dab-rule-head",
|
|
5642
5992
|
children: [
|
|
5643
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5644
|
-
type: "button",
|
|
5645
|
-
className: "dab-icon-btn",
|
|
5646
|
-
onClick: () => setOpen((o) => !o),
|
|
5647
|
-
title: open ? "Collapse" : "Expand",
|
|
5648
|
-
"aria-expanded": open,
|
|
5649
|
-
children: open ? "▾" : "▸"
|
|
5650
|
-
}),
|
|
5651
5993
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5652
5994
|
className: "dab-rule-num",
|
|
5653
5995
|
children: index + 1
|
|
@@ -5689,14 +6031,25 @@ window.__ModuleLoader__.load({
|
|
|
5689
6031
|
}),
|
|
5690
6032
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5691
6033
|
type: "button",
|
|
5692
|
-
className:
|
|
5693
|
-
title: t("ruleRemove"),
|
|
5694
|
-
|
|
5695
|
-
|
|
6034
|
+
className: `dab-icon-btn dab-icon-btn-danger${confirmRemove ? " is-armed" : ""}`,
|
|
6035
|
+
title: confirmRemove ? t("ruleRemoveConfirm") : t("ruleRemove"),
|
|
6036
|
+
onBlur: () => setConfirmRemove(false),
|
|
6037
|
+
onClick: () => {
|
|
6038
|
+
if (!confirmRemove) {
|
|
6039
|
+
setConfirmRemove(true);
|
|
6040
|
+
return;
|
|
6041
|
+
}
|
|
6042
|
+
setConfirmRemove(false);
|
|
6043
|
+
onRemove();
|
|
6044
|
+
},
|
|
6045
|
+
children: confirmRemove ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6046
|
+
className: "dab-confirm-txt",
|
|
6047
|
+
children: t("ruleRemoveConfirm")
|
|
6048
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 })
|
|
5696
6049
|
})
|
|
5697
6050
|
]
|
|
5698
6051
|
}),
|
|
5699
|
-
|
|
6052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5700
6053
|
className: "dab-rule-body",
|
|
5701
6054
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5702
6055
|
className: "dab-rule-cols",
|
|
@@ -5756,7 +6109,7 @@ window.__ModuleLoader__.load({
|
|
|
5756
6109
|
draggedRef.current = false;
|
|
5757
6110
|
return;
|
|
5758
6111
|
}
|
|
5759
|
-
|
|
6112
|
+
onSel(i);
|
|
5760
6113
|
},
|
|
5761
6114
|
onDoubleClick: () => {
|
|
5762
6115
|
if (!active) {
|
|
@@ -5905,140 +6258,6 @@ window.__ModuleLoader__.load({
|
|
|
5905
6258
|
style: { marginTop: 8 },
|
|
5906
6259
|
children: t("ruleImagePendingHint")
|
|
5907
6260
|
}) : null,
|
|
5908
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5909
|
-
style: { marginTop: 14 },
|
|
5910
|
-
children: [
|
|
5911
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5912
|
-
className: "dab-rule-section-title",
|
|
5913
|
-
children: t("rotTitle")
|
|
5914
|
-
}),
|
|
5915
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5916
|
-
className: "dab-chip-row",
|
|
5917
|
-
style: { alignItems: "center" },
|
|
5918
|
-
children: [
|
|
5919
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5920
|
-
type: "button",
|
|
5921
|
-
className: `dab-toggle${rule.rotate.enabled ? " is-on" : ""}`,
|
|
5922
|
-
role: "switch",
|
|
5923
|
-
"aria-checked": rule.rotate.enabled,
|
|
5924
|
-
title: t("rotEnable"),
|
|
5925
|
-
disabled: !rotatable,
|
|
5926
|
-
onClick: () => p.setRuleRotation(rule.id, { enabled: !rule.rotate.enabled }),
|
|
5927
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
5928
|
-
}),
|
|
5929
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5930
|
-
className: "dab-hint",
|
|
5931
|
-
style: { padding: 0 },
|
|
5932
|
-
children: t("rotEnable")
|
|
5933
|
-
}),
|
|
5934
|
-
!rotatable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5935
|
-
className: "dab-hint",
|
|
5936
|
-
style: { padding: 0 },
|
|
5937
|
-
children: t("rotNeedTwo")
|
|
5938
|
-
}) : null
|
|
5939
|
-
]
|
|
5940
|
-
}),
|
|
5941
|
-
rotatable && rule.rotate.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5942
|
-
className: "dab-rot-panel",
|
|
5943
|
-
children: [
|
|
5944
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5945
|
-
className: "dab-rule-section-title",
|
|
5946
|
-
children: t("rotEvery")
|
|
5947
|
-
}),
|
|
5948
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5949
|
-
className: "dab-chip-row",
|
|
5950
|
-
children: [
|
|
5951
|
-
ROTATE_PRESETS.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5952
|
-
type: "button",
|
|
5953
|
-
className: `dab-chip${item.ms === rule.rotate.intervalMs ? " is-active" : ""}`,
|
|
5954
|
-
onClick: () => p.setRuleRotation(rule.id, { intervalMs: item.ms }),
|
|
5955
|
-
children: t(item.key)
|
|
5956
|
-
}, item.ms)),
|
|
5957
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5958
|
-
type: "number",
|
|
5959
|
-
className: "dab-num dab-num-sec",
|
|
5960
|
-
min: 5,
|
|
5961
|
-
max: 86400,
|
|
5962
|
-
step: 5,
|
|
5963
|
-
value: secDraft ?? (preset === void 0 ? Math.round(rule.rotate.intervalMs / 1e3) : ""),
|
|
5964
|
-
placeholder: t("rotCustom"),
|
|
5965
|
-
onChange: (e) => setSecDraft(e.target.value),
|
|
5966
|
-
onBlur: commitSeconds,
|
|
5967
|
-
onKeyDown: (e) => {
|
|
5968
|
-
if (e.key === "Enter") {
|
|
5969
|
-
e.preventDefault();
|
|
5970
|
-
commitSeconds();
|
|
5971
|
-
}
|
|
5972
|
-
}
|
|
5973
|
-
}),
|
|
5974
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5975
|
-
className: "dab-hint",
|
|
5976
|
-
style: { padding: 0 },
|
|
5977
|
-
children: t("rotSeconds")
|
|
5978
|
-
})
|
|
5979
|
-
]
|
|
5980
|
-
}),
|
|
5981
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5982
|
-
className: "dab-rule-section-title",
|
|
5983
|
-
style: { marginTop: 12 },
|
|
5984
|
-
children: t("rotOrder")
|
|
5985
|
-
}),
|
|
5986
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5987
|
-
className: "dab-chip-row",
|
|
5988
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5989
|
-
type: "button",
|
|
5990
|
-
className: `dab-chip${rule.rotate.order === "order" ? " is-active" : ""}`,
|
|
5991
|
-
onClick: () => p.setRuleRotation(rule.id, { order: "order" }),
|
|
5992
|
-
children: t("rotOrderSeq")
|
|
5993
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5994
|
-
type: "button",
|
|
5995
|
-
className: `dab-chip${rule.rotate.order === "shuffle" ? " is-active" : ""}`,
|
|
5996
|
-
onClick: () => p.setRuleRotation(rule.id, { order: "shuffle" }),
|
|
5997
|
-
children: t("rotOrderShuffle")
|
|
5998
|
-
})]
|
|
5999
|
-
}),
|
|
6000
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6001
|
-
className: "dab-chip-row",
|
|
6002
|
-
style: {
|
|
6003
|
-
marginTop: 12,
|
|
6004
|
-
alignItems: "center"
|
|
6005
|
-
},
|
|
6006
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6007
|
-
type: "button",
|
|
6008
|
-
className: `dab-toggle${rule.rotate.advanceOnSwitch ? " is-on" : ""}`,
|
|
6009
|
-
role: "switch",
|
|
6010
|
-
"aria-checked": rule.rotate.advanceOnSwitch,
|
|
6011
|
-
title: t("rotOnSwitch"),
|
|
6012
|
-
onClick: () => p.setRuleRotation(rule.id, { advanceOnSwitch: !rule.rotate.advanceOnSwitch }),
|
|
6013
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
6014
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6015
|
-
className: "dab-hint",
|
|
6016
|
-
style: { padding: 0 },
|
|
6017
|
-
children: t("rotOnSwitch")
|
|
6018
|
-
})]
|
|
6019
|
-
}),
|
|
6020
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6021
|
-
className: "dab-chip-row",
|
|
6022
|
-
style: {
|
|
6023
|
-
marginTop: 12,
|
|
6024
|
-
alignItems: "center"
|
|
6025
|
-
},
|
|
6026
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6027
|
-
type: "button",
|
|
6028
|
-
className: "dab-btn",
|
|
6029
|
-
disabled: !active,
|
|
6030
|
-
onClick: () => p.rotateNow(rule.id),
|
|
6031
|
-
children: t("rotNext")
|
|
6032
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6033
|
-
className: "dab-hint",
|
|
6034
|
-
style: { padding: 0 },
|
|
6035
|
-
children: active ? `${t("rotShowing")} ${rule.images.length === 0 ? 0 : Math.min(liveIndex + 1, rule.images.length)} / ${rule.images.length}` : t("rotNextInactive")
|
|
6036
|
-
})]
|
|
6037
|
-
})
|
|
6038
|
-
]
|
|
6039
|
-
}) : null
|
|
6040
|
-
]
|
|
6041
|
-
}),
|
|
6042
6261
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6043
6262
|
style: { marginTop: 14 },
|
|
6044
6263
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -6109,71 +6328,53 @@ window.__ModuleLoader__.load({
|
|
|
6109
6328
|
style: { marginTop: 6 },
|
|
6110
6329
|
children: t("ruleColorPendingImageHint")
|
|
6111
6330
|
})
|
|
6112
|
-
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6331
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6332
|
+
style: {
|
|
6333
|
+
display: "flex",
|
|
6334
|
+
gap: 16,
|
|
6335
|
+
alignItems: "center",
|
|
6336
|
+
flexWrap: "wrap"
|
|
6337
|
+
},
|
|
6338
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorWheel, {
|
|
6339
|
+
hue: wheel[0],
|
|
6340
|
+
sat: wheel[1],
|
|
6341
|
+
lit: wheel[2],
|
|
6342
|
+
onChange: onColor
|
|
6343
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6344
|
+
className: "dab-inputs",
|
|
6345
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorInputs, {
|
|
6121
6346
|
hue: wheel[0],
|
|
6122
6347
|
sat: wheel[1],
|
|
6123
6348
|
lit: wheel[2],
|
|
6124
6349
|
onChange: onColor
|
|
6125
|
-
})
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
})
|
|
6133
|
-
})]
|
|
6134
|
-
}),
|
|
6135
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6136
|
-
className: "dab-chip-row",
|
|
6137
|
-
style: { marginTop: 12 },
|
|
6138
|
-
children: [
|
|
6139
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6140
|
-
type: "button",
|
|
6141
|
-
className: "dab-btn",
|
|
6142
|
-
disabled: url === null || extracting,
|
|
6143
|
-
onClick: () => void onExtract(),
|
|
6144
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 13 }), extracting ? t("extracting") : t("ruleColorExtract")]
|
|
6145
|
-
}),
|
|
6146
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6147
|
-
type: "button",
|
|
6148
|
-
className: "dab-btn",
|
|
6149
|
-
disabled: url === null,
|
|
6150
|
-
onClick: () => setPickerOpen(true),
|
|
6151
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PipetteIcon, { size: 13 }), t("eyedropper")]
|
|
6152
|
-
}),
|
|
6153
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6154
|
-
type: "button",
|
|
6155
|
-
className: "dab-btn dab-btn-ghost",
|
|
6156
|
-
onClick: () => setColor(null),
|
|
6157
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }), t("ruleColorNone")]
|
|
6158
|
-
})
|
|
6159
|
-
]
|
|
6160
|
-
}),
|
|
6161
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6162
|
-
className: "dab-swatches",
|
|
6163
|
-
style: { marginTop: 12 },
|
|
6164
|
-
children: PALETTE.map(([sh, ss, sl], i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6350
|
+
})
|
|
6351
|
+
})]
|
|
6352
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6353
|
+
className: "dab-chip-row",
|
|
6354
|
+
style: { marginTop: 12 },
|
|
6355
|
+
children: [
|
|
6356
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6165
6357
|
type: "button",
|
|
6166
|
-
className: "dab-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6358
|
+
className: "dab-btn",
|
|
6359
|
+
disabled: url === null || extracting,
|
|
6360
|
+
onClick: () => void onExtract(),
|
|
6361
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 13 }), extracting ? t("extracting") : t("ruleColorExtract")]
|
|
6362
|
+
}),
|
|
6363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6364
|
+
type: "button",
|
|
6365
|
+
className: "dab-btn",
|
|
6366
|
+
disabled: url === null,
|
|
6367
|
+
onClick: () => setPickerOpen(true),
|
|
6368
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PipetteIcon, { size: 13 }), t("eyedropper")]
|
|
6369
|
+
}),
|
|
6370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6371
|
+
type: "button",
|
|
6372
|
+
className: "dab-btn dab-btn-ghost",
|
|
6373
|
+
onClick: () => setColor(null),
|
|
6374
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }), t("ruleColorNone")]
|
|
6375
|
+
})
|
|
6376
|
+
]
|
|
6377
|
+
})] }),
|
|
6177
6378
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6178
6379
|
style: { marginTop: 16 },
|
|
6179
6380
|
children: [
|
|
@@ -6201,10 +6402,136 @@ window.__ModuleLoader__.load({
|
|
|
6201
6402
|
children: t("ruleBlurHint")
|
|
6202
6403
|
})
|
|
6203
6404
|
]
|
|
6405
|
+
}),
|
|
6406
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6407
|
+
style: { marginTop: 14 },
|
|
6408
|
+
children: [
|
|
6409
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6410
|
+
className: "dab-rule-section-title",
|
|
6411
|
+
children: t("rotTitle")
|
|
6412
|
+
}),
|
|
6413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6414
|
+
className: "dab-chip-row",
|
|
6415
|
+
style: { alignItems: "center" },
|
|
6416
|
+
children: [
|
|
6417
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6418
|
+
type: "button",
|
|
6419
|
+
className: `dab-toggle${rule.rotate.enabled ? " is-on" : ""}`,
|
|
6420
|
+
role: "switch",
|
|
6421
|
+
"aria-checked": rule.rotate.enabled,
|
|
6422
|
+
title: t("rotEnable"),
|
|
6423
|
+
disabled: !rotatable,
|
|
6424
|
+
onClick: () => p.setRuleRotation(rule.id, { enabled: !rule.rotate.enabled }),
|
|
6425
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
6426
|
+
}),
|
|
6427
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6428
|
+
className: "dab-hint",
|
|
6429
|
+
style: { padding: 0 },
|
|
6430
|
+
children: t("rotEnable")
|
|
6431
|
+
}),
|
|
6432
|
+
!rotatable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6433
|
+
className: "dab-hint",
|
|
6434
|
+
style: { padding: 0 },
|
|
6435
|
+
children: t("rotNeedTwo")
|
|
6436
|
+
}) : null,
|
|
6437
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6438
|
+
type: "button",
|
|
6439
|
+
className: "dab-btn",
|
|
6440
|
+
disabled: !active || !rotatable,
|
|
6441
|
+
onClick: () => p.rotateNow(rule.id),
|
|
6442
|
+
children: t("rotNext")
|
|
6443
|
+
}),
|
|
6444
|
+
rotatable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6445
|
+
className: "dab-hint",
|
|
6446
|
+
style: { padding: 0 },
|
|
6447
|
+
children: active ? `${t("rotShowing")} ${Math.min(liveIndex + 1, rule.images.length)} / ${rule.images.length}` : t("rotNextInactive")
|
|
6448
|
+
}) : null
|
|
6449
|
+
]
|
|
6450
|
+
}),
|
|
6451
|
+
rotatable && rule.rotate.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6452
|
+
className: "dab-rot-panel",
|
|
6453
|
+
children: [
|
|
6454
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6455
|
+
className: "dab-rule-section-title",
|
|
6456
|
+
children: t("rotEvery")
|
|
6457
|
+
}),
|
|
6458
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6459
|
+
className: "dab-chip-row",
|
|
6460
|
+
children: [
|
|
6461
|
+
ROTATE_PRESETS.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6462
|
+
type: "button",
|
|
6463
|
+
className: `dab-chip${item.ms === rule.rotate.intervalMs ? " is-active" : ""}`,
|
|
6464
|
+
onClick: () => p.setRuleRotation(rule.id, { intervalMs: item.ms }),
|
|
6465
|
+
children: t(item.key)
|
|
6466
|
+
}, item.ms)),
|
|
6467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6468
|
+
type: "number",
|
|
6469
|
+
className: "dab-num dab-num-sec",
|
|
6470
|
+
min: 5,
|
|
6471
|
+
max: 86400,
|
|
6472
|
+
step: 5,
|
|
6473
|
+
value: secDraft ?? (preset === void 0 ? Math.round(rule.rotate.intervalMs / 1e3) : ""),
|
|
6474
|
+
placeholder: t("rotCustom"),
|
|
6475
|
+
onChange: (e) => setSecDraft(e.target.value),
|
|
6476
|
+
onBlur: commitSeconds,
|
|
6477
|
+
onKeyDown: (e) => {
|
|
6478
|
+
if (e.key === "Enter") {
|
|
6479
|
+
e.preventDefault();
|
|
6480
|
+
commitSeconds();
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
}),
|
|
6484
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6485
|
+
className: "dab-hint",
|
|
6486
|
+
style: { padding: 0 },
|
|
6487
|
+
children: t("rotSeconds")
|
|
6488
|
+
})
|
|
6489
|
+
]
|
|
6490
|
+
}),
|
|
6491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6492
|
+
className: "dab-rule-section-title",
|
|
6493
|
+
style: { marginTop: 12 },
|
|
6494
|
+
children: t("rotOrder")
|
|
6495
|
+
}),
|
|
6496
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6497
|
+
className: "dab-chip-row",
|
|
6498
|
+
style: { alignItems: "center" },
|
|
6499
|
+
children: [
|
|
6500
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6501
|
+
type: "button",
|
|
6502
|
+
className: `dab-chip${rule.rotate.order === "order" ? " is-active" : ""}`,
|
|
6503
|
+
onClick: () => p.setRuleRotation(rule.id, { order: "order" }),
|
|
6504
|
+
children: t("rotOrderSeq")
|
|
6505
|
+
}),
|
|
6506
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6507
|
+
type: "button",
|
|
6508
|
+
className: `dab-chip${rule.rotate.order === "shuffle" ? " is-active" : ""}`,
|
|
6509
|
+
onClick: () => p.setRuleRotation(rule.id, { order: "shuffle" }),
|
|
6510
|
+
children: t("rotOrderShuffle")
|
|
6511
|
+
}),
|
|
6512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6513
|
+
type: "button",
|
|
6514
|
+
className: `dab-toggle${rule.rotate.advanceOnSwitch ? " is-on" : ""}`,
|
|
6515
|
+
role: "switch",
|
|
6516
|
+
"aria-checked": rule.rotate.advanceOnSwitch,
|
|
6517
|
+
title: t("rotOnSwitch"),
|
|
6518
|
+
onClick: () => p.setRuleRotation(rule.id, { advanceOnSwitch: !rule.rotate.advanceOnSwitch }),
|
|
6519
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
6520
|
+
}),
|
|
6521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6522
|
+
className: "dab-hint",
|
|
6523
|
+
style: { padding: 0 },
|
|
6524
|
+
children: t("rotOnSwitch")
|
|
6525
|
+
})
|
|
6526
|
+
]
|
|
6527
|
+
})
|
|
6528
|
+
]
|
|
6529
|
+
}) : null
|
|
6530
|
+
]
|
|
6204
6531
|
})
|
|
6205
6532
|
] })]
|
|
6206
6533
|
})
|
|
6207
|
-
})
|
|
6534
|
+
}),
|
|
6208
6535
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6209
6536
|
ref: fileRef,
|
|
6210
6537
|
type: "file",
|
|
@@ -6263,12 +6590,57 @@ window.__ModuleLoader__.load({
|
|
|
6263
6590
|
"ease-out": "trEasingOut",
|
|
6264
6591
|
"ease-in-out": "trEasingInOut"
|
|
6265
6592
|
};
|
|
6593
|
+
/**
|
|
6594
|
+
* Failing-hop codes the model watcher can report, mapped to their copy. Only
|
|
6595
|
+
* these are rendered: `waiting` and `fallback` already have their own hints.
|
|
6596
|
+
*/
|
|
6597
|
+
const MODEL_NOTE_KEYS = {
|
|
6598
|
+
"no-service": "statusNoteNoService",
|
|
6599
|
+
"no-session": "statusNoteNoSession",
|
|
6600
|
+
"no-projection": "statusNoteNoProjection",
|
|
6601
|
+
"empty-selection": "statusNoteEmptySelection"
|
|
6602
|
+
};
|
|
6603
|
+
/**
|
|
6604
|
+
* The ways the recommended-profile download can fail, mapped to their copy.
|
|
6605
|
+
*
|
|
6606
|
+
* They are worth telling apart because they ask different things of the user and
|
|
6607
|
+
* of whoever maintains the assets repository: nothing answering is a network to
|
|
6608
|
+
* retry, a mirror that answered with something that is not a profile is not
|
|
6609
|
+
* going to start working on a retry, a profile written for a newer plugin is a
|
|
6610
|
+
* "update this build", and too many images is a profile nobody should install.
|
|
6611
|
+
* A failure that names a SLOT is a fifth case and is rendered separately — that
|
|
6612
|
+
* one is the author's file naming, and only the slot tells them which.
|
|
6613
|
+
*/
|
|
6614
|
+
const REC_ERROR_KEYS = {
|
|
6615
|
+
"download failed": "recFailNetwork",
|
|
6616
|
+
"not a profile": "recFailNotProfile",
|
|
6617
|
+
"newer version": "recFailNewer",
|
|
6618
|
+
"too many images": "recFailTooMany",
|
|
6619
|
+
"too large": "recFailTooLarge"
|
|
6620
|
+
};
|
|
6621
|
+
/** Size of a download, at the precision a person reads it at. */
|
|
6622
|
+
const fmtBytes = (n) => n >= 1048576 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
6266
6623
|
function ProfilePage({ p, notify }) {
|
|
6267
6624
|
const { t, exportTheme, importTheme, setTransition } = p;
|
|
6268
6625
|
const importRef = (0, react.useRef)(null);
|
|
6269
6626
|
p.useStore((s) => s.rev + s.rulesRev);
|
|
6270
6627
|
const holidays = cfg.holidays;
|
|
6271
6628
|
const tr = cfg.transition;
|
|
6629
|
+
const store = p.useStore((s) => s);
|
|
6630
|
+
const [text, setText] = (0, react.useState)("");
|
|
6631
|
+
const [touched, setTouched] = (0, react.useState)(false);
|
|
6632
|
+
(0, react.useEffect)(() => {
|
|
6633
|
+
if (!touched) setText(store.model);
|
|
6634
|
+
}, [store.model, touched]);
|
|
6635
|
+
const probe = text.trim();
|
|
6636
|
+
const result = (() => {
|
|
6637
|
+
if (probe === "") return null;
|
|
6638
|
+
const hit = matchRule(cfg.rules, probe);
|
|
6639
|
+
const rule = hit.rule;
|
|
6640
|
+
if (rule === null) return null;
|
|
6641
|
+
const n = cfg.rules.findIndex((r) => r.id === rule.id) + 1;
|
|
6642
|
+
return `${hit.matched ? t("tryoutHit") : t("tryoutFallback")} ${n}`;
|
|
6643
|
+
})();
|
|
6272
6644
|
const previewRef = (0, react.useRef)(null);
|
|
6273
6645
|
/** The OS preference, read at render: it vetoes the real switch (see
|
|
6274
6646
|
* `transitionPlan`), so the card has to say so rather than show a preview of
|
|
@@ -6309,6 +6681,47 @@ window.__ModuleLoader__.load({
|
|
|
6309
6681
|
notify(t("importFail"), false);
|
|
6310
6682
|
}
|
|
6311
6683
|
};
|
|
6684
|
+
const [pending, setPending] = (0, react.useState)(null);
|
|
6685
|
+
const [recBusy, setRecBusy] = (0, react.useState)(false);
|
|
6686
|
+
const [recDone, setRecDone] = (0, react.useState)(0);
|
|
6687
|
+
const [recTotal, setRecTotal] = (0, react.useState)(0);
|
|
6688
|
+
/** What the confirmation describes: countable facts about the download. */
|
|
6689
|
+
const recStats = (() => {
|
|
6690
|
+
if (pending === null) return null;
|
|
6691
|
+
const rules = pending.config?.rules;
|
|
6692
|
+
const images = pending.images ?? {};
|
|
6693
|
+
let bytes = 0;
|
|
6694
|
+
for (const url of Object.values(images)) bytes += url.length;
|
|
6695
|
+
return {
|
|
6696
|
+
rules: Array.isArray(rules) ? rules.length : 0,
|
|
6697
|
+
images: Object.keys(images).length,
|
|
6698
|
+
bytes
|
|
6699
|
+
};
|
|
6700
|
+
})();
|
|
6701
|
+
const onRecommended = async () => {
|
|
6702
|
+
setRecBusy(true);
|
|
6703
|
+
setRecDone(0);
|
|
6704
|
+
setRecTotal(0);
|
|
6705
|
+
const res = await p.fetchRecommended((done, total) => {
|
|
6706
|
+
setRecDone(done);
|
|
6707
|
+
setRecTotal(total);
|
|
6708
|
+
});
|
|
6709
|
+
setRecBusy(false);
|
|
6710
|
+
if (!res.ok) {
|
|
6711
|
+
notify(res.slot === void 0 ? t(REC_ERROR_KEYS[res.error] ?? "recFailNetwork") : `${t("recFailImage")} ${res.slot}`, false);
|
|
6712
|
+
return;
|
|
6713
|
+
}
|
|
6714
|
+
setPending(res.preset);
|
|
6715
|
+
};
|
|
6716
|
+
const onConfirmRecommended = async () => {
|
|
6717
|
+
const preset = pending;
|
|
6718
|
+
if (preset === null) return;
|
|
6719
|
+
setRecBusy(true);
|
|
6720
|
+
const ok = await p.applyRecommended(preset);
|
|
6721
|
+
setRecBusy(false);
|
|
6722
|
+
setPending(null);
|
|
6723
|
+
notify(ok ? t("recDone") : t("recFailApply"), ok);
|
|
6724
|
+
};
|
|
6312
6725
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
6313
6726
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
6314
6727
|
className: "dab-head dab-rise",
|
|
@@ -6440,70 +6853,210 @@ window.__ModuleLoader__.load({
|
|
|
6440
6853
|
}) : null
|
|
6441
6854
|
]
|
|
6442
6855
|
}),
|
|
6856
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6857
|
+
className: "dab-card dab-rise",
|
|
6858
|
+
style: { "--d": 2 },
|
|
6859
|
+
children: [
|
|
6860
|
+
store.hostStale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6861
|
+
className: "dab-hint dab-stale",
|
|
6862
|
+
style: { marginBottom: 10 },
|
|
6863
|
+
children: t("hostStaleHint")
|
|
6864
|
+
}) : null,
|
|
6865
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6866
|
+
className: "dab-swatch-title",
|
|
6867
|
+
children: t("tryoutTitle")
|
|
6868
|
+
}),
|
|
6869
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6870
|
+
style: {
|
|
6871
|
+
marginTop: 10,
|
|
6872
|
+
display: "flex",
|
|
6873
|
+
flexWrap: "wrap",
|
|
6874
|
+
gap: 8,
|
|
6875
|
+
alignItems: "center"
|
|
6876
|
+
},
|
|
6877
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6878
|
+
type: "text",
|
|
6879
|
+
className: "dab-urlinput",
|
|
6880
|
+
value: text,
|
|
6881
|
+
placeholder: t("tryoutPlaceholder"),
|
|
6882
|
+
onChange: (e) => {
|
|
6883
|
+
setTouched(true);
|
|
6884
|
+
setText(e.target.value);
|
|
6885
|
+
}
|
|
6886
|
+
}), store.model !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6887
|
+
type: "button",
|
|
6888
|
+
className: "dab-btn",
|
|
6889
|
+
onClick: () => {
|
|
6890
|
+
setTouched(true);
|
|
6891
|
+
setText(store.model);
|
|
6892
|
+
},
|
|
6893
|
+
children: t("tryoutUseCurrent")
|
|
6894
|
+
}) : null]
|
|
6895
|
+
}),
|
|
6896
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6897
|
+
className: "dab-hint",
|
|
6898
|
+
style: { marginTop: 9 },
|
|
6899
|
+
children: probe === "" ? t("tryoutEmpty") : result === null ? t("tryoutNone") : result
|
|
6900
|
+
}),
|
|
6901
|
+
store.model === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6902
|
+
className: "dab-hint",
|
|
6903
|
+
style: { marginTop: 6 },
|
|
6904
|
+
children: t("statusUnknownHint")
|
|
6905
|
+
}) : null,
|
|
6906
|
+
store.model !== "" && store.modelSource === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6907
|
+
className: "dab-hint",
|
|
6908
|
+
style: { marginTop: 6 },
|
|
6909
|
+
children: t("statusSourceDefaultHint")
|
|
6910
|
+
}) : null,
|
|
6911
|
+
MODEL_NOTE_KEYS[store.modelNote] !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6912
|
+
className: "dab-hint",
|
|
6913
|
+
style: { marginTop: 6 },
|
|
6914
|
+
children: t(MODEL_NOTE_KEYS[store.modelNote])
|
|
6915
|
+
}) : null
|
|
6916
|
+
]
|
|
6917
|
+
}),
|
|
6443
6918
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6444
6919
|
className: "dab-profile-grid dab-rise",
|
|
6445
6920
|
style: { "--d": 3 },
|
|
6446
|
-
children: [
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6921
|
+
children: [
|
|
6922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6923
|
+
className: "dab-card dab-card-hover",
|
|
6924
|
+
children: [
|
|
6925
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6926
|
+
className: "dab-profile-ico",
|
|
6927
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 17 })
|
|
6928
|
+
}),
|
|
6929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6930
|
+
className: "dab-profile-title",
|
|
6931
|
+
children: t("recCardTitle")
|
|
6932
|
+
}),
|
|
6933
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6934
|
+
className: "dab-profile-desc",
|
|
6935
|
+
children: t("recCardDesc")
|
|
6936
|
+
}),
|
|
6937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6938
|
+
type: "button",
|
|
6939
|
+
className: "dab-btn dab-btn-primary",
|
|
6940
|
+
disabled: recBusy || store.hostStale,
|
|
6941
|
+
title: store.hostStale ? t("hostStaleHint") : t("recAdopt"),
|
|
6942
|
+
onClick: () => void onRecommended(),
|
|
6943
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), !recBusy ? t("recAdopt") : recTotal > 0 ? `${t("recBusy")} ${recDone}/${recTotal}` : t("recBusy")]
|
|
6944
|
+
})
|
|
6945
|
+
]
|
|
6946
|
+
}),
|
|
6947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6948
|
+
className: "dab-card dab-card-hover",
|
|
6949
|
+
children: [
|
|
6950
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6951
|
+
className: "dab-profile-ico",
|
|
6952
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, { size: 17 })
|
|
6953
|
+
}),
|
|
6954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6955
|
+
className: "dab-profile-title",
|
|
6956
|
+
children: t("exportCardTitle")
|
|
6957
|
+
}),
|
|
6958
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6959
|
+
className: "dab-profile-desc",
|
|
6960
|
+
children: t("exportCardDesc")
|
|
6961
|
+
}),
|
|
6962
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6963
|
+
type: "button",
|
|
6964
|
+
className: "dab-btn dab-btn-primary",
|
|
6965
|
+
onClick: () => {
|
|
6966
|
+
exportTheme();
|
|
6967
|
+
notify(t("toastExportDone"));
|
|
6968
|
+
},
|
|
6969
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, { size: 14 }), t("exportTheme")]
|
|
6970
|
+
})
|
|
6971
|
+
]
|
|
6972
|
+
}),
|
|
6973
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
6974
|
+
className: "dab-card dab-card-hover",
|
|
6975
|
+
children: [
|
|
6976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6977
|
+
className: "dab-profile-ico",
|
|
6978
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 17 })
|
|
6979
|
+
}),
|
|
6980
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6981
|
+
className: "dab-profile-title",
|
|
6982
|
+
children: t("importCardTitle")
|
|
6983
|
+
}),
|
|
6984
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6985
|
+
className: "dab-profile-desc",
|
|
6986
|
+
children: t("importCardDesc")
|
|
6987
|
+
}),
|
|
6988
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6989
|
+
type: "button",
|
|
6990
|
+
className: "dab-btn",
|
|
6991
|
+
onClick: () => importRef.current?.click(),
|
|
6992
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 14 }), t("importTheme")]
|
|
6993
|
+
}),
|
|
6994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6995
|
+
ref: importRef,
|
|
6996
|
+
type: "file",
|
|
6997
|
+
accept: "application/json,.json",
|
|
6998
|
+
style: { display: "none" },
|
|
6999
|
+
onChange: (e) => {
|
|
7000
|
+
const f = e.target.files?.[0];
|
|
7001
|
+
if (!f) return;
|
|
7002
|
+
onImport(f);
|
|
7003
|
+
e.target.value = "";
|
|
7004
|
+
}
|
|
7005
|
+
})
|
|
7006
|
+
]
|
|
7007
|
+
})
|
|
7008
|
+
]
|
|
7009
|
+
}),
|
|
7010
|
+
pending !== null && recStats !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7011
|
+
className: "dab-overlay",
|
|
7012
|
+
onClick: (e) => {
|
|
7013
|
+
if (e.target === e.currentTarget && !recBusy) setPending(null);
|
|
7014
|
+
},
|
|
7015
|
+
children: [
|
|
7016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7017
|
+
className: "dab-overlay-title",
|
|
7018
|
+
children: t("recConfirmTitle")
|
|
7019
|
+
}),
|
|
7020
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7021
|
+
className: "dab-modal-card",
|
|
7022
|
+
style: {
|
|
7023
|
+
maxWidth: "min(92vw, 460px)",
|
|
7024
|
+
textAlign: "center"
|
|
7025
|
+
},
|
|
7026
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7027
|
+
className: "dab-overlay-hint",
|
|
7028
|
+
style: { lineHeight: 1.7 },
|
|
7029
|
+
children: t("recConfirmBody")
|
|
7030
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7031
|
+
className: "dab-overlay-hint",
|
|
7032
|
+
style: {
|
|
7033
|
+
marginTop: 10,
|
|
7034
|
+
fontFamily: "var(--dab-mono, monospace)"
|
|
6467
7035
|
},
|
|
6468
|
-
children:
|
|
6469
|
-
})
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
}),
|
|
6478
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6479
|
-
className: "dab-profile-title",
|
|
6480
|
-
children: t("importCardTitle")
|
|
6481
|
-
}),
|
|
6482
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6483
|
-
className: "dab-profile-desc",
|
|
6484
|
-
children: t("importCardDesc")
|
|
6485
|
-
}),
|
|
6486
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7036
|
+
children: `${recStats.rules} ${t("recStatRules")} · ${recStats.images} ${t("recStatImages")} · ${fmtBytes(recStats.bytes)}`
|
|
7037
|
+
})]
|
|
7038
|
+
}),
|
|
7039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7040
|
+
style: {
|
|
7041
|
+
display: "flex",
|
|
7042
|
+
gap: 10
|
|
7043
|
+
},
|
|
7044
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6487
7045
|
type: "button",
|
|
6488
7046
|
className: "dab-btn",
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
}
|
|
6503
|
-
})
|
|
6504
|
-
]
|
|
6505
|
-
})]
|
|
6506
|
-
}),
|
|
7047
|
+
disabled: recBusy,
|
|
7048
|
+
onClick: () => setPending(null),
|
|
7049
|
+
children: t("recConfirmCancel")
|
|
7050
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7051
|
+
type: "button",
|
|
7052
|
+
className: "dab-btn dab-btn-danger",
|
|
7053
|
+
disabled: recBusy,
|
|
7054
|
+
onClick: () => void onConfirmRecommended(),
|
|
7055
|
+
children: t("recConfirmOk")
|
|
7056
|
+
})]
|
|
7057
|
+
})
|
|
7058
|
+
]
|
|
7059
|
+
}) }) : null,
|
|
6507
7060
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
6508
7061
|
className: "dab-footer dab-rise",
|
|
6509
7062
|
style: { "--d": 4 },
|
|
@@ -6637,6 +7190,33 @@ window.__ModuleLoader__.load({
|
|
|
6637
7190
|
"connection"
|
|
6638
7191
|
];
|
|
6639
7192
|
const CUSTOM_ID = "custom-color";
|
|
7193
|
+
/**
|
|
7194
|
+
* Ceiling on what one recommended profile may add up to, across every wallpaper.
|
|
7195
|
+
*
|
|
7196
|
+
* The node half caps each file and the number of files, but it answers one
|
|
7197
|
+
* independent call at a time and never sees a running total — the client is the
|
|
7198
|
+
* only side that does, so the total is bounded here. The real profile is 2.2 MB
|
|
7199
|
+
* of images across seven of them, so this is generous by design: it exists to
|
|
7200
|
+
* stop a config that names sixty-four eight-megabyte slots from turning one
|
|
7201
|
+
* button press into half a gigabyte, not to second-guess the author.
|
|
7202
|
+
*/
|
|
7203
|
+
const PRESET_TOTAL_MAX = 33554432;
|
|
7204
|
+
/**
|
|
7205
|
+
* How many wallpapers of a profile are in flight at once.
|
|
7206
|
+
*
|
|
7207
|
+
* Measured rather than guessed. The real seven-file profile, fetched against the
|
|
7208
|
+
* live asset host: 8.3 s one at a time, 4.8 s with four in flight (25 s when the
|
|
7209
|
+
* CDN edge was still cold). What is being paid is per-request latency, and the
|
|
7210
|
+
* host speaks HTTP/2 — the bytes are identical either way and the connection is
|
|
7211
|
+
* reused — so overlapping them buys wall-clock time and risks nothing but a
|
|
7212
|
+
* little contention.
|
|
7213
|
+
*
|
|
7214
|
+
* Four rather than all seven: a burst that wide competes for the same bandwidth
|
|
7215
|
+
* and makes the per-file timeout harder to reason about, and four already hides
|
|
7216
|
+
* most of the latency. Whichever worker finishes first takes the next file, so
|
|
7217
|
+
* the pool stays busy to the end rather than idling on the largest one.
|
|
7218
|
+
*/
|
|
7219
|
+
const PRESET_CONCURRENCY = 4;
|
|
6640
7220
|
function apply(ctx) {
|
|
6641
7221
|
initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
|
|
6642
7222
|
let customDispose = null;
|
|
@@ -6982,10 +7562,12 @@ window.__ModuleLoader__.load({
|
|
|
6982
7562
|
/**
|
|
6983
7563
|
* The slot of the holiday that could paint TODAY, or null.
|
|
6984
7564
|
*
|
|
6985
|
-
* Gated on the CALENDAR, not merely on the switch
|
|
6986
|
-
*
|
|
6987
|
-
*
|
|
6988
|
-
*
|
|
7565
|
+
* Gated on the CALENDAR, not merely on the switch — and this gate is worth more
|
|
7566
|
+
* than it used to be. The art does not ship in the package any more: the first
|
|
7567
|
+
* read on the day sends the node half to the network, so a profile that is
|
|
7568
|
+
* simply not on a holiday must not merely avoid ~730 KB of base64 over this
|
|
7569
|
+
* channel, it must avoid starting a download at all. On every other day this
|
|
7570
|
+
* returns null and nothing is fetched, cached or paid for.
|
|
6989
7571
|
*/
|
|
6990
7572
|
const todayHolidaySlot = () => {
|
|
6991
7573
|
if (!cfg.holidays.enabled) return null;
|
|
@@ -7163,6 +7745,53 @@ window.__ModuleLoader__.load({
|
|
|
7163
7745
|
matched: activeMatched
|
|
7164
7746
|
};
|
|
7165
7747
|
};
|
|
7748
|
+
/**
|
|
7749
|
+
* Write a whole theme file into the store: its rules, its wallpapers, nothing
|
|
7750
|
+
* else. The ONE door both imports come through — the file the user picked and
|
|
7751
|
+
* the profile downloaded from the project's asset host — so a downloaded
|
|
7752
|
+
* profile cannot arrive by a laxer route than a hand-picked file does.
|
|
7753
|
+
*
|
|
7754
|
+
* `wipeFirst` is the entire difference between them, and it belongs to the
|
|
7755
|
+
* recommended profile alone. `nextSlot` hands slots out as `m1`, `m2`, … — a
|
|
7756
|
+
* counter, not a random token — so a downloaded profile and the store it lands
|
|
7757
|
+
* in collide on those names as a matter of course rather than by coincidence.
|
|
7758
|
+
* Emptying the store first is what makes "replace everything with this"
|
|
7759
|
+
* literally true, instead of overwriting whichever of the user's own pictures
|
|
7760
|
+
* happened to share a name. The file path keeps the older behaviour: it
|
|
7761
|
+
* replaces the rules but pre-emptively deletes nothing.
|
|
7762
|
+
*
|
|
7763
|
+
* The wipe happens HERE rather than before the download, and that ordering is
|
|
7764
|
+
* the one thing worth being pedantic about: an unreachable CDN must not cost
|
|
7765
|
+
* the user their configuration on the way to a profile that never arrived. By
|
|
7766
|
+
* the time this runs the bytes are already in hand, so the caller has nothing
|
|
7767
|
+
* left to fail at.
|
|
7768
|
+
*/
|
|
7769
|
+
const applyThemeFile = async (data, wipeFirst) => {
|
|
7770
|
+
if (typeof data.version === "number" && data.version > 6) return false;
|
|
7771
|
+
if (typeof data.config !== "object" || data.config === null) return false;
|
|
7772
|
+
if (wipeFirst) {
|
|
7773
|
+
await resetStore();
|
|
7774
|
+
resetConfig();
|
|
7775
|
+
}
|
|
7776
|
+
adoptConfig(data.config);
|
|
7777
|
+
const incoming = data.images ?? {};
|
|
7778
|
+
const keep = /* @__PURE__ */ new Set();
|
|
7779
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
7780
|
+
keep.add(slot);
|
|
7781
|
+
const raw = incoming[slot];
|
|
7782
|
+
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
7783
|
+
setImage(slot, raw);
|
|
7784
|
+
writeImage(slot, raw);
|
|
7785
|
+
} else {
|
|
7786
|
+
setImage(slot, null);
|
|
7787
|
+
deleteImage(slot);
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && /^[A-Za-z0-9_-]{1,32}$/.test(slot)) deleteImage(slot);
|
|
7791
|
+
persistConfig();
|
|
7792
|
+
applyActive();
|
|
7793
|
+
return true;
|
|
7794
|
+
};
|
|
7166
7795
|
const sectionInject = (actions) => {
|
|
7167
7796
|
bound = actions;
|
|
7168
7797
|
sync();
|
|
@@ -7490,29 +8119,93 @@ window.__ModuleLoader__.load({
|
|
|
7490
8119
|
try {
|
|
7491
8120
|
const data = JSON.parse(await file.text());
|
|
7492
8121
|
if (!data || typeof data !== "object") return false;
|
|
7493
|
-
|
|
7494
|
-
if (typeof d.config !== "object" || d.config === null) return false;
|
|
7495
|
-
adoptConfig(d.config);
|
|
7496
|
-
const incoming = d.images ?? {};
|
|
7497
|
-
const keep = /* @__PURE__ */ new Set();
|
|
7498
|
-
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
7499
|
-
keep.add(slot);
|
|
7500
|
-
const raw = incoming[slot];
|
|
7501
|
-
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
7502
|
-
setImage(slot, raw);
|
|
7503
|
-
writeImage(slot, raw);
|
|
7504
|
-
} else {
|
|
7505
|
-
setImage(slot, null);
|
|
7506
|
-
deleteImage(slot);
|
|
7507
|
-
}
|
|
7508
|
-
}
|
|
7509
|
-
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && /^[A-Za-z0-9_-]{1,32}$/.test(slot)) deleteImage(slot);
|
|
7510
|
-
persistConfig();
|
|
7511
|
-
applyActive();
|
|
7512
|
-
return true;
|
|
8122
|
+
return await applyThemeFile(data, false);
|
|
7513
8123
|
} catch {
|
|
7514
8124
|
return false;
|
|
7515
8125
|
}
|
|
8126
|
+
},
|
|
8127
|
+
/**
|
|
8128
|
+
* Fetch the project's recommended profile. Downloads only.
|
|
8129
|
+
*
|
|
8130
|
+
* Split from applying it on purpose: the page has to ask before anything is
|
|
8131
|
+
* destroyed, and a confirmation can only describe what is actually in hand
|
|
8132
|
+
* — "7 rules, 7 wallpapers, 2.9 MB" — which means the download comes first
|
|
8133
|
+
* and the question second.
|
|
8134
|
+
*
|
|
8135
|
+
* The profile is a DIRECTORY now, so this is a pool rather than one call:
|
|
8136
|
+
* the manifest and config say which slots exist, and each wallpaper is one
|
|
8137
|
+
* request, up to `PRESET_CONCURRENCY` of them at a time. What the fan-out
|
|
8138
|
+
* buys is wall-clock time — the real profile measured 8.3 s fetched one at
|
|
8139
|
+
* a time against 4.8 s with four in flight — while each file keeps its own
|
|
8140
|
+
* mirror fallback, its own validation, and its own slot name in a failure.
|
|
8141
|
+
*
|
|
8142
|
+
* STRICT, and the pool is what makes that a decision rather than a
|
|
8143
|
+
* formality: the first wallpaper that cannot be had stops the queue, so the
|
|
8144
|
+
* files nobody has asked for yet are never asked for. Whatever was already
|
|
8145
|
+
* in flight finishes and is thrown away. Nothing has been touched at this
|
|
8146
|
+
* point, so "the author renamed a file" arrives as an error naming the file
|
|
8147
|
+
* rather than as a profile that is quietly missing a picture — and that
|
|
8148
|
+
* loudness is the whole reason this feature is allowed to follow a branch.
|
|
8149
|
+
*/
|
|
8150
|
+
fetchRecommended: async (onProgress) => {
|
|
8151
|
+
const head = await fetchPresetConfig();
|
|
8152
|
+
if (!head.ok) return {
|
|
8153
|
+
ok: false,
|
|
8154
|
+
error: head.error
|
|
8155
|
+
};
|
|
8156
|
+
const slots = head.slots;
|
|
8157
|
+
const images = {};
|
|
8158
|
+
let next = 0;
|
|
8159
|
+
let done = 0;
|
|
8160
|
+
let bytes = 0;
|
|
8161
|
+
/** Latched by the first worker to fail, so the rest stop asking. */
|
|
8162
|
+
let stopped = false;
|
|
8163
|
+
onProgress?.(0, slots.length);
|
|
8164
|
+
const worker = async () => {
|
|
8165
|
+
for (;;) {
|
|
8166
|
+
if (stopped || next >= slots.length) return null;
|
|
8167
|
+
const slot = slots[next++];
|
|
8168
|
+
const got = await fetchPresetImage(slot);
|
|
8169
|
+
if (!got.ok) {
|
|
8170
|
+
stopped = true;
|
|
8171
|
+
return {
|
|
8172
|
+
error: got.error,
|
|
8173
|
+
slot
|
|
8174
|
+
};
|
|
8175
|
+
}
|
|
8176
|
+
images[slot] = got.dataUrl;
|
|
8177
|
+
bytes += got.dataUrl.length;
|
|
8178
|
+
if (bytes > PRESET_TOTAL_MAX) {
|
|
8179
|
+
stopped = true;
|
|
8180
|
+
return { error: "too large" };
|
|
8181
|
+
}
|
|
8182
|
+
onProgress?.(++done, slots.length);
|
|
8183
|
+
}
|
|
8184
|
+
};
|
|
8185
|
+
const failure = (await Promise.all(Array.from({ length: Math.min(PRESET_CONCURRENCY, slots.length) }, worker))).find((r) => r !== null) ?? null;
|
|
8186
|
+
if (failure !== null) return {
|
|
8187
|
+
ok: false,
|
|
8188
|
+
error: failure.error,
|
|
8189
|
+
slot: failure.slot
|
|
8190
|
+
};
|
|
8191
|
+
return {
|
|
8192
|
+
ok: true,
|
|
8193
|
+
preset: {
|
|
8194
|
+
version: head.version,
|
|
8195
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8196
|
+
config: head.config,
|
|
8197
|
+
images
|
|
8198
|
+
}
|
|
8199
|
+
};
|
|
8200
|
+
},
|
|
8201
|
+
/**
|
|
8202
|
+
* Replace EVERYTHING with a downloaded profile: empty the store, then write
|
|
8203
|
+
* the profile into it. Irreversible by design, which is why the page asks
|
|
8204
|
+
* first and why nothing here is offered as an undo.
|
|
8205
|
+
*/
|
|
8206
|
+
applyRecommended: async (preset) => {
|
|
8207
|
+
if (!canWriteConfig()) return false;
|
|
8208
|
+
return await applyThemeFile(preset, true);
|
|
7516
8209
|
}
|
|
7517
8210
|
};
|
|
7518
8211
|
};
|