dsh-background-by-model 0.8.2 → 0.9.0
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 +12 -4
- package/CHANGELOG.zh.md +12 -1
- package/README.md +7 -8
- package/README.zh.md +7 -8
- package/lib/client.js +1651 -1451
- package/lib/index.js +228 -379
- package/package.json +4 -2
package/lib/client.js
CHANGED
|
@@ -4,8 +4,8 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
let react = require("react");
|
|
8
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
let react = require("react");
|
|
9
9
|
let react_dom = require("react-dom");
|
|
10
10
|
//#region src/client/runtime.ts
|
|
11
11
|
function resolveStore() {
|
|
@@ -16,384 +16,39 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
}
|
|
17
17
|
const defineStore = resolveStore().defineStore;
|
|
18
18
|
//#endregion
|
|
19
|
-
//#region src/client/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
tryoutHit: "命中规则",
|
|
53
|
-
tryoutFallback: "无匹配 · 走兜底规则",
|
|
54
|
-
tryoutNone: "没有可用规则(规则需要启用,并且有图片或主题色)",
|
|
55
|
-
tryoutEmpty: "输入模型名即可试跑",
|
|
56
|
-
autoExtract: "选图后自动取主题色",
|
|
57
|
-
ruleAdd: "新增规则",
|
|
58
|
-
ruleNoneHint: "还没有规则,点上面的「新增规则」新建一条",
|
|
59
|
-
ruleFallbackBadge: "兜底",
|
|
60
|
-
ruleMatchPlaceholder: "如 flash / glm(忽略大小写,留空则不参与匹配)",
|
|
61
|
-
ruleEnabled: "启用",
|
|
62
|
-
ruleUp: "上移",
|
|
63
|
-
ruleDown: "下移",
|
|
64
|
-
ruleRemove: "删除规则",
|
|
65
|
-
tabListLabel: "规则",
|
|
66
|
-
tabPick: "选择规则",
|
|
67
|
-
tabMore: "更多规则",
|
|
68
|
-
tabUnnamed: "未命名规则",
|
|
69
|
-
tabLive: "正在使用",
|
|
70
|
-
tabRotating: "轮换中",
|
|
71
|
-
tabOff: "已停用",
|
|
72
|
-
ruleRemoveConfirm: "再点一次删除",
|
|
73
|
-
ruleNoImage: "尚未选择图片",
|
|
74
|
-
ruleFromUrl: "从网址",
|
|
75
|
-
ruleUrlPlaceholder: "粘贴图片网址 https://…",
|
|
76
|
-
ruleUrlApply: "应用",
|
|
77
|
-
ruleUrlCancel: "取消",
|
|
78
|
-
ruleUrlApplying: "加载中…",
|
|
79
|
-
ruleUrlBadHttp: "仅支持 http/https 图片网址",
|
|
80
|
-
ruleUrlFail: "获取图片失败",
|
|
81
|
-
ruleImageRemove: "移除",
|
|
82
|
-
ruleEmptyHint: "这条规则还没有图片,也没有自己的主题色:没有可显示的内容,匹配和兜底都会跳过它",
|
|
83
|
-
ruleColorOnlyHint: "这条规则还没有图片:不会铺壁纸,界面只用这个主题色",
|
|
84
|
-
ruleImagePendingHint: "这张图片还没有取到内容,稍后会自己出现(也可以直接替换它)",
|
|
85
|
-
ruleColor: "主题色",
|
|
86
|
-
ruleColorNone: "跟随系统主题",
|
|
87
|
-
ruleColorNoneHint: "未设颜色时跟随系统主题:界面用宿主的配色,壁纸与各部位的透明度照旧生效",
|
|
88
|
-
ruleColorPick: "自定义颜色",
|
|
89
|
-
ruleColorPickHint: "优先从本图取色,取不到就用默认色",
|
|
90
|
-
ruleColorNoImageHint: "这条规则还没有图片,取不到色:选一个颜色可以只铺主题色,或先添加图片",
|
|
91
|
-
ruleColorPendingImageHint: "这张图片还没有取到内容,暂时取不了色:可以直接选一个颜色,或等它加载完",
|
|
92
|
-
ruleColorExtract: "从本图提取",
|
|
93
|
-
ruleColorTargetImage: "作用于选中的图片:",
|
|
94
|
-
ruleColorTargetRule: "作用于本规则(它还没有图片)",
|
|
95
|
-
ruleLayout: "布局模式",
|
|
96
|
-
ruleLayoutNoImage: "添加图片后可分别设置每张图的布局模式",
|
|
97
|
-
ruleFramingEdit: "编辑位置",
|
|
98
|
-
ruleOpacity: "背景透明度",
|
|
99
|
-
ruleBrightness: "背景亮度",
|
|
100
|
-
ruleBlur: "背景模糊",
|
|
101
|
-
ruleBlurHint: "这三条滑块都作用于壁纸层;界面页里的透明度作用于界面各区域,两者互不影响",
|
|
102
|
-
rotTitle: "多图轮换",
|
|
103
|
-
rotEnable: "轮换这组图",
|
|
104
|
-
rotNeedTwo: "至少两张图才能轮换",
|
|
105
|
-
rotEvery: "每张停留",
|
|
106
|
-
rotCustom: "自定义秒数",
|
|
107
|
-
rotSeconds: "秒",
|
|
108
|
-
rotOrder: "换图顺序",
|
|
109
|
-
rotOrderSeq: "按顺序",
|
|
110
|
-
rotOrderShuffle: "随机",
|
|
111
|
-
rotOnSwitch: "切换模型时也换一张",
|
|
112
|
-
rotNext: "下一张",
|
|
113
|
-
rotNextInactive: "只有正在生效的规则能换画面上的图",
|
|
114
|
-
rotPos: "第",
|
|
115
|
-
rotShowing: "正在显示",
|
|
116
|
-
rotAddImage: "添加",
|
|
117
|
-
rotReplaceImage: "替换",
|
|
118
|
-
rotDragHint: "拖动排序",
|
|
119
|
-
rotDblHint: "双击切换",
|
|
120
|
-
rotEvery10s: "10 秒",
|
|
121
|
-
rotEvery30s: "30 秒",
|
|
122
|
-
rotEvery1m: "1 分钟",
|
|
123
|
-
rotEvery5m: "5 分钟",
|
|
124
|
-
rotEvery30m: "30 分钟",
|
|
125
|
-
bgModeFit: "适应",
|
|
126
|
-
bgModeFill: "填充",
|
|
127
|
-
bgModeStretch: "拉伸",
|
|
128
|
-
bgModeTile: "平铺",
|
|
129
|
-
bgModeCenter: "居中",
|
|
130
|
-
editorTitle: "背景编辑器",
|
|
131
|
-
editorHint: "拖动移动画面,滚轮缩放大小",
|
|
132
|
-
editorCommit: "确认",
|
|
133
|
-
editorCancel: "取消",
|
|
134
|
-
editorReset: "重置",
|
|
135
|
-
extracting: "取色中…",
|
|
136
|
-
extractDone: "已应用图片配色",
|
|
137
|
-
extractFail: "未在这张图里找到鲜明的颜色,请换一张",
|
|
138
|
-
eyedropper: "从图片取色",
|
|
139
|
-
pickerTitle: "从图片取色",
|
|
140
|
-
pickerHint: "移动鼠标预览颜色,点击图片选取为主题色",
|
|
141
|
-
pickerClose: "关闭",
|
|
142
|
-
crashTitle: "界面渲染出错",
|
|
143
|
-
crashDesc: "设置面板遇到问题,点击下方按钮重置后重试",
|
|
144
|
-
crashReset: "重置面板",
|
|
145
|
-
exportTheme: "导出配置",
|
|
146
|
-
importTheme: "导入配置",
|
|
147
|
-
exportCardTitle: "导出配置",
|
|
148
|
-
exportCardDesc: "把全部规则、每条规则的颜色/布局/透明度/模糊与图片打包为 dsh-background-by-model-theme.json",
|
|
149
|
-
importCardTitle: "导入配置",
|
|
150
|
-
importCardDesc: "从之前导出的 JSON 文件一键恢复全部规则(会覆盖当前规则与图片)",
|
|
151
|
-
toastExportDone: "配置文件已开始下载",
|
|
152
|
-
importDone: "已导入配置",
|
|
153
|
-
importFail: "导入失败,文件格式不正确",
|
|
154
|
-
recCardTitle: "推荐配置",
|
|
155
|
-
recCardDesc: "从项目素材库下载作者维护的一整套配置(规则、壁纸与接口设置),一键替换当前的全部内容",
|
|
156
|
-
recAdopt: "采用推荐配置",
|
|
157
|
-
recBusy: "正在下载",
|
|
158
|
-
recConfirmTitle: "替换当前的全部配置?",
|
|
159
|
-
recConfirmBody: "会先删除你现在的全部规则与壁纸,再写入这份推荐配置。删除的文件无法恢复。",
|
|
160
|
-
recStatRules: "条规则",
|
|
161
|
-
recStatImages: "张壁纸",
|
|
162
|
-
recConfirmOk: "删除并替换",
|
|
163
|
-
recConfirmCancel: "取消",
|
|
164
|
-
recDone: "已采用推荐配置",
|
|
165
|
-
recFailNetwork: "推荐配置下载失败,请检查网络后重试",
|
|
166
|
-
recFailNotProfile: "素材库返回的内容不是配置,请稍后重试",
|
|
167
|
-
recFailNewer: "这份推荐配置需要更新的插件版本,未做任何改动",
|
|
168
|
-
recFailTooMany: "这份推荐配置列出的图片数量超出限制,未做任何改动",
|
|
169
|
-
recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
|
|
170
|
-
recFailImage: "这张壁纸没拿到(素材库里缺这个文件):",
|
|
171
|
-
recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
|
|
172
|
-
footerTag: "按模型换背景插件",
|
|
173
|
-
repoLink: "在 GitHub 上打开仓库",
|
|
174
|
-
holidayTitle: "节日特殊背景",
|
|
175
|
-
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)",
|
|
176
|
-
trTitle: "切换效果",
|
|
177
|
-
trEffect: "切换方式",
|
|
178
|
-
trEffectFade: "淡入淡出",
|
|
179
|
-
trEffectNone: "直接切换",
|
|
180
|
-
trEffectZoom: "缩放淡入",
|
|
181
|
-
trEffectSlide: "平移推入",
|
|
182
|
-
trEasing: "缓动",
|
|
183
|
-
trEasingEase: "标准",
|
|
184
|
-
trEasingLinear: "匀速",
|
|
185
|
-
trEasingOut: "先快后慢",
|
|
186
|
-
trEasingInOut: "两端慢",
|
|
187
|
-
trDuration: "切换时长",
|
|
188
|
-
trPreview: "试放",
|
|
189
|
-
trHint: "效果对所有换图生效:切模型、轮换、手动下一张、节日。「直接切换」和 0 秒一样是硬切",
|
|
190
|
-
trReducedHint: "系统开启了「减少动态效果」,实际切换不会播放动画"
|
|
191
|
-
};
|
|
192
|
-
const en = {
|
|
193
|
-
nav: "Model background",
|
|
194
|
-
brandTag: "Per-model wallpaper",
|
|
195
|
-
pageInterface: "Interface",
|
|
196
|
-
pageModelBg: "Model background",
|
|
197
|
-
pageProfile: "Profile",
|
|
198
|
-
descInterface: "Tune opacity per surface to build depth. These settings are global and apply to every model",
|
|
199
|
-
descModelBg: "Each rule is a match string plus a set of wallpapers and one look. On a model switch the list is scanned top-down for the first match string contained in the model name; if nothing hits, rule 1 is used. A rule can hold several images and rotate through them on a rhythm you set",
|
|
200
|
-
descProfile: "Tune how the wallpaper switches, and back up every rule together with its image — or restore one from a file",
|
|
201
|
-
uiTitle: "Interface",
|
|
202
|
-
uiOpacity: "Opacity",
|
|
203
|
-
uiOpacityBg: "Main background",
|
|
204
|
-
uiOpacitySide: "Left panel",
|
|
205
|
-
uiOpacityCard: "Cards & menus",
|
|
206
|
-
uiOpacityCode: "Code blocks",
|
|
207
|
-
uiOpacityInput: "Input & controls",
|
|
208
|
-
uiSop: "Settings interface opacity",
|
|
209
|
-
uiChatRegion: "Conversation text frame",
|
|
210
|
-
uiTrajectory: "Trajectory view",
|
|
211
|
-
uiPage: "Plugin pages",
|
|
212
|
-
uiPreview: "Right panel",
|
|
213
|
-
statusUnknownHint: "Without a detected model rule 1 is always used; switching the model once or waiting a moment retries automatically",
|
|
214
|
-
statusSourceDefaultHint: "Only the host-wide default model could be read — that is not necessarily this session's selection. It self-corrects once this session's model selection is available.",
|
|
215
|
-
statusNoteNoService: "The sessions service is not available yet (the session controller loads after this plugin); retrying automatically",
|
|
216
|
-
statusNoteNoSession: "The sessions service is up but reports no current session yet",
|
|
217
|
-
statusNoteNoProjection: "This session's model-selection projection is unreachable (no binding / projections)",
|
|
218
|
-
statusNoteEmptySelection: "The model-selection projection exists but carries no value yet (selecting a model once writes it)",
|
|
219
|
-
hostStaleHint: "The DSH host process is still running the older plugin, which cannot read the new config shape. To avoid dropping rules, this session changes the interface but holds every config write — restart DSH and multi-image plus rotation will save normally",
|
|
220
|
-
tryoutTitle: "Match test",
|
|
221
|
-
tryoutPlaceholder: "Type a model name, e.g. deepseek-flash",
|
|
222
|
-
tryoutUseCurrent: "Use current model",
|
|
223
|
-
tryoutHit: "matches rule",
|
|
224
|
-
tryoutFallback: "no match · fallback rule",
|
|
225
|
-
tryoutNone: "No usable rule (a rule needs to be enabled and carry an image or a theme color)",
|
|
226
|
-
tryoutEmpty: "Type a model name to test the list",
|
|
227
|
-
autoExtract: "Extract the theme color when an image is chosen",
|
|
228
|
-
ruleAdd: "Add rule",
|
|
229
|
-
ruleNoneHint: "No rules yet — use “Add rule” in the tab strip above",
|
|
230
|
-
ruleFallbackBadge: "fallback",
|
|
231
|
-
ruleMatchPlaceholder: "e.g. flash / glm (case-insensitive, empty never matches)",
|
|
232
|
-
ruleEnabled: "Enabled",
|
|
233
|
-
ruleUp: "Move up",
|
|
234
|
-
ruleDown: "Move down",
|
|
235
|
-
ruleRemove: "Remove rule",
|
|
236
|
-
tabListLabel: "Rules",
|
|
237
|
-
tabPick: "Pick a rule",
|
|
238
|
-
tabMore: "More rules",
|
|
239
|
-
tabUnnamed: "Unnamed rule",
|
|
240
|
-
tabLive: "In use",
|
|
241
|
-
tabRotating: "Rotating",
|
|
242
|
-
tabOff: "Disabled",
|
|
243
|
-
ruleRemoveConfirm: "Click again to delete",
|
|
244
|
-
ruleNoImage: "No image yet",
|
|
245
|
-
ruleFromUrl: "From URL",
|
|
246
|
-
ruleUrlPlaceholder: "Paste an image URL https://…",
|
|
247
|
-
ruleUrlApply: "Apply",
|
|
248
|
-
ruleUrlCancel: "Cancel",
|
|
249
|
-
ruleUrlApplying: "Loading…",
|
|
250
|
-
ruleUrlBadHttp: "Only http/https image URLs are supported",
|
|
251
|
-
ruleUrlFail: "Could not fetch the image",
|
|
252
|
-
ruleImageRemove: "Remove",
|
|
253
|
-
ruleEmptyHint: "This rule has no image and no theme color of its own: it has nothing to show, so matching and the fallback both skip it",
|
|
254
|
-
ruleColorOnlyHint: "No image in this rule yet: no wallpaper is shown, and the interface is painted from this theme color alone",
|
|
255
|
-
ruleImagePendingHint: "This image has no content yet — it appears once it loads; you can also replace it",
|
|
256
|
-
ruleColor: "Theme color",
|
|
257
|
-
ruleColorNone: "Follow system theme",
|
|
258
|
-
ruleColorNoneHint: "Without a color this rule follows the system theme: the host palette paints the interface while the wallpaper and every opacity slider keep working",
|
|
259
|
-
ruleColorPick: "Custom color",
|
|
260
|
-
ruleColorPickHint: "Extracts from this image when it can, otherwise uses a default color",
|
|
261
|
-
ruleColorNoImageHint: "This rule has no image to take a color from: pick a color to tint the interface alone, or add an image first",
|
|
262
|
-
ruleColorPendingImageHint: "This image has no content yet, so there is nothing to extract from: pick a color directly, or wait for it to load",
|
|
263
|
-
ruleColorTargetImage: "Applies to the selected image:",
|
|
264
|
-
ruleColorTargetRule: "Applies to this rule (it has no image yet)",
|
|
265
|
-
ruleColorExtract: "Extract from image",
|
|
266
|
-
ruleLayout: "Layout mode",
|
|
267
|
-
ruleLayoutNoImage: "Add an image to give each one its own layout mode",
|
|
268
|
-
ruleFramingEdit: "Edit position",
|
|
269
|
-
ruleOpacity: "Background opacity",
|
|
270
|
-
ruleBrightness: "Background brightness",
|
|
271
|
-
ruleBlur: "Background blur",
|
|
272
|
-
ruleBlurHint: "These three sliders all treat the wallpaper layer; the opacity on the Interface page affects interface surfaces — they are independent",
|
|
273
|
-
rotTitle: "Image rotation",
|
|
274
|
-
rotEnable: "Rotate these images",
|
|
275
|
-
rotNeedTwo: "At least two images are needed to rotate",
|
|
276
|
-
rotEvery: "Dwell time per image",
|
|
277
|
-
rotCustom: "Custom seconds",
|
|
278
|
-
rotSeconds: "s",
|
|
279
|
-
rotOrder: "Order",
|
|
280
|
-
rotOrderSeq: "In order",
|
|
281
|
-
rotOrderShuffle: "Shuffle",
|
|
282
|
-
rotOnSwitch: "Also step on a model switch",
|
|
283
|
-
rotNext: "Next image",
|
|
284
|
-
rotNextInactive: "Only the active rule can step the background",
|
|
285
|
-
rotPos: "Image",
|
|
286
|
-
rotShowing: "On screen",
|
|
287
|
-
rotAddImage: "Add",
|
|
288
|
-
rotReplaceImage: "Replace",
|
|
289
|
-
rotDragHint: "drag to reorder",
|
|
290
|
-
rotDblHint: "double-click to show",
|
|
291
|
-
rotEvery10s: "10 s",
|
|
292
|
-
rotEvery30s: "30 s",
|
|
293
|
-
rotEvery1m: "1 min",
|
|
294
|
-
rotEvery5m: "5 min",
|
|
295
|
-
rotEvery30m: "30 min",
|
|
296
|
-
bgModeFit: "Fit",
|
|
297
|
-
bgModeFill: "Fill",
|
|
298
|
-
bgModeStretch: "Stretch",
|
|
299
|
-
bgModeTile: "Tile",
|
|
300
|
-
bgModeCenter: "Center",
|
|
301
|
-
editorTitle: "Background editor",
|
|
302
|
-
editorHint: "Drag to move, scroll to zoom",
|
|
303
|
-
editorCommit: "Confirm",
|
|
304
|
-
editorCancel: "Cancel",
|
|
305
|
-
editorReset: "Reset",
|
|
306
|
-
extracting: "Extracting…",
|
|
307
|
-
extractDone: "Image colors applied",
|
|
308
|
-
extractFail: "No vivid color found in this image, try another",
|
|
309
|
-
eyedropper: "Eyedropper",
|
|
310
|
-
pickerTitle: "Pick from image",
|
|
311
|
-
pickerHint: "Hover to preview, click to pick as the theme color",
|
|
312
|
-
pickerClose: "Close",
|
|
313
|
-
crashTitle: "Section crashed",
|
|
314
|
-
crashDesc: "The panel hit an error. Reset below to recover.",
|
|
315
|
-
crashReset: "Reset panel",
|
|
316
|
-
exportTheme: "Export",
|
|
317
|
-
importTheme: "Import",
|
|
318
|
-
exportCardTitle: "Export profile",
|
|
319
|
-
exportCardDesc: "Bundle every rule — color, layout, opacity, blur — and its image into dsh-background-by-model-theme.json",
|
|
320
|
-
importCardTitle: "Import profile",
|
|
321
|
-
importCardDesc: "Restore every rule and image from a previously exported JSON file (replaces the current set)",
|
|
322
|
-
toastExportDone: "Export started",
|
|
323
|
-
importDone: "Profile imported",
|
|
324
|
-
importFail: "Import failed — invalid file",
|
|
325
|
-
recCardTitle: "Recommended profile",
|
|
326
|
-
recCardDesc: "Download the author's own full setup — rules, wallpapers and interface settings — and replace everything with it",
|
|
327
|
-
recAdopt: "Use recommended profile",
|
|
328
|
-
recBusy: "Downloading",
|
|
329
|
-
recConfirmTitle: "Replace your entire profile?",
|
|
330
|
-
recConfirmBody: "Every rule and wallpaper you have is deleted first, then this profile is written in its place. Deleted files cannot be recovered.",
|
|
331
|
-
recStatRules: "rules",
|
|
332
|
-
recStatImages: "wallpapers",
|
|
333
|
-
recConfirmOk: "Delete and replace",
|
|
334
|
-
recConfirmCancel: "Cancel",
|
|
335
|
-
recDone: "Recommended profile applied",
|
|
336
|
-
recFailNetwork: "Could not download the recommended profile — check your connection and retry",
|
|
337
|
-
recFailNotProfile: "The asset host answered with something that is not a profile — try again later",
|
|
338
|
-
recFailNewer: "This profile needs a newer plugin version. Nothing was changed.",
|
|
339
|
-
recFailTooMany: "The recommended profile names more images than the plugin will fetch. Nothing was changed.",
|
|
340
|
-
recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
|
|
341
|
-
recFailImage: "Could not fetch this wallpaper — the asset host has no such file:",
|
|
342
|
-
recFailApply: "Could not write the recommended profile — your own is untouched",
|
|
343
|
-
footerTag: "Per-model wallpaper plugin",
|
|
344
|
-
repoLink: "Open the repository on GitHub",
|
|
345
|
-
holidayTitle: "Holiday backgrounds",
|
|
346
|
-
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)",
|
|
347
|
-
trTitle: "Switch effect",
|
|
348
|
-
trEffect: "Transition",
|
|
349
|
-
trEffectFade: "Cross-fade",
|
|
350
|
-
trEffectNone: "Instant",
|
|
351
|
-
trEffectZoom: "Zoom in",
|
|
352
|
-
trEffectSlide: "Slide in",
|
|
353
|
-
trEasing: "Easing",
|
|
354
|
-
trEasingEase: "Standard",
|
|
355
|
-
trEasingLinear: "Linear",
|
|
356
|
-
trEasingOut: "Ease out",
|
|
357
|
-
trEasingInOut: "Ease in-out",
|
|
358
|
-
trDuration: "Duration",
|
|
359
|
-
trPreview: "Preview",
|
|
360
|
-
trHint: "The effect applies to every change: model switch, rotation, manual next, holidays. “Instant” and 0 s are the same hard cut",
|
|
361
|
-
trReducedHint: "Your system asks for reduced motion, so a real switch will not animate"
|
|
362
|
-
};
|
|
363
|
-
//#endregion
|
|
364
|
-
//#region src/client/transition.ts
|
|
365
|
-
/**
|
|
366
|
-
* Resolve the duration one switch uses, in ms (`0` = a hard cut).
|
|
367
|
-
*
|
|
368
|
-
* 1. `effect: 'none'` is a hard cut whatever the duration says — asking for no
|
|
369
|
-
* animation and asking for 0 ms are the same request, and the effect is the
|
|
370
|
-
* more explicit of the two;
|
|
371
|
-
* 2. otherwise the global `durationMs` decides, because a rule has no notion of
|
|
372
|
-
* a transition and there is exactly one duration for every switch.
|
|
373
|
-
*
|
|
374
|
-
* A non-finite or negative value answers `0` (a hard cut) rather than a default:
|
|
375
|
-
* animating for an unknown length is the one answer that cannot be right. The
|
|
376
|
-
* input is already clamped by the shared sanitizer (`./schema`), so this is a
|
|
377
|
-
* guard against a hand-edited config, not a second clamp.
|
|
378
|
-
*/
|
|
379
|
-
function resolveFadeMs(transition) {
|
|
380
|
-
if (transition.effect === "none") return 0;
|
|
381
|
-
return Number.isFinite(transition.durationMs) && transition.durationMs > 0 ? transition.durationMs : 0;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* The transform the incoming layer starts from, per effect.
|
|
385
|
-
*
|
|
386
|
-
* `zoom` starts ABOVE 1 and settles back to exactly `none`, never below or past
|
|
387
|
-
* it: the wallpaper would otherwise be left larger than the viewport, cropping a
|
|
388
|
-
* `fit` framing the user committed by hand. `slide` is a small push because a
|
|
389
|
-
* full-width travel of a full-bleed wallpaper reads as two wallpapers, not one
|
|
390
|
-
* transition.
|
|
391
|
-
*/
|
|
392
|
-
const START_TRANSFORM = {
|
|
393
|
-
fade: "none",
|
|
394
|
-
none: "none",
|
|
395
|
-
zoom: "scale(1.06)",
|
|
396
|
-
slide: "translateX(6%)"
|
|
19
|
+
//#region src/client/transition.ts
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the duration one switch uses, in ms (`0` = a hard cut).
|
|
22
|
+
*
|
|
23
|
+
* 1. `effect: 'none'` is a hard cut whatever the duration says — asking for no
|
|
24
|
+
* animation and asking for 0 ms are the same request, and the effect is the
|
|
25
|
+
* more explicit of the two;
|
|
26
|
+
* 2. otherwise the global `durationMs` decides, because a rule has no notion of
|
|
27
|
+
* a transition and there is exactly one duration for every switch.
|
|
28
|
+
*
|
|
29
|
+
* A non-finite or negative value answers `0` (a hard cut) rather than a default:
|
|
30
|
+
* animating for an unknown length is the one answer that cannot be right. The
|
|
31
|
+
* input is already clamped by the shared sanitizer (`./schema`), so this is a
|
|
32
|
+
* guard against a hand-edited config, not a second clamp.
|
|
33
|
+
*/
|
|
34
|
+
function resolveFadeMs(transition) {
|
|
35
|
+
if (transition.effect === "none") return 0;
|
|
36
|
+
return Number.isFinite(transition.durationMs) && transition.durationMs > 0 ? transition.durationMs : 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The transform the incoming layer starts from, per effect.
|
|
40
|
+
*
|
|
41
|
+
* `zoom` starts ABOVE 1 and settles back to exactly `none`, never below or past
|
|
42
|
+
* it: the wallpaper would otherwise be left larger than the viewport, cropping a
|
|
43
|
+
* `fit` framing the user committed by hand. `slide` is a small push because a
|
|
44
|
+
* full-width travel of a full-bleed wallpaper reads as two wallpapers, not one
|
|
45
|
+
* transition.
|
|
46
|
+
*/
|
|
47
|
+
const START_TRANSFORM = {
|
|
48
|
+
fade: "none",
|
|
49
|
+
none: "none",
|
|
50
|
+
zoom: "scale(1.06)",
|
|
51
|
+
slide: "translateX(6%)"
|
|
397
52
|
};
|
|
398
53
|
/** The properties one effect actually animates — an unchanged property must not
|
|
399
54
|
* be put on the transition list, or `fade` would stop being byte-for-byte the
|
|
@@ -582,23 +237,16 @@ window.__ModuleLoader__.load({
|
|
|
582
237
|
/**
|
|
583
238
|
* The persisted configuration shape — declared exactly ONCE, for both halves.
|
|
584
239
|
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
* `
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
* The types, the key lists, the defaults and the pure normalizers therefore live
|
|
594
|
-
* here and both halves import them: adding a field means editing this file, and
|
|
595
|
-
* the two halves cannot disagree about the shape. The host additionally logs one
|
|
596
|
-
* warning per unknown key while sanitizing a write (see `warnUnknownConfigKeys`
|
|
597
|
-
* in `src/index.ts`), so a field that reaches the node half without being
|
|
598
|
-
* declared here shows up in the host log instead of quietly disappearing.
|
|
240
|
+
* The types, the key lists, the defaults and the pure normalizers live here and
|
|
241
|
+
* both halves import them, so adding a field means editing this file and the two
|
|
242
|
+
* halves cannot disagree about the shape. (This was written twice until 0.3.2,
|
|
243
|
+
* when a field declared on one side was silently dropped by the other side's
|
|
244
|
+
* sanitizer.) The host also logs one warning per unknown key while sanitizing a
|
|
245
|
+
* write (`warnUnknownConfigKeys` in `src/index.ts`), so a field that reaches the
|
|
246
|
+
* node half without being declared here shows up in the log.
|
|
599
247
|
*
|
|
600
|
-
* Nothing
|
|
601
|
-
*
|
|
248
|
+
* Nothing here may touch the DOM, Node or the Cordis context: it is imported by
|
|
249
|
+
* the browser bundle and by the node half alike.
|
|
602
250
|
*/
|
|
603
251
|
const DEFAULT_BG_STATE = {
|
|
604
252
|
zoom: 1,
|
|
@@ -633,14 +281,14 @@ window.__ModuleLoader__.load({
|
|
|
633
281
|
return { ...DEFAULT_ROTATION };
|
|
634
282
|
}
|
|
635
283
|
/**
|
|
636
|
-
* Main interface opacities (0..1), global (Interface page)
|
|
637
|
-
*
|
|
638
|
-
* `
|
|
639
|
-
* `
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* `input`
|
|
284
|
+
* Main interface opacities (0..1), global (Interface page) — each part is the token
|
|
285
|
+
* set it owns:
|
|
286
|
+
* `bg` `--dsw-alias-bg-base`
|
|
287
|
+
* `sidebar` `--dsw-specific-sidebar-fill`
|
|
288
|
+
* `card` `--dsw-alias-bg-layer-1/2/3`, `--dsw-specific-menu` (and its alias
|
|
289
|
+
* `--dsw-menu-surface-fill`)
|
|
290
|
+
* `code` `--dsw-alias-markdown-code-block`, `…-banner`
|
|
291
|
+
* `input` `--dsw-specific-input-major`
|
|
644
292
|
*/
|
|
645
293
|
const PART_OPACITY_KEYS = [
|
|
646
294
|
"bg",
|
|
@@ -688,27 +336,17 @@ window.__ModuleLoader__.load({
|
|
|
688
336
|
return rule.images.map((i) => i.slot);
|
|
689
337
|
}
|
|
690
338
|
/**
|
|
691
|
-
* Whether a rule has anything to show at all: an image, or a theme
|
|
339
|
+
* Whether a rule has anything to show at all: an image, or a theme colour of its
|
|
692
340
|
* own. Used by matching (`usable` in ./client/modelbg) and by the panel's copy.
|
|
693
341
|
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
* its card (the wheel, the swatches, the clear-to-system-theme button) dead — the
|
|
700
|
-
* one state where the panel was confidently showing a lie. It also silently
|
|
701
|
-
* handed the model to the NEXT rule, so clearing the default rule's images put
|
|
702
|
-
* another model's wallpaper on the screen.
|
|
342
|
+
* The rule's own colour counts because it recolours the interface with no
|
|
343
|
+
* wallpaper involved: a rule whose pictures were all removed still paints, and
|
|
344
|
+
* treating it as unusable handed the model to the NEXT rule (another model's
|
|
345
|
+
* wallpaper on screen) while every control left on its card went dead. Per-image
|
|
346
|
+
* colours do not enter the question at all — an image is already enough.
|
|
703
347
|
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
* non-empty, never about what those images are painted in.
|
|
707
|
-
*
|
|
708
|
-
* A rule with no image and no color of its own paints nothing: the system theme
|
|
709
|
-
* is not a paint of this rule's own, so such a rule is skipped by matching, which
|
|
710
|
-
* is what makes a freshly added rule (empty, colorless) invisible until the user
|
|
711
|
-
* gives it something.
|
|
348
|
+
* A rule with neither paints nothing, so matching skips it: a freshly added rule
|
|
349
|
+
* stays invisible until the user gives it something.
|
|
712
350
|
*/
|
|
713
351
|
function ruleCanPaint(rule) {
|
|
714
352
|
return rule.enabled && (ruleSlots(rule).length > 0 || rule.color !== null);
|
|
@@ -718,12 +356,10 @@ window.__ModuleLoader__.load({
|
|
|
718
356
|
const DEFAULT_RIGHTBAR_OPACITY = .45;
|
|
719
357
|
/**
|
|
720
358
|
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
721
|
-
* null when the string is not a hex
|
|
359
|
+
* null when the string is not a hex colour.
|
|
722
360
|
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* conversion lives here rather than in ./holiday because `[h, s, l]` is this
|
|
726
|
-
* module's own rule shape.
|
|
361
|
+
* Holidays name their colour as hex (`HOLIDAYS`) because a hand-converted triple
|
|
362
|
+
* is a magic number nothing downstream can check.
|
|
727
363
|
*/
|
|
728
364
|
function hexToHsl(hex) {
|
|
729
365
|
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
@@ -756,11 +392,8 @@ window.__ModuleLoader__.load({
|
|
|
756
392
|
/**
|
|
757
393
|
* One holiday's appearance, untouched.
|
|
758
394
|
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
* art was not cut for. The mode now lives on the IMAGE (see `BgImage.bgMode`), so
|
|
762
|
-
* that default is written there — and `fill` is what a holiday's read-back
|
|
763
|
-
* sanitizer refuses to lose (see `normalizeHolidayRule`).
|
|
395
|
+
* The mode is `fill` rather than a rule's `fit`: full-bleed festival art would be
|
|
396
|
+
* letterboxed by `fit` on every aspect ratio it was not cut for.
|
|
764
397
|
*/
|
|
765
398
|
function defaultHolidayRule(def) {
|
|
766
399
|
const color = hexToHsl(def.color);
|
|
@@ -786,21 +419,13 @@ window.__ModuleLoader__.load({
|
|
|
786
419
|
return HOLIDAYS.map(defaultHolidayRule);
|
|
787
420
|
}
|
|
788
421
|
/**
|
|
789
|
-
* A rule with nothing in it yet: no picture, no match string, no
|
|
790
|
-
*
|
|
791
|
-
* Two states need this factory and they are the same state — a fresh install
|
|
792
|
-
* (see `freshThemeConfig` and `normalizeConfig`), and the "add rule" tile one
|
|
793
|
-
* entry further down the list.
|
|
422
|
+
* A rule with nothing in it yet: no picture, no match string, no colour — a fresh
|
|
423
|
+
* install's rule 1 and the "add rule" tile alike.
|
|
794
424
|
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
* It used to be born with an allocated slot and an empty image entry, which is
|
|
801
|
-
* what put a blank tile at position 1 of a new rule's strip — and, worse, stopped
|
|
802
|
-
* the picture the user added next from being the first one (a rule paints its
|
|
803
|
-
* FIRST image). The slot is allocated when the first picture arrives.
|
|
425
|
+
* Valid rather than half-built: `ruleCanPaint` skips it until it has something of
|
|
426
|
+
* its own, so it cannot steal the fallback from rule 1. It deliberately carries NO
|
|
427
|
+
* image entry: a rule paints its FIRST image, and a placeholder slot left the
|
|
428
|
+
* picture the user added next in position 2. The slot is allocated on arrival.
|
|
804
429
|
*/
|
|
805
430
|
function blankRule(id) {
|
|
806
431
|
return {
|
|
@@ -816,14 +441,11 @@ window.__ModuleLoader__.load({
|
|
|
816
441
|
};
|
|
817
442
|
}
|
|
818
443
|
/**
|
|
819
|
-
* A default config with independent nested objects (never
|
|
444
|
+
* A default config with independent nested objects (never the shared ones).
|
|
820
445
|
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
* for that is the rule card, which an empty list has no way to show. The empty
|
|
825
|
-
* state is still reachable (deleting the last rule is a thing the user can mean),
|
|
826
|
-
* and is described by `ruleNoneHint`.
|
|
446
|
+
* Seeded with one blank rule: an empty list has no rule card, and the card is the
|
|
447
|
+
* only drop target a first picture has. Deleting every rule is still reachable and
|
|
448
|
+
* is described by `ruleNoneHint`.
|
|
827
449
|
*/
|
|
828
450
|
function freshThemeConfig() {
|
|
829
451
|
return {
|
|
@@ -843,7 +465,7 @@ window.__ModuleLoader__.load({
|
|
|
843
465
|
};
|
|
844
466
|
}
|
|
845
467
|
/** Slot names reach the filesystem, so they are strictly whitelisted. */
|
|
846
|
-
const SLOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
468
|
+
const SLOT_RE$1 = /^[A-Za-z0-9_-]{1,32}$/;
|
|
847
469
|
function clamp$1(n, lo, hi, def) {
|
|
848
470
|
return typeof n === "number" && isFinite(n) ? Math.min(hi, Math.max(lo, n)) : def;
|
|
849
471
|
}
|
|
@@ -908,18 +530,12 @@ window.__ModuleLoader__.load({
|
|
|
908
530
|
}
|
|
909
531
|
}
|
|
910
532
|
/**
|
|
911
|
-
* Coerce one persisted transition block; a config
|
|
912
|
-
*
|
|
913
|
-
* the behaviour it already had.
|
|
533
|
+
* Coerce one persisted transition block; a config without one gets the shipped
|
|
534
|
+
* default, which is what it was already showing.
|
|
914
535
|
*
|
|
915
|
-
* `legacyMs` is the per-rule `fadeMs` this shape replaced (
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
* the field it used to live on.
|
|
919
|
-
*
|
|
920
|
-
* `durationMs` is clamped with `0` kept as a real value (a hard cut) rather than
|
|
921
|
-
* read as "absent": 0 is exactly how a hard cut is asked for without changing
|
|
922
|
-
* the effect.
|
|
536
|
+
* `legacyMs` is the per-rule `fadeMs` this shape replaced (`legacyFadeMs`), adopted
|
|
537
|
+
* only when the incoming config carries no `durationMs` of its own. `0` is kept as
|
|
538
|
+
* a real value (a hard cut), not read as "absent".
|
|
923
539
|
*/
|
|
924
540
|
function normalizeTransition(raw, legacyMs) {
|
|
925
541
|
const t = raw ?? {};
|
|
@@ -934,29 +550,19 @@ window.__ModuleLoader__.load({
|
|
|
934
550
|
/**
|
|
935
551
|
* Coerce one persisted image entry, or null when it lacks a usable slot.
|
|
936
552
|
*
|
|
937
|
-
* `inherit`
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
* silently dropping to the system palette, and it happens in the shared sanitizer
|
|
942
|
-
* so both halves lift identically.
|
|
943
|
-
*
|
|
944
|
-
* The KEY's presence is the whole distinction, which is why `null` is not the
|
|
945
|
-
* same as absent: an entry with `color: null` is an image the user explicitly set
|
|
946
|
-
* to "follow the system theme", and re-inheriting the rule's color over that
|
|
947
|
-
* would undo a deliberate choice on every load.
|
|
553
|
+
* `inherit` (the RULE's colour) is applied only to an entry with no `color` KEY at
|
|
554
|
+
* all — the shape written before per-image colours, where the rule's colour was the
|
|
555
|
+
* only one there was. The key's PRESENCE is the whole distinction: `color: null` is
|
|
556
|
+
* a deliberate "follow the system theme" and must not be re-inherited on every load.
|
|
948
557
|
*
|
|
949
|
-
* `inheritMode` is the same one-shot lift for the layout mode
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
* own default, which is not up for the lift to change). An entry that HAS a
|
|
954
|
-
* `bgMode` key keeps it — including one that has just been set to a value equal
|
|
955
|
-
* to the default, which is a decision and not an absence.
|
|
558
|
+
* `inheritMode` is the same one-shot lift for the layout mode. It is the caller's to
|
|
559
|
+
* resolve because the callers answer differently: a user rule passes the mode its
|
|
560
|
+
* config carried at the RULE level (schema 5), a holiday passes its own `fill`. An
|
|
561
|
+
* entry that HAS a `bgMode` key keeps it, even when it equals the default.
|
|
956
562
|
*/
|
|
957
563
|
function normalizeImage(raw, inherit = null, inheritMode = "fit") {
|
|
958
564
|
const i = raw ?? {};
|
|
959
|
-
if (typeof i.slot !== "string" || !SLOT_RE.test(i.slot)) return null;
|
|
565
|
+
if (typeof i.slot !== "string" || !SLOT_RE$1.test(i.slot)) return null;
|
|
960
566
|
return {
|
|
961
567
|
slot: i.slot,
|
|
962
568
|
bgMode: BG_MODES$1.includes(i.bgMode) ? i.bgMode : inheritMode,
|
|
@@ -965,36 +571,21 @@ window.__ModuleLoader__.load({
|
|
|
965
571
|
};
|
|
966
572
|
}
|
|
967
573
|
/**
|
|
968
|
-
* Coerce one persisted rule, or null when it names no image
|
|
574
|
+
* Coerce one persisted rule, or null when it names no image ANYWHERE.
|
|
969
575
|
*
|
|
970
|
-
*
|
|
971
|
-
* `slot`/`bgState` pair every earlier version wrote
|
|
972
|
-
* one-entry list
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
* rotation look stuck.
|
|
576
|
+
* Both image shapes are accepted here and nowhere else: the 0.7 list (`images`),
|
|
577
|
+
* and the single `slot`/`bgState` pair every earlier version wrote, lifted into a
|
|
578
|
+
* one-entry list. That is what keeps the rest of the codebase free of "old config"
|
|
579
|
+
* branches. Duplicate slots inside one rule are dropped — two entries pointing at
|
|
580
|
+
* the same bytes only make a rotation look stuck.
|
|
976
581
|
*
|
|
977
|
-
* The rule's
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
582
|
+
* The rule's colour is resolved FIRST and handed to every image entry (what a
|
|
583
|
+
* pre-0.7.1 entry inherits), and the rule keeps it: it is what the rule paints once
|
|
584
|
+
* its last picture is gone. The layout mode makes the same trip from the RULE level
|
|
585
|
+
* a pre-0.7.5 config carries, so the user's choice survives the move.
|
|
981
586
|
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
* level, where one value covered every picture of a rotation, so it is read here
|
|
985
|
-
* once and handed to each image as its inherited mode. What the user chose
|
|
986
|
-
* therefore survives the move the same way it survived the color's — the mode is
|
|
987
|
-
* not reset to the default on upgrade, and the field it used to live on is simply
|
|
988
|
-
* not written back.
|
|
989
|
-
*
|
|
990
|
-
* An EMPTY list is a legitimate rule, not a broken one: removing a rule's last
|
|
991
|
-
* picture has to be expressible, and forcing an entry back in is what produced a
|
|
992
|
-
* phantom blank image that took slot 1 and could never be deleted again (the next
|
|
993
|
-
* upload landed at position 2 and the rule kept painting nothing). What such a
|
|
994
|
-
* rule then does is `ruleCanPaint`'s business: with a color of its own it paints
|
|
995
|
-
* the interface, with neither it is skipped by matching — which the card's own
|
|
996
|
-
* hint says in so many words. Only a rule that names no image anywhere — no list,
|
|
997
|
-
* no legacy slot, or a slot that could never be a filename — is dropped.
|
|
587
|
+
* An EMPTY list is a legitimate rule: removing a rule's last picture has to be
|
|
588
|
+
* expressible. `ruleCanPaint` decides what such a rule then does.
|
|
998
589
|
*/
|
|
999
590
|
function normalizeRule(raw) {
|
|
1000
591
|
const r = raw ?? {};
|
|
@@ -1011,7 +602,7 @@ window.__ModuleLoader__.load({
|
|
|
1011
602
|
seen.add(image.slot);
|
|
1012
603
|
images.push(image);
|
|
1013
604
|
}
|
|
1014
|
-
const legacySlot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
|
|
605
|
+
const legacySlot = typeof r.slot === "string" && SLOT_RE$1.test(r.slot) ? r.slot : null;
|
|
1015
606
|
if (images.length === 0 && legacySlot !== null) images.push({
|
|
1016
607
|
slot: legacySlot,
|
|
1017
608
|
bgMode: mode,
|
|
@@ -1038,17 +629,11 @@ window.__ModuleLoader__.load({
|
|
|
1038
629
|
/**
|
|
1039
630
|
* Coerce one persisted holiday entry.
|
|
1040
631
|
*
|
|
1041
|
-
* `id`,
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
* Everything else is sanitized exactly like a rule's field — except three things
|
|
1047
|
-
* that stay the definition's own: the image list and the rotation (so a festival
|
|
1048
|
-
* can never be swapped or cycled), and the layout mode, which is the definition's
|
|
1049
|
-
* `fill` for the same reason the color is its own — full-bleed art letterboxed by
|
|
1050
|
-
* a stale `fit` is the one thing this feature must not do, and the mode is not
|
|
1051
|
-
* editable for a holiday anywhere in the panel.
|
|
632
|
+
* `id`, the image list and every theme colour come from the DEFINITION, never from
|
|
633
|
+
* disk: a stale or hand-edited value would orphan the wallpaper, point the holiday
|
|
634
|
+
* at a rule's image, or paint a festival in a colour it does not have. The layout
|
|
635
|
+
* mode and the rotation stay the definition's own for the same reason (a festival
|
|
636
|
+
* is never swapped or cycled). Everything else is sanitized like a rule's field.
|
|
1052
637
|
*/
|
|
1053
638
|
function normalizeHolidayRule(def, raw) {
|
|
1054
639
|
const r = raw ?? {};
|
|
@@ -1081,21 +666,12 @@ window.__ModuleLoader__.load({
|
|
|
1081
666
|
};
|
|
1082
667
|
}
|
|
1083
668
|
/**
|
|
1084
|
-
* The file-preview panel's own opacity
|
|
1085
|
-
*
|
|
1086
|
-
* Three states, and the middle one is why this is a function rather than the
|
|
1087
|
-
* one-line fallback every other field gets: a real number is the card's own
|
|
1088
|
-
* value, `null` is "never owned, so follow `opacities.bg`", and a KEY that is not
|
|
1089
|
-
* there at all — a hand-written config, or one written before the option existed
|
|
1090
|
-
* — is the shipped profile's 45% (see DEFAULT_RIGHTBAR_OPACITY).
|
|
669
|
+
* The file-preview panel's own opacity: a number is the card's own value, `null` is
|
|
670
|
+
* "never owned, follow `opacities.bg`", and a MISSING key is the shipped 45%.
|
|
1091
671
|
*
|
|
1092
|
-
* `null` has to survive as
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
* shipped 45% to every configuration whose preview panel was following the main
|
|
1096
|
-
* background, a silent change to a setup nobody asked to change. It is also
|
|
1097
|
-
* unreachable from the UI (no control returns to it); it exists for configs that
|
|
1098
|
-
* were written while the shipped default was `null`.
|
|
672
|
+
* `null` has to survive as itself: every release so far persisted it for a panel the
|
|
673
|
+
* user never dragged, so treating it as missing would silently hand the shipped 45%
|
|
674
|
+
* to every configuration that was following the main background.
|
|
1099
675
|
*/
|
|
1100
676
|
function normalizeRightbarOpacity(v) {
|
|
1101
677
|
if (v === null) return null;
|
|
@@ -1179,7 +755,6 @@ window.__ModuleLoader__.load({
|
|
|
1179
755
|
}
|
|
1180
756
|
let activeRuleId = null;
|
|
1181
757
|
let activeMatched = false;
|
|
1182
|
-
let modelLabel = "";
|
|
1183
758
|
/**
|
|
1184
759
|
* Which image of the ACTIVE rule is painted right now.
|
|
1185
760
|
*
|
|
@@ -1194,9 +769,6 @@ window.__ModuleLoader__.load({
|
|
|
1194
769
|
activeRuleId = ruleId;
|
|
1195
770
|
activeMatched = matched;
|
|
1196
771
|
}
|
|
1197
|
-
function setModelLabel(label) {
|
|
1198
|
-
modelLabel = label;
|
|
1199
|
-
}
|
|
1200
772
|
/** The image index the render layer is on, clamped into `rule`'s own range. */
|
|
1201
773
|
function imageIndexOf(rule) {
|
|
1202
774
|
if (rule.images.length === 0) return 0;
|
|
@@ -1218,23 +790,16 @@ window.__ModuleLoader__.load({
|
|
|
1218
790
|
return activeImage()?.slot ?? "";
|
|
1219
791
|
}
|
|
1220
792
|
/**
|
|
1221
|
-
* The theme
|
|
1222
|
-
*
|
|
1223
|
-
* Two rungs, in this order and no others:
|
|
1224
|
-
* 1. the color of the IMAGE that is on screen, when the rule has one;
|
|
1225
|
-
* 2. the rule's own color, which is all an image-less rule has to paint with.
|
|
793
|
+
* The theme colour in force right now, or null for "follow the system theme".
|
|
1226
794
|
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
* back to the rule's
|
|
1232
|
-
* per-image colors get the rule's color LIFTED onto them at read time instead
|
|
1233
|
-
* (`normalizeImage`), so nothing loses its theme on upgrade.
|
|
795
|
+
* Two rungs and no others: the colour of the IMAGE being painted, else the rule's
|
|
796
|
+
* own colour (all an image-less rule has). There is deliberately no third rung
|
|
797
|
+
* where an image inherits its rule's colour — that would make a rotation through a
|
|
798
|
+
* green, a red and a system-themed picture inexpressible, and clearing a picture's
|
|
799
|
+
* colour would only hand it back to the rule's.
|
|
1234
800
|
*
|
|
1235
|
-
* The image is the one being PAINTED
|
|
1236
|
-
*
|
|
1237
|
-
* interface while the user is fixing another picture's framing.
|
|
801
|
+
* The image is the one being PAINTED, not the one the panel has selected: the
|
|
802
|
+
* selection is an editing cursor and must not repaint the interface.
|
|
1238
803
|
*/
|
|
1239
804
|
function activeColor() {
|
|
1240
805
|
const rule = activeRule();
|
|
@@ -1414,187 +979,31 @@ window.__ModuleLoader__.load({
|
|
|
1414
979
|
clearImages();
|
|
1415
980
|
activeRuleId = null;
|
|
1416
981
|
activeMatched = false;
|
|
1417
|
-
modelLabel = "";
|
|
1418
982
|
rotIndex = 0;
|
|
1419
983
|
}
|
|
1420
984
|
//#endregion
|
|
1421
|
-
//#region src/
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
* the unsafe one — so this is deliberately total as well: anything JSON cannot
|
|
1431
|
-
* carry (a function, a `Symbol`, `undefined`) serializes as `null` and an
|
|
1432
|
-
* `undefined` property is skipped, exactly as `JSON.stringify` treats it.
|
|
1433
|
-
*/
|
|
1434
|
-
function canonical(value) {
|
|
1435
|
-
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
1436
|
-
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
1437
|
-
return JSON.stringify(value) ?? "null";
|
|
985
|
+
//#region src/client/rpc.ts
|
|
986
|
+
const RPC_CHANNEL = "/dsh-background-by-model";
|
|
987
|
+
const RPC_NS = "dshBackgroundByModel";
|
|
988
|
+
const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
|
|
989
|
+
let hostShape = "unknown";
|
|
990
|
+
let warnedLegacy = false;
|
|
991
|
+
/** Whether this bundle may write its config to the host it is talking to. */
|
|
992
|
+
function canWriteConfig() {
|
|
993
|
+
return hostShape !== "legacy";
|
|
1438
994
|
}
|
|
1439
|
-
/**
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
*
|
|
1443
|
-
* The profile is served at ONE version of the assets repository, but a version
|
|
1444
|
-
* reference is only as fixed as the tag behind it: re-point the tag and every
|
|
1445
|
-
* edge that has not been purged keeps answering with the previous bytes of the
|
|
1446
|
-
* same URL. So "the profile" is still not guaranteed to be one fixed thing, and
|
|
1447
|
-
* the bytes a previous attempt is holding are only known to belong to what is
|
|
1448
|
-
* being installed when that config comes back the same. Order-insensitivity is
|
|
1449
|
-
* what lets this be a comparison of VALUES rather than of spellings —
|
|
1450
|
-
* `normalizeConfig` (./schema) rebuilds the config it reads, so pinning the key
|
|
1451
|
-
* to the exact key order the wire happened to use would make the hold useless
|
|
1452
|
-
* the moment anything upstream normalized differently.
|
|
1453
|
-
*/
|
|
1454
|
-
function presetKey(version, config) {
|
|
1455
|
-
return `${version}\n${canonical(config)}`;
|
|
995
|
+
/** Whether the running host process cannot read this bundle's config shape. */
|
|
996
|
+
function hostIsLegacy() {
|
|
997
|
+
return hostShape === "legacy";
|
|
1456
998
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
* as the rest of the profile arrives, and handing out the same mutable record
|
|
1462
|
-
* would let a re-fetch of one file edit the state the NEXT attempt resumes from.
|
|
1463
|
-
* An empty answer — nothing held, or held for a different revision — is the honest
|
|
1464
|
-
* one either way: the pool then asks for every slot the config names.
|
|
1465
|
-
*/
|
|
1466
|
-
function presetResume(held, key) {
|
|
1467
|
-
if (held === null || held.key !== key) return {};
|
|
1468
|
-
return { ...held.images };
|
|
999
|
+
function holdWrite() {
|
|
1000
|
+
if (warnedLegacy) return;
|
|
1001
|
+
warnedLegacy = true;
|
|
1002
|
+
console.warn("dsh-background-by-model: the running host is an older build that cannot read this bundle's config shape — holding config writes so its sanitizer cannot drop this profile's rules. Restart DSH to load the new node half.");
|
|
1469
1003
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
* Order matters in the only way it can here: it decides which file a worker takes
|
|
1474
|
-
* first, and the files are not the same size. Only names the CONFIG asked for are
|
|
1475
|
-
* ever returned, so a held picture nothing points at — a slot the author removed
|
|
1476
|
-
* from the profile — can never be fetched again or shipped.
|
|
1477
|
-
*/
|
|
1478
|
-
function presetMissing(slots, images) {
|
|
1479
|
-
return slots.filter((slot) => images[slot] === void 0);
|
|
1480
|
-
}
|
|
1481
|
-
//#endregion
|
|
1482
|
-
//#region src/client/rotation.ts
|
|
1483
|
-
/** Dwell-time presets the settings UI offers, in ms. */
|
|
1484
|
-
const ROTATE_PRESETS = [
|
|
1485
|
-
{
|
|
1486
|
-
ms: 1e4,
|
|
1487
|
-
key: "rotEvery10s"
|
|
1488
|
-
},
|
|
1489
|
-
{
|
|
1490
|
-
ms: 3e4,
|
|
1491
|
-
key: "rotEvery30s"
|
|
1492
|
-
},
|
|
1493
|
-
{
|
|
1494
|
-
ms: 6e4,
|
|
1495
|
-
key: "rotEvery1m"
|
|
1496
|
-
},
|
|
1497
|
-
{
|
|
1498
|
-
ms: 3e5,
|
|
1499
|
-
key: "rotEvery5m"
|
|
1500
|
-
},
|
|
1501
|
-
{
|
|
1502
|
-
ms: 18e5,
|
|
1503
|
-
key: "rotEvery30m"
|
|
1504
|
-
}
|
|
1505
|
-
];
|
|
1506
|
-
/**
|
|
1507
|
-
* The index to paint after the current one, or null when there is nowhere to go
|
|
1508
|
-
* (fewer than two images — the caller must not start a timer at all then).
|
|
1509
|
-
*
|
|
1510
|
-
* `order` walks the list and wraps; `shuffle` picks uniformly among every index
|
|
1511
|
-
* EXCEPT the current one, because landing on the same image reads as a missed
|
|
1512
|
-
* tick. `current` outside the list is treated as 0 rather than trusted: it comes
|
|
1513
|
-
* from the UI as often as from the timer, and a stale index must not be able to
|
|
1514
|
-
* index past the end.
|
|
1515
|
-
*
|
|
1516
|
-
* `rand` is injectable so the shuffle branch is testable without stubbing a
|
|
1517
|
-
* global.
|
|
1518
|
-
*/
|
|
1519
|
-
function nextIndex(len, order, current, rand = Math.random) {
|
|
1520
|
-
if (!Number.isFinite(len) || len < 2) return null;
|
|
1521
|
-
const count = Math.floor(len);
|
|
1522
|
-
const cur = Number.isFinite(current) && current >= 0 && current < count ? Math.floor(current) : 0;
|
|
1523
|
-
if (order === "shuffle") {
|
|
1524
|
-
const roll = Math.min(Math.floor(Math.max(rand(), 0) * (count - 1)), count - 2);
|
|
1525
|
-
return roll >= cur ? roll + 1 : roll;
|
|
1526
|
-
}
|
|
1527
|
-
return (cur + 1) % count;
|
|
1528
|
-
}
|
|
1529
|
-
/**
|
|
1530
|
-
* Whether a rotation is worth running: two images, the switch on, and a positive
|
|
1531
|
-
* dwell time. Kept here (rather than at the call site) so the UI's "rotating"
|
|
1532
|
-
* badge and the timer agree by construction — a badge that says "rotating" while
|
|
1533
|
-
* no timer exists is the one failure this feature can produce silently.
|
|
1534
|
-
*/
|
|
1535
|
-
function isRotating(len, enabled, intervalMs) {
|
|
1536
|
-
return enabled && len >= 2 && Number.isFinite(intervalMs) && intervalMs > 0;
|
|
1537
|
-
}
|
|
1538
|
-
//#endregion
|
|
1539
|
-
//#region src/client/repaint.ts
|
|
1540
|
-
/**
|
|
1541
|
-
* The repaint decision, as a pure function.
|
|
1542
|
-
*
|
|
1543
|
-
* Every edit to a rule ends with the same question: does the interface have to be
|
|
1544
|
-
* repainted, or is refreshing the panel's readout enough? Repainting is the
|
|
1545
|
-
* expensive half (the theme skin is re-registered, the wallpaper layer is
|
|
1546
|
-
* rewritten, the host's theme is switched), so it must not be done for a rule
|
|
1547
|
-
* that is not the one on screen — but it MUST be done the moment the edit
|
|
1548
|
-
* changes what should be on screen.
|
|
1549
|
-
*
|
|
1550
|
-
* The naive test — "is the edited rule the active one?" — is wrong in exactly the
|
|
1551
|
-
* case that kept being reported as "更新的不太及时,只有我动调色盘或者切模型的时候
|
|
1552
|
-
* 才出现": an edit that CREATES the winner. Uploading a rule's first picture, or
|
|
1553
|
-
* an auto-extracted color landing on a rule that had none, makes that rule
|
|
1554
|
-
* paintable *through that very edit* (see `ruleCanPaint`), so the rule is not the
|
|
1555
|
-
* active one at the moment of the check; the wallpaper and the skin then only
|
|
1556
|
-
* showed up later, when an unrelated drag or model switch happened to re-run the
|
|
1557
|
-
* resolution.
|
|
1558
|
-
*
|
|
1559
|
-
* Sampling the winner on BOTH sides of the edit is what makes such a change land
|
|
1560
|
-
* at once, and `editedId === winnerAfter` additionally covers the edit that does
|
|
1561
|
-
* not move the resolution but changes the look of the rule that IS painting (a
|
|
1562
|
-
* color, a slider, a framing).
|
|
1563
|
-
*
|
|
1564
|
-
* `null` is "nothing of ours is painting" (the interface falls back to the host's
|
|
1565
|
-
* own look), which is a winner like any other id.
|
|
1566
|
-
*
|
|
1567
|
-
* Kept free of imports on purpose: `scripts/repaint-check.ts` exercises it on
|
|
1568
|
-
* plain node, the same way `rotation.ts` is exercised. A rule's `color`/`match`
|
|
1569
|
-
* are the inputs that make the resolution move, which is why they are the fields
|
|
1570
|
-
* whose write paths all end here.
|
|
1571
|
-
*/
|
|
1572
|
-
function shouldRepaint(editedId, winnerBefore, winnerAfter) {
|
|
1573
|
-
return winnerAfter === editedId || winnerAfter !== winnerBefore;
|
|
1574
|
-
}
|
|
1575
|
-
//#endregion
|
|
1576
|
-
//#region src/client/rpc.ts
|
|
1577
|
-
const RPC_CHANNEL = "/dsh-background-by-model";
|
|
1578
|
-
const RPC_NS = "dshBackgroundByModel";
|
|
1579
|
-
const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
|
|
1580
|
-
let hostShape = "unknown";
|
|
1581
|
-
let warnedLegacy = false;
|
|
1582
|
-
/** Whether this bundle may write its config to the host it is talking to. */
|
|
1583
|
-
function canWriteConfig() {
|
|
1584
|
-
return hostShape !== "legacy";
|
|
1585
|
-
}
|
|
1586
|
-
/** Whether the running host process is older than this client bundle. */
|
|
1587
|
-
function hostIsLegacy() {
|
|
1588
|
-
return hostShape === "legacy";
|
|
1589
|
-
}
|
|
1590
|
-
function holdWrite() {
|
|
1591
|
-
if (warnedLegacy) return;
|
|
1592
|
-
warnedLegacy = true;
|
|
1593
|
-
console.warn("dsh-background-by-model: the running host is an older build that cannot read the multi-image config shape — holding config writes so its sanitizer cannot drop this profile's rules. Restart DSH to load the new node half.");
|
|
1594
|
-
}
|
|
1595
|
-
let rpcCallFn = null;
|
|
1596
|
-
function initRpc(call) {
|
|
1597
|
-
rpcCallFn = call;
|
|
1004
|
+
let rpcCallFn = null;
|
|
1005
|
+
function initRpc(call) {
|
|
1006
|
+
rpcCallFn = call;
|
|
1598
1007
|
}
|
|
1599
1008
|
async function rpcCall(method, payload) {
|
|
1600
1009
|
if (!rpcCallFn) return void 0;
|
|
@@ -1655,7 +1064,7 @@ window.__ModuleLoader__.load({
|
|
|
1655
1064
|
const d = data;
|
|
1656
1065
|
const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
|
|
1657
1066
|
const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
|
|
1658
|
-
hostShape = schema ===
|
|
1067
|
+
hostShape = typeof schema === "number" && schema >= 9 ? "current" : "legacy";
|
|
1659
1068
|
if (hostShape === "legacy") holdWrite();
|
|
1660
1069
|
return {
|
|
1661
1070
|
config: d.config,
|
|
@@ -1759,61 +1168,6 @@ window.__ModuleLoader__.load({
|
|
|
1759
1168
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
1760
1169
|
}
|
|
1761
1170
|
//#endregion
|
|
1762
|
-
//#region src/client/palette-fade.ts
|
|
1763
|
-
/**
|
|
1764
|
-
* Whether a palette change may interpolate at all.
|
|
1765
|
-
*
|
|
1766
|
-
* Only while the SCHEME stays put. A light↔dark flip is not a tint of the same
|
|
1767
|
-
* palette, it is a different palette: interpolating between them walks the whole
|
|
1768
|
-
* interface through mid-tones whose ink is wrong in both directions (dark text
|
|
1769
|
-
* half-way into a dark palette), which looks worse than the switch it replaces.
|
|
1770
|
-
* The wallpaper has no such problem — one image fades into another either way —
|
|
1771
|
-
* so its own animation is deliberately untouched by this.
|
|
1772
|
-
*
|
|
1773
|
-
* `previous === null` means nothing has been painted yet (the first apply after a
|
|
1774
|
-
* load): there is no colour to come from, so there is nothing to fade.
|
|
1775
|
-
*/
|
|
1776
|
-
function paletteFadeAllowed(previous, next) {
|
|
1777
|
-
return previous === next;
|
|
1778
|
-
}
|
|
1779
|
-
/** One `@property` block per registered token. */
|
|
1780
|
-
function registrations(names, initial) {
|
|
1781
|
-
return names.map((n) => `@property ${n}{syntax:'<color>';inherits:true;initial-value:${initial[n]}}`).join("");
|
|
1782
|
-
}
|
|
1783
|
-
/**
|
|
1784
|
-
* The stylesheet for one state: the registrations always, the transition only
|
|
1785
|
-
* while a palette change is actually being painted.
|
|
1786
|
-
*
|
|
1787
|
-
* `durationMs = 0` deliberately emits NO transition rule. That is not only
|
|
1788
|
-
* "instant": it is what keeps a slider drag instant, because the same call sites
|
|
1789
|
-
* rewrite the `body{…}` rule on every frame of a drag and a live transition on
|
|
1790
|
-
* those tokens would make the alphas lag behind the pointer.
|
|
1791
|
-
*/
|
|
1792
|
-
function paletteFadeCss(input) {
|
|
1793
|
-
const names = Object.keys(input.initial).filter((n) => n.startsWith("--"));
|
|
1794
|
-
if (names.length === 0) return "";
|
|
1795
|
-
const css = registrations(names, input.initial);
|
|
1796
|
-
if (!(input.durationMs > 0)) return css;
|
|
1797
|
-
const written = new Set(input.written);
|
|
1798
|
-
const timing = (n) => `${n} ${input.durationMs}ms ${input.easing}`;
|
|
1799
|
-
return `${css}${input.scopes.map((s) => ({
|
|
1800
|
-
selector: s.selector,
|
|
1801
|
-
transitions: [...s.tokens.filter((n) => names.includes(n) && written.has(n)), ...s.properties ?? []]
|
|
1802
|
-
})).filter((s) => s.transitions.length > 0).map((s) => `${s.selector}{transition:${s.transitions.map(timing).join(",")}}`).join("")}`;
|
|
1803
|
-
}
|
|
1804
|
-
/**
|
|
1805
|
-
* The same fade for an element the plugin paints INLINE (its columns and cards).
|
|
1806
|
-
*
|
|
1807
|
-
* No custom property is involved there — the element's own `background-color` is
|
|
1808
|
-
* what changes — so the standard property is what has to be told. `0` means
|
|
1809
|
-
* `none` rather than an empty value: an empty value would leave whatever the host
|
|
1810
|
-
* had declared on that element, and the point of the 0 case is that a drag must
|
|
1811
|
-
* not fade at all.
|
|
1812
|
-
*/
|
|
1813
|
-
function paletteFadeInline(durationMs, easing) {
|
|
1814
|
-
return durationMs > 0 ? `background-color ${durationMs}ms ${easing}` : "none";
|
|
1815
|
-
}
|
|
1816
|
-
//#endregion
|
|
1817
1171
|
//#region src/client/utils/color.ts
|
|
1818
1172
|
function hsvToHsl(h, s, v) {
|
|
1819
1173
|
const l = v * (1 - s / 2);
|
|
@@ -2183,106 +1537,277 @@ window.__ModuleLoader__.load({
|
|
|
2183
1537
|
Math.round((b + m) * 255)
|
|
2184
1538
|
];
|
|
2185
1539
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
.
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
.
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
.
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
.
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
.
|
|
2237
|
-
.
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
.
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
.
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
.
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
.
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
.
|
|
1540
|
+
//#endregion
|
|
1541
|
+
//#region src/client/auto-extract.ts
|
|
1542
|
+
function createAutoExtract(host) {
|
|
1543
|
+
/** The entry this extraction is about, re-resolved on every read. */
|
|
1544
|
+
const target = (ruleId, slot) => {
|
|
1545
|
+
const rule = ruleById(ruleId);
|
|
1546
|
+
if (rule === null) return void 0;
|
|
1547
|
+
return (slot === void 0 ? void 0 : rule.images.find((i) => i.slot === slot)) ?? rule.images[0];
|
|
1548
|
+
};
|
|
1549
|
+
const one = async (ruleId, slot) => {
|
|
1550
|
+
if (!cfg.autoExtract) return;
|
|
1551
|
+
const image = target(ruleId, slot);
|
|
1552
|
+
if (image === void 0 || image.color !== null) return;
|
|
1553
|
+
const url = host.paintable(image.slot);
|
|
1554
|
+
if (url === null) return;
|
|
1555
|
+
let hsl = null;
|
|
1556
|
+
try {
|
|
1557
|
+
hsl = await extractWallpaperColor(url, image.bgState);
|
|
1558
|
+
} catch {
|
|
1559
|
+
hsl = null;
|
|
1560
|
+
}
|
|
1561
|
+
if (hsl === null) return;
|
|
1562
|
+
const current = target(ruleId, slot);
|
|
1563
|
+
if (current === void 0 || current.color !== null) return;
|
|
1564
|
+
const winner = host.winnerId();
|
|
1565
|
+
current.color = hsl;
|
|
1566
|
+
host.bumpRulesRev();
|
|
1567
|
+
saveConfig();
|
|
1568
|
+
host.repaint(ruleId, winner);
|
|
1569
|
+
};
|
|
1570
|
+
return {
|
|
1571
|
+
one,
|
|
1572
|
+
async each(ruleId, slots) {
|
|
1573
|
+
for (const slot of slots) await one(ruleId, slot);
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
//#endregion
|
|
1578
|
+
//#region src/client/theme-skin.ts
|
|
1579
|
+
/** Id of the plugin's own registered theme. */
|
|
1580
|
+
const CUSTOM_ID = "custom-color";
|
|
1581
|
+
/** How long a colour drag waits before the (host-visible) skin switch is made. */
|
|
1582
|
+
const SKIN_DEBOUNCE_MS = 60;
|
|
1583
|
+
function createSkin(ctx) {
|
|
1584
|
+
let customDispose = null;
|
|
1585
|
+
let skinTimer = null;
|
|
1586
|
+
/** Colour scheme of the skin this plugin registered, or null when none is. */
|
|
1587
|
+
let skinScheme = null;
|
|
1588
|
+
/** Scheme the host currently holds for our theme id; null when it is not there. */
|
|
1589
|
+
const registeredScheme = () => {
|
|
1590
|
+
const theme = ctx.theme.getTheme().themes.find((t) => t.id === CUSTOM_ID);
|
|
1591
|
+
return theme === void 0 ? null : theme.colorScheme;
|
|
1592
|
+
};
|
|
1593
|
+
const hasSkin = () => ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
|
|
1594
|
+
const register = (h, s, l) => {
|
|
1595
|
+
const { colorScheme, tokens } = genTokens(h, s, l);
|
|
1596
|
+
if (skinScheme === colorScheme && registeredScheme() !== null) {
|
|
1597
|
+
if (ctx.theme.getTheme().preference !== "custom-color") ctx.theme.setTheme(CUSTOM_ID);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
customDispose?.();
|
|
1601
|
+
try {
|
|
1602
|
+
customDispose = ctx.theme.register({
|
|
1603
|
+
id: CUSTOM_ID,
|
|
1604
|
+
colorScheme,
|
|
1605
|
+
tokens
|
|
1606
|
+
});
|
|
1607
|
+
} catch {
|
|
1608
|
+
customDispose = null;
|
|
1609
|
+
}
|
|
1610
|
+
skinScheme = registeredScheme() ?? colorScheme;
|
|
1611
|
+
if (hasSkin()) ctx.theme.setTheme(CUSTOM_ID);
|
|
1612
|
+
};
|
|
1613
|
+
const drop = () => {
|
|
1614
|
+
customDispose?.();
|
|
1615
|
+
customDispose = null;
|
|
1616
|
+
skinScheme = null;
|
|
1617
|
+
try {
|
|
1618
|
+
if (ctx.theme.getTheme().preference === "custom-color") ctx.theme.setTheme("system");
|
|
1619
|
+
} catch {}
|
|
1620
|
+
};
|
|
1621
|
+
return {
|
|
1622
|
+
commit(color) {
|
|
1623
|
+
if (skinTimer !== null) window.clearTimeout(skinTimer);
|
|
1624
|
+
if (color === null) {
|
|
1625
|
+
skinTimer = null;
|
|
1626
|
+
drop();
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
skinTimer = window.setTimeout(() => {
|
|
1630
|
+
skinTimer = null;
|
|
1631
|
+
register(color[0], color[1], color[2]);
|
|
1632
|
+
}, SKIN_DEBOUNCE_MS);
|
|
1633
|
+
},
|
|
1634
|
+
reassert(color) {
|
|
1635
|
+
if (!hasSkin()) {
|
|
1636
|
+
register(color[0], color[1], color[2]);
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
if (ctx.theme.getTheme().preference !== "custom-color") {
|
|
1640
|
+
ctx.theme.setTheme(CUSTOM_ID);
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
return false;
|
|
1644
|
+
},
|
|
1645
|
+
drop,
|
|
1646
|
+
dispose() {
|
|
1647
|
+
if (skinTimer !== null) {
|
|
1648
|
+
window.clearTimeout(skinTimer);
|
|
1649
|
+
skinTimer = null;
|
|
1650
|
+
}
|
|
1651
|
+
customDispose?.();
|
|
1652
|
+
customDispose = null;
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
//#endregion
|
|
1657
|
+
//#region src/client/palette-fade.ts
|
|
1658
|
+
/**
|
|
1659
|
+
* Whether a palette change may interpolate at all.
|
|
1660
|
+
*
|
|
1661
|
+
* Only while the SCHEME stays put. A light↔dark flip is not a tint of the same
|
|
1662
|
+
* palette, it is a different palette: interpolating between them walks the whole
|
|
1663
|
+
* interface through mid-tones whose ink is wrong in both directions (dark text
|
|
1664
|
+
* half-way into a dark palette), which looks worse than the switch it replaces.
|
|
1665
|
+
* The wallpaper has no such problem — one image fades into another either way —
|
|
1666
|
+
* so its own animation is deliberately untouched by this.
|
|
1667
|
+
*
|
|
1668
|
+
* `previous === null` means nothing has been painted yet (the first apply after a
|
|
1669
|
+
* load): there is no colour to come from, so there is nothing to fade.
|
|
1670
|
+
*/
|
|
1671
|
+
function paletteFadeAllowed(previous, next) {
|
|
1672
|
+
return previous === next;
|
|
1673
|
+
}
|
|
1674
|
+
/** One `@property` block per registered token. */
|
|
1675
|
+
function registrations(names, initial) {
|
|
1676
|
+
return names.map((n) => `@property ${n}{syntax:'<color>';inherits:true;initial-value:${initial[n]}}`).join("");
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* The stylesheet for one state: the registrations always, the transition only
|
|
1680
|
+
* while a palette change is actually being painted.
|
|
1681
|
+
*
|
|
1682
|
+
* `durationMs = 0` deliberately emits NO transition rule. That is not only
|
|
1683
|
+
* "instant": it is what keeps a slider drag instant, because the same call sites
|
|
1684
|
+
* rewrite the `body{…}` rule on every frame of a drag and a live transition on
|
|
1685
|
+
* those tokens would make the alphas lag behind the pointer.
|
|
1686
|
+
*/
|
|
1687
|
+
function paletteFadeCss(input) {
|
|
1688
|
+
const names = Object.keys(input.initial).filter((n) => n.startsWith("--"));
|
|
1689
|
+
if (names.length === 0) return "";
|
|
1690
|
+
const css = registrations(names, input.initial);
|
|
1691
|
+
if (!(input.durationMs > 0)) return css;
|
|
1692
|
+
const written = new Set(input.written);
|
|
1693
|
+
const timing = (n) => `${n} ${input.durationMs}ms ${input.easing}`;
|
|
1694
|
+
return `${css}${input.scopes.map((s) => ({
|
|
1695
|
+
selector: s.selector,
|
|
1696
|
+
transitions: [...s.tokens.filter((n) => names.includes(n) && written.has(n)), ...s.properties ?? []]
|
|
1697
|
+
})).filter((s) => s.transitions.length > 0).map((s) => `${s.selector}{transition:${s.transitions.map(timing).join(",")}}`).join("")}`;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* The same fade for an element the plugin paints INLINE (its columns and cards).
|
|
1701
|
+
*
|
|
1702
|
+
* No custom property is involved there — the element's own `background-color` is
|
|
1703
|
+
* what changes — so the standard property is what has to be told. `0` means
|
|
1704
|
+
* `none` rather than an empty value: an empty value would leave whatever the host
|
|
1705
|
+
* had declared on that element, and the point of the 0 case is that a drag must
|
|
1706
|
+
* not fade at all.
|
|
1707
|
+
*/
|
|
1708
|
+
function paletteFadeInline(durationMs, easing) {
|
|
1709
|
+
return durationMs > 0 ? `background-color ${durationMs}ms ${easing}` : "none";
|
|
1710
|
+
}
|
|
1711
|
+
const UI_CSS = `
|
|
1712
|
+
/* The section is rendered INLINE inside the host settings dialog's content
|
|
1713
|
+
* column: the host provides the modal chrome (backdrop, centering, closing).
|
|
1714
|
+
* These classes style only the embedded shell; transient fixed layers (toast,
|
|
1715
|
+
* color picker, background editor) escape through Portals on <html>. */
|
|
1716
|
+
.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}
|
|
1717
|
+
.dab-root *,.dab-root *::before,.dab-root *::after{box-sizing:border-box}
|
|
1718
|
+
.dab-root button{font-family:inherit}
|
|
1719
|
+
/* ── scrollbars: the same bar as the settings panel we sit inside ────────────
|
|
1720
|
+
* The host styles scrollbars globally off its --dsh-scrollbar-* tokens, but those
|
|
1721
|
+
* declarations describe the app shell and its own scroll containers; a strip
|
|
1722
|
+
* nested this deep got the platform default instead — a thick light bar with
|
|
1723
|
+
* arrows, which is what the tab strip used to show beside the panel's own slim
|
|
1724
|
+
* vertical one.
|
|
1725
|
+
*
|
|
1726
|
+
* So the plugin declares the block for its OWN scroll containers. The values are
|
|
1727
|
+
* the host theme package's, for the layer-2 surface this panel is painted in:
|
|
1728
|
+
* 5px, rounded, layer-2 tint, transparent track. Two notes for whoever edits it:
|
|
1729
|
+
*
|
|
1730
|
+
* - the tokens go ON THE SCROLLER (the selector that carries the overflow), not
|
|
1731
|
+
* on .dab-root. A container that declares scrollbar-width:thin (as the nav
|
|
1732
|
+
* rail does on narrow panels) paints its own standard bar regardless of the
|
|
1733
|
+
* webkit block, and the host's standard-property fallback reads the thumb
|
|
1734
|
+
* token from that same element.
|
|
1735
|
+
* - every selector here is scoped to a dab- class: the stylesheet is injected
|
|
1736
|
+
* into the host page once, so a bare webkit-scrollbar selector would restyle
|
|
1737
|
+
* the whole harness. */
|
|
1738
|
+
.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}
|
|
1739
|
+
.dab-tabs::-webkit-scrollbar,.dab-pick-menu::-webkit-scrollbar{width:var(--dsh-scrollbar-width,5px);height:var(--dsh-scrollbar-width,5px)}
|
|
1740
|
+
.dab-tabs::-webkit-scrollbar-track,.dab-pick-menu::-webkit-scrollbar-track{background:transparent}
|
|
1741
|
+
.dab-tabs::-webkit-scrollbar-thumb,.dab-pick-menu::-webkit-scrollbar-thumb{background:var(--dsh-scrollbar-thumb);background-clip:content-box;border-radius:999px}
|
|
1742
|
+
.dab-tabs::-webkit-scrollbar-thumb:hover,.dab-pick-menu::-webkit-scrollbar-thumb:hover{background:var(--dsh-scrollbar-thumb-hover)}
|
|
1743
|
+
.dab-tabs::-webkit-scrollbar-corner,.dab-pick-menu::-webkit-scrollbar-corner{background:transparent}
|
|
1744
|
+
/* The host hides the platform's stepper arrows; without this the strip keeps the
|
|
1745
|
+
little os arrows at both ends of the track while the panel's bar has none. */
|
|
1746
|
+
.dab-tabs::-webkit-scrollbar-button,.dab-pick-menu::-webkit-scrollbar-button{display:none}
|
|
1747
|
+
|
|
1748
|
+
/* ── shell: nav rail + page body ─────────────────────────────────────────── */
|
|
1749
|
+
.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}
|
|
1750
|
+
.dab-nav{position:sticky;top:0;display:flex;flex-direction:column;gap:18px}
|
|
1751
|
+
/* The brand is an <a> (it opens the repository): an anchor brings its own colour
|
|
1752
|
+
and underline, so both are reset here rather than left to the host's link
|
|
1753
|
+
styles, which would repaint the tile's label. */
|
|
1754
|
+
.dab-brand{display:flex;align-items:center;gap:10px;padding:2px 6px;color:inherit;text-decoration:none}
|
|
1755
|
+
.dab-brand:hover .dab-brand-name{color:var(--dsw-alias-brand-primary)}
|
|
1756
|
+
.dab-brand-tile{width:30px;height:30px;flex:none;border-radius:9px;display:grid;place-items:center;color:var(--dsw-alias-label-primary-inverted);background:var(--dsw-alias-brand-primary);box-shadow:0 4px 14px -4px var(--dsw-alias-brand-primary)}
|
|
1757
|
+
.dab-brand-name{font-size:13px;font-weight:650;letter-spacing:.01em;line-height:1.25}
|
|
1758
|
+
/* The weak token was written as quaternary-WITH-a-fallback, so this tag has
|
|
1759
|
+
always painted tertiary: the dead name is dropped rather than being kept as
|
|
1760
|
+
decoration nothing resolves to (check:ui rejects the name outright now). */
|
|
1761
|
+
.dab-brand-tag{font-size:9px;letter-spacing:.16em;font-weight:600;color:var(--dsw-alias-label-tertiary);text-transform:uppercase}
|
|
1762
|
+
.dab-nav-list{position:relative;display:flex;flex-direction:column;gap:4px}
|
|
1763
|
+
.dab-nav-ind{position:absolute;left:0;right:0;top:0;height:38px;border-radius:11px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,transparent);transition:transform .38s cubic-bezier(.22,1,.36,1)}
|
|
1764
|
+
.dab-nav-item{position:relative;z-index:1;display:flex;align-items:center;gap:10px;height:38px;padding:0 12px;border:0;background:none;border-radius:11px;color:var(--dsw-alias-label-tertiary);font-size:13px;cursor:pointer;text-align:left;transition:color .22s ease}
|
|
1765
|
+
.dab-nav-item:hover{color:var(--dsw-alias-label-primary)}
|
|
1766
|
+
.dab-nav-item.is-active{color:var(--dsw-alias-brand-primary);font-weight:600}
|
|
1767
|
+
.dab-nav-item svg{flex:none;transition:transform .3s cubic-bezier(.34,1.56,.64,1)}
|
|
1768
|
+
.dab-nav-item:hover svg{transform:scale(1.14) rotate(-5deg)}
|
|
1769
|
+
|
|
1770
|
+
/* ── page chrome ─────────────────────────────────────────────────────────── */
|
|
1771
|
+
.dab-page{animation:dab-page-in .4s cubic-bezier(.22,1,.36,1) both;min-width:0;display:flex;flex-direction:column;gap:13px}
|
|
1772
|
+
.dab-head{margin:2px 0 5px}
|
|
1773
|
+
.dab-overline{font-size:10.5px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--dsw-alias-brand-primary);opacity:.9}
|
|
1774
|
+
.dab-h1{margin:4px 0 0;font-size:21px;font-weight:700;letter-spacing:-.01em}
|
|
1775
|
+
/* No max-width on the page description: it is a single line of context under the
|
|
1776
|
+
title, and capping it at a prose measure left it wrapping early with dead space
|
|
1777
|
+
to its right while the cards below ran the full width of the panel. */
|
|
1778
|
+
.dab-desc{margin:6px 0 0;font-size:12.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary)}
|
|
1779
|
+
.dab-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;padding:18px}
|
|
1780
|
+
.dab-card-hover{transition:transform .28s ease,box-shadow .28s ease,border-color .28s ease}
|
|
1781
|
+
.dab-card-hover:hover{transform:translateY(-2px);box-shadow:0 10px 28px -12px rgba(0,0,0,.28)}
|
|
1782
|
+
.dab-rise{animation:dab-rise-in .55s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--d,0) * 62ms)}
|
|
1783
|
+
|
|
1784
|
+
/* ── accent hero (color orb) ─────────────────────────────────────────────── */
|
|
1785
|
+
.dab-hero-accent{display:flex;align-items:center;gap:24px;flex-wrap:wrap}
|
|
1786
|
+
.dab-orb-wrap{position:relative;width:118px;height:118px;flex:none}
|
|
1787
|
+
.dab-orb{position:absolute;inset:11px;border-radius:50%;background:radial-gradient(circle at 32% 28%,rgba(255,255,255,.5),rgba(255,255,255,0) 44%),var(--c,#888);box-shadow:0 16px 36px -10px var(--c-soft,transparent),inset 0 -10px 20px rgba(0,0,0,.16);animation:dab-orb-in .7s cubic-bezier(.22,1,.36,1) both}
|
|
1788
|
+
.dab-orb-ring{position:absolute;inset:0;border-radius:50%;background:conic-gradient(from 0deg,transparent 0 30%,var(--c,#888) 46%,transparent 62%,transparent 76%,var(--c,#888) 90%,transparent 100%);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 3.5px),#000 calc(100% - 2.5px));mask:radial-gradient(farthest-side,transparent calc(100% - 3.5px),#000 calc(100% - 2.5px));animation:dab-spin 7s linear infinite;opacity:.9}
|
|
1789
|
+
.dab-hex{font-family:var(--dab-mono);font-size:24px;font-weight:600;letter-spacing:.02em;line-height:1.2;margin-top:2px}
|
|
1790
|
+
.dab-hsl-row{display:flex;gap:16px;margin-top:7px;font-family:var(--dab-mono);font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
1791
|
+
.dab-hsl-row b{font-weight:600;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
1792
|
+
|
|
1793
|
+
/* ── swatches ────────────────────────────────────────────────────────────── */
|
|
1794
|
+
.dab-swatch-title{font-size:12px;font-weight:600;margin-bottom:10px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
1795
|
+
|
|
1796
|
+
/* ── wheel card ──────────────────────────────────────────────────────────── */
|
|
1797
|
+
.dab-wheel-card{position:relative;display:flex;align-items:center;justify-content:center;gap:28px;flex-wrap:wrap;padding:24px 18px}
|
|
1798
|
+
.dab-wheel-glow{position:absolute;width:230px;height:230px;border-radius:50%;filter:blur(48px);opacity:.2;background:var(--c,#888);pointer-events:none;transition:background .4s ease}
|
|
1799
|
+
.dab-wheel{position:relative;cursor:crosshair;border-radius:50%;box-shadow:0 12px 32px -14px rgba(0,0,0,.4)}
|
|
1800
|
+
.dab-hint{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);padding:0 4px}
|
|
1801
|
+
/* The "your edits are being held" notice: it reports a state the user has to act
|
|
1802
|
+
on, so it is tinted like a warning rather than left as grey hint text. */
|
|
1803
|
+
.dab-stale{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,transparent);border-radius:9px;padding:8px 10px}
|
|
1804
|
+
|
|
1805
|
+
/* ── precise color inputs ────────────────────────────────────────────────── */
|
|
1806
|
+
.dab-inputs{display:flex;flex-direction:column;gap:10px;min-width:172px}
|
|
1807
|
+
.dab-field{display:flex;align-items:center;gap:8px}
|
|
1808
|
+
.dab-field-label{width:14px;text-align:center;font-family:var(--dab-mono);font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary)}
|
|
1809
|
+
.dab-num{flex:1;min-width:0;height:30px;padding:0 10px;border-radius:9px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-family:var(--dab-mono);font-size:12px;outline:none;transition:border-color .2s,box-shadow .2s}
|
|
1810
|
+
.dab-num:focus{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
|
|
2286
1811
|
.dab-num::-webkit-outer-spin-button,.dab-num::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
|
2287
1812
|
.dab-num{-moz-appearance:textfield;appearance:textfield}
|
|
2288
1813
|
.dab-urlinput{flex:1;min-width:180px;height:34px;padding:0 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12.5px;outline:none;transition:border-color .2s}
|
|
@@ -2708,6 +2233,7 @@ window.__ModuleLoader__.load({
|
|
|
2708
2233
|
appliedTokenNames = [];
|
|
2709
2234
|
baseTokenKey = "";
|
|
2710
2235
|
lastBgKey = "";
|
|
2236
|
+
document.documentElement.removeAttribute(THEMED_ATTR);
|
|
2711
2237
|
}
|
|
2712
2238
|
/** Drop every surface the plugin owns: the variables the scoped
|
|
2713
2239
|
* dialog/trajectory rules read, and the inline backgrounds written onto the
|
|
@@ -2873,6 +2399,18 @@ window.__ModuleLoader__.load({
|
|
|
2873
2399
|
}
|
|
2874
2400
|
}
|
|
2875
2401
|
/**
|
|
2402
|
+
* A scheme verdict for the third caller: "whoever is in force". Reads the host
|
|
2403
|
+
* preference the browser half published, falling back to the host's own boot
|
|
2404
|
+
* flag and then the OS query — the same two rungs `resolveHostDark` uses.
|
|
2405
|
+
*/
|
|
2406
|
+
function hostSchemeIsDark() {
|
|
2407
|
+
return resolveHostDark();
|
|
2408
|
+
}
|
|
2409
|
+
/** Whether this plugin is the one holding the scheme flag right now. */
|
|
2410
|
+
function ownsSchemeFlag() {
|
|
2411
|
+
return document.body.getAttribute(DARK_ATTR) === OWN_SCHEME;
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2876
2414
|
* Keep `data-ds-dark-theme` in the HOST's own form (a boolean attribute) while
|
|
2877
2415
|
* the plugin does not own a palette.
|
|
2878
2416
|
*
|
|
@@ -2882,15 +2420,23 @@ window.__ModuleLoader__.load({
|
|
|
2882
2420
|
* its light palette on a dark system, the readback then hands those light
|
|
2883
2421
|
* surfaces back to the plugin, and the interface stays light until some
|
|
2884
2422
|
* unrelated event happens to re-run an apply. Restoring the flag from the
|
|
2885
|
-
* resolved scheme closes that hole
|
|
2886
|
-
*
|
|
2423
|
+
* resolved scheme closes that hole.
|
|
2424
|
+
*
|
|
2425
|
+
* It must not PUSH a host scheme either, though, and that is the other half of
|
|
2426
|
+
* the same hole: the verdict it works from is the plugin's own last reading,
|
|
2427
|
+
* which is already stale by the time the host has dropped the flag without
|
|
2428
|
+
* telling anyone. Writing the stale verdict turns a host that just went dark
|
|
2429
|
+
* back to light and sticks it there — the flag now "matches", so every later
|
|
2430
|
+
* pass skips it. So only the OWNER of the flag is corrected: the plugin's own
|
|
2431
|
+
* value is replaced, and a flag nobody has claimed is left to the host.
|
|
2432
|
+
*
|
|
2433
|
+
* The write is skipped when it already matches, so this never churns the theme
|
|
2434
|
+
* observer.
|
|
2887
2435
|
*/
|
|
2888
2436
|
function adoptHostScheme() {
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
if (current !== "") document.body.setAttribute(DARK_ATTR, "");
|
|
2893
|
-
} else if (current !== null) document.body.removeAttribute(DARK_ATTR);
|
|
2437
|
+
if (!ownsSchemeFlag()) return;
|
|
2438
|
+
if (resolveHostDark()) document.body.setAttribute(DARK_ATTR, "");
|
|
2439
|
+
else document.body.removeAttribute(DARK_ATTR);
|
|
2894
2440
|
}
|
|
2895
2441
|
/** Re-read the host palette and repaint from it when it moved. Called while a
|
|
2896
2442
|
* color-less rule is active (the host can re-project its palette at any time,
|
|
@@ -3235,11 +2781,10 @@ window.__ModuleLoader__.load({
|
|
|
3235
2781
|
/**
|
|
3236
2782
|
* Whether one of the element's class names ENDS with `suffix`.
|
|
3237
2783
|
*
|
|
3238
|
-
* A SUFFIX test,
|
|
3239
|
-
*
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3242
|
-
* to a weaker guess and a setting disappears without an error.
|
|
2784
|
+
* A SUFFIX test, not `classList.contains`: the host emits these classes from a CSS
|
|
2785
|
+
* module, so a column carries `pI_x6G_centerCol` and an exact match is false for
|
|
2786
|
+
* every column of every build — quietly, with the part falling through to a weaker
|
|
2787
|
+
* guess.
|
|
3243
2788
|
*/
|
|
3244
2789
|
function hasClassSuffix(el, suffix) {
|
|
3245
2790
|
for (const name of Array.from(el.classList)) if (name === suffix || name.endsWith(suffix)) return true;
|
|
@@ -3291,13 +2836,11 @@ window.__ModuleLoader__.load({
|
|
|
3291
2836
|
}
|
|
3292
2837
|
}
|
|
3293
2838
|
/**
|
|
3294
|
-
* Re-derive every plugin-owned interface surface that is not a palette token:
|
|
3295
|
-
*
|
|
2839
|
+
* Re-derive every plugin-owned interface surface that is not a palette token: the
|
|
2840
|
+
* conversation view card and the function pages in the main slot.
|
|
3296
2841
|
*
|
|
3297
|
-
* ONE pass, run by the full apply and
|
|
3298
|
-
* unmounts with a NAVIGATION,
|
|
3299
|
-
* on the same beat as everything else. `fade` carries the palette-moved case
|
|
3300
|
-
* through to the inline-painted view card (see `applyCustomTokens`).
|
|
2842
|
+
* ONE pass, run by the full apply and the frame observer — a page mounts and
|
|
2843
|
+
* unmounts with a NAVIGATION, so its discovery happens on the same beat.
|
|
3301
2844
|
*/
|
|
3302
2845
|
function applyInterfaceParts(fade = false) {
|
|
3303
2846
|
applyViewCards(fade);
|
|
@@ -3306,10 +2849,9 @@ window.__ModuleLoader__.load({
|
|
|
3306
2849
|
const PAGE_ANCHOR_SEL = "[data-slot=\"main\"]";
|
|
3307
2850
|
const PAGE_MARK = "data-dab-page";
|
|
3308
2851
|
/**
|
|
3309
|
-
* `!important`, because the page's own background is a class rule
|
|
3310
|
-
*
|
|
3311
|
-
*
|
|
3312
|
-
* paints what that page would have painted anyway.
|
|
2852
|
+
* `!important`, because the page's own background is a host class rule and cascade
|
|
2853
|
+
* order at equal specificity is not something to rely on. The fallback is the
|
|
2854
|
+
* host's own token, so a stale mark paints what the page would have painted.
|
|
3313
2855
|
*/
|
|
3314
2856
|
const PAGE_STYLE_RULE = `[${PAGE_MARK}]{background:var(--dsh-any-bg-page,var(--dsw-alias-bg-base))!important}`;
|
|
3315
2857
|
/** The elements currently marked — unmarked again at zero or on teardown. */
|
|
@@ -3346,14 +2888,7 @@ window.__ModuleLoader__.load({
|
|
|
3346
2888
|
function pageIsShowing() {
|
|
3347
2889
|
return discoverPageEls().length > 0;
|
|
3348
2890
|
}
|
|
3349
|
-
/**
|
|
3350
|
-
* Point the 插件页面 item at whatever the main slot is showing.
|
|
3351
|
-
*
|
|
3352
|
-
* Called from `applyInterfaceParts`, the one pass the full apply and the frame
|
|
3353
|
-
* observer both run, and from the item's own drag: a page mounts and unmounts
|
|
3354
|
-
* with a NAVIGATION, not with a slider, so the discovery has to happen on the
|
|
3355
|
-
* same beat as everything else.
|
|
3356
|
-
*/
|
|
2891
|
+
/** Point the 插件页面 item at whatever the main slot is showing. */
|
|
3357
2892
|
function applyPageSurfaces() {
|
|
3358
2893
|
discoverParts();
|
|
3359
2894
|
const base = palette()?.tokens["--dsw-alias-bg-base"];
|
|
@@ -3917,32 +3452,182 @@ window.__ModuleLoader__.load({
|
|
|
3917
3452
|
stopWatchingParts();
|
|
3918
3453
|
}
|
|
3919
3454
|
//#endregion
|
|
3920
|
-
//#region src/client/
|
|
3921
|
-
/** Rules that can actually paint: see `ruleCanPaint` (./schema). */
|
|
3922
|
-
function usable(rules) {
|
|
3923
|
-
return rules.filter(ruleCanPaint);
|
|
3924
|
-
}
|
|
3455
|
+
//#region src/client/cadence.ts
|
|
3925
3456
|
/**
|
|
3926
|
-
*
|
|
3927
|
-
*
|
|
3928
|
-
* Top→bottom, first case-insensitive substring hit wins; an empty `match` never
|
|
3929
|
-
* hits on its own. When nothing hits, the FIRST usable rule is the fallback
|
|
3930
|
-
* (rule 1 deliberately doubles as both a matcher and the fallback).
|
|
3457
|
+
* The scheduler entry points the plugin registers, and the cadences they run at.
|
|
3931
3458
|
*
|
|
3932
|
-
*
|
|
3933
|
-
*
|
|
3934
|
-
*
|
|
3459
|
+
* Every interval the plugin owns is declared HERE rather than at its call site, so
|
|
3460
|
+
* "what does this plugin poll, and how often" has one answer. The scheduler itself
|
|
3461
|
+
* lives in ./watchdogs; the questions live in their own modules (boot restore and
|
|
3462
|
+
* the theme watchdog in ./appearance, the holiday rollover in ./holiday-status).
|
|
3935
3463
|
*/
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3464
|
+
/** The theme skin / host palette re-assert, once a second while a colour is live. */
|
|
3465
|
+
const THEME_WATCH_MS = 1e3;
|
|
3466
|
+
/**
|
|
3467
|
+
* The delayed boot restores. Two of them because the theme service and the host
|
|
3468
|
+
* settings scope settle asynchronously after `apply` — a single late pass misses a
|
|
3469
|
+
* host adoption that happens after it, and an early one misses the one before it.
|
|
3470
|
+
*/
|
|
3471
|
+
const RESTORE_DELAYS_MS = [300, 1500];
|
|
3472
|
+
/** The holiday rollover: the answer only moves at Beijing midnight, a whole day wide. */
|
|
3473
|
+
const HOLIDAY_WATCH_MS = 3e4;
|
|
3474
|
+
//#endregion
|
|
3475
|
+
//#region src/client/appearance.ts
|
|
3476
|
+
function createAppearance(host) {
|
|
3477
|
+
const skin = createSkin(host.ctx);
|
|
3478
|
+
/** The host scheme last published, so a move can be told from a repeat. */
|
|
3479
|
+
let lastScheme = null;
|
|
3480
|
+
/**
|
|
3481
|
+
* Publish the scheme the host's own preference resolves to. The theme service is
|
|
3482
|
+
* the only place that knows it (a custom active theme included); the wallpaper
|
|
3483
|
+
* layer needs it to hand `data-ds-dark-theme` back in the host's own form while a
|
|
3484
|
+
* colour-less rule follows that theme, instead of leaving the flag off — which
|
|
3485
|
+
* pins the host to its LIGHT palette on a dark system.
|
|
3486
|
+
*/
|
|
3487
|
+
const syncHostScheme = () => {
|
|
3488
|
+
try {
|
|
3489
|
+
setHostScheme(host.ctx.theme.getTheme().active.colorScheme === "dark");
|
|
3490
|
+
} catch {
|
|
3491
|
+
setHostScheme(null);
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3494
|
+
const reassert = () => {
|
|
3495
|
+
const color = activeColor();
|
|
3496
|
+
return color === null ? false : skin.reassert(color);
|
|
3497
|
+
};
|
|
3498
|
+
/**
|
|
3499
|
+
* Re-read the scheme the HOST preference resolves to, repainting when it moved.
|
|
3500
|
+
*
|
|
3501
|
+
* The host can flip it without telling anyone: the OS while the preference is
|
|
3502
|
+
* `system`, a host-side settings adoption, and — the one that hides — a
|
|
3503
|
+
* re-projection the host performs on its own, which our flag write can swallow.
|
|
3504
|
+
* So the value is compared, not assumed: a moved scheme re-publishes it and
|
|
3505
|
+
* repaints; an unchanged one only does the cheap readback while a
|
|
3506
|
+
* colour-less rule paints from the host palette.
|
|
3507
|
+
*
|
|
3508
|
+
* @returns whether the host scheme moved (the caller repaints).
|
|
3509
|
+
*/
|
|
3510
|
+
const syncScheme = () => {
|
|
3511
|
+
let dark;
|
|
3512
|
+
try {
|
|
3513
|
+
dark = host.ctx.theme.getTheme().active.colorScheme === "dark";
|
|
3514
|
+
} catch {
|
|
3515
|
+
dark = hostSchemeIsDark();
|
|
3516
|
+
}
|
|
3517
|
+
const moved = dark !== null && dark !== lastScheme;
|
|
3518
|
+
if (dark !== null) {
|
|
3519
|
+
lastScheme = dark;
|
|
3520
|
+
setHostScheme(dark);
|
|
3521
|
+
}
|
|
3522
|
+
if (!moved && activeColor() === null) refreshSystemTheme();
|
|
3523
|
+
return moved;
|
|
3524
|
+
};
|
|
3525
|
+
const restore = () => {
|
|
3526
|
+
syncHostScheme();
|
|
3527
|
+
if (reassert()) host.refresh();
|
|
3528
|
+
applyWp();
|
|
3529
|
+
};
|
|
3530
|
+
return {
|
|
3531
|
+
syncHostScheme,
|
|
3532
|
+
reassert,
|
|
3533
|
+
restore,
|
|
3534
|
+
applyPalette() {
|
|
3535
|
+
skin.commit(activeColor());
|
|
3536
|
+
syncHostScheme();
|
|
3537
|
+
},
|
|
3538
|
+
watch() {
|
|
3539
|
+
if (syncScheme()) {
|
|
3540
|
+
host.refresh();
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
if (reassert()) host.refresh();
|
|
3544
|
+
},
|
|
3545
|
+
/**
|
|
3546
|
+
* Two delayed restores, because the theme service and the host settings scope
|
|
3547
|
+
* settle asynchronously after `apply`: a single late pass misses an adoption
|
|
3548
|
+
* that happens after it, and an early one misses the one before it.
|
|
3549
|
+
*/
|
|
3550
|
+
armRestores() {
|
|
3551
|
+
const timers = RESTORE_DELAYS_MS.map((ms) => window.setTimeout(restore, ms));
|
|
3552
|
+
return () => {
|
|
3553
|
+
for (const id of timers) window.clearTimeout(id);
|
|
3554
|
+
};
|
|
3555
|
+
},
|
|
3556
|
+
dispose() {
|
|
3557
|
+
skin.dispose();
|
|
3558
|
+
}
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
//#endregion
|
|
3562
|
+
//#region src/client/appearance-checks.ts
|
|
3563
|
+
/**
|
|
3564
|
+
* The theme watchdog. The theme service keeps only built-in preferences in
|
|
3565
|
+
* memory, so any host-scope adoption can silently drop the custom theme —
|
|
3566
|
+
* reverting the label colours and the inner surfaces to the system palette.
|
|
3567
|
+
*
|
|
3568
|
+
* A colour-less rule needs the same slow check for the opposite reason: it paints
|
|
3569
|
+
* from the HOST palette, which the host can re-project (or which the scheme flag
|
|
3570
|
+
* can fall behind) with no event of ours to hang a repaint on.
|
|
3571
|
+
*/
|
|
3572
|
+
function themeWatchdog(host) {
|
|
3573
|
+
return {
|
|
3574
|
+
name: "theme",
|
|
3575
|
+
everyMs: THEME_WATCH_MS,
|
|
3576
|
+
run() {
|
|
3577
|
+
if (host.activeColor() === null) {
|
|
3578
|
+
host.refreshHostPalette();
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
host.restore();
|
|
3582
|
+
}
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* The holiday rollover: a slow tick that does nothing at all until the day key
|
|
3587
|
+
* actually moves. 30 s is plenty for a boundary that is a whole day wide.
|
|
3588
|
+
*/
|
|
3589
|
+
function holidayRollover(host) {
|
|
3590
|
+
let day = host.activeHolidayId() ?? "";
|
|
3591
|
+
return {
|
|
3592
|
+
name: "holiday",
|
|
3593
|
+
everyMs: HOLIDAY_WATCH_MS,
|
|
3594
|
+
run() {
|
|
3595
|
+
const next = host.activeHolidayId() ?? "";
|
|
3596
|
+
if (next === day) return;
|
|
3597
|
+
day = next;
|
|
3598
|
+
const slot = host.todayHolidaySlot();
|
|
3599
|
+
if (slot === null) host.applyActive();
|
|
3600
|
+
else host.loadAndRepaint(slot);
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
//#endregion
|
|
3605
|
+
//#region src/client/modelbg.ts
|
|
3606
|
+
/** Rules that can actually paint: see `ruleCanPaint` (./schema). */
|
|
3607
|
+
function usable(rules) {
|
|
3608
|
+
return rules.filter(ruleCanPaint);
|
|
3609
|
+
}
|
|
3610
|
+
/**
|
|
3611
|
+
* Resolve the rule for one model text.
|
|
3612
|
+
*
|
|
3613
|
+
* Top→bottom, first case-insensitive substring hit wins; an empty `match` never
|
|
3614
|
+
* hits on its own. When nothing hits, the FIRST usable rule is the fallback
|
|
3615
|
+
* (rule 1 deliberately doubles as both a matcher and the fallback).
|
|
3616
|
+
*
|
|
3617
|
+
* "Usable" is `ruleCanPaint` — an image, or a theme color of its own — so a rule
|
|
3618
|
+
* whose pictures were all removed still holds the models it matches (and paints
|
|
3619
|
+
* its color alone) instead of quietly handing them to the next rule.
|
|
3620
|
+
*/
|
|
3621
|
+
function matchRule(rules, modelText) {
|
|
3622
|
+
const list = usable(rules);
|
|
3623
|
+
if (list.length === 0) return {
|
|
3624
|
+
rule: null,
|
|
3625
|
+
matched: false
|
|
3626
|
+
};
|
|
3627
|
+
const hay = modelText.toLowerCase();
|
|
3628
|
+
if (hay.trim() !== "") for (const rule of list) {
|
|
3629
|
+
const needle = rule.match.trim().toLowerCase();
|
|
3630
|
+
if (needle === "") continue;
|
|
3946
3631
|
if (hay.includes(needle)) return {
|
|
3947
3632
|
rule,
|
|
3948
3633
|
matched: true
|
|
@@ -4183,6 +3868,265 @@ window.__ModuleLoader__.load({
|
|
|
4183
3868
|
};
|
|
4184
3869
|
}
|
|
4185
3870
|
//#endregion
|
|
3871
|
+
//#region src/client/boot.ts
|
|
3872
|
+
/**
|
|
3873
|
+
* Boot: which image slots are read before the first frame, and in what order.
|
|
3874
|
+
*
|
|
3875
|
+
* Deliberately its own module, because the ORDER is the whole feature. A setup
|
|
3876
|
+
* with five rules and four pictures each holds twenty full-size data URLs, and
|
|
3877
|
+
* pulling all of them before painting would mean the interface opens blank for
|
|
3878
|
+
* as long as they take. So exactly three things are read eagerly:
|
|
3879
|
+
*
|
|
3880
|
+
* 1. the slot that paints FIRST — today's holiday art, else the current model's
|
|
3881
|
+
* first image (see `prioritySlot`);
|
|
3882
|
+
* 2. the first image of every other rule, because a rule card draws its first
|
|
3883
|
+
* image as its thumbnail;
|
|
3884
|
+
* 3. today's holiday art, when it is not already (1).
|
|
3885
|
+
*
|
|
3886
|
+
* Everything past that is read ON DEMAND: by the rotation, which warms the image
|
|
3887
|
+
* it is about to need as soon as its schedule is armed, and by the settings page
|
|
3888
|
+
* when a card is expanded. The slot list the node half reports is not hydrated at
|
|
3889
|
+
* all — an orphaned slot nothing points at has no card to appear on.
|
|
3890
|
+
*/
|
|
3891
|
+
/** One slot, read once: the cache is authoritative for "already here". */
|
|
3892
|
+
async function loadSlot(slot) {
|
|
3893
|
+
if (imageOf(slot) !== null) return;
|
|
3894
|
+
const url = await readImage(slot);
|
|
3895
|
+
if (url !== null) setImage(slot, url);
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* The slot that paints before anything else: the holiday in force when there is
|
|
3899
|
+
* one, otherwise the FIRST image of the current model's rule. A holiday outranks
|
|
3900
|
+
* the rule here for the same reason it outranks it when painting.
|
|
3901
|
+
*
|
|
3902
|
+
* Always the first image (never the one `advanceOnSwitch` would step to): boot
|
|
3903
|
+
* paints without an advance, and hydrating an image the first frame is not going
|
|
3904
|
+
* to use would leave the frame blank until the real one arrived.
|
|
3905
|
+
*/
|
|
3906
|
+
function prioritySlot(spec) {
|
|
3907
|
+
if (spec.holidaySlot !== null) return spec.holidaySlot;
|
|
3908
|
+
return matchRule(cfg.rules, spec.modelText).rule?.images[0]?.slot ?? null;
|
|
3909
|
+
}
|
|
3910
|
+
/** Every slot boot hydrates, the painted one first. */
|
|
3911
|
+
function bootSlots(spec) {
|
|
3912
|
+
const firsts = cfg.rules.map((r) => r.images[0]?.slot).filter((slot) => slot !== void 0);
|
|
3913
|
+
const first = prioritySlot(spec);
|
|
3914
|
+
return Array.from(/* @__PURE__ */ new Set([
|
|
3915
|
+
...first === null ? [] : [first],
|
|
3916
|
+
...firsts,
|
|
3917
|
+
...spec.holidaySlot === null ? [] : [spec.holidaySlot]
|
|
3918
|
+
]));
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Read the boot set, calling `painted` once the first slot is in.
|
|
3922
|
+
*
|
|
3923
|
+
* Sequential on purpose: the painted one is requested first and the rest stream
|
|
3924
|
+
* in after it, so a large multi-rule setup never delays the first frame. The
|
|
3925
|
+
* callback fires on EVERY slot's turn, not once — the caller's repaint is cheap
|
|
3926
|
+
* when the URL has not moved.
|
|
3927
|
+
*/
|
|
3928
|
+
async function hydrateBoot(spec, painted) {
|
|
3929
|
+
const first = prioritySlot(spec);
|
|
3930
|
+
for (const slot of bootSlots(spec)) {
|
|
3931
|
+
await loadSlot(slot);
|
|
3932
|
+
if (slot === first) painted();
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
//#endregion
|
|
3936
|
+
//#region src/client/model-state.ts
|
|
3937
|
+
/** A key that no real model text can equal, so the first report always lands. */
|
|
3938
|
+
const EMPTY = "\0";
|
|
3939
|
+
function createModelTracker() {
|
|
3940
|
+
let state = {
|
|
3941
|
+
text: "",
|
|
3942
|
+
label: "",
|
|
3943
|
+
source: "default",
|
|
3944
|
+
note: "waiting"
|
|
3945
|
+
};
|
|
3946
|
+
let key = EMPTY;
|
|
3947
|
+
const take = (next) => {
|
|
3948
|
+
const nextKey = `${next.text}\u0001${next.source}\u0001${next.note}`;
|
|
3949
|
+
const changed = nextKey !== key;
|
|
3950
|
+
const textChanged = next.text !== state.text;
|
|
3951
|
+
key = nextKey;
|
|
3952
|
+
state = next;
|
|
3953
|
+
return {
|
|
3954
|
+
textChanged,
|
|
3955
|
+
changed
|
|
3956
|
+
};
|
|
3957
|
+
};
|
|
3958
|
+
return {
|
|
3959
|
+
get state() {
|
|
3960
|
+
return state;
|
|
3961
|
+
},
|
|
3962
|
+
update(text, label, source, note) {
|
|
3963
|
+
return take({
|
|
3964
|
+
text,
|
|
3965
|
+
label: label !== "" ? label : text,
|
|
3966
|
+
source,
|
|
3967
|
+
note
|
|
3968
|
+
});
|
|
3969
|
+
},
|
|
3970
|
+
adopt(text, source, note) {
|
|
3971
|
+
return take({
|
|
3972
|
+
text,
|
|
3973
|
+
label: text,
|
|
3974
|
+
source,
|
|
3975
|
+
note
|
|
3976
|
+
}).changed;
|
|
3977
|
+
},
|
|
3978
|
+
facts(rule, matched) {
|
|
3979
|
+
return {
|
|
3980
|
+
text: state.text,
|
|
3981
|
+
source: state.source,
|
|
3982
|
+
note: state.note,
|
|
3983
|
+
rule,
|
|
3984
|
+
matched
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
//#endregion
|
|
3990
|
+
//#region src/client/rotation.ts
|
|
3991
|
+
/** Dwell-time presets the settings UI offers, in ms. */
|
|
3992
|
+
const ROTATE_PRESETS = [
|
|
3993
|
+
{
|
|
3994
|
+
ms: 1e4,
|
|
3995
|
+
key: "rotEvery10s"
|
|
3996
|
+
},
|
|
3997
|
+
{
|
|
3998
|
+
ms: 3e4,
|
|
3999
|
+
key: "rotEvery30s"
|
|
4000
|
+
},
|
|
4001
|
+
{
|
|
4002
|
+
ms: 6e4,
|
|
4003
|
+
key: "rotEvery1m"
|
|
4004
|
+
},
|
|
4005
|
+
{
|
|
4006
|
+
ms: 3e5,
|
|
4007
|
+
key: "rotEvery5m"
|
|
4008
|
+
},
|
|
4009
|
+
{
|
|
4010
|
+
ms: 18e5,
|
|
4011
|
+
key: "rotEvery30m"
|
|
4012
|
+
}
|
|
4013
|
+
];
|
|
4014
|
+
/**
|
|
4015
|
+
* The index to paint after the current one, or null when there is nowhere to go
|
|
4016
|
+
* (fewer than two images — the caller must not start a timer at all then).
|
|
4017
|
+
*
|
|
4018
|
+
* `order` walks the list and wraps; `shuffle` picks uniformly among every index
|
|
4019
|
+
* EXCEPT the current one, because landing on the same image reads as a missed
|
|
4020
|
+
* tick. `current` outside the list is treated as 0 rather than trusted: it comes
|
|
4021
|
+
* from the UI as often as from the timer, and a stale index must not be able to
|
|
4022
|
+
* index past the end.
|
|
4023
|
+
*
|
|
4024
|
+
* `rand` is injectable so the shuffle branch is testable without stubbing a
|
|
4025
|
+
* global.
|
|
4026
|
+
*/
|
|
4027
|
+
function nextIndex(len, order, current, rand = Math.random) {
|
|
4028
|
+
if (!Number.isFinite(len) || len < 2) return null;
|
|
4029
|
+
const count = Math.floor(len);
|
|
4030
|
+
const cur = Number.isFinite(current) && current >= 0 && current < count ? Math.floor(current) : 0;
|
|
4031
|
+
if (order === "shuffle") {
|
|
4032
|
+
const roll = Math.min(Math.floor(Math.max(rand(), 0) * (count - 1)), count - 2);
|
|
4033
|
+
return roll >= cur ? roll + 1 : roll;
|
|
4034
|
+
}
|
|
4035
|
+
return (cur + 1) % count;
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Whether a rotation is worth running: two images, the switch on, and a positive
|
|
4039
|
+
* dwell time. Kept here (rather than at the call site) so the UI's "rotating"
|
|
4040
|
+
* badge and the timer agree by construction — a badge that says "rotating" while
|
|
4041
|
+
* no timer exists is the one failure this feature can produce silently.
|
|
4042
|
+
*/
|
|
4043
|
+
function isRotating(len, enabled, intervalMs) {
|
|
4044
|
+
return enabled && len >= 2 && Number.isFinite(intervalMs) && intervalMs > 0;
|
|
4045
|
+
}
|
|
4046
|
+
//#endregion
|
|
4047
|
+
//#region src/client/rotation-timer.ts
|
|
4048
|
+
/**
|
|
4049
|
+
* The image rotation timer: ONE interval, always aimed at the ACTIVE rule.
|
|
4050
|
+
*
|
|
4051
|
+
* A timer per rule would burn wakeups on rules that are not painting anything,
|
|
4052
|
+
* and since only one rule's images can be on screen at a time, one timer is all
|
|
4053
|
+
* that is needed. It is kept out of `apply()` because a rotation is the one piece
|
|
4054
|
+
* of this plugin that must keep working while nothing else changes — and the
|
|
4055
|
+
* questions it has to answer (did the schedule's inputs move? is the tab
|
|
4056
|
+
* hidden? which image is next?) read better gathered in one place.
|
|
4057
|
+
*
|
|
4058
|
+
* The schedule is armed by SIGNATURE rather than by "already armed": a slider drag
|
|
4059
|
+
* on the active rule re-runs the whole apply dozens of times per second, and
|
|
4060
|
+
* restarting the interval on each of those would mean the dwell timer never
|
|
4061
|
+
* elapses while the user is dragging (a 10 s rotation would never advance). The
|
|
4062
|
+
* signature carries exactly the inputs the schedule depends on, so a real change —
|
|
4063
|
+
* another rule, another interval, the tab becoming visible again — re-arms while a
|
|
4064
|
+
* repaint does not.
|
|
4065
|
+
*/
|
|
4066
|
+
function createRotation(host) {
|
|
4067
|
+
let timer = null;
|
|
4068
|
+
/** Identity of the schedule currently armed; a change means "re-arm". */
|
|
4069
|
+
let signature = "";
|
|
4070
|
+
const tick = () => {
|
|
4071
|
+
const rule = activeRule();
|
|
4072
|
+
if (rule === null) return;
|
|
4073
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
4074
|
+
if (next === null) {
|
|
4075
|
+
schedule(true);
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
setRotIndex(next);
|
|
4079
|
+
host.paintImage(rule, next);
|
|
4080
|
+
const after = nextIndex(rule.images.length, rule.rotate.order, next);
|
|
4081
|
+
if (after !== null) host.warmImage(rule, after);
|
|
4082
|
+
};
|
|
4083
|
+
const schedule = (force = false) => {
|
|
4084
|
+
const rule = activeRule();
|
|
4085
|
+
const sig = rule === null ? "" : [
|
|
4086
|
+
rule.id,
|
|
4087
|
+
rule.rotate.enabled,
|
|
4088
|
+
rule.rotate.intervalMs,
|
|
4089
|
+
rule.rotate.order,
|
|
4090
|
+
rule.images.length,
|
|
4091
|
+
document.hidden
|
|
4092
|
+
].join("|");
|
|
4093
|
+
if (!force && sig === signature) return;
|
|
4094
|
+
signature = sig;
|
|
4095
|
+
if (timer !== null) {
|
|
4096
|
+
window.clearInterval(timer);
|
|
4097
|
+
timer = null;
|
|
4098
|
+
}
|
|
4099
|
+
if (rule === null || document.hidden) return;
|
|
4100
|
+
if (!isRotating(rule.images.length, rule.rotate.enabled, rule.rotate.intervalMs)) return;
|
|
4101
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
4102
|
+
if (next !== null) host.warmImage(rule, next);
|
|
4103
|
+
timer = window.setInterval(tick, rule.rotate.intervalMs);
|
|
4104
|
+
};
|
|
4105
|
+
return {
|
|
4106
|
+
schedule,
|
|
4107
|
+
step() {
|
|
4108
|
+
const rule = activeRule();
|
|
4109
|
+
if (rule === null) return false;
|
|
4110
|
+
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
4111
|
+
if (next === null) return false;
|
|
4112
|
+
setRotIndex(next);
|
|
4113
|
+
host.paintImage(rule, next);
|
|
4114
|
+
schedule(true);
|
|
4115
|
+
return true;
|
|
4116
|
+
},
|
|
4117
|
+
onVisibility() {
|
|
4118
|
+
schedule();
|
|
4119
|
+
host.sync();
|
|
4120
|
+
},
|
|
4121
|
+
dispose() {
|
|
4122
|
+
if (timer !== null) {
|
|
4123
|
+
window.clearInterval(timer);
|
|
4124
|
+
timer = null;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
};
|
|
4128
|
+
}
|
|
4129
|
+
//#endregion
|
|
4186
4130
|
//#region src/client/components/icons.tsx
|
|
4187
4131
|
/** Sun glyph, migrated from @deepseek-ai/dsh-client-ui-primitives IconLightOutline16. */
|
|
4188
4132
|
const SUN_PATHS = "<path d=\"M11.3496 8C11.3496 6.14985 9.85015 4.65039 8 4.65039C6.14985 4.65039 4.65039 6.14985 4.65039 8C4.65039 9.85015 6.14985 11.3496 8 11.3496C9.85015 11.3496 11.3496 9.85015 11.3496 8ZM12.6504 8C12.6504 10.5681 10.5681 12.6504 8 12.6504C5.43188 12.6504 3.34961 10.5681 3.34961 8C3.34961 5.43188 5.43188 3.34961 8 3.34961C10.5681 3.34961 12.6504 5.43188 12.6504 8Z\" fill=\"currentColor\"/><path d=\"M8.65039 0.5V2.5H7.34961V0.5H8.65039Z\" fill=\"currentColor\"/><path d=\"M8.65039 13.5V15.5H7.34961V13.5H8.65039Z\" fill=\"currentColor\"/><path d=\"M3.15808 2.24035L4.57229 3.65456L3.6525 4.57435L2.23829 3.16014L3.15808 2.24035Z\" fill=\"currentColor\"/><path d=\"M12.3505 11.4327L13.7647 12.8469L12.8449 13.7667L11.4307 12.3525L12.3505 11.4327Z\" fill=\"currentColor\"/><path d=\"M2.24537 12.8469L3.65958 11.4327L4.57937 12.3525L3.16516 13.7667L2.24537 12.8469Z\" fill=\"currentColor\"/><path d=\"M11.4377 3.65455L12.852 2.24033L13.7718 3.16012L12.3575 4.57434L11.4377 3.65455Z\" fill=\"currentColor\"/><path d=\"M0.5 7.35461H2.5V8.6554H0.5L0.5 7.35461Z\" fill=\"currentColor\"/><path d=\"M13.5 7.35461H15.5V8.6554H13.5V8.6554Z\" fill=\"currentColor\"/>";
|
|
@@ -4442,6 +4386,667 @@ window.__ModuleLoader__.load({
|
|
|
4442
4386
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.3 6.4 8 10.1l3.7-3.7" })
|
|
4443
4387
|
});
|
|
4444
4388
|
//#endregion
|
|
4389
|
+
//#region src/client/nav-icon.ts
|
|
4390
|
+
/**
|
|
4391
|
+
* The settings-nav glyph.
|
|
4392
|
+
*
|
|
4393
|
+
* The harness derives every section's nav icon from its id and exposes no hook for
|
|
4394
|
+
* a plugin to supply one, so the mounted nav cell has to be found in the DOM and
|
|
4395
|
+
* its `<svg>` swapped for this plugin's sun. That is a patch on the host's markup,
|
|
4396
|
+
* and the two failure modes are both silent: patching too early finds no cell, and
|
|
4397
|
+
* patching on every mutation burns a scan of the whole document per keystroke in
|
|
4398
|
+
* the composer. Hence the narrow trigger below — only a settings DIALOG arriving
|
|
4399
|
+
* in the tree starts a scan.
|
|
4400
|
+
*/
|
|
4401
|
+
/** The host's settings dialog, the only place this section's nav exists. */
|
|
4402
|
+
const DIALOG_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
|
|
4403
|
+
const MARK = "dabNavIcon";
|
|
4404
|
+
/**
|
|
4405
|
+
* Replace the nav glyph of the section whose label is `navLabel()`.
|
|
4406
|
+
*
|
|
4407
|
+
* Reads the label on every scan rather than capturing it: the section is rebuilt
|
|
4408
|
+
* when the language changes, and a stale label would simply stop matching.
|
|
4409
|
+
*/
|
|
4410
|
+
function applyNavIcon(navLabel) {
|
|
4411
|
+
const nav = document.querySelector(DIALOG_SEL)?.querySelector("nav");
|
|
4412
|
+
if (!nav) return;
|
|
4413
|
+
for (const cell of Array.from(nav.querySelectorAll("button"))) {
|
|
4414
|
+
const label = cell.querySelector("span");
|
|
4415
|
+
if (label === null || label.textContent?.trim() !== navLabel) continue;
|
|
4416
|
+
const svg = cell.querySelector("svg");
|
|
4417
|
+
if (svg !== null && svg.dataset[MARK] === "1") return;
|
|
4418
|
+
const sun = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
4419
|
+
sun.setAttribute("width", "16");
|
|
4420
|
+
sun.setAttribute("height", "16");
|
|
4421
|
+
sun.setAttribute("viewBox", "0 0 16 16");
|
|
4422
|
+
sun.setAttribute("fill", "none");
|
|
4423
|
+
sun.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
4424
|
+
sun.dataset[MARK] = "1";
|
|
4425
|
+
sun.innerHTML = SUN_PATHS;
|
|
4426
|
+
svg?.replaceWith(sun);
|
|
4427
|
+
if (svg === null) cell.prepend(sun);
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
/** Watch for the settings dialog (re)appearing; returns the disposer. */
|
|
4432
|
+
function watchNavIcon(navLabel) {
|
|
4433
|
+
if (typeof MutationObserver === "undefined") return () => void 0;
|
|
4434
|
+
const observer = new MutationObserver((records) => {
|
|
4435
|
+
if (records.some((r) => {
|
|
4436
|
+
for (const n of r.addedNodes) {
|
|
4437
|
+
if (n.nodeType !== 1) continue;
|
|
4438
|
+
const el = n;
|
|
4439
|
+
if (el.matches?.(DIALOG_SEL) || el.querySelector?.(DIALOG_SEL)) return true;
|
|
4440
|
+
}
|
|
4441
|
+
return false;
|
|
4442
|
+
})) applyNavIcon(navLabel());
|
|
4443
|
+
});
|
|
4444
|
+
observer.observe(document.body, {
|
|
4445
|
+
childList: true,
|
|
4446
|
+
subtree: true
|
|
4447
|
+
});
|
|
4448
|
+
applyNavIcon(navLabel());
|
|
4449
|
+
return () => observer.disconnect();
|
|
4450
|
+
}
|
|
4451
|
+
//#endregion
|
|
4452
|
+
//#region src/client/theme-file.ts
|
|
4453
|
+
/** Slot names reach the filesystem, so an incoming file's are whitelisted. */
|
|
4454
|
+
const SLOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
4455
|
+
/** The JSON document `exportTheme` writes; `images` carries every rule slot. */
|
|
4456
|
+
function buildThemeFile() {
|
|
4457
|
+
const images = {};
|
|
4458
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
4459
|
+
const url = imageOf(slot);
|
|
4460
|
+
if (url !== null) images[slot] = url;
|
|
4461
|
+
}
|
|
4462
|
+
return {
|
|
4463
|
+
version: 9,
|
|
4464
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4465
|
+
config: cfg,
|
|
4466
|
+
images
|
|
4467
|
+
};
|
|
4468
|
+
}
|
|
4469
|
+
/**
|
|
4470
|
+
* Write the current rules and wallpapers out as one JSON file.
|
|
4471
|
+
*
|
|
4472
|
+
* The object URL is attached to a hidden anchor and released on the NEXT task:
|
|
4473
|
+
* a click is not a synchronous download, and releasing in the same tick cancels
|
|
4474
|
+
* it in some browsers — while Firefox only honours the click on an anchor that is
|
|
4475
|
+
* in the document.
|
|
4476
|
+
*/
|
|
4477
|
+
function saveThemeFile(file) {
|
|
4478
|
+
const blob = new Blob([JSON.stringify(file, null, 2)], { type: "application/json" });
|
|
4479
|
+
const url = URL.createObjectURL(blob);
|
|
4480
|
+
const a = document.createElement("a");
|
|
4481
|
+
a.href = url;
|
|
4482
|
+
a.download = "dsh-background-by-model-theme.json";
|
|
4483
|
+
a.style.display = "none";
|
|
4484
|
+
document.body.append(a);
|
|
4485
|
+
a.click();
|
|
4486
|
+
window.setTimeout(() => {
|
|
4487
|
+
a.remove();
|
|
4488
|
+
URL.revokeObjectURL(url);
|
|
4489
|
+
}, 0);
|
|
4490
|
+
}
|
|
4491
|
+
/**
|
|
4492
|
+
* Write a whole theme file into the store: its rules, its wallpapers, nothing
|
|
4493
|
+
* else.
|
|
4494
|
+
*
|
|
4495
|
+
* `wipeFirst` is the entire difference between the two imports, and it belongs to
|
|
4496
|
+
* the recommended profile alone: `nextSlot` hands slots out as `m1`, `m2`, … — a
|
|
4497
|
+
* counter, not a random token — so a downloaded profile and the store it lands in
|
|
4498
|
+
* collide on those names as a matter of course. Emptying first is what makes
|
|
4499
|
+
* "replace everything with this" literally true.
|
|
4500
|
+
*/
|
|
4501
|
+
async function applyThemeFile(host, data, wipeFirst) {
|
|
4502
|
+
if (typeof data.version === "number" && data.version > 9) return false;
|
|
4503
|
+
if (typeof data.config !== "object" || data.config === null) return false;
|
|
4504
|
+
if (wipeFirst) {
|
|
4505
|
+
await resetStore();
|
|
4506
|
+
resetConfig();
|
|
4507
|
+
}
|
|
4508
|
+
adoptConfig(data.config);
|
|
4509
|
+
const incoming = data.images ?? {};
|
|
4510
|
+
const keep = /* @__PURE__ */ new Set();
|
|
4511
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
4512
|
+
keep.add(slot);
|
|
4513
|
+
const raw = incoming[slot];
|
|
4514
|
+
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
4515
|
+
setImage(slot, raw);
|
|
4516
|
+
writeImage(slot, raw);
|
|
4517
|
+
} else {
|
|
4518
|
+
setImage(slot, null);
|
|
4519
|
+
deleteImage(slot);
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && SLOT_RE.test(slot)) deleteImage(slot);
|
|
4523
|
+
persistConfig();
|
|
4524
|
+
host.applyActive();
|
|
4525
|
+
return true;
|
|
4526
|
+
}
|
|
4527
|
+
//#endregion
|
|
4528
|
+
//#region src/client/watchdogs.ts
|
|
4529
|
+
/** The shortest cadence allowed — also the scheduler's own tick. */
|
|
4530
|
+
const MIN_TICK_MS = 1e3;
|
|
4531
|
+
/**
|
|
4532
|
+
* Start one scheduler. It does NOT run anything on registration: the caller
|
|
4533
|
+
* decides when the first pass happens (the plugin's boot sequence needs the
|
|
4534
|
+
* order — wallpaper, model, palette), and registers with the intent that the
|
|
4535
|
+
* first tick is the first run.
|
|
4536
|
+
*
|
|
4537
|
+
* The clock is `performance.now()` rather than `Date.now()`: a wall-clock jump
|
|
4538
|
+
* (NTP, a laptop waking from sleep) would otherwise fire every entry at once or
|
|
4539
|
+
* park them all for hours, and "how long ago did we last ask" is a duration, not
|
|
4540
|
+
* a date.
|
|
4541
|
+
*/
|
|
4542
|
+
function createSchedule() {
|
|
4543
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4544
|
+
let timer = null;
|
|
4545
|
+
let disposed = false;
|
|
4546
|
+
const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
4547
|
+
const fire = (w, stamp) => {
|
|
4548
|
+
stamp.last = now();
|
|
4549
|
+
try {
|
|
4550
|
+
w.run();
|
|
4551
|
+
} catch (e) {
|
|
4552
|
+
console.warn(`dsh-background-by-model: the "${w.name}" check threw`, e);
|
|
4553
|
+
}
|
|
4554
|
+
};
|
|
4555
|
+
const tick = () => {
|
|
4556
|
+
if (disposed || document.hidden) return;
|
|
4557
|
+
const at = now();
|
|
4558
|
+
for (const [w, stamp] of entries) if (at - stamp.last >= w.everyMs) fire(w, stamp);
|
|
4559
|
+
};
|
|
4560
|
+
/**
|
|
4561
|
+
* One beat for the whole page, and it is the SHORTEST cadence any entry asks
|
|
4562
|
+
* for, so each one fires within one beat of its own due time. With nothing
|
|
4563
|
+
* registered there is no timer at all.
|
|
4564
|
+
*/
|
|
4565
|
+
const resync = () => {
|
|
4566
|
+
if (timer !== null) {
|
|
4567
|
+
window.clearInterval(timer);
|
|
4568
|
+
timer = null;
|
|
4569
|
+
}
|
|
4570
|
+
if (disposed || entries.size === 0) return;
|
|
4571
|
+
let beat = Infinity;
|
|
4572
|
+
for (const w of entries.keys()) beat = Math.min(beat, w.everyMs);
|
|
4573
|
+
timer = window.setInterval(tick, Math.max(MIN_TICK_MS, beat));
|
|
4574
|
+
};
|
|
4575
|
+
const onVisibility = () => {
|
|
4576
|
+
if (!document.hidden) tick();
|
|
4577
|
+
};
|
|
4578
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
4579
|
+
resync();
|
|
4580
|
+
return {
|
|
4581
|
+
add(watchdog) {
|
|
4582
|
+
if (disposed) return () => void 0;
|
|
4583
|
+
entries.set(watchdog, { last: now() });
|
|
4584
|
+
resync();
|
|
4585
|
+
return () => {
|
|
4586
|
+
entries.delete(watchdog);
|
|
4587
|
+
resync();
|
|
4588
|
+
};
|
|
4589
|
+
},
|
|
4590
|
+
runAll() {
|
|
4591
|
+
if (disposed) return;
|
|
4592
|
+
for (const [w, stamp] of entries) fire(w, stamp);
|
|
4593
|
+
},
|
|
4594
|
+
dispose() {
|
|
4595
|
+
if (disposed) return;
|
|
4596
|
+
disposed = true;
|
|
4597
|
+
entries.clear();
|
|
4598
|
+
if (timer !== null) {
|
|
4599
|
+
window.clearInterval(timer);
|
|
4600
|
+
timer = null;
|
|
4601
|
+
}
|
|
4602
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
4603
|
+
}
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
//#endregion
|
|
4607
|
+
//#region src/client/i18n.ts
|
|
4608
|
+
const NS = "settings.anyBg";
|
|
4609
|
+
const zh = {
|
|
4610
|
+
nav: "模型背景",
|
|
4611
|
+
brandTag: "按模型换背景",
|
|
4612
|
+
pageInterface: "界面",
|
|
4613
|
+
pageModelBg: "模型背景",
|
|
4614
|
+
pageProfile: "配置",
|
|
4615
|
+
descInterface: "为主界面的各个区域单独调节透明度,营造空间层次感。这里的设置对所有模型生效",
|
|
4616
|
+
descModelBg: "每条规则 = 一个匹配串 + 一组背景图与外观。切换模型时,从上往下找第一条匹配串出现在模型名里的规则;都没中就使用第 1 条。一条规则可以放多张图,按你设的节奏轮换",
|
|
4617
|
+
descProfile: "调整壁纸切换效果,并把当前全部规则连同图片导出备份,或从文件一键恢复",
|
|
4618
|
+
uiTitle: "主界面",
|
|
4619
|
+
uiOpacity: "透明度",
|
|
4620
|
+
uiOpacityBg: "主背景",
|
|
4621
|
+
uiOpacitySide: "左侧栏",
|
|
4622
|
+
uiOpacityCard: "卡片与菜单面板",
|
|
4623
|
+
uiOpacityCode: "代码块",
|
|
4624
|
+
uiOpacityInput: "输入框与控件",
|
|
4625
|
+
uiSop: "设置界面透明度",
|
|
4626
|
+
uiChatRegion: "对话文本框",
|
|
4627
|
+
uiTrajectory: "轨迹页",
|
|
4628
|
+
uiPage: "插件页面",
|
|
4629
|
+
uiPreview: "右侧栏",
|
|
4630
|
+
statusUnknownHint: "检测不到时一律使用第 1 条可用规则;切换一次模型或稍等片刻会自动重试",
|
|
4631
|
+
statusSourceDefaultHint: "只读到宿主默认模型,它不是本会话当前选择的模型;本会话的模型选择可用后会自动纠正",
|
|
4632
|
+
statusNoteNoService: "仍未拿到会话服务(会话控制器晚于本插件加载),正在自动重试",
|
|
4633
|
+
statusNoteNoSession: "会话服务已就绪,但还没有当前会话 id",
|
|
4634
|
+
statusNoteNoProjection: "拿不到本会话的模型选择投影(binding / projections 不可用)",
|
|
4635
|
+
statusNoteEmptySelection: "本会话的模型选择投影还没有值(切一次模型即可写入)",
|
|
4636
|
+
hostStaleHint: "检测到 DSH 主进程仍在运行旧版插件:新版配置格式它还读不懂,为避免丢规则,本次只改界面不写入配置。重启 DSH 后多图与轮换即可正常保存",
|
|
4637
|
+
tryoutTitle: "匹配试跑",
|
|
4638
|
+
tryoutPlaceholder: "输入模型名,如 deepseek-flash",
|
|
4639
|
+
tryoutUseCurrent: "填入当前模型",
|
|
4640
|
+
tryoutHit: "命中规则",
|
|
4641
|
+
tryoutFallback: "无匹配 · 走兜底规则",
|
|
4642
|
+
tryoutNone: "没有可用规则(规则需要启用,并且有图片或主题色)",
|
|
4643
|
+
tryoutEmpty: "输入模型名即可试跑",
|
|
4644
|
+
autoExtract: "选图后自动取主题色",
|
|
4645
|
+
ruleAdd: "新增规则",
|
|
4646
|
+
ruleNoneHint: "还没有规则,点上面的「新增规则」新建一条",
|
|
4647
|
+
ruleFallbackBadge: "兜底",
|
|
4648
|
+
ruleMatchPlaceholder: "如 flash / glm(忽略大小写,留空则不参与匹配)",
|
|
4649
|
+
ruleEnabled: "启用",
|
|
4650
|
+
ruleUp: "上移",
|
|
4651
|
+
ruleDown: "下移",
|
|
4652
|
+
ruleRemove: "删除规则",
|
|
4653
|
+
tabListLabel: "规则",
|
|
4654
|
+
tabPick: "选择规则",
|
|
4655
|
+
tabMore: "更多规则",
|
|
4656
|
+
tabUnnamed: "未命名规则",
|
|
4657
|
+
tabLive: "正在使用",
|
|
4658
|
+
tabRotating: "轮换中",
|
|
4659
|
+
tabOff: "已停用",
|
|
4660
|
+
ruleRemoveConfirm: "再点一次删除",
|
|
4661
|
+
ruleNoImage: "尚未选择图片",
|
|
4662
|
+
ruleFromUrl: "从网址",
|
|
4663
|
+
ruleUrlPlaceholder: "粘贴图片网址 https://…",
|
|
4664
|
+
ruleUrlApply: "应用",
|
|
4665
|
+
ruleUrlCancel: "取消",
|
|
4666
|
+
ruleUrlApplying: "加载中…",
|
|
4667
|
+
ruleUrlBadHttp: "仅支持 http/https 图片网址",
|
|
4668
|
+
ruleUrlFail: "获取图片失败",
|
|
4669
|
+
ruleImageRemove: "移除",
|
|
4670
|
+
ruleEmptyHint: "这条规则还没有图片,也没有自己的主题色:没有可显示的内容,匹配和兜底都会跳过它",
|
|
4671
|
+
ruleColorOnlyHint: "这条规则还没有图片:不会铺壁纸,界面只用这个主题色",
|
|
4672
|
+
ruleImagePendingHint: "这张图片还没有取到内容,稍后会自己出现(也可以直接替换它)",
|
|
4673
|
+
ruleColor: "主题色",
|
|
4674
|
+
ruleColorNone: "跟随系统主题",
|
|
4675
|
+
ruleColorNoneHint: "未设颜色时跟随系统主题:界面用宿主的配色,壁纸与各部位的透明度照旧生效",
|
|
4676
|
+
ruleColorPick: "自定义颜色",
|
|
4677
|
+
ruleColorPickHint: "优先从本图取色,取不到就用默认色",
|
|
4678
|
+
ruleColorNoImageHint: "这条规则还没有图片,取不到色:选一个颜色可以只铺主题色,或先添加图片",
|
|
4679
|
+
ruleColorPendingImageHint: "这张图片还没有取到内容,暂时取不了色:可以直接选一个颜色,或等它加载完",
|
|
4680
|
+
ruleColorExtract: "从本图提取",
|
|
4681
|
+
ruleColorTargetImage: "作用于选中的图片:",
|
|
4682
|
+
ruleColorTargetRule: "作用于本规则(它还没有图片)",
|
|
4683
|
+
ruleLayout: "布局模式",
|
|
4684
|
+
ruleLayoutNoImage: "添加图片后可分别设置每张图的布局模式",
|
|
4685
|
+
ruleFramingEdit: "编辑位置",
|
|
4686
|
+
ruleOpacity: "背景透明度",
|
|
4687
|
+
ruleBrightness: "背景亮度",
|
|
4688
|
+
ruleBlur: "背景模糊",
|
|
4689
|
+
ruleBlurHint: "这三条滑块都作用于壁纸层;界面页里的透明度作用于界面各区域,两者互不影响",
|
|
4690
|
+
rotTitle: "多图轮换",
|
|
4691
|
+
rotEnable: "轮换这组图",
|
|
4692
|
+
rotNeedTwo: "至少两张图才能轮换",
|
|
4693
|
+
rotEvery: "每张停留",
|
|
4694
|
+
rotCustom: "自定义秒数",
|
|
4695
|
+
rotSeconds: "秒",
|
|
4696
|
+
rotOrder: "换图顺序",
|
|
4697
|
+
rotOrderSeq: "按顺序",
|
|
4698
|
+
rotOrderShuffle: "随机",
|
|
4699
|
+
rotOnSwitch: "切换模型时也换一张",
|
|
4700
|
+
rotNext: "下一张",
|
|
4701
|
+
rotNextInactive: "只有正在生效的规则能换画面上的图",
|
|
4702
|
+
rotPos: "第",
|
|
4703
|
+
rotShowing: "正在显示",
|
|
4704
|
+
rotAddImage: "添加",
|
|
4705
|
+
rotReplaceImage: "替换",
|
|
4706
|
+
rotDragHint: "拖动排序",
|
|
4707
|
+
rotDblHint: "双击切换",
|
|
4708
|
+
rotEvery10s: "10 秒",
|
|
4709
|
+
rotEvery30s: "30 秒",
|
|
4710
|
+
rotEvery1m: "1 分钟",
|
|
4711
|
+
rotEvery5m: "5 分钟",
|
|
4712
|
+
rotEvery30m: "30 分钟",
|
|
4713
|
+
bgModeFit: "适应",
|
|
4714
|
+
bgModeFill: "填充",
|
|
4715
|
+
bgModeStretch: "拉伸",
|
|
4716
|
+
bgModeTile: "平铺",
|
|
4717
|
+
bgModeCenter: "居中",
|
|
4718
|
+
editorTitle: "背景编辑器",
|
|
4719
|
+
editorHint: "拖动移动画面,滚轮缩放大小",
|
|
4720
|
+
editorCommit: "确认",
|
|
4721
|
+
editorCancel: "取消",
|
|
4722
|
+
editorReset: "重置",
|
|
4723
|
+
extracting: "取色中…",
|
|
4724
|
+
extractDone: "已应用图片配色",
|
|
4725
|
+
extractFail: "未在这张图里找到鲜明的颜色,请换一张",
|
|
4726
|
+
eyedropper: "从图片取色",
|
|
4727
|
+
pickerTitle: "从图片取色",
|
|
4728
|
+
pickerHint: "移动鼠标预览颜色,点击图片选取为主题色",
|
|
4729
|
+
pickerClose: "关闭",
|
|
4730
|
+
crashTitle: "界面渲染出错",
|
|
4731
|
+
crashDesc: "设置面板遇到问题,点击下方按钮重置后重试",
|
|
4732
|
+
crashReset: "重置面板",
|
|
4733
|
+
exportTheme: "导出配置",
|
|
4734
|
+
importTheme: "导入配置",
|
|
4735
|
+
exportCardTitle: "导出配置",
|
|
4736
|
+
exportCardDesc: "把全部规则、每条规则的颜色/布局/透明度/模糊与图片打包为 dsh-background-by-model-theme.json",
|
|
4737
|
+
importCardTitle: "导入配置",
|
|
4738
|
+
importCardDesc: "从之前导出的 JSON 文件一键恢复全部规则(会覆盖当前规则与图片)",
|
|
4739
|
+
toastExportDone: "配置文件已开始下载",
|
|
4740
|
+
importDone: "已导入配置",
|
|
4741
|
+
importFail: "导入失败,文件格式不正确",
|
|
4742
|
+
recCardTitle: "推荐配置",
|
|
4743
|
+
recCardDesc: "从项目素材库下载作者维护的一整套配置(规则、壁纸与接口设置),一键替换当前的全部内容",
|
|
4744
|
+
recAdopt: "采用推荐配置",
|
|
4745
|
+
recBusy: "正在下载",
|
|
4746
|
+
recConfirmTitle: "替换当前的全部配置?",
|
|
4747
|
+
recConfirmBody: "会先删除你现在的全部规则与壁纸,再写入这份推荐配置。删除的文件无法恢复。",
|
|
4748
|
+
recStatRules: "条规则",
|
|
4749
|
+
recStatImages: "张壁纸",
|
|
4750
|
+
recConfirmOk: "删除并替换",
|
|
4751
|
+
recConfirmCancel: "取消",
|
|
4752
|
+
recDone: "已采用推荐配置",
|
|
4753
|
+
recFailNetwork: "推荐配置下载失败,请检查网络后重试",
|
|
4754
|
+
recFailNotProfile: "素材库返回的内容不是配置,请稍后重试",
|
|
4755
|
+
recFailNewer: "这份推荐配置需要更新的插件版本,未做任何改动",
|
|
4756
|
+
recFailTooMany: "这份推荐配置列出的图片数量超出限制,未做任何改动",
|
|
4757
|
+
recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
|
|
4758
|
+
recFailImage: "这张壁纸没拿到(素材库里缺这个文件):",
|
|
4759
|
+
recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
|
|
4760
|
+
footerTag: "按模型换背景插件",
|
|
4761
|
+
repoLink: "在 GitHub 上打开仓库",
|
|
4762
|
+
holidayTitle: "节日特殊背景",
|
|
4763
|
+
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)",
|
|
4764
|
+
trTitle: "切换效果",
|
|
4765
|
+
trEffect: "切换方式",
|
|
4766
|
+
trEffectFade: "淡入淡出",
|
|
4767
|
+
trEffectNone: "直接切换",
|
|
4768
|
+
trEffectZoom: "缩放淡入",
|
|
4769
|
+
trEffectSlide: "平移推入",
|
|
4770
|
+
trEasing: "缓动",
|
|
4771
|
+
trEasingEase: "标准",
|
|
4772
|
+
trEasingLinear: "匀速",
|
|
4773
|
+
trEasingOut: "先快后慢",
|
|
4774
|
+
trEasingInOut: "两端慢",
|
|
4775
|
+
trDuration: "切换时长",
|
|
4776
|
+
trPreview: "试放",
|
|
4777
|
+
trHint: "效果对所有换图生效:切模型、轮换、手动下一张、节日。「直接切换」和 0 秒一样是硬切",
|
|
4778
|
+
trReducedHint: "系统开启了「减少动态效果」,实际切换不会播放动画"
|
|
4779
|
+
};
|
|
4780
|
+
const en = {
|
|
4781
|
+
nav: "Model background",
|
|
4782
|
+
brandTag: "Per-model wallpaper",
|
|
4783
|
+
pageInterface: "Interface",
|
|
4784
|
+
pageModelBg: "Model background",
|
|
4785
|
+
pageProfile: "Profile",
|
|
4786
|
+
descInterface: "Tune opacity per surface to build depth. These settings are global and apply to every model",
|
|
4787
|
+
descModelBg: "Each rule is a match string plus a set of wallpapers and one look. On a model switch the list is scanned top-down for the first match string contained in the model name; if nothing hits, rule 1 is used. A rule can hold several images and rotate through them on a rhythm you set",
|
|
4788
|
+
descProfile: "Tune how the wallpaper switches, and back up every rule together with its image — or restore one from a file",
|
|
4789
|
+
uiTitle: "Interface",
|
|
4790
|
+
uiOpacity: "Opacity",
|
|
4791
|
+
uiOpacityBg: "Main background",
|
|
4792
|
+
uiOpacitySide: "Left panel",
|
|
4793
|
+
uiOpacityCard: "Cards & menus",
|
|
4794
|
+
uiOpacityCode: "Code blocks",
|
|
4795
|
+
uiOpacityInput: "Input & controls",
|
|
4796
|
+
uiSop: "Settings interface opacity",
|
|
4797
|
+
uiChatRegion: "Conversation text frame",
|
|
4798
|
+
uiTrajectory: "Trajectory view",
|
|
4799
|
+
uiPage: "Plugin pages",
|
|
4800
|
+
uiPreview: "Right panel",
|
|
4801
|
+
statusUnknownHint: "Without a detected model rule 1 is always used; switching the model once or waiting a moment retries automatically",
|
|
4802
|
+
statusSourceDefaultHint: "Only the host-wide default model could be read — that is not necessarily this session's selection. It self-corrects once this session's model selection is available.",
|
|
4803
|
+
statusNoteNoService: "The sessions service is not available yet (the session controller loads after this plugin); retrying automatically",
|
|
4804
|
+
statusNoteNoSession: "The sessions service is up but reports no current session yet",
|
|
4805
|
+
statusNoteNoProjection: "This session's model-selection projection is unreachable (no binding / projections)",
|
|
4806
|
+
statusNoteEmptySelection: "The model-selection projection exists but carries no value yet (selecting a model once writes it)",
|
|
4807
|
+
hostStaleHint: "The DSH host process is still running the older plugin, which cannot read the new config shape. To avoid dropping rules, this session changes the interface but holds every config write — restart DSH and multi-image plus rotation will save normally",
|
|
4808
|
+
tryoutTitle: "Match test",
|
|
4809
|
+
tryoutPlaceholder: "Type a model name, e.g. deepseek-flash",
|
|
4810
|
+
tryoutUseCurrent: "Use current model",
|
|
4811
|
+
tryoutHit: "matches rule",
|
|
4812
|
+
tryoutFallback: "no match · fallback rule",
|
|
4813
|
+
tryoutNone: "No usable rule (a rule needs to be enabled and carry an image or a theme color)",
|
|
4814
|
+
tryoutEmpty: "Type a model name to test the list",
|
|
4815
|
+
autoExtract: "Extract the theme color when an image is chosen",
|
|
4816
|
+
ruleAdd: "Add rule",
|
|
4817
|
+
ruleNoneHint: "No rules yet — use “Add rule” in the tab strip above",
|
|
4818
|
+
ruleFallbackBadge: "fallback",
|
|
4819
|
+
ruleMatchPlaceholder: "e.g. flash / glm (case-insensitive, empty never matches)",
|
|
4820
|
+
ruleEnabled: "Enabled",
|
|
4821
|
+
ruleUp: "Move up",
|
|
4822
|
+
ruleDown: "Move down",
|
|
4823
|
+
ruleRemove: "Remove rule",
|
|
4824
|
+
tabListLabel: "Rules",
|
|
4825
|
+
tabPick: "Pick a rule",
|
|
4826
|
+
tabMore: "More rules",
|
|
4827
|
+
tabUnnamed: "Unnamed rule",
|
|
4828
|
+
tabLive: "In use",
|
|
4829
|
+
tabRotating: "Rotating",
|
|
4830
|
+
tabOff: "Disabled",
|
|
4831
|
+
ruleRemoveConfirm: "Click again to delete",
|
|
4832
|
+
ruleNoImage: "No image yet",
|
|
4833
|
+
ruleFromUrl: "From URL",
|
|
4834
|
+
ruleUrlPlaceholder: "Paste an image URL https://…",
|
|
4835
|
+
ruleUrlApply: "Apply",
|
|
4836
|
+
ruleUrlCancel: "Cancel",
|
|
4837
|
+
ruleUrlApplying: "Loading…",
|
|
4838
|
+
ruleUrlBadHttp: "Only http/https image URLs are supported",
|
|
4839
|
+
ruleUrlFail: "Could not fetch the image",
|
|
4840
|
+
ruleImageRemove: "Remove",
|
|
4841
|
+
ruleEmptyHint: "This rule has no image and no theme color of its own: it has nothing to show, so matching and the fallback both skip it",
|
|
4842
|
+
ruleColorOnlyHint: "No image in this rule yet: no wallpaper is shown, and the interface is painted from this theme color alone",
|
|
4843
|
+
ruleImagePendingHint: "This image has no content yet — it appears once it loads; you can also replace it",
|
|
4844
|
+
ruleColor: "Theme color",
|
|
4845
|
+
ruleColorNone: "Follow system theme",
|
|
4846
|
+
ruleColorNoneHint: "Without a color this rule follows the system theme: the host palette paints the interface while the wallpaper and every opacity slider keep working",
|
|
4847
|
+
ruleColorPick: "Custom color",
|
|
4848
|
+
ruleColorPickHint: "Extracts from this image when it can, otherwise uses a default color",
|
|
4849
|
+
ruleColorNoImageHint: "This rule has no image to take a color from: pick a color to tint the interface alone, or add an image first",
|
|
4850
|
+
ruleColorPendingImageHint: "This image has no content yet, so there is nothing to extract from: pick a color directly, or wait for it to load",
|
|
4851
|
+
ruleColorTargetImage: "Applies to the selected image:",
|
|
4852
|
+
ruleColorTargetRule: "Applies to this rule (it has no image yet)",
|
|
4853
|
+
ruleColorExtract: "Extract from image",
|
|
4854
|
+
ruleLayout: "Layout mode",
|
|
4855
|
+
ruleLayoutNoImage: "Add an image to give each one its own layout mode",
|
|
4856
|
+
ruleFramingEdit: "Edit position",
|
|
4857
|
+
ruleOpacity: "Background opacity",
|
|
4858
|
+
ruleBrightness: "Background brightness",
|
|
4859
|
+
ruleBlur: "Background blur",
|
|
4860
|
+
ruleBlurHint: "These three sliders all treat the wallpaper layer; the opacity on the Interface page affects interface surfaces — they are independent",
|
|
4861
|
+
rotTitle: "Image rotation",
|
|
4862
|
+
rotEnable: "Rotate these images",
|
|
4863
|
+
rotNeedTwo: "At least two images are needed to rotate",
|
|
4864
|
+
rotEvery: "Dwell time per image",
|
|
4865
|
+
rotCustom: "Custom seconds",
|
|
4866
|
+
rotSeconds: "s",
|
|
4867
|
+
rotOrder: "Order",
|
|
4868
|
+
rotOrderSeq: "In order",
|
|
4869
|
+
rotOrderShuffle: "Shuffle",
|
|
4870
|
+
rotOnSwitch: "Also step on a model switch",
|
|
4871
|
+
rotNext: "Next image",
|
|
4872
|
+
rotNextInactive: "Only the active rule can step the background",
|
|
4873
|
+
rotPos: "Image",
|
|
4874
|
+
rotShowing: "On screen",
|
|
4875
|
+
rotAddImage: "Add",
|
|
4876
|
+
rotReplaceImage: "Replace",
|
|
4877
|
+
rotDragHint: "drag to reorder",
|
|
4878
|
+
rotDblHint: "double-click to show",
|
|
4879
|
+
rotEvery10s: "10 s",
|
|
4880
|
+
rotEvery30s: "30 s",
|
|
4881
|
+
rotEvery1m: "1 min",
|
|
4882
|
+
rotEvery5m: "5 min",
|
|
4883
|
+
rotEvery30m: "30 min",
|
|
4884
|
+
bgModeFit: "Fit",
|
|
4885
|
+
bgModeFill: "Fill",
|
|
4886
|
+
bgModeStretch: "Stretch",
|
|
4887
|
+
bgModeTile: "Tile",
|
|
4888
|
+
bgModeCenter: "Center",
|
|
4889
|
+
editorTitle: "Background editor",
|
|
4890
|
+
editorHint: "Drag to move, scroll to zoom",
|
|
4891
|
+
editorCommit: "Confirm",
|
|
4892
|
+
editorCancel: "Cancel",
|
|
4893
|
+
editorReset: "Reset",
|
|
4894
|
+
extracting: "Extracting…",
|
|
4895
|
+
extractDone: "Image colors applied",
|
|
4896
|
+
extractFail: "No vivid color found in this image, try another",
|
|
4897
|
+
eyedropper: "Eyedropper",
|
|
4898
|
+
pickerTitle: "Pick from image",
|
|
4899
|
+
pickerHint: "Hover to preview, click to pick as the theme color",
|
|
4900
|
+
pickerClose: "Close",
|
|
4901
|
+
crashTitle: "Section crashed",
|
|
4902
|
+
crashDesc: "The panel hit an error. Reset below to recover.",
|
|
4903
|
+
crashReset: "Reset panel",
|
|
4904
|
+
exportTheme: "Export",
|
|
4905
|
+
importTheme: "Import",
|
|
4906
|
+
exportCardTitle: "Export profile",
|
|
4907
|
+
exportCardDesc: "Bundle every rule — color, layout, opacity, blur — and its image into dsh-background-by-model-theme.json",
|
|
4908
|
+
importCardTitle: "Import profile",
|
|
4909
|
+
importCardDesc: "Restore every rule and image from a previously exported JSON file (replaces the current set)",
|
|
4910
|
+
toastExportDone: "Export started",
|
|
4911
|
+
importDone: "Profile imported",
|
|
4912
|
+
importFail: "Import failed — invalid file",
|
|
4913
|
+
recCardTitle: "Recommended profile",
|
|
4914
|
+
recCardDesc: "Download the author's own full setup — rules, wallpapers and interface settings — and replace everything with it",
|
|
4915
|
+
recAdopt: "Use recommended profile",
|
|
4916
|
+
recBusy: "Downloading",
|
|
4917
|
+
recConfirmTitle: "Replace your entire profile?",
|
|
4918
|
+
recConfirmBody: "Every rule and wallpaper you have is deleted first, then this profile is written in its place. Deleted files cannot be recovered.",
|
|
4919
|
+
recStatRules: "rules",
|
|
4920
|
+
recStatImages: "wallpapers",
|
|
4921
|
+
recConfirmOk: "Delete and replace",
|
|
4922
|
+
recConfirmCancel: "Cancel",
|
|
4923
|
+
recDone: "Recommended profile applied",
|
|
4924
|
+
recFailNetwork: "Could not download the recommended profile — check your connection and retry",
|
|
4925
|
+
recFailNotProfile: "The asset host answered with something that is not a profile — try again later",
|
|
4926
|
+
recFailNewer: "This profile needs a newer plugin version. Nothing was changed.",
|
|
4927
|
+
recFailTooMany: "The recommended profile names more images than the plugin will fetch. Nothing was changed.",
|
|
4928
|
+
recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
|
|
4929
|
+
recFailImage: "Could not fetch this wallpaper — the asset host has no such file:",
|
|
4930
|
+
recFailApply: "Could not write the recommended profile — your own is untouched",
|
|
4931
|
+
footerTag: "Per-model wallpaper plugin",
|
|
4932
|
+
repoLink: "Open the repository on GitHub",
|
|
4933
|
+
holidayTitle: "Holiday backgrounds",
|
|
4934
|
+
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)",
|
|
4935
|
+
trTitle: "Switch effect",
|
|
4936
|
+
trEffect: "Transition",
|
|
4937
|
+
trEffectFade: "Cross-fade",
|
|
4938
|
+
trEffectNone: "Instant",
|
|
4939
|
+
trEffectZoom: "Zoom in",
|
|
4940
|
+
trEffectSlide: "Slide in",
|
|
4941
|
+
trEasing: "Easing",
|
|
4942
|
+
trEasingEase: "Standard",
|
|
4943
|
+
trEasingLinear: "Linear",
|
|
4944
|
+
trEasingOut: "Ease out",
|
|
4945
|
+
trEasingInOut: "Ease in-out",
|
|
4946
|
+
trDuration: "Duration",
|
|
4947
|
+
trPreview: "Preview",
|
|
4948
|
+
trHint: "The effect applies to every change: model switch, rotation, manual next, holidays. “Instant” and 0 s are the same hard cut",
|
|
4949
|
+
trReducedHint: "Your system asks for reduced motion, so a real switch will not animate"
|
|
4950
|
+
};
|
|
4951
|
+
//#endregion
|
|
4952
|
+
//#region src/preset.ts
|
|
4953
|
+
/**
|
|
4954
|
+
* JSON with object keys in a fixed (sorted) order — the same string for the same
|
|
4955
|
+
* value, whatever order the object that carried it happened to build its keys in.
|
|
4956
|
+
*
|
|
4957
|
+
* `JSON.stringify` alone is not an identity for a value that came off the wire and
|
|
4958
|
+
* through a sanitizer: key order is an accident of how the object was assembled,
|
|
4959
|
+
* and a resume that missed on it would silently re-download everything (the safe
|
|
4960
|
+
* direction) while a resume that MATCHED on a value it should not have would be
|
|
4961
|
+
* the unsafe one — so this is deliberately total as well: anything JSON cannot
|
|
4962
|
+
* carry (a function, a `Symbol`, `undefined`) serializes as `null` and an
|
|
4963
|
+
* `undefined` property is skipped, exactly as `JSON.stringify` treats it.
|
|
4964
|
+
*/
|
|
4965
|
+
function canonical(value) {
|
|
4966
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
4967
|
+
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
4968
|
+
return JSON.stringify(value) ?? "null";
|
|
4969
|
+
}
|
|
4970
|
+
/**
|
|
4971
|
+
* The identity of one revision of the profile: the manifest's shape version plus
|
|
4972
|
+
* the config, in canonical form.
|
|
4973
|
+
*
|
|
4974
|
+
* The profile is served at ONE version of the assets repository, but a version
|
|
4975
|
+
* reference is only as fixed as the tag behind it: re-point the tag and every
|
|
4976
|
+
* edge that has not been purged keeps answering with the previous bytes of the
|
|
4977
|
+
* same URL. So "the profile" is still not guaranteed to be one fixed thing, and
|
|
4978
|
+
* the bytes a previous attempt is holding are only known to belong to what is
|
|
4979
|
+
* being installed when that config comes back the same. Order-insensitivity is
|
|
4980
|
+
* what lets this be a comparison of VALUES rather than of spellings —
|
|
4981
|
+
* `normalizeConfig` (./schema) rebuilds the config it reads, so pinning the key
|
|
4982
|
+
* to the exact key order the wire happened to use would make the hold useless
|
|
4983
|
+
* the moment anything upstream normalized differently.
|
|
4984
|
+
*/
|
|
4985
|
+
function presetKey(version, config) {
|
|
4986
|
+
return `${version}\n${canonical(config)}`;
|
|
4987
|
+
}
|
|
4988
|
+
/**
|
|
4989
|
+
* The bytes of a failed attempt that may be reused for the revision `key` names.
|
|
4990
|
+
*
|
|
4991
|
+
* A fresh object every time, never the held record itself: the caller fills it in
|
|
4992
|
+
* as the rest of the profile arrives, and handing out the same mutable record
|
|
4993
|
+
* would let a re-fetch of one file edit the state the NEXT attempt resumes from.
|
|
4994
|
+
* An empty answer — nothing held, or held for a different revision — is the honest
|
|
4995
|
+
* one either way: the pool then asks for every slot the config names.
|
|
4996
|
+
*/
|
|
4997
|
+
function presetResume(held, key) {
|
|
4998
|
+
if (held === null || held.key !== key) return {};
|
|
4999
|
+
return { ...held.images };
|
|
5000
|
+
}
|
|
5001
|
+
/**
|
|
5002
|
+
* The slots of `slots` that `images` does not hold yet, in the config's own order.
|
|
5003
|
+
*
|
|
5004
|
+
* Order matters in the only way it can here: it decides which file a worker takes
|
|
5005
|
+
* first, and the files are not the same size. Only names the CONFIG asked for are
|
|
5006
|
+
* ever returned, so a held picture nothing points at — a slot the author removed
|
|
5007
|
+
* from the profile — can never be fetched again or shipped.
|
|
5008
|
+
*/
|
|
5009
|
+
function presetMissing(slots, images) {
|
|
5010
|
+
return slots.filter((slot) => images[slot] === void 0);
|
|
5011
|
+
}
|
|
5012
|
+
//#endregion
|
|
5013
|
+
//#region src/client/repaint.ts
|
|
5014
|
+
/**
|
|
5015
|
+
* The repaint decision, as a pure function.
|
|
5016
|
+
*
|
|
5017
|
+
* Every edit to a rule ends with the same question: does the interface have to be
|
|
5018
|
+
* repainted, or is refreshing the panel's readout enough? Repainting is the
|
|
5019
|
+
* expensive half (the theme skin is re-registered, the wallpaper layer is
|
|
5020
|
+
* rewritten, the host's theme is switched), so it must not be done for a rule
|
|
5021
|
+
* that is not the one on screen — but it MUST be done the moment the edit
|
|
5022
|
+
* changes what should be on screen.
|
|
5023
|
+
*
|
|
5024
|
+
* The naive test — "is the edited rule the active one?" — is wrong in exactly the
|
|
5025
|
+
* case that kept being reported as "更新的不太及时,只有我动调色盘或者切模型的时候
|
|
5026
|
+
* 才出现": an edit that CREATES the winner. Uploading a rule's first picture, or
|
|
5027
|
+
* an auto-extracted color landing on a rule that had none, makes that rule
|
|
5028
|
+
* paintable *through that very edit* (see `ruleCanPaint`), so the rule is not the
|
|
5029
|
+
* active one at the moment of the check; the wallpaper and the skin then only
|
|
5030
|
+
* showed up later, when an unrelated drag or model switch happened to re-run the
|
|
5031
|
+
* resolution.
|
|
5032
|
+
*
|
|
5033
|
+
* Sampling the winner on BOTH sides of the edit is what makes such a change land
|
|
5034
|
+
* at once, and `editedId === winnerAfter` additionally covers the edit that does
|
|
5035
|
+
* not move the resolution but changes the look of the rule that IS painting (a
|
|
5036
|
+
* color, a slider, a framing).
|
|
5037
|
+
*
|
|
5038
|
+
* `null` is "nothing of ours is painting" (the interface falls back to the host's
|
|
5039
|
+
* own look), which is a winner like any other id.
|
|
5040
|
+
*
|
|
5041
|
+
* Kept free of imports on purpose: `scripts/repaint-check.ts` exercises it on
|
|
5042
|
+
* plain node, the same way `rotation.ts` is exercised. A rule's `color`/`match`
|
|
5043
|
+
* are the inputs that make the resolution move, which is why they are the fields
|
|
5044
|
+
* whose write paths all end here.
|
|
5045
|
+
*/
|
|
5046
|
+
function shouldRepaint(editedId, winnerBefore, winnerAfter) {
|
|
5047
|
+
return winnerAfter === editedId || winnerAfter !== winnerBefore;
|
|
5048
|
+
}
|
|
5049
|
+
//#endregion
|
|
4445
5050
|
//#region src/client/components/ErrorBoundary.tsx
|
|
4446
5051
|
/**
|
|
4447
5052
|
* Catches render errors from the theme section subtree (wheel, sliders, editor)
|
|
@@ -7255,10 +7860,11 @@ window.__ModuleLoader__.load({
|
|
|
7255
7860
|
/**
|
|
7256
7861
|
* dsh-background-by-model — browser half entry.
|
|
7257
7862
|
*
|
|
7258
|
-
* Wires the plugin lifecycle: rule resolution
|
|
7259
|
-
*
|
|
7260
|
-
*
|
|
7261
|
-
* (
|
|
7863
|
+
* Wires the plugin lifecycle and nothing else: rule resolution, the repaint gate,
|
|
7864
|
+
* the settings section's actions and the teardown effects. The machinery lives in
|
|
7865
|
+
* the sibling modules — appearance (skin, palette, restores), appearance-checks and
|
|
7866
|
+
* watchdogs (recurring checks on one schedule), rotation-timer, boot, model-state,
|
|
7867
|
+
* auto-extract, theme-file, nav-icon, wallpaper, state, modelbg, rpc.
|
|
7262
7868
|
*/
|
|
7263
7869
|
const name = "dsh-background-by-model";
|
|
7264
7870
|
const inject = [
|
|
@@ -7267,7 +7873,6 @@ window.__ModuleLoader__.load({
|
|
|
7267
7873
|
"theme",
|
|
7268
7874
|
"connection"
|
|
7269
7875
|
];
|
|
7270
|
-
const CUSTOM_ID = "custom-color";
|
|
7271
7876
|
/**
|
|
7272
7877
|
* Ceiling on what one recommended profile may add up to, across every wallpaper.
|
|
7273
7878
|
*
|
|
@@ -7297,76 +7902,13 @@ window.__ModuleLoader__.load({
|
|
|
7297
7902
|
const PRESET_CONCURRENCY = 4;
|
|
7298
7903
|
function apply(ctx) {
|
|
7299
7904
|
initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
const
|
|
7306
|
-
|
|
7307
|
-
return theme === void 0 ? null : theme.colorScheme;
|
|
7308
|
-
};
|
|
7309
|
-
const registerCustom = (h, s, l) => {
|
|
7310
|
-
const { colorScheme, tokens } = genTokens(h, s, l);
|
|
7311
|
-
if (skinScheme === colorScheme && registeredScheme() !== null) {
|
|
7312
|
-
if (ctx.theme.getTheme().preference !== CUSTOM_ID) ctx.theme.setTheme(CUSTOM_ID);
|
|
7313
|
-
return;
|
|
7314
|
-
}
|
|
7315
|
-
customDispose?.();
|
|
7316
|
-
try {
|
|
7317
|
-
customDispose = ctx.theme.register({
|
|
7318
|
-
id: CUSTOM_ID,
|
|
7319
|
-
colorScheme,
|
|
7320
|
-
tokens
|
|
7321
|
-
});
|
|
7322
|
-
} catch {
|
|
7323
|
-
customDispose = null;
|
|
7324
|
-
}
|
|
7325
|
-
skinScheme = registeredScheme() ?? colorScheme;
|
|
7326
|
-
if (ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
|
|
7327
|
-
};
|
|
7328
|
-
/** A rule without a saved color means "follow the system theme". */
|
|
7329
|
-
const dropCustom = () => {
|
|
7330
|
-
customDispose?.();
|
|
7331
|
-
customDispose = null;
|
|
7332
|
-
skinScheme = null;
|
|
7333
|
-
try {
|
|
7334
|
-
if (ctx.theme.getTheme().preference === CUSTOM_ID) ctx.theme.setTheme("system");
|
|
7335
|
-
} catch {}
|
|
7336
|
-
};
|
|
7337
|
-
/**
|
|
7338
|
-
* Re-emit the interface palette for whatever is painted RIGHT NOW.
|
|
7339
|
-
*
|
|
7340
|
-
* Called from two places, and the second one is why this is a function: a
|
|
7341
|
-
* model switch (`applyActive`) and a ROTATION STEP (`paintImage`). A theme color
|
|
7342
|
-
* belongs to an image since 0.7.1, so stepping to the next picture changes the
|
|
7343
|
-
* palette as much as switching models does — and a step that only repainted the
|
|
7344
|
-
* wallpaper would leave the previous picture's color on the interface until
|
|
7345
|
-
* something else happened to re-run an apply (dragging the wheel, switching
|
|
7346
|
-
* models), which is precisely the "it only updates when I touch the color wheel"
|
|
7347
|
-
* report this plugin already had once.
|
|
7348
|
-
*
|
|
7349
|
-
* The debounce is for the OTHER caller: a color drag runs a full apply dozens of
|
|
7350
|
-
* times per second, and the host-visible skin switch (dispose + register +
|
|
7351
|
-
* activate) must not happen on every pointer move. A rotation step pays the same
|
|
7352
|
-
* 60 ms, which is invisible next to a dwell time of 5 s or more.
|
|
7353
|
-
*/
|
|
7354
|
-
const applyPalette = () => {
|
|
7355
|
-
const color = activeColor();
|
|
7356
|
-
if (skinTimer !== null) window.clearTimeout(skinTimer);
|
|
7357
|
-
if (color === null) {
|
|
7358
|
-
skinTimer = null;
|
|
7359
|
-
dropCustom();
|
|
7360
|
-
} else skinTimer = window.setTimeout(() => {
|
|
7361
|
-
skinTimer = null;
|
|
7362
|
-
registerCustom(color[0], color[1], color[2]);
|
|
7363
|
-
}, 60);
|
|
7364
|
-
syncHostScheme();
|
|
7365
|
-
};
|
|
7366
|
-
ctx.effect(() => () => {
|
|
7367
|
-
if (skinTimer !== null) window.clearTimeout(skinTimer);
|
|
7368
|
-
customDispose?.();
|
|
7369
|
-
}, "dsh-background-by-model: skin dispose");
|
|
7905
|
+
const appearance = createAppearance({
|
|
7906
|
+
ctx,
|
|
7907
|
+
refresh: () => applyWp()
|
|
7908
|
+
});
|
|
7909
|
+
ctx.effect(() => () => appearance.dispose(), "dsh-background-by-model: skin dispose");
|
|
7910
|
+
const applyPalette = () => appearance.applyPalette();
|
|
7911
|
+
const syncHostScheme = () => appearance.syncHostScheme();
|
|
7370
7912
|
const styleEl = document.createElement("style");
|
|
7371
7913
|
styleEl.dataset.plugin = "dsh-background-by-model";
|
|
7372
7914
|
markOwnSheet(styleEl);
|
|
@@ -7377,10 +7919,8 @@ window.__ModuleLoader__.load({
|
|
|
7377
7919
|
}, "dsh-background-by-model: gradient");
|
|
7378
7920
|
let rev = 0;
|
|
7379
7921
|
let rulesRev = 0;
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
let modelNote = "waiting";
|
|
7383
|
-
let modelKey = "\0";
|
|
7922
|
+
/** The model the rules are matched against, and whether it actually moved. */
|
|
7923
|
+
const model = createModelTracker();
|
|
7384
7924
|
const store = defineStore({
|
|
7385
7925
|
init: () => ({
|
|
7386
7926
|
url: null,
|
|
@@ -7431,21 +7971,8 @@ window.__ModuleLoader__.load({
|
|
|
7431
7971
|
const sync = () => {
|
|
7432
7972
|
rev++;
|
|
7433
7973
|
const rot = rotationFacts();
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
/**
|
|
7437
|
-
* Publish the scheme the host's own preference resolves to. The theme service
|
|
7438
|
-
* is the only place that knows it (a custom active theme included); the
|
|
7439
|
-
* wallpaper layer needs it to hand `data-ds-dark-theme` back in the host's own
|
|
7440
|
-
* form while a color-less rule follows that theme, instead of leaving the flag
|
|
7441
|
-
* off — which pins the host to its LIGHT palette on a dark system.
|
|
7442
|
-
*/
|
|
7443
|
-
const syncHostScheme = () => {
|
|
7444
|
-
try {
|
|
7445
|
-
setHostScheme(ctx.theme.getTheme().active.colorScheme === "dark");
|
|
7446
|
-
} catch {
|
|
7447
|
-
setHostScheme(null);
|
|
7448
|
-
}
|
|
7974
|
+
const m = model.state;
|
|
7975
|
+
bound?.sync(rWp(), rev, rulesRev, m.label !== "" ? m.label : m.text, m.source, m.note, activeRuleId, activeMatched, rot.index, rot.total, rot.rotating, hostIsLegacy());
|
|
7449
7976
|
};
|
|
7450
7977
|
/**
|
|
7451
7978
|
* Which rule wins for the current model RIGHT NOW, and whether it was a
|
|
@@ -7464,7 +7991,7 @@ window.__ModuleLoader__.load({
|
|
|
7464
7991
|
return holiday !== null ? {
|
|
7465
7992
|
rule: holiday,
|
|
7466
7993
|
matched: false
|
|
7467
|
-
} : matchRule(cfg.rules,
|
|
7994
|
+
} : matchRule(cfg.rules, model.state.text);
|
|
7468
7995
|
};
|
|
7469
7996
|
/** Id of the rule that would paint right now; `null` = the host's own look. */
|
|
7470
7997
|
const winnerId = () => resolveRule().rule?.id ?? null;
|
|
@@ -7501,9 +8028,6 @@ window.__ModuleLoader__.load({
|
|
|
7501
8028
|
applyWp();
|
|
7502
8029
|
sync();
|
|
7503
8030
|
};
|
|
7504
|
-
let rotTimer = null;
|
|
7505
|
-
/** Identity of the schedule currently armed; a change means "re-arm". */
|
|
7506
|
-
let rotSig = "";
|
|
7507
8031
|
/** Warm one image so a later switch fades toward bytes that are already here. */
|
|
7508
8032
|
const warmImage = (rule, idx) => {
|
|
7509
8033
|
const slot = rule.images[idx]?.slot;
|
|
@@ -7524,64 +8048,18 @@ window.__ModuleLoader__.load({
|
|
|
7524
8048
|
sync();
|
|
7525
8049
|
});
|
|
7526
8050
|
};
|
|
7527
|
-
const
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
}
|
|
7535
|
-
setRotIndex(next);
|
|
7536
|
-
paintImage(rule, next);
|
|
7537
|
-
const after = nextIndex(rule.images.length, rule.rotate.order, next);
|
|
7538
|
-
if (after !== null) warmImage(rule, after);
|
|
7539
|
-
};
|
|
7540
|
-
/**
|
|
7541
|
-
* Arm (or re-arm) the rotation for the active rule.
|
|
7542
|
-
*
|
|
7543
|
-
* Idempotent by SIGNATURE rather than by "already armed": a slider drag on the
|
|
7544
|
-
* active rule re-runs the whole apply dozens of times per second, and restarting
|
|
7545
|
-
* the interval on each of those would mean the dwell timer never elapses while
|
|
7546
|
-
* the user is dragging (and a 10 s rotation would never advance). The signature
|
|
7547
|
-
* carries exactly the inputs the schedule depends on, so a real change — another
|
|
7548
|
-
* rule, another interval, the tab becoming visible again — re-arms while a
|
|
7549
|
-
* repaint does not.
|
|
7550
|
-
*/
|
|
7551
|
-
const scheduleRotation = (force = false) => {
|
|
7552
|
-
const rule = activeRule();
|
|
7553
|
-
const sig = rule === null ? "" : [
|
|
7554
|
-
rule.id,
|
|
7555
|
-
rule.rotate.enabled,
|
|
7556
|
-
rule.rotate.intervalMs,
|
|
7557
|
-
rule.rotate.order,
|
|
7558
|
-
rule.images.length,
|
|
7559
|
-
document.hidden
|
|
7560
|
-
].join("|");
|
|
7561
|
-
if (!force && sig === rotSig) return;
|
|
7562
|
-
rotSig = sig;
|
|
7563
|
-
if (rotTimer !== null) {
|
|
7564
|
-
window.clearInterval(rotTimer);
|
|
7565
|
-
rotTimer = null;
|
|
7566
|
-
}
|
|
7567
|
-
if (rule === null || document.hidden) return;
|
|
7568
|
-
if (!isRotating(rule.images.length, rule.rotate.enabled, rule.rotate.intervalMs)) return;
|
|
7569
|
-
const next = nextIndex(rule.images.length, rule.rotate.order, imageIndexOf(rule));
|
|
7570
|
-
if (next !== null) warmImage(rule, next);
|
|
7571
|
-
rotTimer = window.setInterval(rotateTick, rule.rotate.intervalMs);
|
|
7572
|
-
};
|
|
7573
|
-
ctx.effect(() => () => {
|
|
7574
|
-
if (rotTimer !== null) {
|
|
7575
|
-
window.clearInterval(rotTimer);
|
|
7576
|
-
rotTimer = null;
|
|
7577
|
-
}
|
|
7578
|
-
}, "dsh-background-by-model: rotation timer");
|
|
7579
|
-
const onVisibility = () => {
|
|
7580
|
-
scheduleRotation();
|
|
7581
|
-
sync();
|
|
7582
|
-
};
|
|
8051
|
+
const rotation = createRotation({
|
|
8052
|
+
paintImage,
|
|
8053
|
+
warmImage,
|
|
8054
|
+
sync
|
|
8055
|
+
});
|
|
8056
|
+
const scheduleRotation = (force = false) => rotation.schedule(force);
|
|
8057
|
+
const onVisibility = () => rotation.onVisibility();
|
|
7583
8058
|
document.addEventListener("visibilitychange", onVisibility);
|
|
7584
|
-
ctx.effect(() => () =>
|
|
8059
|
+
ctx.effect(() => () => {
|
|
8060
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
8061
|
+
rotation.dispose();
|
|
8062
|
+
}, "dsh-background-by-model: rotation");
|
|
7585
8063
|
/**
|
|
7586
8064
|
* The holiday the background must follow right now, or null.
|
|
7587
8065
|
*
|
|
@@ -7595,57 +8073,27 @@ window.__ModuleLoader__.load({
|
|
|
7595
8073
|
sync();
|
|
7596
8074
|
watchParts();
|
|
7597
8075
|
const offModel = watchModel(ctx, (text, label, source, note) => {
|
|
7598
|
-
const
|
|
7599
|
-
if (
|
|
7600
|
-
|
|
7601
|
-
modelKey = key;
|
|
7602
|
-
modelText = text;
|
|
7603
|
-
modelSource = source;
|
|
7604
|
-
modelNote = note;
|
|
7605
|
-
setModelLabel(label !== "" ? label : text);
|
|
7606
|
-
if (textChanged) applyActive(true);
|
|
8076
|
+
const change = model.update(text, label, source, note);
|
|
8077
|
+
if (!change.changed) return;
|
|
8078
|
+
if (change.textChanged) applyActive(true);
|
|
7607
8079
|
else sync();
|
|
7608
8080
|
});
|
|
7609
8081
|
ctx.effect(() => () => offModel(), "dsh-background-by-model: model watch");
|
|
7610
8082
|
(async () => {
|
|
7611
|
-
if (
|
|
8083
|
+
if (model.state.text !== "") return;
|
|
7612
8084
|
const fallback = await readDefaultModel();
|
|
7613
|
-
if (fallback !== null &&
|
|
7614
|
-
|
|
7615
|
-
modelText = fallback;
|
|
7616
|
-
modelSource = "default";
|
|
7617
|
-
modelNote = "fallback";
|
|
7618
|
-
setModelLabel(fallback);
|
|
8085
|
+
if (fallback !== null && model.state.text === "") {
|
|
8086
|
+
model.adopt(fallback, "default", "fallback");
|
|
7619
8087
|
applyActive();
|
|
7620
8088
|
}
|
|
7621
8089
|
})();
|
|
7622
|
-
let holidayKey = activeHoliday() ?? "";
|
|
7623
|
-
const holidayTimer = window.setInterval(() => {
|
|
7624
|
-
const next = activeHoliday() ?? "";
|
|
7625
|
-
if (next === holidayKey) return;
|
|
7626
|
-
holidayKey = next;
|
|
7627
|
-
const slot = todayHolidaySlot();
|
|
7628
|
-
if (slot === null) applyActive();
|
|
7629
|
-
else loadSlot(slot).then(() => applyActive());
|
|
7630
|
-
}, 3e4);
|
|
7631
|
-
ctx.effect(() => () => {
|
|
7632
|
-
window.clearInterval(holidayTimer);
|
|
7633
|
-
}, "dsh-background-by-model: holiday rollover");
|
|
7634
|
-
/** Read one slot's bytes into the cache unless they are already there. */
|
|
7635
|
-
const loadSlot = async (slot) => {
|
|
7636
|
-
if (imageOf(slot) !== null) return;
|
|
7637
|
-
const url = await readImage(slot);
|
|
7638
|
-
if (url !== null) setImage(slot, url);
|
|
7639
|
-
};
|
|
7640
8090
|
/**
|
|
7641
8091
|
* The slot of the holiday that could paint TODAY, or null.
|
|
7642
8092
|
*
|
|
7643
|
-
* Gated on the CALENDAR, not merely on the switch
|
|
7644
|
-
*
|
|
7645
|
-
*
|
|
7646
|
-
*
|
|
7647
|
-
* channel, it must avoid starting a download at all. On every other day this
|
|
7648
|
-
* returns null and nothing is fetched, cached or paid for.
|
|
8093
|
+
* Gated on the CALENDAR, not merely on the switch: the art does not ship in the
|
|
8094
|
+
* package any more, so the first read on the day sends the node half to the
|
|
8095
|
+
* network. On every other day this returns null and nothing is fetched, cached
|
|
8096
|
+
* or paid for.
|
|
7649
8097
|
*/
|
|
7650
8098
|
const todayHolidaySlot = () => {
|
|
7651
8099
|
if (!cfg.holidays.enabled) return null;
|
|
@@ -7655,57 +8103,38 @@ window.__ModuleLoader__.load({
|
|
|
7655
8103
|
if (item === null || !item.enabled) return null;
|
|
7656
8104
|
return ruleSlots(item)[0] ?? null;
|
|
7657
8105
|
};
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
* multi-image existed — a rule card draws its first image as its thumbnail, so
|
|
7664
|
-
* the settings page looks the same as it always did. Everything past the first
|
|
7665
|
-
* image of a rule is read ON DEMAND: by the rotation (which warms the image it
|
|
7666
|
-
* is about to need as soon as its schedule is armed) and by the settings page
|
|
7667
|
-
* when a card is expanded. Hydrating every image of every rule here would mean
|
|
7668
|
-
* twenty full-size data URLs over this channel before the first frame on a
|
|
7669
|
-
* five-rule setup with four pictures each, which is the one cost this feature
|
|
7670
|
-
* must not pay.
|
|
7671
|
-
*
|
|
7672
|
-
* The slot list the store knows about (`readPersisted().slots`) is deliberately
|
|
7673
|
-
* NOT hydrated any more: an orphaned slot that no rule references has no card to
|
|
7674
|
-
* appear on, so pulling its bytes over the channel at boot is pure waste. It
|
|
7675
|
-
* stays on disk — and is still reported by the node half — until a rule
|
|
7676
|
-
* references it again or it is deleted with its rule.
|
|
7677
|
-
*/
|
|
7678
|
-
const bootSlots = () => {
|
|
7679
|
-
const holiday = todayHolidaySlot();
|
|
7680
|
-
const firsts = cfg.rules.map((r) => r.images[0]?.slot).filter((slot) => slot !== void 0);
|
|
7681
|
-
const current = prioritySlot();
|
|
7682
|
-
return Array.from(/* @__PURE__ */ new Set([
|
|
7683
|
-
...firsts,
|
|
7684
|
-
...holiday === null ? [] : [holiday],
|
|
7685
|
-
...current === null ? [] : [current]
|
|
7686
|
-
]));
|
|
8106
|
+
const activeHolidayId = () => activeHoliday();
|
|
8107
|
+
/** Load one slot and repaint once its bytes are here (the art download). */
|
|
8108
|
+
const loadAndRepaint = async (slot) => {
|
|
8109
|
+
await loadSlot(slot);
|
|
8110
|
+
applyActive();
|
|
7687
8111
|
};
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
8112
|
+
const schedule = createSchedule();
|
|
8113
|
+
ctx.effect(() => () => schedule.dispose(), "dsh-background-by-model: schedule");
|
|
8114
|
+
ctx.effect(appearance.armRestores(), "dsh-background-by-model: boot restore");
|
|
8115
|
+
const checkHost = {
|
|
8116
|
+
restore: appearance.watch,
|
|
8117
|
+
activeColor,
|
|
8118
|
+
refreshHostPalette: () => {
|
|
8119
|
+
syncHostScheme();
|
|
8120
|
+
applyWp();
|
|
8121
|
+
},
|
|
8122
|
+
refresh: () => applyWp(),
|
|
8123
|
+
applyActive,
|
|
8124
|
+
activeHolidayId,
|
|
8125
|
+
todayHolidaySlot,
|
|
8126
|
+
loadAndRepaint
|
|
8127
|
+
};
|
|
8128
|
+
schedule.add(themeWatchdog(checkHost));
|
|
8129
|
+
schedule.add(holidayRollover(checkHost));
|
|
7698
8130
|
(async () => {
|
|
7699
8131
|
const persisted = await loadPersisted();
|
|
7700
8132
|
if (persisted !== null) {
|
|
7701
8133
|
adoptConfig(persisted.config);
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
await loadSlot(slot);
|
|
7707
|
-
if (slot === first) applyActive();
|
|
7708
|
-
}
|
|
8134
|
+
await hydrateBoot({
|
|
8135
|
+
modelText: model.state.text,
|
|
8136
|
+
holidaySlot: todayHolidaySlot()
|
|
8137
|
+
}, applyActive);
|
|
7709
8138
|
}
|
|
7710
8139
|
applyActive();
|
|
7711
8140
|
})();
|
|
@@ -7714,10 +8143,7 @@ window.__ModuleLoader__.load({
|
|
|
7714
8143
|
}, "dsh-background-by-model: wp cleanup");
|
|
7715
8144
|
ctx.effect(() => ctx.on("theme/change", () => {
|
|
7716
8145
|
syncHostScheme();
|
|
7717
|
-
|
|
7718
|
-
const snapshot = ctx.theme.getTheme();
|
|
7719
|
-
if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
|
|
7720
|
-
}
|
|
8146
|
+
appearance.reassert();
|
|
7721
8147
|
applyWp();
|
|
7722
8148
|
}), "dsh-background-by-model: theme change");
|
|
7723
8149
|
let frame = 0;
|
|
@@ -7754,56 +8180,22 @@ window.__ModuleLoader__.load({
|
|
|
7754
8180
|
* their own palette since 0.7.1 and the one the user just added is the one they
|
|
7755
8181
|
* are looking at. Omitted (or unknown) falls back to the rule's first image.
|
|
7756
8182
|
*
|
|
7757
|
-
* The question is asked of the IMAGE, not of the rule: a rule whose pictures
|
|
7758
|
-
* were all cleared and then re-filled must theme each new picture, and a rule
|
|
7759
|
-
* with a color of its own still has no say over the colors of its pictures.
|
|
7760
|
-
*
|
|
7761
8183
|
* The decode is asynchronous, so the "still empty?" question is asked AGAIN on
|
|
7762
8184
|
* arrival: a color chosen while it ran must win over the one being computed.
|
|
8185
|
+
* Sequential over a batch on purpose: a batch is a handful of full-size photos,
|
|
8186
|
+
* and ten parallel decodes buy nothing while landing the colors in list order
|
|
8187
|
+
* does (the card fills in from its first picture).
|
|
7763
8188
|
*/
|
|
7764
|
-
const
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
};
|
|
7771
|
-
const image = target(ruleById(id));
|
|
7772
|
-
if (image === void 0 || image.color !== null) return;
|
|
7773
|
-
const url = displayImageOf(image.slot);
|
|
7774
|
-
if (url === null) return;
|
|
7775
|
-
let hsl = null;
|
|
7776
|
-
try {
|
|
7777
|
-
hsl = await extractWallpaperColor(url, image.bgState);
|
|
7778
|
-
} catch {
|
|
7779
|
-
hsl = null;
|
|
8189
|
+
const extract = createAutoExtract({
|
|
8190
|
+
paintable: (slot) => displayImageOf(slot),
|
|
8191
|
+
repaint: (id, winner) => repaintIfMoved(id, winner),
|
|
8192
|
+
winnerId,
|
|
8193
|
+
bumpRulesRev: () => {
|
|
8194
|
+
rulesRev++;
|
|
7780
8195
|
}
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
const winner = winnerId();
|
|
7785
|
-
current.color = hsl;
|
|
7786
|
-
rulesRev++;
|
|
7787
|
-
saveConfig();
|
|
7788
|
-
repaintIfMoved(id, winner);
|
|
7789
|
-
};
|
|
7790
|
-
/**
|
|
7791
|
-
* The batch form, for one upload that carried several pictures.
|
|
7792
|
-
*
|
|
7793
|
-
* EVERY picture that arrived is themed from its own bytes, the first ones
|
|
7794
|
-
* included. The single-slot reading of an upload ("the last image added is the
|
|
7795
|
-
* one being looked at") is true of the CARD's selection and false of the
|
|
7796
|
-
* palette: since a theme color belongs to an image, a batch that themed only
|
|
7797
|
-
* its last file left the rest of a fresh rotation on the system theme, with
|
|
7798
|
-
* nothing in the interface to say why.
|
|
7799
|
-
*
|
|
7800
|
-
* Sequential on purpose: a batch is a handful of full-size photos, and ten
|
|
7801
|
-
* parallel decodes buy nothing the user can see, while landing the colors in
|
|
7802
|
-
* list order does (the card fills in from its first picture).
|
|
7803
|
-
*/
|
|
7804
|
-
const maybeAutoExtractEach = async (id, slots) => {
|
|
7805
|
-
for (const slot of slots) await maybeAutoExtract(id, slot);
|
|
7806
|
-
};
|
|
8196
|
+
});
|
|
8197
|
+
const maybeAutoExtract = (id, slot) => extract.one(id, slot);
|
|
8198
|
+
const maybeAutoExtractEach = (id, slots) => extract.each(id, slots);
|
|
7807
8199
|
const activeLang = () => {
|
|
7808
8200
|
try {
|
|
7809
8201
|
const snapshot = ctx.locale.getSnapshot();
|
|
@@ -7815,74 +8207,8 @@ window.__ModuleLoader__.load({
|
|
|
7815
8207
|
/** The model-resolution facts the host self-check reports, read on demand. */
|
|
7816
8208
|
const readModelFacts = () => {
|
|
7817
8209
|
const rule = activeRuleId === null ? null : ruleById(activeRuleId);
|
|
7818
|
-
return
|
|
7819
|
-
text: modelText,
|
|
7820
|
-
source: modelSource,
|
|
7821
|
-
note: modelNote,
|
|
7822
|
-
rule: rule === null ? "" : rule.match !== "" ? rule.match : rule.id,
|
|
7823
|
-
matched: activeMatched
|
|
7824
|
-
};
|
|
7825
|
-
};
|
|
7826
|
-
/**
|
|
7827
|
-
* Write a whole theme file into the store: its rules, its wallpapers, nothing
|
|
7828
|
-
* else. The ONE door both imports come through — the file the user picked and
|
|
7829
|
-
* the profile downloaded from the project's asset host — so a downloaded
|
|
7830
|
-
* profile cannot arrive by a laxer route than a hand-picked file does.
|
|
7831
|
-
*
|
|
7832
|
-
* `wipeFirst` is the entire difference between them, and it belongs to the
|
|
7833
|
-
* recommended profile alone. `nextSlot` hands slots out as `m1`, `m2`, … — a
|
|
7834
|
-
* counter, not a random token — so a downloaded profile and the store it lands
|
|
7835
|
-
* in collide on those names as a matter of course rather than by coincidence.
|
|
7836
|
-
* Emptying the store first is what makes "replace everything with this"
|
|
7837
|
-
* literally true, instead of overwriting whichever of the user's own pictures
|
|
7838
|
-
* happened to share a name. The file path keeps the older behaviour: it
|
|
7839
|
-
* replaces the rules but pre-emptively deletes nothing.
|
|
7840
|
-
*
|
|
7841
|
-
* The wipe happens HERE rather than before the download, and that ordering is
|
|
7842
|
-
* the one thing worth being pedantic about: an unreachable CDN must not cost
|
|
7843
|
-
* the user their configuration on the way to a profile that never arrived. By
|
|
7844
|
-
* the time this runs the bytes are already in hand, so the caller has nothing
|
|
7845
|
-
* left to fail at.
|
|
7846
|
-
*/
|
|
7847
|
-
const applyThemeFile = async (data, wipeFirst) => {
|
|
7848
|
-
if (typeof data.version === "number" && data.version > 9) return false;
|
|
7849
|
-
if (typeof data.config !== "object" || data.config === null) return false;
|
|
7850
|
-
if (wipeFirst) {
|
|
7851
|
-
await resetStore();
|
|
7852
|
-
resetConfig();
|
|
7853
|
-
}
|
|
7854
|
-
adoptConfig(data.config);
|
|
7855
|
-
const incoming = data.images ?? {};
|
|
7856
|
-
const keep = /* @__PURE__ */ new Set();
|
|
7857
|
-
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
7858
|
-
keep.add(slot);
|
|
7859
|
-
const raw = incoming[slot];
|
|
7860
|
-
if (typeof raw === "string" && /^data:image\//.test(raw)) {
|
|
7861
|
-
setImage(slot, raw);
|
|
7862
|
-
writeImage(slot, raw);
|
|
7863
|
-
} else {
|
|
7864
|
-
setImage(slot, null);
|
|
7865
|
-
deleteImage(slot);
|
|
7866
|
-
}
|
|
7867
|
-
}
|
|
7868
|
-
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && /^[A-Za-z0-9_-]{1,32}$/.test(slot)) deleteImage(slot);
|
|
7869
|
-
persistConfig();
|
|
7870
|
-
applyActive();
|
|
7871
|
-
return true;
|
|
8210
|
+
return model.facts(rule === null ? "" : rule.match !== "" ? rule.match : rule.id, activeMatched);
|
|
7872
8211
|
};
|
|
7873
|
-
/**
|
|
7874
|
-
* What the last FAILED profile attempt already had in hand.
|
|
7875
|
-
*
|
|
7876
|
-
* Only a failure leaves anything here — a download that completes is applied or
|
|
7877
|
-
* thrown away whole, and holding megabytes of base64 for a re-click that a
|
|
7878
|
-
* second re-download would settle is not a trade this makes. What it buys is the
|
|
7879
|
-
* one case that had no answer at all before: a profile whose sixth of seven files
|
|
7880
|
-
* failed now costs ONE file on the next press instead of seven.
|
|
7881
|
-
*
|
|
7882
|
-
* Held across section re-injections on purpose (it is declared here, in `apply`,
|
|
7883
|
-
* not inside `sectionInject`): the retry is usually the next click of the same
|
|
7884
|
-
* button, but the panel can be closed and reopened between them.
|
|
7885
|
-
*/
|
|
7886
8212
|
let presetHeld = null;
|
|
7887
8213
|
const sectionInject = (actions) => {
|
|
7888
8214
|
bound = actions;
|
|
@@ -8010,16 +8336,11 @@ window.__ModuleLoader__.load({
|
|
|
8010
8336
|
repaintIfMoved(id, winner);
|
|
8011
8337
|
},
|
|
8012
8338
|
/**
|
|
8013
|
-
* Store ONE image's layout mode. Addressed by slot like `setImageFraming
|
|
8014
|
-
*
|
|
8015
|
-
* picture, and 适应 on a tall screenshot says nothing about the landscape
|
|
8016
|
-
* photo beside it in the rotation.
|
|
8339
|
+
* Store ONE image's layout mode. Addressed by slot like `setImageFraming`:
|
|
8340
|
+
* how a picture meets the viewport is a property of the picture.
|
|
8017
8341
|
*
|
|
8018
|
-
* A mode is a placement, not a paint
|
|
8019
|
-
* on screen
|
|
8020
|
-
* the layer repainted through the same `repaintIfMoved` the framing commit
|
|
8021
|
-
* uses — that call repaints the current image whether or not the winner
|
|
8022
|
-
* moved, which is what makes the chips feel live.
|
|
8342
|
+
* A mode is a placement, not a paint — nothing changes about WHICH image is
|
|
8343
|
+
* on screen — so it ends in the same gate the framing commit uses.
|
|
8023
8344
|
*/
|
|
8024
8345
|
setImageMode: (id, slot, mode) => {
|
|
8025
8346
|
const rule = ruleById(id);
|
|
@@ -8098,12 +8419,9 @@ window.__ModuleLoader__.load({
|
|
|
8098
8419
|
return res;
|
|
8099
8420
|
},
|
|
8100
8421
|
/**
|
|
8101
|
-
* Store ONE image's theme
|
|
8102
|
-
*
|
|
8103
|
-
*
|
|
8104
|
-
* image 1's, and it must not touch the RULE's color either — that one is
|
|
8105
|
-
* what the rule paints once its last picture is gone, and it is edited only
|
|
8106
|
-
* while there is no image to edit (the panel's own branch).
|
|
8422
|
+
* Store ONE image's theme colour (null = that image follows the system theme).
|
|
8423
|
+
* Addressed by slot like `setImageFraming`: editing one picture's colour must
|
|
8424
|
+
* touch neither another picture's nor the RULE's own.
|
|
8107
8425
|
*/
|
|
8108
8426
|
setImageColor: (id, slot, color) => {
|
|
8109
8427
|
const rule = ruleById(id);
|
|
@@ -8156,14 +8474,10 @@ window.__ModuleLoader__.load({
|
|
|
8156
8474
|
sync();
|
|
8157
8475
|
},
|
|
8158
8476
|
/**
|
|
8159
|
-
* Patch the GLOBAL wallpaper-switch transition
|
|
8160
|
-
* duration comes from).
|
|
8477
|
+
* Patch the GLOBAL wallpaper-switch transition.
|
|
8161
8478
|
*
|
|
8162
|
-
* Deliberately no `applyWp()`: this
|
|
8163
|
-
*
|
|
8164
|
-
* whole apply (token rewrite, every interface part) for something that
|
|
8165
|
-
* paints nothing would be the expensive way to say "saved". The Config
|
|
8166
|
-
* page's own preview plays the effect instead.
|
|
8479
|
+
* Deliberately no `applyWp()`: this decides what the NEXT switch looks like
|
|
8480
|
+
* and paints nothing now. The page's own preview plays the effect.
|
|
8167
8481
|
*/
|
|
8168
8482
|
setTransition: (patch) => {
|
|
8169
8483
|
cfg.transition = normalizeTransition({
|
|
@@ -8181,61 +8495,33 @@ window.__ModuleLoader__.load({
|
|
|
8181
8495
|
applyActive();
|
|
8182
8496
|
},
|
|
8183
8497
|
exportTheme: () => {
|
|
8184
|
-
|
|
8185
|
-
const slots = cfg.rules.flatMap(ruleSlots);
|
|
8186
|
-
for (const slot of slots) {
|
|
8187
|
-
const url = imageOf(slot);
|
|
8188
|
-
if (url !== null) images[slot] = url;
|
|
8189
|
-
}
|
|
8190
|
-
const payload = {
|
|
8191
|
-
version: 9,
|
|
8192
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8193
|
-
config: cfg,
|
|
8194
|
-
images
|
|
8195
|
-
};
|
|
8196
|
-
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
|
|
8197
|
-
const url = URL.createObjectURL(blob);
|
|
8198
|
-
const a = document.createElement("a");
|
|
8199
|
-
a.href = url;
|
|
8200
|
-
a.download = "dsh-background-by-model-theme.json";
|
|
8201
|
-
a.click();
|
|
8202
|
-
URL.revokeObjectURL(url);
|
|
8498
|
+
saveThemeFile(buildThemeFile());
|
|
8203
8499
|
},
|
|
8204
8500
|
importTheme: async (file) => {
|
|
8205
8501
|
try {
|
|
8206
8502
|
const data = JSON.parse(await file.text());
|
|
8207
8503
|
if (!data || typeof data !== "object") return false;
|
|
8208
|
-
return await applyThemeFile(data, false);
|
|
8504
|
+
return await applyThemeFile({ applyActive }, data, false);
|
|
8209
8505
|
} catch {
|
|
8210
8506
|
return false;
|
|
8211
8507
|
}
|
|
8212
8508
|
},
|
|
8213
8509
|
/**
|
|
8214
|
-
* Fetch the project's recommended profile. Downloads only
|
|
8215
|
-
*
|
|
8216
|
-
*
|
|
8217
|
-
* destroyed, and a confirmation can only describe what is actually in hand
|
|
8218
|
-
* — "7 rules, 7 wallpapers, 2.9 MB" — which means the download comes first
|
|
8219
|
-
* and the question second.
|
|
8510
|
+
* Fetch the project's recommended profile. Downloads only — applying it is
|
|
8511
|
+
* separate, because the page has to ask before anything is destroyed and the
|
|
8512
|
+
* question should describe what is actually in hand.
|
|
8220
8513
|
*
|
|
8221
|
-
* The profile is a DIRECTORY
|
|
8222
|
-
*
|
|
8223
|
-
*
|
|
8224
|
-
*
|
|
8225
|
-
* a
|
|
8226
|
-
* mirror fallback, its own retry rounds (the node half's business, see
|
|
8227
|
-
* ./preset), its own validation, and its own slot name in a failure.
|
|
8514
|
+
* The profile is a DIRECTORY, so this is a pool rather than one call: the
|
|
8515
|
+
* config names the slots and each wallpaper is one request, up to
|
|
8516
|
+
* `PRESET_CONCURRENCY` at a time. Each file keeps its own mirror fallback,
|
|
8517
|
+
* retry rounds (the node half's business, ./preset), validation and slot name
|
|
8518
|
+
* in a failure.
|
|
8228
8519
|
*
|
|
8229
|
-
* STRICT
|
|
8230
|
-
*
|
|
8231
|
-
*
|
|
8232
|
-
*
|
|
8233
|
-
*
|
|
8234
|
-
* one file and retrying the lot. Nothing has been touched at this point, so
|
|
8235
|
-
* "the author renamed a file" arrives as an error naming the file rather
|
|
8236
|
-
* than as a profile that is quietly missing a picture — and that loudness
|
|
8237
|
-
* is what makes a reference the plugin does not fully control acceptable
|
|
8238
|
-
* here.
|
|
8520
|
+
* STRICT: the first wallpaper that cannot be had stops the queue. What had
|
|
8521
|
+
* already arrived is kept for the next press (`presetHeld` below) — the
|
|
8522
|
+
* difference between retrying one file and retrying the lot. Nothing is
|
|
8523
|
+
* touched here, so "the author renamed a file" arrives as an error naming the
|
|
8524
|
+
* file rather than as a profile quietly missing a picture.
|
|
8239
8525
|
*/
|
|
8240
8526
|
fetchRecommended: async (onProgress) => {
|
|
8241
8527
|
const head = await fetchPresetConfig();
|
|
@@ -8300,14 +8586,10 @@ window.__ModuleLoader__.load({
|
|
|
8300
8586
|
}
|
|
8301
8587
|
};
|
|
8302
8588
|
},
|
|
8303
|
-
/**
|
|
8304
|
-
* Replace EVERYTHING with a downloaded profile: empty the store, then write
|
|
8305
|
-
* the profile into it. Irreversible by design, which is why the page asks
|
|
8306
|
-
* first and why nothing here is offered as an undo.
|
|
8307
|
-
*/
|
|
8589
|
+
/** Replace EVERYTHING with a downloaded profile: empty the store, then write it. */
|
|
8308
8590
|
applyRecommended: async (preset) => {
|
|
8309
8591
|
if (!canWriteConfig()) return false;
|
|
8310
|
-
return await applyThemeFile(preset, true);
|
|
8592
|
+
return await applyThemeFile({ applyActive }, preset, true);
|
|
8311
8593
|
}
|
|
8312
8594
|
};
|
|
8313
8595
|
};
|
|
@@ -8320,89 +8602,7 @@ window.__ModuleLoader__.load({
|
|
|
8320
8602
|
store,
|
|
8321
8603
|
inject: sectionInject
|
|
8322
8604
|
}, ThemeSection));
|
|
8323
|
-
|
|
8324
|
-
const applyNavIcon = () => {
|
|
8325
|
-
const nav = document.querySelector("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]")?.querySelector("nav");
|
|
8326
|
-
if (!nav) return;
|
|
8327
|
-
const target = navLabel();
|
|
8328
|
-
for (const cell of Array.from(nav.querySelectorAll("button"))) {
|
|
8329
|
-
const label = cell.querySelector("span");
|
|
8330
|
-
if (label && label.textContent?.trim() === target) {
|
|
8331
|
-
const svg = cell.querySelector("svg");
|
|
8332
|
-
if (svg && svg.dataset.dshAnyIcon !== "1") {
|
|
8333
|
-
const sun = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
8334
|
-
sun.setAttribute("width", "16");
|
|
8335
|
-
sun.setAttribute("height", "16");
|
|
8336
|
-
sun.setAttribute("viewBox", "0 0 16 16");
|
|
8337
|
-
sun.setAttribute("fill", "none");
|
|
8338
|
-
sun.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
8339
|
-
sun.dataset.dshAnyIcon = "1";
|
|
8340
|
-
sun.innerHTML = SUN_PATHS;
|
|
8341
|
-
svg.replaceWith(sun);
|
|
8342
|
-
}
|
|
8343
|
-
return;
|
|
8344
|
-
}
|
|
8345
|
-
}
|
|
8346
|
-
};
|
|
8347
|
-
let navIconObserver = null;
|
|
8348
|
-
const watchNavIcon = () => {
|
|
8349
|
-
if (navIconObserver !== null || typeof MutationObserver === "undefined") return;
|
|
8350
|
-
navIconObserver = new MutationObserver((records) => {
|
|
8351
|
-
if (records.some((r) => {
|
|
8352
|
-
for (const n of r.addedNodes) {
|
|
8353
|
-
if (n.nodeType !== 1) continue;
|
|
8354
|
-
const el = n;
|
|
8355
|
-
if (el.matches?.("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]") || el.querySelector?.("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]")) return true;
|
|
8356
|
-
}
|
|
8357
|
-
return false;
|
|
8358
|
-
})) applyNavIcon();
|
|
8359
|
-
});
|
|
8360
|
-
navIconObserver.observe(document.body, {
|
|
8361
|
-
childList: true,
|
|
8362
|
-
subtree: true
|
|
8363
|
-
});
|
|
8364
|
-
applyNavIcon();
|
|
8365
|
-
};
|
|
8366
|
-
watchNavIcon();
|
|
8367
|
-
ctx.effect(() => () => {
|
|
8368
|
-
navIconObserver?.disconnect();
|
|
8369
|
-
navIconObserver = null;
|
|
8370
|
-
}, "dsh-background-by-model: nav icon watch");
|
|
8371
|
-
const restoreSaved = () => {
|
|
8372
|
-
syncHostScheme();
|
|
8373
|
-
const color = activeColor();
|
|
8374
|
-
if (color !== null) {
|
|
8375
|
-
const snapshot = ctx.theme.getTheme();
|
|
8376
|
-
if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) registerCustom(color[0], color[1], color[2]);
|
|
8377
|
-
else if (snapshot.preference !== CUSTOM_ID) ctx.theme.setTheme(CUSTOM_ID);
|
|
8378
|
-
}
|
|
8379
|
-
applyWp();
|
|
8380
|
-
};
|
|
8381
|
-
const restoreTimers = [300, 1500].map((delay) => window.setTimeout(restoreSaved, delay));
|
|
8382
|
-
ctx.effect(() => () => {
|
|
8383
|
-
restoreTimers.forEach((id) => window.clearTimeout(id));
|
|
8384
|
-
}, "dsh-background-by-model: boot restore");
|
|
8385
|
-
const watchdogId = window.setInterval(() => {
|
|
8386
|
-
syncHostScheme();
|
|
8387
|
-
const color = activeColor();
|
|
8388
|
-
if (color === null) {
|
|
8389
|
-
refreshSystemTheme();
|
|
8390
|
-
return;
|
|
8391
|
-
}
|
|
8392
|
-
const snapshot = ctx.theme.getTheme();
|
|
8393
|
-
let changed = false;
|
|
8394
|
-
if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) {
|
|
8395
|
-
registerCustom(color[0], color[1], color[2]);
|
|
8396
|
-
changed = true;
|
|
8397
|
-
} else if (snapshot.preference !== CUSTOM_ID) {
|
|
8398
|
-
ctx.theme.setTheme(CUSTOM_ID);
|
|
8399
|
-
changed = true;
|
|
8400
|
-
}
|
|
8401
|
-
if (changed) applyWp();
|
|
8402
|
-
}, 1e3);
|
|
8403
|
-
ctx.effect(() => () => {
|
|
8404
|
-
window.clearInterval(watchdogId);
|
|
8405
|
-
}, "dsh-background-by-model: theme watchdog");
|
|
8605
|
+
ctx.effect(() => watchNavIcon(() => ctx.locale.bind(NS)("nav")), "dsh-background-by-model: nav icon watch");
|
|
8406
8606
|
const disposeThemeResets = watchThemeResets();
|
|
8407
8607
|
ctx.effect(() => () => {
|
|
8408
8608
|
disposeThemeResets();
|