dsh-any-background 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -274
- package/README.zh.md +283 -259
- package/lib/client.js +1626 -502
- package/lib/client.js.map +1 -1
- package/lib/index.js +25 -2
- package/package.json +106 -106
package/lib/client.js
CHANGED
|
@@ -4,32 +4,23 @@ 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 _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
8
|
+
let react = require("react");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
-
//#region src/client/
|
|
11
|
-
const name = "dsh-any-background";
|
|
12
|
-
const inject = [
|
|
13
|
-
"slots",
|
|
14
|
-
"locale",
|
|
15
|
-
"theme",
|
|
16
|
-
"connection"
|
|
17
|
-
];
|
|
10
|
+
//#region src/client/i18n.ts
|
|
18
11
|
const NS = "settings.anyBg";
|
|
19
|
-
const DEF_OP = .85;
|
|
20
|
-
const DEF_BL = 0;
|
|
21
|
-
const DEF_SOP = 1;
|
|
22
|
-
const CUSTOM_ID = "custom-color";
|
|
23
12
|
const zh = {
|
|
24
13
|
nav: "主题",
|
|
25
14
|
subtitle: "自定义界面外观",
|
|
26
15
|
colorTitle: "主题色",
|
|
27
16
|
colorHint: "在色轮上选择色相,在方形中调整饱和度和明度",
|
|
28
|
-
uiTitle: "
|
|
29
|
-
uiOpacity: "
|
|
30
|
-
|
|
17
|
+
uiTitle: "主界面",
|
|
18
|
+
uiOpacity: "透明度",
|
|
19
|
+
uiBlur: "模糊度",
|
|
20
|
+
uiOpacityBg: "主背景",
|
|
21
|
+
uiOpacitySide: "侧边栏",
|
|
22
|
+
uiOpacityCard: "对话框中选项面板",
|
|
31
23
|
uiSop: "设置界面透明度",
|
|
32
|
-
uiSopHint: "拖动滑块调整设置页界面背景的透明度",
|
|
33
24
|
bgTitle: "背景图片",
|
|
34
25
|
bgChoose: "选择图片",
|
|
35
26
|
bgRemove: "移除图片",
|
|
@@ -41,18 +32,36 @@ window.__ModuleLoader__.load({
|
|
|
41
32
|
editorHint: "拖动移动图片,滚轮缩放大小",
|
|
42
33
|
editorCommit: "确认",
|
|
43
34
|
editorCancel: "取消",
|
|
44
|
-
editorReset: "重置"
|
|
35
|
+
editorReset: "重置",
|
|
36
|
+
extractColor: "从壁纸提取主题色",
|
|
37
|
+
extracting: "取色中…",
|
|
38
|
+
extractNoWp: "请先设置背景图片",
|
|
39
|
+
extractDone: "已应用壁纸主色调",
|
|
40
|
+
extractFail: "未在壁纸中找到鲜明的颜色,请换一张壁纸",
|
|
41
|
+
crashTitle: "界面渲染出错",
|
|
42
|
+
crashDesc: "主题设置面板遇到问题,点击下方按钮重置后重试",
|
|
43
|
+
crashReset: "重置面板",
|
|
44
|
+
exportTheme: "导出配置",
|
|
45
|
+
importTheme: "导入配置",
|
|
46
|
+
importDone: "已导入主题配置",
|
|
47
|
+
importFail: "导入失败,文件格式不正确",
|
|
48
|
+
eyedropper: "从壁纸取色",
|
|
49
|
+
pickerTitle: "从壁纸取色",
|
|
50
|
+
pickerHint: "移动鼠标预览颜色,点击壁纸选取为主题色",
|
|
51
|
+
pickerClose: "关闭"
|
|
45
52
|
};
|
|
46
53
|
const en = {
|
|
47
54
|
nav: "Theme",
|
|
48
55
|
subtitle: "Customize appearance",
|
|
49
56
|
colorTitle: "Theme color",
|
|
50
57
|
colorHint: "Pick hue on the ring, adjust saturation & lightness in the square",
|
|
51
|
-
uiTitle: "Interface",
|
|
52
|
-
uiOpacity: "
|
|
53
|
-
|
|
58
|
+
uiTitle: "Interface opacity",
|
|
59
|
+
uiOpacity: "Opacity",
|
|
60
|
+
uiBlur: "Blur",
|
|
61
|
+
uiOpacityBg: "Main background",
|
|
62
|
+
uiOpacitySide: "Sidebar",
|
|
63
|
+
uiOpacityCard: "Cards & panels",
|
|
54
64
|
uiSop: "Settings interface opacity",
|
|
55
|
-
uiSopHint: "Drag to adjust the settings page background opacity",
|
|
56
65
|
bgTitle: "Wallpaper",
|
|
57
66
|
bgChoose: "Choose image",
|
|
58
67
|
bgRemove: "Remove image",
|
|
@@ -64,11 +73,39 @@ window.__ModuleLoader__.load({
|
|
|
64
73
|
editorHint: "Drag to move, scroll to zoom",
|
|
65
74
|
editorCommit: "Confirm",
|
|
66
75
|
editorCancel: "Cancel",
|
|
67
|
-
editorReset: "Reset"
|
|
76
|
+
editorReset: "Reset",
|
|
77
|
+
extractColor: "Extract from wallpaper",
|
|
78
|
+
extracting: "Extracting…",
|
|
79
|
+
extractNoWp: "Set a wallpaper first",
|
|
80
|
+
extractDone: "Wallpaper color applied",
|
|
81
|
+
extractFail: "No vivid color found in this wallpaper, try another",
|
|
82
|
+
crashTitle: "Section crashed",
|
|
83
|
+
crashDesc: "The theme panel hit an error. Reset below to recover.",
|
|
84
|
+
crashReset: "Reset panel",
|
|
85
|
+
exportTheme: "Export",
|
|
86
|
+
importTheme: "Import",
|
|
87
|
+
importDone: "Theme imported",
|
|
88
|
+
importFail: "Import failed — invalid file",
|
|
89
|
+
eyedropper: "Eyedropper",
|
|
90
|
+
pickerTitle: "Pick from wallpaper",
|
|
91
|
+
pickerHint: "Hover to preview, click to pick as theme color",
|
|
92
|
+
pickerClose: "Close"
|
|
68
93
|
};
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/client/state.ts
|
|
69
96
|
const DEFAULT_CONFIG = {
|
|
70
97
|
color: null,
|
|
71
|
-
|
|
98
|
+
opacities: {
|
|
99
|
+
bg: .85,
|
|
100
|
+
sidebar: .93,
|
|
101
|
+
card: 1
|
|
102
|
+
},
|
|
103
|
+
blurs: {
|
|
104
|
+
bg: 0,
|
|
105
|
+
sidebar: 0,
|
|
106
|
+
card: 0,
|
|
107
|
+
settings: 0
|
|
108
|
+
},
|
|
72
109
|
settingsOpacity: 1,
|
|
73
110
|
wallpaperOpacity: 1,
|
|
74
111
|
blur: 0,
|
|
@@ -82,24 +119,58 @@ window.__ModuleLoader__.load({
|
|
|
82
119
|
};
|
|
83
120
|
let cfg = {
|
|
84
121
|
...DEFAULT_CONFIG,
|
|
122
|
+
opacities: { ...DEFAULT_CONFIG.opacities },
|
|
123
|
+
blurs: { ...DEFAULT_CONFIG.blurs },
|
|
85
124
|
bgState: { ...DEFAULT_CONFIG.bgState }
|
|
86
125
|
};
|
|
87
126
|
let wpUrl = null;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
function setWpUrl(url) {
|
|
128
|
+
wpUrl = url;
|
|
129
|
+
}
|
|
130
|
+
function setBgState(s) {
|
|
131
|
+
cfg.bgState = s;
|
|
132
|
+
}
|
|
133
|
+
function rHasColor() {
|
|
134
|
+
return cfg.color !== null;
|
|
135
|
+
}
|
|
136
|
+
function rColor() {
|
|
137
|
+
return cfg.color ?? [
|
|
138
|
+
220,
|
|
139
|
+
.55,
|
|
140
|
+
.25
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
function rWp() {
|
|
144
|
+
return wpUrl;
|
|
145
|
+
}
|
|
146
|
+
function rOps() {
|
|
147
|
+
const o = cfg.opacities ?? {};
|
|
148
|
+
return {
|
|
149
|
+
bg: typeof o.bg === "number" ? Math.min(1, Math.max(0, o.bg)) : DEFAULT_CONFIG.opacities.bg,
|
|
150
|
+
sidebar: typeof o.sidebar === "number" ? Math.min(1, Math.max(0, o.sidebar)) : DEFAULT_CONFIG.opacities.sidebar,
|
|
151
|
+
card: typeof o.card === "number" ? Math.min(1, Math.max(0, o.card)) : DEFAULT_CONFIG.opacities.card
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function rBlurs() {
|
|
155
|
+
const b = cfg.blurs ?? {};
|
|
156
|
+
return {
|
|
157
|
+
bg: typeof b.bg === "number" ? Math.min(60, Math.max(0, b.bg)) : DEFAULT_CONFIG.blurs.bg,
|
|
158
|
+
sidebar: typeof b.sidebar === "number" ? Math.min(60, Math.max(0, b.sidebar)) : DEFAULT_CONFIG.blurs.sidebar,
|
|
159
|
+
card: typeof b.card === "number" ? Math.min(60, Math.max(0, b.card)) : DEFAULT_CONFIG.blurs.card,
|
|
160
|
+
settings: typeof b.settings === "number" ? Math.min(60, Math.max(0, b.settings)) : DEFAULT_CONFIG.blurs.settings
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function rWop() {
|
|
164
|
+
return typeof cfg.wallpaperOpacity === "number" ? Math.min(1, Math.max(0, cfg.wallpaperOpacity)) : DEFAULT_CONFIG.wallpaperOpacity;
|
|
165
|
+
}
|
|
166
|
+
function rBl() {
|
|
167
|
+
return typeof cfg.blur === "number" ? Math.min(60, Math.max(0, cfg.blur)) : DEFAULT_CONFIG.blur;
|
|
168
|
+
}
|
|
169
|
+
function rSop() {
|
|
170
|
+
return typeof cfg.settingsOpacity === "number" ? Math.min(1, Math.max(0, cfg.settingsOpacity)) : DEFAULT_CONFIG.settingsOpacity;
|
|
171
|
+
}
|
|
172
|
+
function rBgState() {
|
|
173
|
+
return cfg.bgState;
|
|
103
174
|
}
|
|
104
175
|
/** Move a possibly-absent partial config into the shape the UI reads. */
|
|
105
176
|
function adoptConfig(raw) {
|
|
@@ -110,9 +181,22 @@ window.__ModuleLoader__.load({
|
|
|
110
181
|
c.color[2]
|
|
111
182
|
] : null;
|
|
112
183
|
const bg = c.bgState ?? {};
|
|
184
|
+
const legacy = typeof c.opacity === "number" ? c.opacity : null;
|
|
185
|
+
const ops = c.opacities ?? {};
|
|
186
|
+
const bl = c.blurs ?? {};
|
|
113
187
|
cfg = {
|
|
114
188
|
color,
|
|
115
|
-
|
|
189
|
+
opacities: {
|
|
190
|
+
bg: typeof ops.bg === "number" ? ops.bg : legacy ?? DEFAULT_CONFIG.opacities.bg,
|
|
191
|
+
sidebar: typeof ops.sidebar === "number" ? ops.sidebar : legacy !== null ? Math.min(1, legacy + .08) : DEFAULT_CONFIG.opacities.sidebar,
|
|
192
|
+
card: typeof ops.card === "number" ? ops.card : DEFAULT_CONFIG.opacities.card
|
|
193
|
+
},
|
|
194
|
+
blurs: {
|
|
195
|
+
bg: typeof bl.bg === "number" ? bl.bg : DEFAULT_CONFIG.blurs.bg,
|
|
196
|
+
sidebar: typeof bl.sidebar === "number" ? bl.sidebar : DEFAULT_CONFIG.blurs.sidebar,
|
|
197
|
+
card: typeof bl.card === "number" ? bl.card : DEFAULT_CONFIG.blurs.card,
|
|
198
|
+
settings: typeof bl.settings === "number" ? bl.settings : DEFAULT_CONFIG.blurs.settings
|
|
199
|
+
},
|
|
116
200
|
settingsOpacity: typeof c.settingsOpacity === "number" ? c.settingsOpacity : DEFAULT_CONFIG.settingsOpacity,
|
|
117
201
|
wallpaperOpacity: typeof c.wallpaperOpacity === "number" ? c.wallpaperOpacity : DEFAULT_CONFIG.wallpaperOpacity,
|
|
118
202
|
blur: typeof c.blur === "number" ? c.blur : DEFAULT_CONFIG.blur,
|
|
@@ -125,47 +209,62 @@ window.__ModuleLoader__.load({
|
|
|
125
209
|
}
|
|
126
210
|
};
|
|
127
211
|
}
|
|
128
|
-
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/client/rpc.ts
|
|
214
|
+
const RPC_CHANNEL = "/dsh-any-background";
|
|
215
|
+
const RPC_NS = "dshAnyBackground";
|
|
216
|
+
const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
|
|
217
|
+
let rpcCallFn = null;
|
|
218
|
+
function initRpc(call) {
|
|
219
|
+
rpcCallFn = call;
|
|
220
|
+
}
|
|
221
|
+
async function rpcCall(method, payload) {
|
|
222
|
+
if (!rpcCallFn) return void 0;
|
|
223
|
+
try {
|
|
224
|
+
const res = await rpcCallFn(rpcEndpoint(method), payload);
|
|
225
|
+
if (res && res.ok === true) return res.value;
|
|
226
|
+
console.warn(`dsh-any-background: rpc "${method}" failed`, res?.error);
|
|
227
|
+
return;
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.warn(`dsh-any-background: rpc "${method}" threw`, e);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const SAVE_DEBOUNCE_MS = 250;
|
|
234
|
+
let saveTimer;
|
|
129
235
|
function saveConfig() {
|
|
236
|
+
if (saveTimer !== void 0) window.clearTimeout(saveTimer);
|
|
237
|
+
saveTimer = window.setTimeout(() => {
|
|
238
|
+
saveTimer = void 0;
|
|
239
|
+
rpcCall("writeConfig", { config: cfg });
|
|
240
|
+
}, SAVE_DEBOUNCE_MS);
|
|
241
|
+
}
|
|
242
|
+
function flushSave() {
|
|
243
|
+
if (saveTimer === void 0) return;
|
|
244
|
+
window.clearTimeout(saveTimer);
|
|
245
|
+
saveTimer = void 0;
|
|
246
|
+
rpcCall("writeConfig", { config: cfg });
|
|
247
|
+
}
|
|
248
|
+
/** Persist the current config immediately (import path — no debounce). */
|
|
249
|
+
function persistConfig() {
|
|
130
250
|
rpcCall("writeConfig", { config: cfg });
|
|
131
251
|
}
|
|
132
252
|
/** Load the persisted theme (config + wallpaper) from the node half. */
|
|
133
253
|
async function loadPersisted() {
|
|
134
254
|
const data = await rpcCall("read", {});
|
|
135
|
-
if (data) {
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
255
|
+
if (data && typeof data === "object") {
|
|
256
|
+
const d = data;
|
|
257
|
+
if (d.config) adoptConfig(d.config);
|
|
258
|
+
if (typeof d.wallpaper === "string") setWpUrl(d.wallpaper);
|
|
259
|
+
else if (d.wallpaper === null) setWpUrl(null);
|
|
139
260
|
}
|
|
140
261
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
function rColor() {
|
|
145
|
-
return cfg.color ?? [
|
|
146
|
-
220,
|
|
147
|
-
.55,
|
|
148
|
-
.25
|
|
149
|
-
];
|
|
150
|
-
}
|
|
151
|
-
function rWp() {
|
|
152
|
-
return wpUrl;
|
|
153
|
-
}
|
|
154
|
-
function rOp() {
|
|
155
|
-
return typeof cfg.opacity === "number" ? Math.min(1, Math.max(0, cfg.opacity)) : DEF_OP;
|
|
156
|
-
}
|
|
157
|
-
function rWop() {
|
|
158
|
-
return typeof cfg.wallpaperOpacity === "number" ? Math.min(1, Math.max(0, cfg.wallpaperOpacity)) : 1;
|
|
159
|
-
}
|
|
160
|
-
function rBl() {
|
|
161
|
-
return typeof cfg.blur === "number" ? Math.min(60, Math.max(0, cfg.blur)) : DEF_BL;
|
|
162
|
-
}
|
|
163
|
-
function rSop() {
|
|
164
|
-
return typeof cfg.settingsOpacity === "number" ? Math.min(1, Math.max(0, cfg.settingsOpacity)) : DEF_SOP;
|
|
165
|
-
}
|
|
166
|
-
function rBgState() {
|
|
167
|
-
return cfg.bgState;
|
|
262
|
+
/** Persist a wallpaper (null removes it). One-shot large payload, no debounce. */
|
|
263
|
+
function persistWallpaper(dataUrl) {
|
|
264
|
+
rpcCall("setWallpaper", { dataUrl });
|
|
168
265
|
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/client/utils/color.ts
|
|
169
268
|
function hsvToHsl(h, s, v) {
|
|
170
269
|
const l = v * (1 - s / 2);
|
|
171
270
|
return [
|
|
@@ -182,16 +281,28 @@ window.__ModuleLoader__.load({
|
|
|
182
281
|
v
|
|
183
282
|
];
|
|
184
283
|
}
|
|
284
|
+
let tokensCacheKey = "";
|
|
285
|
+
let tokensCache = null;
|
|
286
|
+
/**
|
|
287
|
+
* Memoized token generation: the same (hue, sat, lit) input always yields the
|
|
288
|
+
* same token set, and applyWp / applyCustomTokens / applySettingsOverrides call
|
|
289
|
+
* this repeatedly (slider drags, viewport re-applies), so cache the last result
|
|
290
|
+
* and skip the 30+ hsl() string builds when nothing changed.
|
|
291
|
+
*/
|
|
185
292
|
function genTokens(hue, sat, lit) {
|
|
293
|
+
const key = `${hue}|${sat}|${lit}`;
|
|
294
|
+
if (tokensCacheKey === key && tokensCache) return tokensCache;
|
|
295
|
+
tokensCacheKey = key;
|
|
296
|
+
tokensCache = buildTokens(hue, sat, lit);
|
|
297
|
+
return tokensCache;
|
|
298
|
+
}
|
|
299
|
+
function buildTokens(hue, sat, lit) {
|
|
186
300
|
const dark = lit < .55;
|
|
187
301
|
const h = (d) => ((hue + d) % 360 + 360) % 360;
|
|
188
302
|
const s = (d) => Math.max(0, Math.min(1, sat + d));
|
|
189
303
|
const l = (d) => Math.max(0, Math.min(1, lit + d));
|
|
190
304
|
const hsl = (hh, ss, ll) => `hsl(${Math.round(hh)},${Math.round(ss * 100)}%,${Math.round(ll * 100)}%)`;
|
|
191
|
-
const rgba = (hh, ss, ll, a) => {
|
|
192
|
-
`${Math.round(hh)}${Math.round(ss * 100)}${Math.round(ll * 100)}`;
|
|
193
|
-
return `hsla(${Math.round(hh)},${Math.round(ss * 100)}%,${Math.round(ll * 100)}%,${a})`;
|
|
194
|
-
};
|
|
305
|
+
const rgba = (hh, ss, ll, a) => `hsla(${Math.round(hh)},${Math.round(ss * 100)}%,${Math.round(ll * 100)}%,${a})`;
|
|
195
306
|
if (dark) return {
|
|
196
307
|
colorScheme: "dark",
|
|
197
308
|
tokens: {
|
|
@@ -266,8 +377,6 @@ window.__ModuleLoader__.load({
|
|
|
266
377
|
}
|
|
267
378
|
};
|
|
268
379
|
}
|
|
269
|
-
let wpEl = null;
|
|
270
|
-
let ctxRef = null;
|
|
271
380
|
function toRgba(c, a) {
|
|
272
381
|
const hx = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(c.trim());
|
|
273
382
|
if (hx) {
|
|
@@ -282,8 +391,141 @@ window.__ModuleLoader__.load({
|
|
|
282
391
|
if (hsl) return `hsla(${hsl[1]},${hsl[2]}%,${hsl[3]}%,${a})`;
|
|
283
392
|
return c.trim();
|
|
284
393
|
}
|
|
394
|
+
function rgbToHsl(r, g, b) {
|
|
395
|
+
const rn = r / 255, gn = g / 255, bn = b / 255;
|
|
396
|
+
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
|
|
397
|
+
const l = (max + min) / 2;
|
|
398
|
+
if (max === min) return [
|
|
399
|
+
0,
|
|
400
|
+
0,
|
|
401
|
+
l
|
|
402
|
+
];
|
|
403
|
+
const d = max - min;
|
|
404
|
+
const s = l > .5 ? d / (2 - max - min) : d / (max + min);
|
|
405
|
+
let h;
|
|
406
|
+
if (max === rn) h = (gn - bn) / d % 6;
|
|
407
|
+
else if (max === gn) h = (bn - rn) / d + 2;
|
|
408
|
+
else h = (rn - gn) / d + 4;
|
|
409
|
+
h *= 60;
|
|
410
|
+
if (h < 0) h += 360;
|
|
411
|
+
return [
|
|
412
|
+
h,
|
|
413
|
+
s,
|
|
414
|
+
l
|
|
415
|
+
];
|
|
416
|
+
}
|
|
417
|
+
const EXTRACT_SIDE = 64;
|
|
418
|
+
function extractWallpaperColor(dataUrl, bgState) {
|
|
419
|
+
return new Promise((resolve) => {
|
|
420
|
+
const img = new Image();
|
|
421
|
+
img.onerror = () => resolve(null);
|
|
422
|
+
img.onload = () => {
|
|
423
|
+
try {
|
|
424
|
+
const iw = img.naturalWidth || img.width;
|
|
425
|
+
const ih = img.naturalHeight || img.height;
|
|
426
|
+
const bg = bgState;
|
|
427
|
+
let sx = 0, sy = 0, sw = iw, sh = ih;
|
|
428
|
+
if (bg.iw === iw && bg.ih === ih && bg.iw > 0) {
|
|
429
|
+
const fit = Math.min(window.innerWidth / iw, window.innerHeight / ih);
|
|
430
|
+
const w = iw * fit * bg.zoom;
|
|
431
|
+
const h = ih * fit * bg.zoom;
|
|
432
|
+
sx = Math.max(0, bg.x * window.innerWidth - w / 2);
|
|
433
|
+
sy = Math.max(0, bg.y * window.innerHeight - h / 2);
|
|
434
|
+
sw = Math.min(iw - sx, w);
|
|
435
|
+
sh = Math.min(ih - sy, h);
|
|
436
|
+
if (sw <= 0 || sh <= 0) {
|
|
437
|
+
sx = 0;
|
|
438
|
+
sy = 0;
|
|
439
|
+
sw = iw;
|
|
440
|
+
sh = ih;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const c = document.createElement("canvas");
|
|
444
|
+
c.width = EXTRACT_SIDE;
|
|
445
|
+
c.height = EXTRACT_SIDE;
|
|
446
|
+
const g = c.getContext("2d", { willReadFrequently: true });
|
|
447
|
+
g.drawImage(img, sx, sy, sw, sh, 0, 0, EXTRACT_SIDE, EXTRACT_SIDE);
|
|
448
|
+
const px = g.getImageData(0, 0, EXTRACT_SIDE, EXTRACT_SIDE).data;
|
|
449
|
+
const counts = /* @__PURE__ */ new Uint32Array(4096);
|
|
450
|
+
const sums = /* @__PURE__ */ new Float64Array(12288);
|
|
451
|
+
for (let i = 0; i < px.length; i += 4) {
|
|
452
|
+
const r = px[i], gg = px[i + 1], b = px[i + 2];
|
|
453
|
+
const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
|
|
454
|
+
const v = max / 255;
|
|
455
|
+
if ((max === 0 ? 0 : (max - min) / max) < .08 || v < .12 || v > .97) continue;
|
|
456
|
+
const key = r >> 4 << 8 | gg >> 4 << 4 | b >> 4;
|
|
457
|
+
counts[key]++;
|
|
458
|
+
sums[key * 3] += r;
|
|
459
|
+
sums[key * 3 + 1] += gg;
|
|
460
|
+
sums[key * 3 + 2] += b;
|
|
461
|
+
}
|
|
462
|
+
let best = -1, bestW = 0;
|
|
463
|
+
for (let k = 0; k < 4096; k++) {
|
|
464
|
+
if (counts[k] === 0) continue;
|
|
465
|
+
const r = sums[k * 3] / counts[k];
|
|
466
|
+
const gg = sums[k * 3 + 1] / counts[k];
|
|
467
|
+
const b = sums[k * 3 + 2] / counts[k];
|
|
468
|
+
const max = Math.max(r, gg, b);
|
|
469
|
+
const s = max === 0 ? 0 : (max - Math.min(r, gg, b)) / max;
|
|
470
|
+
const w = counts[k] * (.5 + s);
|
|
471
|
+
if (w > bestW) {
|
|
472
|
+
bestW = w;
|
|
473
|
+
best = k;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (best < 0) {
|
|
477
|
+
resolve(null);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const [h, s, l] = rgbToHsl(sums[best * 3] / counts[best], sums[best * 3 + 1] / counts[best], sums[best * 3 + 2] / counts[best]);
|
|
481
|
+
resolve([
|
|
482
|
+
h,
|
|
483
|
+
Math.min(.9, Math.max(.15, s)),
|
|
484
|
+
l < .5 ? Math.min(.44, Math.max(.2, l)) : Math.max(.6, Math.min(.82, l))
|
|
485
|
+
]);
|
|
486
|
+
} catch {
|
|
487
|
+
resolve(null);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
img.src = dataUrl;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
/** HSL (h 0-360, s/l 0-1) → RGB (0-255 integers). */
|
|
494
|
+
function hslToRgb(h, s, l) {
|
|
495
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
496
|
+
const hp = h / 60;
|
|
497
|
+
const x = c * (1 - Math.abs(hp % 2 - 1));
|
|
498
|
+
let r = 0, g = 0, b = 0;
|
|
499
|
+
if (hp < 1) {
|
|
500
|
+
r = c;
|
|
501
|
+
g = x;
|
|
502
|
+
} else if (hp < 2) {
|
|
503
|
+
r = x;
|
|
504
|
+
g = c;
|
|
505
|
+
} else if (hp < 3) {
|
|
506
|
+
g = c;
|
|
507
|
+
b = x;
|
|
508
|
+
} else if (hp < 4) {
|
|
509
|
+
g = x;
|
|
510
|
+
b = c;
|
|
511
|
+
} else if (hp < 5) {
|
|
512
|
+
r = x;
|
|
513
|
+
b = c;
|
|
514
|
+
} else {
|
|
515
|
+
r = c;
|
|
516
|
+
b = x;
|
|
517
|
+
}
|
|
518
|
+
const m = l - c / 2;
|
|
519
|
+
return [
|
|
520
|
+
Math.round((r + m) * 255),
|
|
521
|
+
Math.round((g + m) * 255),
|
|
522
|
+
Math.round((b + m) * 255)
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/client/wallpaper.ts
|
|
527
|
+
let wpEl = null;
|
|
285
528
|
let appliedTokenNames = [];
|
|
286
|
-
/** Remove every inline token this plugin wrote (teardown symmetry). */
|
|
287
529
|
function clearCustomTokens() {
|
|
288
530
|
for (const name of appliedTokenNames) document.body.style.removeProperty(name);
|
|
289
531
|
appliedTokenNames = [];
|
|
@@ -292,27 +534,27 @@ window.__ModuleLoader__.load({
|
|
|
292
534
|
* Write the saved color's full token set as inline variables on body — the
|
|
293
535
|
* same write surface the theme presenter owns, but derived DIRECTLY from the
|
|
294
536
|
* saved pick, so the theme color never depends on the theme service's active
|
|
295
|
-
* state or the presenter's timing. The bg-base and
|
|
296
|
-
* re-emitted at
|
|
297
|
-
*
|
|
298
|
-
*
|
|
537
|
+
* state or the presenter's timing. The bg-base, sidebar and layer surfaces are
|
|
538
|
+
* re-emitted at their per-part alpha; every other token (labels, borders,
|
|
539
|
+
* brand) is written verbatim. No reads: nothing can observe a stale or reset
|
|
540
|
+
* theme value and leave the homepage on the system color.
|
|
299
541
|
*/
|
|
300
|
-
function applyCustomTokens(
|
|
542
|
+
function applyCustomTokens(ops) {
|
|
301
543
|
const [h, s, l] = rColor();
|
|
302
544
|
const { tokens } = genTokens(h, s, l);
|
|
303
|
-
const sideOp = Math.min(1, op + .08);
|
|
304
545
|
clearCustomTokens();
|
|
305
|
-
if (l < .55) document.body.setAttribute("data-ds-dark-theme", "");
|
|
546
|
+
if (l < .55) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
|
|
306
547
|
else document.body.removeAttribute("data-ds-dark-theme");
|
|
307
548
|
for (const [name, value] of Object.entries(tokens)) {
|
|
308
549
|
let v = value;
|
|
309
|
-
if (name === "--dsw-alias-bg-base") v = toRgba(value,
|
|
310
|
-
else if (name === "--dsw-specific-sidebar-fill") v = toRgba(value,
|
|
550
|
+
if (name === "--dsw-alias-bg-base") v = toRgba(value, ops.bg);
|
|
551
|
+
else if (name === "--dsw-specific-sidebar-fill") v = toRgba(value, ops.sidebar);
|
|
552
|
+
else if (name === "--dsw-alias-bg-layer-1" || name === "--dsw-alias-bg-layer-2" || name === "--dsw-alias-bg-layer-3") v = toRgba(value, ops.card);
|
|
311
553
|
document.body.style.setProperty(name, v);
|
|
312
554
|
appliedTokenNames.push(name);
|
|
313
555
|
}
|
|
314
556
|
}
|
|
315
|
-
const SETTINGS_STYLE_RULE = `[role="dialog"][aria-modal="true"][aria-labelledby]{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2))}`;
|
|
557
|
+
const SETTINGS_STYLE_RULE = `[role="dialog"][aria-modal="true"][aria-labelledby]{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none)}`;
|
|
316
558
|
function applySettingsOverrides(op) {
|
|
317
559
|
if (op >= 1) {
|
|
318
560
|
document.documentElement.style.removeProperty("--dsh-any-bg-settings-surface");
|
|
@@ -322,7 +564,73 @@ window.__ModuleLoader__.load({
|
|
|
322
564
|
const layer2 = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-2"];
|
|
323
565
|
if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-surface", toRgba(layer2, op));
|
|
324
566
|
}
|
|
325
|
-
|
|
567
|
+
let frameEl = null;
|
|
568
|
+
let sidebarEl = null;
|
|
569
|
+
let centerEl = null;
|
|
570
|
+
let detailsEl = null;
|
|
571
|
+
function discoverParts() {
|
|
572
|
+
const overlay = document.querySelector("[data-shell-overlay]");
|
|
573
|
+
if (overlay === null) return;
|
|
574
|
+
const frame = overlay.parentElement;
|
|
575
|
+
if (frame === null) return;
|
|
576
|
+
frameEl = frame;
|
|
577
|
+
const idx = Array.from(frame.children).indexOf(overlay);
|
|
578
|
+
sidebarEl = frame.children[idx - 3] ?? null;
|
|
579
|
+
centerEl = frame.children[idx - 2] ?? null;
|
|
580
|
+
detailsEl = frame.children[idx - 1] ?? null;
|
|
581
|
+
}
|
|
582
|
+
function setBlur(el, px) {
|
|
583
|
+
if (el === null) return;
|
|
584
|
+
if (px > 0) el.style.backdropFilter = `blur(${px}px)`;
|
|
585
|
+
else el.style.removeProperty("backdrop-filter");
|
|
586
|
+
}
|
|
587
|
+
function applySettingsBlur(px) {
|
|
588
|
+
if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-settings", `blur(${px}px)`);
|
|
589
|
+
else document.documentElement.style.removeProperty("--dsh-any-blur-settings");
|
|
590
|
+
}
|
|
591
|
+
/** Apply per-part interface blur to the AppFrame columns + settings panel. */
|
|
592
|
+
function applyPartBlurs(blurs) {
|
|
593
|
+
discoverParts();
|
|
594
|
+
setBlur(frameEl, blurs.bg);
|
|
595
|
+
setBlur(sidebarEl, blurs.sidebar);
|
|
596
|
+
setBlur(centerEl, blurs.card);
|
|
597
|
+
setBlur(detailsEl, blurs.card);
|
|
598
|
+
applySettingsBlur(blurs.settings);
|
|
599
|
+
}
|
|
600
|
+
/** Live per-part blur update during slider drag (no full re-apply). */
|
|
601
|
+
function setPartBlur(part, v) {
|
|
602
|
+
if (part === "settings") {
|
|
603
|
+
applySettingsBlur(v);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
discoverParts();
|
|
607
|
+
if (part === "bg") setBlur(frameEl, v);
|
|
608
|
+
else if (part === "sidebar") setBlur(sidebarEl, v);
|
|
609
|
+
else {
|
|
610
|
+
setBlur(centerEl, v);
|
|
611
|
+
setBlur(detailsEl, v);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
let partsObserver = null;
|
|
615
|
+
/** Watch for the AppFrame mounting so persisted blurs land even when the shell
|
|
616
|
+
* renders after this plugin's apply. Cheap: once all parts are found, the
|
|
617
|
+
* callback returns. */
|
|
618
|
+
function watchParts() {
|
|
619
|
+
if (partsObserver !== null || typeof MutationObserver === "undefined") return;
|
|
620
|
+
partsObserver = new MutationObserver(() => {
|
|
621
|
+
if (frameEl !== null && sidebarEl !== null && centerEl !== null && detailsEl !== null && document.body.contains(frameEl)) return;
|
|
622
|
+
applyPartBlurs(rBlurs());
|
|
623
|
+
});
|
|
624
|
+
partsObserver.observe(document.body, {
|
|
625
|
+
childList: true,
|
|
626
|
+
subtree: true
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function stopWatchingParts() {
|
|
630
|
+
partsObserver?.disconnect();
|
|
631
|
+
partsObserver = null;
|
|
632
|
+
}
|
|
633
|
+
function applyWp() {
|
|
326
634
|
const url = rWp();
|
|
327
635
|
if (!url) {
|
|
328
636
|
wpEl?.remove();
|
|
@@ -349,185 +657,32 @@ window.__ModuleLoader__.load({
|
|
|
349
657
|
wpEl.style.filter = blur > 0 ? `blur(${blur}px)` : "none";
|
|
350
658
|
wpEl.style.opacity = String(rWop());
|
|
351
659
|
}
|
|
352
|
-
applyCustomTokens(
|
|
660
|
+
applyCustomTokens(rOps());
|
|
353
661
|
applySettingsOverrides(rSop());
|
|
662
|
+
applyPartBlurs(rBlurs());
|
|
354
663
|
}
|
|
355
664
|
function teardownWp() {
|
|
356
665
|
wpEl?.remove();
|
|
357
666
|
wpEl = null;
|
|
358
667
|
clearCustomTokens();
|
|
359
668
|
document.documentElement.style.removeProperty("--dsh-any-bg-settings-surface");
|
|
669
|
+
document.documentElement.style.removeProperty("--dsh-any-blur-settings");
|
|
670
|
+
setBlur(frameEl, 0);
|
|
671
|
+
setBlur(sidebarEl, 0);
|
|
672
|
+
setBlur(centerEl, 0);
|
|
673
|
+
setBlur(detailsEl, 0);
|
|
674
|
+
stopWatchingParts();
|
|
360
675
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const RING_INNER = 82;
|
|
365
|
-
const SQ_HALF = Math.round(RING_INNER / Math.SQRT2);
|
|
366
|
-
function drawWheel(cvs, hue, sat, lit) {
|
|
367
|
-
const c = cvs.getContext("2d");
|
|
368
|
-
c.clearRect(0, 0, WHEEL_SIZE, WHEEL_SIZE);
|
|
369
|
-
for (let a = 0; a < 360; a++) {
|
|
370
|
-
const r1 = (a - 90) * Math.PI / 180;
|
|
371
|
-
const r2 = (a + 1.5 - 90) * Math.PI / 180;
|
|
372
|
-
c.beginPath();
|
|
373
|
-
c.arc(CX, CX, RING_OUTER, r1, r2);
|
|
374
|
-
c.arc(CX, CX, RING_INNER, r2, r1, true);
|
|
375
|
-
c.closePath();
|
|
376
|
-
c.fillStyle = `hsl(${a},100%,50%)`;
|
|
377
|
-
c.fill();
|
|
378
|
-
}
|
|
379
|
-
const gx = CX - SQ_HALF, gy = CX - SQ_HALF, sz = SQ_HALF * 2;
|
|
380
|
-
c.fillStyle = "#fff";
|
|
381
|
-
c.fillRect(gx, gy, sz, sz);
|
|
382
|
-
const gh = c.createLinearGradient(gx, 0, gx + sz, 0);
|
|
383
|
-
gh.addColorStop(0, "rgba(255,255,255,1)");
|
|
384
|
-
gh.addColorStop(1, `hsl(${hue},100%,50%)`);
|
|
385
|
-
c.fillStyle = gh;
|
|
386
|
-
c.fillRect(gx, gy, sz, sz);
|
|
387
|
-
const gv = c.createLinearGradient(0, gy, 0, gy + sz);
|
|
388
|
-
gv.addColorStop(0, "rgba(0,0,0,0)");
|
|
389
|
-
gv.addColorStop(1, "rgba(0,0,0,1)");
|
|
390
|
-
c.fillStyle = gv;
|
|
391
|
-
c.fillRect(gx, gy, sz, sz);
|
|
392
|
-
const hRad = (hue - 90) * Math.PI / 180;
|
|
393
|
-
const hR = 94;
|
|
394
|
-
const hmx = CX + Math.cos(hRad) * hR, hmy = CX + Math.sin(hRad) * hR;
|
|
395
|
-
c.beginPath();
|
|
396
|
-
c.arc(hmx, hmy, 8, 0, Math.PI * 2);
|
|
397
|
-
c.fillStyle = "rgba(0,0,0,0.25)";
|
|
398
|
-
c.fill();
|
|
399
|
-
c.beginPath();
|
|
400
|
-
c.arc(hmx, hmy, 6.5, 0, Math.PI * 2);
|
|
401
|
-
c.strokeStyle = "#fff";
|
|
402
|
-
c.lineWidth = 2;
|
|
403
|
-
c.stroke();
|
|
404
|
-
const smx = gx + sat * sz, smy = gy + (1 - lit) * sz;
|
|
405
|
-
c.beginPath();
|
|
406
|
-
c.arc(smx, smy, 7, 0, Math.PI * 2);
|
|
407
|
-
c.fillStyle = "rgba(0,0,0,0.25)";
|
|
408
|
-
c.fill();
|
|
409
|
-
c.beginPath();
|
|
410
|
-
c.arc(smx, smy, 5.5, 0, Math.PI * 2);
|
|
411
|
-
c.strokeStyle = "#fff";
|
|
412
|
-
c.lineWidth = 2;
|
|
413
|
-
c.stroke();
|
|
414
|
-
c.beginPath();
|
|
415
|
-
c.arc(smx, smy, 3.5, 0, Math.PI * 2);
|
|
416
|
-
c.strokeStyle = "#000";
|
|
417
|
-
c.lineWidth = 1;
|
|
418
|
-
c.stroke();
|
|
419
|
-
}
|
|
420
|
-
function hitTest(x, y) {
|
|
421
|
-
if (Math.abs(x - CX) <= SQ_HALF && Math.abs(y - CX) <= SQ_HALF) return "square";
|
|
422
|
-
const dx = x - CX, dy = y - CX;
|
|
423
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
424
|
-
if (dist >= 78 && dist <= 110) return "ring";
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
function pickHue(x, y) {
|
|
428
|
-
let angle = Math.atan2(y - CX, x - CX) * 180 / Math.PI + 90;
|
|
429
|
-
if (angle < 0) angle += 360;
|
|
430
|
-
return angle;
|
|
676
|
+
/** Live wallpaper-opacity updates during slider drag (no full re-apply). */
|
|
677
|
+
function setWpOpacity(v) {
|
|
678
|
+
if (wpEl) wpEl.style.opacity = String(v);
|
|
431
679
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
function ColorWheel({ hue, sat, lit, onChange }) {
|
|
437
|
-
const cvsRef = (0, react.useRef)(null);
|
|
438
|
-
const [col, setCol] = (0, react.useState)({
|
|
439
|
-
hue,
|
|
440
|
-
sat,
|
|
441
|
-
lit
|
|
442
|
-
});
|
|
443
|
-
const colRef = (0, react.useRef)(col);
|
|
444
|
-
colRef.current = col;
|
|
445
|
-
(0, react.useEffect)(() => {
|
|
446
|
-
setCol((c) => c.hue === hue && c.sat === sat && c.lit === lit ? c : {
|
|
447
|
-
hue,
|
|
448
|
-
sat,
|
|
449
|
-
lit
|
|
450
|
-
});
|
|
451
|
-
}, [
|
|
452
|
-
hue,
|
|
453
|
-
sat,
|
|
454
|
-
lit
|
|
455
|
-
]);
|
|
456
|
-
(0, react.useEffect)(() => {
|
|
457
|
-
if (cvsRef.current) drawWheel(cvsRef.current, col.hue, col.sat, col.lit);
|
|
458
|
-
}, [col]);
|
|
459
|
-
const apply = (0, react.useCallback)((nh, ns, nl) => {
|
|
460
|
-
setCol({
|
|
461
|
-
hue: nh,
|
|
462
|
-
sat: ns,
|
|
463
|
-
lit: nl
|
|
464
|
-
});
|
|
465
|
-
onChange(nh, ns, nl);
|
|
466
|
-
}, [onChange]);
|
|
467
|
-
const onDown = (0, react.useCallback)((e) => {
|
|
468
|
-
const r = cvsRef.current.getBoundingClientRect();
|
|
469
|
-
const x = e.clientX - r.left, y = e.clientY - r.top;
|
|
470
|
-
const region = hitTest(x, y);
|
|
471
|
-
if (!region) return;
|
|
472
|
-
if (region === "ring") apply(pickHue(x, y), colRef.current.sat, colRef.current.lit);
|
|
473
|
-
else {
|
|
474
|
-
const [s, l] = pickSL(x, y);
|
|
475
|
-
apply(colRef.current.hue, s, l);
|
|
476
|
-
}
|
|
477
|
-
const onMove = (ev) => {
|
|
478
|
-
const rr = cvsRef.current.getBoundingClientRect();
|
|
479
|
-
const mx = ev.clientX - rr.left, my = ev.clientY - rr.top;
|
|
480
|
-
if (region === "ring") {
|
|
481
|
-
const d = Math.sqrt((mx - CX) ** 2 + (my - CX) ** 2);
|
|
482
|
-
if (d >= 72 && d <= 116) apply(pickHue(mx, my), colRef.current.sat, colRef.current.lit);
|
|
483
|
-
} else {
|
|
484
|
-
const [s, l] = pickSL(mx, my);
|
|
485
|
-
apply(colRef.current.hue, s, l);
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
const onUp = () => {
|
|
489
|
-
document.removeEventListener("mousemove", onMove);
|
|
490
|
-
document.removeEventListener("mouseup", onUp);
|
|
491
|
-
};
|
|
492
|
-
document.addEventListener("mousemove", onMove);
|
|
493
|
-
document.addEventListener("mouseup", onUp);
|
|
494
|
-
}, [apply]);
|
|
495
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
496
|
-
ref: cvsRef,
|
|
497
|
-
width: WHEEL_SIZE,
|
|
498
|
-
height: WHEEL_SIZE,
|
|
499
|
-
style: ST.wheelCanvas,
|
|
500
|
-
onMouseDown: onDown
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
function compress(img, side, q) {
|
|
504
|
-
const s = Math.min(1, side / Math.max(img.width, img.height));
|
|
505
|
-
const c = document.createElement("canvas");
|
|
506
|
-
c.width = Math.max(1, Math.round(img.width * s));
|
|
507
|
-
c.height = Math.max(1, Math.round(img.height * s));
|
|
508
|
-
c.getContext("2d").drawImage(img, 0, 0, c.width, c.height);
|
|
509
|
-
return c.toDataURL("image/jpeg", q);
|
|
510
|
-
}
|
|
511
|
-
function readImg(file, cb) {
|
|
512
|
-
const r = new FileReader();
|
|
513
|
-
r.onerror = () => cb(null);
|
|
514
|
-
r.onload = () => {
|
|
515
|
-
const img = new Image();
|
|
516
|
-
img.onerror = () => cb(null);
|
|
517
|
-
img.onload = () => {
|
|
518
|
-
try {
|
|
519
|
-
let u = compress(img, 1600, .75);
|
|
520
|
-
if (u.length > 2e6) u = compress(img, 1e3, .6);
|
|
521
|
-
if (u.length > 2e6) u = compress(img, 800, .5);
|
|
522
|
-
cb(u);
|
|
523
|
-
} catch {
|
|
524
|
-
cb(null);
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
img.src = r.result;
|
|
528
|
-
};
|
|
529
|
-
r.readAsDataURL(file);
|
|
680
|
+
/** Live wallpaper-blur updates during slider drag (no full re-apply). */
|
|
681
|
+
function setWpBlur(v) {
|
|
682
|
+
if (wpEl) wpEl.style.filter = v > 0 ? `blur(${v}px)` : "none";
|
|
530
683
|
}
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/client/styles.ts
|
|
531
686
|
const ST = {
|
|
532
687
|
root: {
|
|
533
688
|
display: "flex",
|
|
@@ -536,6 +691,13 @@ window.__ModuleLoader__.load({
|
|
|
536
691
|
padding: "4px 0",
|
|
537
692
|
maxWidth: "640px"
|
|
538
693
|
},
|
|
694
|
+
headerRow: {
|
|
695
|
+
display: "flex",
|
|
696
|
+
alignItems: "flex-start",
|
|
697
|
+
justifyContent: "space-between",
|
|
698
|
+
gap: "12px",
|
|
699
|
+
flexWrap: "wrap"
|
|
700
|
+
},
|
|
539
701
|
h2: {
|
|
540
702
|
color: "var(--dsw-alias-label-primary)",
|
|
541
703
|
fontSize: "18px",
|
|
@@ -579,6 +741,92 @@ window.__ModuleLoader__.load({
|
|
|
579
741
|
display: "flex",
|
|
580
742
|
justifyContent: "center"
|
|
581
743
|
},
|
|
744
|
+
wheelRow: {
|
|
745
|
+
display: "flex",
|
|
746
|
+
alignItems: "center",
|
|
747
|
+
justifyContent: "center",
|
|
748
|
+
gap: "24px",
|
|
749
|
+
flexWrap: "wrap"
|
|
750
|
+
},
|
|
751
|
+
inputPanel: {
|
|
752
|
+
display: "flex",
|
|
753
|
+
flexDirection: "column",
|
|
754
|
+
gap: "10px",
|
|
755
|
+
minWidth: "168px"
|
|
756
|
+
},
|
|
757
|
+
inputTabs: {
|
|
758
|
+
display: "flex",
|
|
759
|
+
gap: "6px"
|
|
760
|
+
},
|
|
761
|
+
inputTab: {
|
|
762
|
+
height: "26px",
|
|
763
|
+
padding: "0 12px",
|
|
764
|
+
borderRadius: "6px",
|
|
765
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
766
|
+
background: "var(--dsw-alias-button-elevated-fill)",
|
|
767
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
768
|
+
cursor: "pointer",
|
|
769
|
+
fontSize: "12px",
|
|
770
|
+
font: "inherit"
|
|
771
|
+
},
|
|
772
|
+
inputTabActive: {
|
|
773
|
+
background: "var(--dsw-alias-brand-primary)",
|
|
774
|
+
color: "var(--dsw-alias-brand-text)",
|
|
775
|
+
borderColor: "transparent"
|
|
776
|
+
},
|
|
777
|
+
inputGrid: {
|
|
778
|
+
display: "flex",
|
|
779
|
+
flexDirection: "column",
|
|
780
|
+
gap: "6px"
|
|
781
|
+
},
|
|
782
|
+
inputField: {
|
|
783
|
+
display: "flex",
|
|
784
|
+
alignItems: "center",
|
|
785
|
+
gap: "8px"
|
|
786
|
+
},
|
|
787
|
+
inputLabel: {
|
|
788
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
789
|
+
fontSize: "12px",
|
|
790
|
+
width: "14px",
|
|
791
|
+
textAlign: "center"
|
|
792
|
+
},
|
|
793
|
+
input: {
|
|
794
|
+
flex: 1,
|
|
795
|
+
height: "28px",
|
|
796
|
+
padding: "0 8px",
|
|
797
|
+
borderRadius: "6px",
|
|
798
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
799
|
+
background: "var(--dsw-alias-button-elevated-fill)",
|
|
800
|
+
color: "var(--dsw-alias-label-primary)",
|
|
801
|
+
fontSize: "12px",
|
|
802
|
+
font: "inherit",
|
|
803
|
+
boxSizing: "border-box",
|
|
804
|
+
minWidth: "0"
|
|
805
|
+
},
|
|
806
|
+
swatch: {
|
|
807
|
+
height: "22px",
|
|
808
|
+
borderRadius: "6px",
|
|
809
|
+
border: "1px solid var(--dsw-alias-border-l2)"
|
|
810
|
+
},
|
|
811
|
+
crashBox: {
|
|
812
|
+
display: "flex",
|
|
813
|
+
flexDirection: "column",
|
|
814
|
+
gap: "10px",
|
|
815
|
+
alignItems: "center",
|
|
816
|
+
padding: "24px 16px",
|
|
817
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
818
|
+
borderRadius: "10px"
|
|
819
|
+
},
|
|
820
|
+
crashTitle: {
|
|
821
|
+
color: "var(--dsw-alias-label-primary)",
|
|
822
|
+
fontSize: "15px",
|
|
823
|
+
fontWeight: 600
|
|
824
|
+
},
|
|
825
|
+
crashDesc: {
|
|
826
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
827
|
+
fontSize: "12px",
|
|
828
|
+
textAlign: "center"
|
|
829
|
+
},
|
|
582
830
|
btnGroup: {
|
|
583
831
|
display: "flex",
|
|
584
832
|
justifyContent: "center",
|
|
@@ -604,11 +852,6 @@ window.__ModuleLoader__.load({
|
|
|
604
852
|
color: "var(--dsw-alias-label-secondary)",
|
|
605
853
|
fontSize: "13px"
|
|
606
854
|
},
|
|
607
|
-
smallHint: {
|
|
608
|
-
color: "var(--dsw-alias-label-tertiary)",
|
|
609
|
-
fontSize: "11px",
|
|
610
|
-
lineHeight: "16px"
|
|
611
|
-
},
|
|
612
855
|
slider: {
|
|
613
856
|
flex: 1,
|
|
614
857
|
accentColor: "var(--dsw-alias-brand-primary)",
|
|
@@ -689,8 +932,516 @@ window.__ModuleLoader__.load({
|
|
|
689
932
|
modalBtns: {
|
|
690
933
|
display: "flex",
|
|
691
934
|
gap: "10px"
|
|
935
|
+
},
|
|
936
|
+
pickerBox: {
|
|
937
|
+
maxWidth: "min(90vw, 720px)",
|
|
938
|
+
maxHeight: "60vh",
|
|
939
|
+
overflow: "hidden",
|
|
940
|
+
borderRadius: "8px",
|
|
941
|
+
border: "2px solid rgba(255,255,255,0.3)",
|
|
942
|
+
background: "#000",
|
|
943
|
+
cursor: "crosshair"
|
|
944
|
+
},
|
|
945
|
+
pickerCanvas: {
|
|
946
|
+
display: "block",
|
|
947
|
+
maxWidth: "100%",
|
|
948
|
+
maxHeight: "60vh",
|
|
949
|
+
objectFit: "contain"
|
|
950
|
+
},
|
|
951
|
+
pickerBar: {
|
|
952
|
+
display: "flex",
|
|
953
|
+
alignItems: "center",
|
|
954
|
+
gap: "10px",
|
|
955
|
+
minWidth: "260px"
|
|
956
|
+
},
|
|
957
|
+
pickerSwatch: {
|
|
958
|
+
width: "28px",
|
|
959
|
+
height: "28px",
|
|
960
|
+
borderRadius: "6px",
|
|
961
|
+
border: "1px solid rgba(255,255,255,0.4)",
|
|
962
|
+
flexShrink: 0
|
|
963
|
+
},
|
|
964
|
+
pickerValue: {
|
|
965
|
+
color: "#fff",
|
|
966
|
+
fontSize: "13px",
|
|
967
|
+
fontFamily: "monospace"
|
|
968
|
+
},
|
|
969
|
+
magnifier: {
|
|
970
|
+
position: "fixed",
|
|
971
|
+
width: "96px",
|
|
972
|
+
height: "96px",
|
|
973
|
+
borderRadius: "50%",
|
|
974
|
+
border: "2px solid rgba(255,255,255,0.7)",
|
|
975
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.5)",
|
|
976
|
+
zIndex: 1e4,
|
|
977
|
+
background: "#000",
|
|
978
|
+
pointerEvents: "none",
|
|
979
|
+
transition: "opacity 0.08s"
|
|
692
980
|
}
|
|
693
981
|
};
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/client/utils/image.ts
|
|
984
|
+
/**
|
|
985
|
+
* Read a chosen image file as a data URL WITHOUT re-encoding: the original
|
|
986
|
+
* pixels are kept as-is (no canvas downscale / JPEG re-compression), so the
|
|
987
|
+
* wallpaper is stored and displayed at full fidelity. The tradeoff is a larger
|
|
988
|
+
* payload over the RPC channel and on disk for big images.
|
|
989
|
+
*/
|
|
990
|
+
function readImg(file, cb) {
|
|
991
|
+
const r = new FileReader();
|
|
992
|
+
r.onerror = () => cb(null);
|
|
993
|
+
r.onload = () => cb(r.result);
|
|
994
|
+
r.readAsDataURL(file);
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/client/components/ColorWheel.tsx
|
|
998
|
+
const WHEEL_SIZE = 220;
|
|
999
|
+
const CX = WHEEL_SIZE / 2;
|
|
1000
|
+
const RING_OUTER = 106;
|
|
1001
|
+
const RING_INNER = 82;
|
|
1002
|
+
const SQ_HALF = Math.round(RING_INNER / Math.SQRT2);
|
|
1003
|
+
function drawWheel(cvs, hue, sat, lit) {
|
|
1004
|
+
const c = cvs.getContext("2d");
|
|
1005
|
+
c.clearRect(0, 0, WHEEL_SIZE, WHEEL_SIZE);
|
|
1006
|
+
for (let a = 0; a < 360; a++) {
|
|
1007
|
+
const r1 = (a - 90) * Math.PI / 180;
|
|
1008
|
+
const r2 = (a + 1.5 - 90) * Math.PI / 180;
|
|
1009
|
+
c.beginPath();
|
|
1010
|
+
c.arc(CX, CX, RING_OUTER, r1, r2);
|
|
1011
|
+
c.arc(CX, CX, RING_INNER, r2, r1, true);
|
|
1012
|
+
c.closePath();
|
|
1013
|
+
c.fillStyle = `hsl(${a},100%,50%)`;
|
|
1014
|
+
c.fill();
|
|
1015
|
+
}
|
|
1016
|
+
const gx = CX - SQ_HALF, gy = CX - SQ_HALF, sz = SQ_HALF * 2;
|
|
1017
|
+
c.fillStyle = "#fff";
|
|
1018
|
+
c.fillRect(gx, gy, sz, sz);
|
|
1019
|
+
const gh = c.createLinearGradient(gx, 0, gx + sz, 0);
|
|
1020
|
+
gh.addColorStop(0, "rgba(255,255,255,1)");
|
|
1021
|
+
gh.addColorStop(1, `hsl(${hue},100%,50%)`);
|
|
1022
|
+
c.fillStyle = gh;
|
|
1023
|
+
c.fillRect(gx, gy, sz, sz);
|
|
1024
|
+
const gv = c.createLinearGradient(0, gy, 0, gy + sz);
|
|
1025
|
+
gv.addColorStop(0, "rgba(0,0,0,0)");
|
|
1026
|
+
gv.addColorStop(1, "rgba(0,0,0,1)");
|
|
1027
|
+
c.fillStyle = gv;
|
|
1028
|
+
c.fillRect(gx, gy, sz, sz);
|
|
1029
|
+
const hRad = (hue - 90) * Math.PI / 180;
|
|
1030
|
+
const hR = 94;
|
|
1031
|
+
const hmx = CX + Math.cos(hRad) * hR, hmy = CX + Math.sin(hRad) * hR;
|
|
1032
|
+
c.beginPath();
|
|
1033
|
+
c.arc(hmx, hmy, 8, 0, Math.PI * 2);
|
|
1034
|
+
c.fillStyle = "rgba(0,0,0,0.25)";
|
|
1035
|
+
c.fill();
|
|
1036
|
+
c.beginPath();
|
|
1037
|
+
c.arc(hmx, hmy, 6.5, 0, Math.PI * 2);
|
|
1038
|
+
c.strokeStyle = "#fff";
|
|
1039
|
+
c.lineWidth = 2;
|
|
1040
|
+
c.stroke();
|
|
1041
|
+
const smx = gx + sat * sz, smy = gy + (1 - lit) * sz;
|
|
1042
|
+
c.beginPath();
|
|
1043
|
+
c.arc(smx, smy, 7, 0, Math.PI * 2);
|
|
1044
|
+
c.fillStyle = "rgba(0,0,0,0.25)";
|
|
1045
|
+
c.fill();
|
|
1046
|
+
c.beginPath();
|
|
1047
|
+
c.arc(smx, smy, 5.5, 0, Math.PI * 2);
|
|
1048
|
+
c.strokeStyle = "#fff";
|
|
1049
|
+
c.lineWidth = 2;
|
|
1050
|
+
c.stroke();
|
|
1051
|
+
c.beginPath();
|
|
1052
|
+
c.arc(smx, smy, 3.5, 0, Math.PI * 2);
|
|
1053
|
+
c.strokeStyle = "#000";
|
|
1054
|
+
c.lineWidth = 1;
|
|
1055
|
+
c.stroke();
|
|
1056
|
+
}
|
|
1057
|
+
function hitTest(x, y) {
|
|
1058
|
+
if (Math.abs(x - CX) <= SQ_HALF && Math.abs(y - CX) <= SQ_HALF) return "square";
|
|
1059
|
+
const dx = x - CX, dy = y - CX;
|
|
1060
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1061
|
+
if (dist >= 78 && dist <= 110) return "ring";
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
function pickHue(x, y) {
|
|
1065
|
+
let angle = Math.atan2(y - CX, x - CX) * 180 / Math.PI + 90;
|
|
1066
|
+
if (angle < 0) angle += 360;
|
|
1067
|
+
return angle;
|
|
1068
|
+
}
|
|
1069
|
+
function pickSL(x, y) {
|
|
1070
|
+
const gx = CX - SQ_HALF, gy = CX - SQ_HALF, sz = SQ_HALF * 2;
|
|
1071
|
+
return [Math.max(0, Math.min(1, (x - gx) / sz)), Math.max(.02, Math.min(.98, 1 - (y - gy) / sz))];
|
|
1072
|
+
}
|
|
1073
|
+
function ColorWheel({ hue, sat, lit, onChange }) {
|
|
1074
|
+
const cvsRef = (0, react.useRef)(null);
|
|
1075
|
+
const [col, setCol] = (0, react.useState)({
|
|
1076
|
+
hue,
|
|
1077
|
+
sat,
|
|
1078
|
+
lit
|
|
1079
|
+
});
|
|
1080
|
+
const colRef = (0, react.useRef)(col);
|
|
1081
|
+
colRef.current = col;
|
|
1082
|
+
(0, react.useEffect)(() => {
|
|
1083
|
+
setCol((c) => c.hue === hue && c.sat === sat && c.lit === lit ? c : {
|
|
1084
|
+
hue,
|
|
1085
|
+
sat,
|
|
1086
|
+
lit
|
|
1087
|
+
});
|
|
1088
|
+
}, [
|
|
1089
|
+
hue,
|
|
1090
|
+
sat,
|
|
1091
|
+
lit
|
|
1092
|
+
]);
|
|
1093
|
+
(0, react.useEffect)(() => {
|
|
1094
|
+
if (cvsRef.current) drawWheel(cvsRef.current, col.hue, col.sat, col.lit);
|
|
1095
|
+
}, [col]);
|
|
1096
|
+
const apply = (0, react.useCallback)((nh, ns, nl) => {
|
|
1097
|
+
setCol({
|
|
1098
|
+
hue: nh,
|
|
1099
|
+
sat: ns,
|
|
1100
|
+
lit: nl
|
|
1101
|
+
});
|
|
1102
|
+
onChange(nh, ns, nl);
|
|
1103
|
+
}, [onChange]);
|
|
1104
|
+
const onDown = (0, react.useCallback)((e) => {
|
|
1105
|
+
const r = cvsRef.current.getBoundingClientRect();
|
|
1106
|
+
const x = e.clientX - r.left, y = e.clientY - r.top;
|
|
1107
|
+
const region = hitTest(x, y);
|
|
1108
|
+
if (!region) return;
|
|
1109
|
+
if (region === "ring") apply(pickHue(x, y), colRef.current.sat, colRef.current.lit);
|
|
1110
|
+
else {
|
|
1111
|
+
const [s, l] = pickSL(x, y);
|
|
1112
|
+
apply(colRef.current.hue, s, l);
|
|
1113
|
+
}
|
|
1114
|
+
const onMove = (ev) => {
|
|
1115
|
+
const rr = cvsRef.current.getBoundingClientRect();
|
|
1116
|
+
const mx = ev.clientX - rr.left, my = ev.clientY - rr.top;
|
|
1117
|
+
if (region === "ring") {
|
|
1118
|
+
const d = Math.sqrt((mx - CX) ** 2 + (my - CX) ** 2);
|
|
1119
|
+
if (d >= 72 && d <= 116) apply(pickHue(mx, my), colRef.current.sat, colRef.current.lit);
|
|
1120
|
+
} else {
|
|
1121
|
+
const [s, l] = pickSL(mx, my);
|
|
1122
|
+
apply(colRef.current.hue, s, l);
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
const onUp = () => {
|
|
1126
|
+
document.removeEventListener("mousemove", onMove);
|
|
1127
|
+
document.removeEventListener("mouseup", onUp);
|
|
1128
|
+
};
|
|
1129
|
+
document.addEventListener("mousemove", onMove);
|
|
1130
|
+
document.addEventListener("mouseup", onUp);
|
|
1131
|
+
}, [apply]);
|
|
1132
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
1133
|
+
ref: cvsRef,
|
|
1134
|
+
width: WHEEL_SIZE,
|
|
1135
|
+
height: WHEEL_SIZE,
|
|
1136
|
+
style: ST.wheelCanvas,
|
|
1137
|
+
onMouseDown: onDown
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
//#endregion
|
|
1141
|
+
//#region src/client/components/ColorInputs.tsx
|
|
1142
|
+
function clamp(v, min, max) {
|
|
1143
|
+
return Math.min(max, Math.max(min, v));
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* A single numeric field that edits one color channel. Keeps its own text
|
|
1147
|
+
* while focused so typing never gets clobbered by the parent re-rendering the
|
|
1148
|
+
* canonical value; commits every valid keystroke live and re-normalizes on
|
|
1149
|
+
* blur. The value prop only pushes back in when the field is not focused
|
|
1150
|
+
* (wheel drags, wallpaper extraction, mode switches).
|
|
1151
|
+
*/
|
|
1152
|
+
function NumField({ label, value, min, max, step, onChange }) {
|
|
1153
|
+
const [text, setText] = (0, react.useState)(String(value));
|
|
1154
|
+
const focused = (0, react.useRef)(false);
|
|
1155
|
+
(0, react.useEffect)(() => {
|
|
1156
|
+
if (!focused.current) setText(String(value));
|
|
1157
|
+
}, [value]);
|
|
1158
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1159
|
+
style: ST.inputField,
|
|
1160
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1161
|
+
style: ST.inputLabel,
|
|
1162
|
+
children: label
|
|
1163
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1164
|
+
type: "number",
|
|
1165
|
+
min,
|
|
1166
|
+
max,
|
|
1167
|
+
step,
|
|
1168
|
+
value: text,
|
|
1169
|
+
style: ST.input,
|
|
1170
|
+
onFocus: () => {
|
|
1171
|
+
focused.current = true;
|
|
1172
|
+
},
|
|
1173
|
+
onBlur: () => {
|
|
1174
|
+
focused.current = false;
|
|
1175
|
+
setText(String(value));
|
|
1176
|
+
},
|
|
1177
|
+
onChange: (e) => {
|
|
1178
|
+
setText(e.target.value);
|
|
1179
|
+
const v = Number(e.target.value);
|
|
1180
|
+
if (Number.isFinite(v)) onChange(clamp(v, min, max));
|
|
1181
|
+
}
|
|
1182
|
+
})]
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Precise color entry next to the wheel: a HSL/RGB mode toggle plus three
|
|
1187
|
+
* numeric channel fields and a live swatch. The wheel is HSV end-to-end, so
|
|
1188
|
+
* this panel converts at the boundary — HSL fields map straight onto the
|
|
1189
|
+
* stored HSL, RGB fields round-trip through rgbToHsl — and both emit HSV via
|
|
1190
|
+
* the same onChange the wheel uses, keeping one canonical color.
|
|
1191
|
+
*/
|
|
1192
|
+
function ColorInputs({ hue, sat, lit, onChange }) {
|
|
1193
|
+
const [mode, setMode] = (0, react.useState)("hsl");
|
|
1194
|
+
const [h, s, l] = hsvToHsl(hue, sat, lit);
|
|
1195
|
+
const [r, g, b] = hslToRgb(h, s, l);
|
|
1196
|
+
const setHsl = (nh, ns, nl) => onChange(...hslToHsv(nh, ns, nl));
|
|
1197
|
+
const setRgb = (nr, ng, nb) => {
|
|
1198
|
+
const [nh, ns, nl] = rgbToHsl(nr, ng, nb);
|
|
1199
|
+
onChange(...hslToHsv(nh, ns, nl));
|
|
1200
|
+
};
|
|
1201
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1202
|
+
style: ST.inputPanel,
|
|
1203
|
+
children: [
|
|
1204
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1205
|
+
style: ST.inputTabs,
|
|
1206
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1207
|
+
type: "button",
|
|
1208
|
+
style: {
|
|
1209
|
+
...ST.inputTab,
|
|
1210
|
+
...mode === "hsl" ? ST.inputTabActive : {}
|
|
1211
|
+
},
|
|
1212
|
+
onClick: () => setMode("hsl"),
|
|
1213
|
+
children: "HSL"
|
|
1214
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1215
|
+
type: "button",
|
|
1216
|
+
style: {
|
|
1217
|
+
...ST.inputTab,
|
|
1218
|
+
...mode === "rgb" ? ST.inputTabActive : {}
|
|
1219
|
+
},
|
|
1220
|
+
onClick: () => setMode("rgb"),
|
|
1221
|
+
children: "RGB"
|
|
1222
|
+
})]
|
|
1223
|
+
}),
|
|
1224
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1225
|
+
style: ST.inputGrid,
|
|
1226
|
+
children: mode === "hsl" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1227
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumField, {
|
|
1228
|
+
label: "H",
|
|
1229
|
+
value: Math.round(h),
|
|
1230
|
+
min: 0,
|
|
1231
|
+
max: 360,
|
|
1232
|
+
step: 1,
|
|
1233
|
+
onChange: (v) => setHsl(v, s, l)
|
|
1234
|
+
}),
|
|
1235
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumField, {
|
|
1236
|
+
label: "S",
|
|
1237
|
+
value: Math.round(s * 100),
|
|
1238
|
+
min: 0,
|
|
1239
|
+
max: 100,
|
|
1240
|
+
step: 1,
|
|
1241
|
+
onChange: (v) => setHsl(h, v / 100, l)
|
|
1242
|
+
}),
|
|
1243
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumField, {
|
|
1244
|
+
label: "L",
|
|
1245
|
+
value: Math.round(l * 100),
|
|
1246
|
+
min: 0,
|
|
1247
|
+
max: 100,
|
|
1248
|
+
step: 1,
|
|
1249
|
+
onChange: (v) => setHsl(h, s, v / 100)
|
|
1250
|
+
})
|
|
1251
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1252
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumField, {
|
|
1253
|
+
label: "R",
|
|
1254
|
+
value: r,
|
|
1255
|
+
min: 0,
|
|
1256
|
+
max: 255,
|
|
1257
|
+
step: 1,
|
|
1258
|
+
onChange: (v) => setRgb(v, g, b)
|
|
1259
|
+
}),
|
|
1260
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumField, {
|
|
1261
|
+
label: "G",
|
|
1262
|
+
value: g,
|
|
1263
|
+
min: 0,
|
|
1264
|
+
max: 255,
|
|
1265
|
+
step: 1,
|
|
1266
|
+
onChange: (v) => setRgb(r, v, b)
|
|
1267
|
+
}),
|
|
1268
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumField, {
|
|
1269
|
+
label: "B",
|
|
1270
|
+
value: b,
|
|
1271
|
+
min: 0,
|
|
1272
|
+
max: 255,
|
|
1273
|
+
step: 1,
|
|
1274
|
+
onChange: (v) => setRgb(r, g, v)
|
|
1275
|
+
})
|
|
1276
|
+
] })
|
|
1277
|
+
}),
|
|
1278
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
1279
|
+
...ST.swatch,
|
|
1280
|
+
background: `hsl(${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%)`
|
|
1281
|
+
} })
|
|
1282
|
+
]
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/client/components/ColorPicker.tsx
|
|
1287
|
+
const MAG_SIZE = 96;
|
|
1288
|
+
const MAG_ZOOM = 8;
|
|
1289
|
+
function toHex(rgb) {
|
|
1290
|
+
return "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Eyedropper modal: shows the wallpaper full-bleed (no drag/zoom) and lets the
|
|
1294
|
+
* user click any pixel to adopt it as the theme color. A magnifier circle
|
|
1295
|
+
* follows the cursor so small details can be picked precisely. The wallpaper
|
|
1296
|
+
* is a data URL, so sampling is CORS-free: draw it once to an offscreen-sized
|
|
1297
|
+
* canvas and read pixels via getImageData.
|
|
1298
|
+
*/
|
|
1299
|
+
function ColorPicker({ url, t, onPick, onClose }) {
|
|
1300
|
+
const canvasRef = (0, react.useRef)(null);
|
|
1301
|
+
const magRef = (0, react.useRef)(null);
|
|
1302
|
+
const imgRef = (0, react.useRef)(null);
|
|
1303
|
+
const [ready, setReady] = (0, react.useState)(false);
|
|
1304
|
+
const [hover, setHover] = (0, react.useState)(null);
|
|
1305
|
+
const [mag, setMag] = (0, react.useState)(null);
|
|
1306
|
+
(0, react.useEffect)(() => {
|
|
1307
|
+
const img = new Image();
|
|
1308
|
+
img.onload = () => {
|
|
1309
|
+
imgRef.current = img;
|
|
1310
|
+
setReady(true);
|
|
1311
|
+
};
|
|
1312
|
+
img.src = url;
|
|
1313
|
+
}, [url]);
|
|
1314
|
+
(0, react.useEffect)(() => {
|
|
1315
|
+
if (!ready || !canvasRef.current || !imgRef.current) return;
|
|
1316
|
+
const canvas = canvasRef.current;
|
|
1317
|
+
canvas.width = imgRef.current.naturalWidth;
|
|
1318
|
+
canvas.height = imgRef.current.naturalHeight;
|
|
1319
|
+
const ctx = canvas.getContext("2d");
|
|
1320
|
+
if (ctx) ctx.drawImage(imgRef.current, 0, 0);
|
|
1321
|
+
}, [ready]);
|
|
1322
|
+
const sampleAt = (0, react.useCallback)((clientX, clientY) => {
|
|
1323
|
+
const canvas = canvasRef.current;
|
|
1324
|
+
if (!canvas) return null;
|
|
1325
|
+
const rect = canvas.getBoundingClientRect();
|
|
1326
|
+
if (rect.width === 0 || rect.height === 0) return null;
|
|
1327
|
+
const ctx = canvas.getContext("2d");
|
|
1328
|
+
if (!ctx) return null;
|
|
1329
|
+
const sx = Math.round((clientX - rect.left) * (canvas.width / rect.width));
|
|
1330
|
+
const sy = Math.round((clientY - rect.top) * (canvas.height / rect.height));
|
|
1331
|
+
if (sx < 0 || sy < 0 || sx >= canvas.width || sy >= canvas.height) return null;
|
|
1332
|
+
const d = ctx.getImageData(sx, sy, 1, 1).data;
|
|
1333
|
+
return {
|
|
1334
|
+
sx,
|
|
1335
|
+
sy,
|
|
1336
|
+
rgb: [
|
|
1337
|
+
d[0],
|
|
1338
|
+
d[1],
|
|
1339
|
+
d[2]
|
|
1340
|
+
]
|
|
1341
|
+
};
|
|
1342
|
+
}, []);
|
|
1343
|
+
const drawMagnifier = (0, react.useCallback)((sx, sy) => {
|
|
1344
|
+
const mag = magRef.current;
|
|
1345
|
+
const src = canvasRef.current;
|
|
1346
|
+
if (!mag || !src) return;
|
|
1347
|
+
const ctx = mag.getContext("2d");
|
|
1348
|
+
if (!ctx) return;
|
|
1349
|
+
const half = MAG_SIZE / MAG_ZOOM / 2;
|
|
1350
|
+
ctx.clearRect(0, 0, MAG_SIZE, MAG_SIZE);
|
|
1351
|
+
ctx.drawImage(src, sx - half, sy - half, 12, 12, 0, 0, MAG_SIZE, MAG_SIZE);
|
|
1352
|
+
ctx.strokeStyle = "rgba(255,255,255,0.85)";
|
|
1353
|
+
ctx.lineWidth = 1;
|
|
1354
|
+
ctx.beginPath();
|
|
1355
|
+
ctx.moveTo(MAG_SIZE / 2, 0);
|
|
1356
|
+
ctx.lineTo(MAG_SIZE / 2, MAG_SIZE);
|
|
1357
|
+
ctx.moveTo(0, MAG_SIZE / 2);
|
|
1358
|
+
ctx.lineTo(MAG_SIZE, MAG_SIZE / 2);
|
|
1359
|
+
ctx.stroke();
|
|
1360
|
+
}, []);
|
|
1361
|
+
const onMove = (e) => {
|
|
1362
|
+
const s = sampleAt(e.clientX, e.clientY);
|
|
1363
|
+
if (!s) {
|
|
1364
|
+
setHover(null);
|
|
1365
|
+
setMag(null);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
setHover({ rgb: s.rgb });
|
|
1369
|
+
drawMagnifier(s.sx, s.sy);
|
|
1370
|
+
const off = 28;
|
|
1371
|
+
let x = e.clientX + off;
|
|
1372
|
+
let y = e.clientY + off;
|
|
1373
|
+
if (x + MAG_SIZE > window.innerWidth) x = e.clientX - off - MAG_SIZE;
|
|
1374
|
+
if (y + MAG_SIZE > window.innerHeight) y = e.clientY - off - MAG_SIZE;
|
|
1375
|
+
setMag({
|
|
1376
|
+
x,
|
|
1377
|
+
y
|
|
1378
|
+
});
|
|
1379
|
+
};
|
|
1380
|
+
const onClick = (e) => {
|
|
1381
|
+
const s = sampleAt(e.clientX, e.clientY);
|
|
1382
|
+
if (!s) return;
|
|
1383
|
+
const [h, sl, l] = rgbToHsl(s.rgb[0], s.rgb[1], s.rgb[2]);
|
|
1384
|
+
onPick(hslToHsv(h, sl, l));
|
|
1385
|
+
};
|
|
1386
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1387
|
+
style: ST.overlay,
|
|
1388
|
+
children: [
|
|
1389
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1390
|
+
style: ST.modalTitle,
|
|
1391
|
+
children: t("pickerTitle")
|
|
1392
|
+
}),
|
|
1393
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1394
|
+
style: ST.modalHint,
|
|
1395
|
+
children: t("pickerHint")
|
|
1396
|
+
}),
|
|
1397
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1398
|
+
style: ST.pickerBox,
|
|
1399
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
1400
|
+
ref: canvasRef,
|
|
1401
|
+
style: ST.pickerCanvas,
|
|
1402
|
+
onMouseMove: onMove,
|
|
1403
|
+
onMouseLeave: () => {
|
|
1404
|
+
setHover(null);
|
|
1405
|
+
setMag(null);
|
|
1406
|
+
},
|
|
1407
|
+
onClick
|
|
1408
|
+
})
|
|
1409
|
+
}),
|
|
1410
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1411
|
+
style: ST.pickerBar,
|
|
1412
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
1413
|
+
...ST.pickerSwatch,
|
|
1414
|
+
background: hover ? toHex(hover.rgb) : "transparent"
|
|
1415
|
+
} }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1416
|
+
style: ST.pickerValue,
|
|
1417
|
+
children: hover ? `${toHex(hover.rgb)} · rgb(${hover.rgb.join(", ")})` : "—"
|
|
1418
|
+
})]
|
|
1419
|
+
}),
|
|
1420
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1421
|
+
style: ST.modalBtns,
|
|
1422
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1423
|
+
type: "button",
|
|
1424
|
+
style: ST.btn,
|
|
1425
|
+
onClick: onClose,
|
|
1426
|
+
children: t("pickerClose")
|
|
1427
|
+
})
|
|
1428
|
+
}),
|
|
1429
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
1430
|
+
ref: magRef,
|
|
1431
|
+
width: MAG_SIZE,
|
|
1432
|
+
height: MAG_SIZE,
|
|
1433
|
+
style: {
|
|
1434
|
+
...ST.magnifier,
|
|
1435
|
+
left: mag?.x ?? 0,
|
|
1436
|
+
top: mag?.y ?? 0,
|
|
1437
|
+
opacity: mag ? 1 : 0
|
|
1438
|
+
}
|
|
1439
|
+
})
|
|
1440
|
+
]
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
//#endregion
|
|
1444
|
+
//#region src/client/components/BgEditor.tsx
|
|
694
1445
|
function BgEditor({ url, t, onClose, onCommit }) {
|
|
695
1446
|
const pw = Math.min(window.innerWidth * .75, 860);
|
|
696
1447
|
const ph = Math.round(pw * window.innerHeight / window.innerWidth);
|
|
@@ -772,8 +1523,10 @@ window.__ModuleLoader__.load({
|
|
|
772
1523
|
}, [pos]);
|
|
773
1524
|
const onWheelCb = (0, react.useCallback)((e) => {
|
|
774
1525
|
e.preventDefault();
|
|
775
|
-
const
|
|
776
|
-
|
|
1526
|
+
const el = containerRef.current;
|
|
1527
|
+
if (!el) return;
|
|
1528
|
+
const rect = el.getBoundingClientRect();
|
|
1529
|
+
const mx = rect.width / 2, my = rect.height / 2;
|
|
777
1530
|
const factor = e.deltaY > 0 ? .92 : 1.08;
|
|
778
1531
|
const nz = Math.max(.1, Math.min(10, zoom * factor));
|
|
779
1532
|
const nx = mx - (mx - pos.x) * (nz / zoom);
|
|
@@ -863,251 +1616,485 @@ window.__ModuleLoader__.load({
|
|
|
863
1616
|
]
|
|
864
1617
|
});
|
|
865
1618
|
}
|
|
866
|
-
|
|
1619
|
+
//#endregion
|
|
1620
|
+
//#region src/client/components/LiveSlider.tsx
|
|
1621
|
+
function LiveSlider({ min, max, step, def, fmt, label, onInput, onChange }) {
|
|
867
1622
|
const valRef = (0, react.useRef)(null);
|
|
868
1623
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
869
1624
|
style: ST.sliderRow,
|
|
870
|
-
children: [
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1625
|
+
children: [
|
|
1626
|
+
label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1627
|
+
style: ST.sliderLabel,
|
|
1628
|
+
children: label
|
|
1629
|
+
}) : null,
|
|
1630
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1631
|
+
type: "range",
|
|
1632
|
+
min,
|
|
1633
|
+
max,
|
|
1634
|
+
step,
|
|
1635
|
+
defaultValue: def,
|
|
1636
|
+
style: ST.slider,
|
|
1637
|
+
onInput: (e) => {
|
|
1638
|
+
const v = Number(e.target.value);
|
|
1639
|
+
onInput(v);
|
|
1640
|
+
if (valRef.current) valRef.current.textContent = fmt(v);
|
|
1641
|
+
},
|
|
1642
|
+
onChange: (e) => onChange(Number(e.target.value))
|
|
1643
|
+
}),
|
|
1644
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1645
|
+
ref: valRef,
|
|
1646
|
+
style: ST.sliderVal,
|
|
1647
|
+
children: fmt(def)
|
|
1648
|
+
})
|
|
1649
|
+
]
|
|
888
1650
|
});
|
|
889
1651
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1652
|
+
//#endregion
|
|
1653
|
+
//#region src/client/components/ErrorBoundary.tsx
|
|
1654
|
+
/**
|
|
1655
|
+
* Catches render errors from the theme section subtree (wheel, sliders, editor)
|
|
1656
|
+
* so a single bad state can never take down the whole settings panel. Shows a
|
|
1657
|
+
* compact fallback with a reset button; the reset re-renders the section with
|
|
1658
|
+
* the current saved config, which is enough to recover from most transient
|
|
1659
|
+
* failures (corrupt transient UI state, stale image decode, etc.).
|
|
1660
|
+
*/
|
|
1661
|
+
var ErrorBoundary = class extends react.Component {
|
|
1662
|
+
state = { error: null };
|
|
1663
|
+
static getDerivedStateFromError(error) {
|
|
1664
|
+
return { error };
|
|
1665
|
+
}
|
|
1666
|
+
componentDidCatch(error, info) {
|
|
1667
|
+
console.error("dsh-any-background: section render crashed", error, info.componentStack);
|
|
1668
|
+
}
|
|
1669
|
+
reset = () => {
|
|
1670
|
+
this.setState({ error: null });
|
|
1671
|
+
this.props.onReset?.();
|
|
1672
|
+
};
|
|
1673
|
+
render() {
|
|
1674
|
+
if (this.state.error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1675
|
+
style: ST.crashBox,
|
|
1676
|
+
children: [
|
|
906
1677
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
907
|
-
style: ST.
|
|
908
|
-
children: t("
|
|
1678
|
+
style: ST.crashTitle,
|
|
1679
|
+
children: this.props.t("crashTitle")
|
|
909
1680
|
}),
|
|
910
1681
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
911
|
-
style: ST.
|
|
912
|
-
children:
|
|
913
|
-
hue,
|
|
914
|
-
sat,
|
|
915
|
-
lit,
|
|
916
|
-
onChange: setColor
|
|
917
|
-
})
|
|
1682
|
+
style: ST.crashDesc,
|
|
1683
|
+
children: this.props.t("crashDesc")
|
|
918
1684
|
}),
|
|
919
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
920
|
-
|
|
921
|
-
|
|
1685
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1686
|
+
type: "button",
|
|
1687
|
+
style: ST.btn,
|
|
1688
|
+
onClick: this.reset,
|
|
1689
|
+
children: this.props.t("crashReset")
|
|
922
1690
|
})
|
|
923
|
-
]
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1691
|
+
]
|
|
1692
|
+
});
|
|
1693
|
+
return this.props.children;
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/client/components/ThemeSection.tsx
|
|
1698
|
+
function ThemeSection(props) {
|
|
1699
|
+
const { t, hue, sat, lit, setColor, setWp, setOps, setBlurs, setWop, setBl, setSop, useStore, extractColor, exportTheme, importTheme } = props;
|
|
1700
|
+
const storeUrl = useStore((s) => s.url);
|
|
1701
|
+
const storeColor = useStore((s) => s.color);
|
|
1702
|
+
const fileRef = (0, react.useRef)(null);
|
|
1703
|
+
const importRef = (0, react.useRef)(null);
|
|
1704
|
+
const [editorOpen, setEditorOpen] = (0, react.useState)(false);
|
|
1705
|
+
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
1706
|
+
const [extracting, setExtracting] = (0, react.useState)(false);
|
|
1707
|
+
const [extractMsg, setExtractMsg] = (0, react.useState)(null);
|
|
1708
|
+
const [importMsg, setImportMsg] = (0, react.useState)(null);
|
|
1709
|
+
const msgTimer = (0, react.useRef)(void 0);
|
|
1710
|
+
(0, react.useEffect)(() => () => window.clearTimeout(msgTimer.current), []);
|
|
1711
|
+
const showMsg = (setter, key) => {
|
|
1712
|
+
setter(t(key));
|
|
1713
|
+
window.clearTimeout(msgTimer.current);
|
|
1714
|
+
msgTimer.current = window.setTimeout(() => setter(null), 2500);
|
|
1715
|
+
};
|
|
1716
|
+
const onExtract = async () => {
|
|
1717
|
+
if (!storeUrl || extracting) return;
|
|
1718
|
+
setExtracting(true);
|
|
1719
|
+
try {
|
|
1720
|
+
showMsg(setExtractMsg, await extractColor() ? "extractDone" : "extractFail");
|
|
1721
|
+
} catch {
|
|
1722
|
+
showMsg(setExtractMsg, "extractFail");
|
|
1723
|
+
} finally {
|
|
1724
|
+
setExtracting(false);
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
const onImport = async (file) => {
|
|
1728
|
+
try {
|
|
1729
|
+
showMsg(setImportMsg, await importTheme(file) ? "importDone" : "importFail");
|
|
1730
|
+
} catch {
|
|
1731
|
+
showMsg(setImportMsg, "importFail");
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
const wheel = storeColor ?? [
|
|
1735
|
+
hue,
|
|
1736
|
+
sat,
|
|
1737
|
+
lit
|
|
1738
|
+
];
|
|
1739
|
+
const blurSlider = (part) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
1740
|
+
label: t("uiBlur"),
|
|
1741
|
+
min: 0,
|
|
1742
|
+
max: 60,
|
|
1743
|
+
step: 1,
|
|
1744
|
+
def: rBlurs()[part],
|
|
1745
|
+
fmt: (v) => `${v}px`,
|
|
1746
|
+
onInput: (v) => {
|
|
1747
|
+
const blurs = { ...rBlurs() };
|
|
1748
|
+
blurs[part] = v;
|
|
1749
|
+
cfg.blurs = blurs;
|
|
1750
|
+
setPartBlur(part, v);
|
|
1751
|
+
saveConfig();
|
|
1752
|
+
},
|
|
1753
|
+
onChange: (v) => {
|
|
1754
|
+
const blurs = { ...rBlurs() };
|
|
1755
|
+
blurs[part] = v;
|
|
1756
|
+
setBlurs(blurs);
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
const partSlider = (labelKey, part) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1760
|
+
style: ST.sliderBlock,
|
|
1761
|
+
children: [
|
|
1762
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1763
|
+
style: ST.sliderLabel,
|
|
1764
|
+
children: t(labelKey)
|
|
1765
|
+
}),
|
|
1766
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
1767
|
+
label: t("uiOpacity"),
|
|
1768
|
+
min: 0,
|
|
1769
|
+
max: 100,
|
|
1770
|
+
step: 1,
|
|
1771
|
+
def: Math.round(rOps()[part] * 100),
|
|
1772
|
+
fmt: (v) => `${v}%`,
|
|
1773
|
+
onInput: (v) => {
|
|
1774
|
+
const ops = { ...rOps() };
|
|
1775
|
+
ops[part] = v / 100;
|
|
1776
|
+
cfg.opacities = ops;
|
|
1777
|
+
applyCustomTokens(ops);
|
|
1778
|
+
saveConfig();
|
|
1779
|
+
},
|
|
1780
|
+
onChange: (v) => {
|
|
1781
|
+
const ops = { ...rOps() };
|
|
1782
|
+
ops[part] = v / 100;
|
|
1783
|
+
setOps(ops);
|
|
1784
|
+
}
|
|
1785
|
+
}),
|
|
1786
|
+
blurSlider(part)
|
|
1787
|
+
]
|
|
1788
|
+
});
|
|
1789
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary, {
|
|
1790
|
+
t,
|
|
1791
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1792
|
+
style: ST.root,
|
|
1793
|
+
children: [
|
|
1794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1795
|
+
style: ST.headerRow,
|
|
1796
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1797
|
+
style: ST.h2,
|
|
1798
|
+
children: t("nav")
|
|
1799
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1800
|
+
style: ST.sub,
|
|
1801
|
+
children: t("subtitle")
|
|
1802
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1002
1803
|
style: ST.row,
|
|
1003
1804
|
children: [
|
|
1004
1805
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1005
1806
|
type: "button",
|
|
1006
1807
|
style: ST.btn,
|
|
1007
|
-
onClick:
|
|
1008
|
-
children: t("
|
|
1808
|
+
onClick: exportTheme,
|
|
1809
|
+
children: t("exportTheme")
|
|
1009
1810
|
}),
|
|
1010
|
-
|
|
1811
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1011
1812
|
type: "button",
|
|
1012
1813
|
style: ST.btn,
|
|
1013
|
-
onClick: () =>
|
|
1014
|
-
children: t("
|
|
1015
|
-
})
|
|
1016
|
-
storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1017
|
-
type: "button",
|
|
1018
|
-
style: {
|
|
1019
|
-
...ST.btn,
|
|
1020
|
-
...ST.btnDanger
|
|
1021
|
-
},
|
|
1022
|
-
onClick: () => setWp(null),
|
|
1023
|
-
children: t("bgRemove")
|
|
1024
|
-
}) : null,
|
|
1814
|
+
onClick: () => importRef.current?.click(),
|
|
1815
|
+
children: t("importTheme")
|
|
1816
|
+
}),
|
|
1025
1817
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1026
|
-
ref:
|
|
1818
|
+
ref: importRef,
|
|
1027
1819
|
type: "file",
|
|
1028
|
-
accept: "
|
|
1820
|
+
accept: "application/json,.json",
|
|
1029
1821
|
style: { display: "none" },
|
|
1030
1822
|
onChange: (e) => {
|
|
1031
1823
|
const f = e.target.files?.[0];
|
|
1032
1824
|
if (!f) return;
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
e.target.value = "";
|
|
1036
|
-
});
|
|
1825
|
+
onImport(f);
|
|
1826
|
+
e.target.value = "";
|
|
1037
1827
|
}
|
|
1038
1828
|
})
|
|
1039
1829
|
]
|
|
1040
|
-
})
|
|
1830
|
+
})]
|
|
1041
1831
|
}),
|
|
1042
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1043
|
-
style: ST.
|
|
1044
|
-
children:
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1832
|
+
importMsg ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1833
|
+
style: ST.colorHint,
|
|
1834
|
+
children: importMsg
|
|
1835
|
+
}) : null,
|
|
1836
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
1837
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1838
|
+
style: ST.label,
|
|
1839
|
+
children: t("colorTitle")
|
|
1840
|
+
}),
|
|
1841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1842
|
+
style: ST.wheelRow,
|
|
1843
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorWheel, {
|
|
1844
|
+
hue: wheel[0],
|
|
1845
|
+
sat: wheel[1],
|
|
1846
|
+
lit: wheel[2],
|
|
1847
|
+
onChange: setColor
|
|
1848
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorInputs, {
|
|
1849
|
+
hue: wheel[0],
|
|
1850
|
+
sat: wheel[1],
|
|
1851
|
+
lit: wheel[2],
|
|
1852
|
+
onChange: setColor
|
|
1062
1853
|
})]
|
|
1063
|
-
}),
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
cfg.blur = v;
|
|
1076
|
-
if (wpEl) wpEl.style.filter = v > 0 ? `blur(${v}px)` : "none";
|
|
1077
|
-
saveConfig();
|
|
1854
|
+
}),
|
|
1855
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1856
|
+
style: ST.colorHint,
|
|
1857
|
+
children: t("colorHint")
|
|
1858
|
+
}),
|
|
1859
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1860
|
+
style: ST.btnGroup,
|
|
1861
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1862
|
+
type: "button",
|
|
1863
|
+
style: {
|
|
1864
|
+
...ST.btn,
|
|
1865
|
+
opacity: !storeUrl || extracting ? .55 : 1
|
|
1078
1866
|
},
|
|
1079
|
-
|
|
1867
|
+
title: !storeUrl ? t("extractNoWp") : void 0,
|
|
1868
|
+
disabled: !storeUrl || extracting,
|
|
1869
|
+
onClick: onExtract,
|
|
1870
|
+
children: extracting ? t("extracting") : t("extractColor")
|
|
1871
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1872
|
+
type: "button",
|
|
1873
|
+
style: {
|
|
1874
|
+
...ST.btn,
|
|
1875
|
+
opacity: !storeUrl ? .55 : 1
|
|
1876
|
+
},
|
|
1877
|
+
title: !storeUrl ? t("extractNoWp") : void 0,
|
|
1878
|
+
disabled: !storeUrl,
|
|
1879
|
+
onClick: () => setPickerOpen(true),
|
|
1880
|
+
children: t("eyedropper")
|
|
1080
1881
|
})]
|
|
1081
|
-
})
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
})
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1882
|
+
}),
|
|
1883
|
+
extractMsg ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1884
|
+
style: ST.colorHint,
|
|
1885
|
+
children: extractMsg
|
|
1886
|
+
}) : null
|
|
1887
|
+
] }),
|
|
1888
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { style: ST.hr }),
|
|
1889
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1890
|
+
style: ST.label,
|
|
1891
|
+
children: t("uiTitle")
|
|
1892
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1893
|
+
style: ST.sliders,
|
|
1894
|
+
children: [
|
|
1895
|
+
partSlider("uiOpacityBg", "bg"),
|
|
1896
|
+
partSlider("uiOpacitySide", "sidebar"),
|
|
1897
|
+
partSlider("uiOpacityCard", "card"),
|
|
1898
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1899
|
+
style: ST.sliderBlock,
|
|
1900
|
+
children: [
|
|
1901
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1902
|
+
style: ST.sliderLabel,
|
|
1903
|
+
children: t("uiSop")
|
|
1904
|
+
}),
|
|
1905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
1906
|
+
label: t("uiOpacity"),
|
|
1907
|
+
min: 0,
|
|
1908
|
+
max: 100,
|
|
1909
|
+
step: 1,
|
|
1910
|
+
def: Math.round(rSop() * 100),
|
|
1911
|
+
fmt: (v) => `${v}%`,
|
|
1912
|
+
onInput: (v) => {
|
|
1913
|
+
const op = v / 100;
|
|
1914
|
+
cfg.settingsOpacity = op;
|
|
1915
|
+
applySettingsOverrides(op);
|
|
1916
|
+
saveConfig();
|
|
1917
|
+
},
|
|
1918
|
+
onChange: (v) => setSop(v / 100)
|
|
1919
|
+
}),
|
|
1920
|
+
blurSlider("settings")
|
|
1921
|
+
]
|
|
1922
|
+
})
|
|
1923
|
+
]
|
|
1924
|
+
})] }),
|
|
1925
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { style: ST.hr }),
|
|
1926
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
1927
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1928
|
+
style: ST.label,
|
|
1929
|
+
children: t("bgTitle")
|
|
1930
|
+
}),
|
|
1931
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1932
|
+
style: ST.center,
|
|
1933
|
+
children: storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1934
|
+
src: storeUrl,
|
|
1935
|
+
alt: "",
|
|
1936
|
+
style: ST.preview,
|
|
1937
|
+
onClick: () => setEditorOpen(true)
|
|
1938
|
+
}) : null
|
|
1939
|
+
}),
|
|
1940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1941
|
+
style: ST.btnGroup,
|
|
1942
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1943
|
+
style: ST.row,
|
|
1944
|
+
children: [
|
|
1945
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1946
|
+
type: "button",
|
|
1947
|
+
style: ST.btn,
|
|
1948
|
+
onClick: () => fileRef.current?.click(),
|
|
1949
|
+
children: t("bgChoose")
|
|
1950
|
+
}),
|
|
1951
|
+
storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1952
|
+
type: "button",
|
|
1953
|
+
style: ST.btn,
|
|
1954
|
+
onClick: () => setEditorOpen(true),
|
|
1955
|
+
children: t("bgEdit")
|
|
1956
|
+
}) : null,
|
|
1957
|
+
storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1958
|
+
type: "button",
|
|
1959
|
+
style: {
|
|
1960
|
+
...ST.btn,
|
|
1961
|
+
...ST.btnDanger
|
|
1962
|
+
},
|
|
1963
|
+
onClick: () => setWp(null),
|
|
1964
|
+
children: t("bgRemove")
|
|
1965
|
+
}) : null,
|
|
1966
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1967
|
+
ref: fileRef,
|
|
1968
|
+
type: "file",
|
|
1969
|
+
accept: "image/*",
|
|
1970
|
+
style: { display: "none" },
|
|
1971
|
+
onChange: (e) => {
|
|
1972
|
+
const f = e.target.files?.[0];
|
|
1973
|
+
if (!f) return;
|
|
1974
|
+
readImg(f, (d) => {
|
|
1975
|
+
if (d) setWp(d);
|
|
1976
|
+
e.target.value = "";
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
})
|
|
1980
|
+
]
|
|
1981
|
+
})
|
|
1982
|
+
}),
|
|
1983
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1984
|
+
style: ST.sliders,
|
|
1985
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1986
|
+
style: ST.sliderBlock,
|
|
1987
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1988
|
+
style: ST.sliderLabel,
|
|
1989
|
+
children: t("wpOpacity")
|
|
1990
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
1991
|
+
min: 0,
|
|
1992
|
+
max: 100,
|
|
1993
|
+
step: 1,
|
|
1994
|
+
def: Math.round(rWop() * 100),
|
|
1995
|
+
fmt: (v) => `${v}%`,
|
|
1996
|
+
onInput: (v) => {
|
|
1997
|
+
const op = v / 100;
|
|
1998
|
+
cfg.wallpaperOpacity = op;
|
|
1999
|
+
setWpOpacity(op);
|
|
2000
|
+
saveConfig();
|
|
2001
|
+
},
|
|
2002
|
+
onChange: (v) => setWop(v / 100)
|
|
2003
|
+
})]
|
|
2004
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2005
|
+
style: ST.sliderBlock,
|
|
2006
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2007
|
+
style: ST.sliderLabel,
|
|
2008
|
+
children: t("bgBlur")
|
|
2009
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
2010
|
+
min: 0,
|
|
2011
|
+
max: 60,
|
|
2012
|
+
step: 1,
|
|
2013
|
+
def: rBl(),
|
|
2014
|
+
fmt: (v) => `${v}px`,
|
|
2015
|
+
onInput: (v) => {
|
|
2016
|
+
cfg.blur = v;
|
|
2017
|
+
setWpBlur(v);
|
|
2018
|
+
saveConfig();
|
|
2019
|
+
},
|
|
2020
|
+
onChange: (v) => setBl(v)
|
|
2021
|
+
})]
|
|
2022
|
+
})]
|
|
2023
|
+
}),
|
|
2024
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2025
|
+
style: ST.hint,
|
|
2026
|
+
children: t("bgHint")
|
|
2027
|
+
})
|
|
2028
|
+
] }),
|
|
2029
|
+
pickerOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorPicker, {
|
|
2030
|
+
url: storeUrl,
|
|
2031
|
+
t,
|
|
2032
|
+
onClose: () => setPickerOpen(false),
|
|
2033
|
+
onPick: (hsv) => {
|
|
2034
|
+
setColor(hsv[0], hsv[1], hsv[2]);
|
|
2035
|
+
setPickerOpen(false);
|
|
2036
|
+
}
|
|
2037
|
+
}) : null,
|
|
2038
|
+
editorOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
|
|
2039
|
+
url: storeUrl,
|
|
2040
|
+
t,
|
|
2041
|
+
onClose: () => setEditorOpen(false),
|
|
2042
|
+
onCommit: (z, x, y, iw, ih) => {
|
|
2043
|
+
cfg.bgState = {
|
|
2044
|
+
zoom: z,
|
|
2045
|
+
x,
|
|
2046
|
+
y,
|
|
2047
|
+
iw,
|
|
2048
|
+
ih
|
|
2049
|
+
};
|
|
2050
|
+
applyWp();
|
|
2051
|
+
saveConfig();
|
|
2052
|
+
setEditorOpen(false);
|
|
2053
|
+
}
|
|
2054
|
+
}) : null
|
|
2055
|
+
]
|
|
2056
|
+
})
|
|
1106
2057
|
});
|
|
1107
2058
|
}
|
|
2059
|
+
//#endregion
|
|
2060
|
+
//#region src/client/components/icons.tsx
|
|
2061
|
+
/** Sun glyph, migrated from @deepseek-ai/dsh-client-ui-primitives IconLightOutline16. */
|
|
2062
|
+
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.5V7.35461Z\" fill=\"currentColor\"/>";
|
|
2063
|
+
//#endregion
|
|
2064
|
+
//#region src/client/index.tsx
|
|
2065
|
+
/**
|
|
2066
|
+
* dsh-any-background — browser half entry.
|
|
2067
|
+
*
|
|
2068
|
+
* Appearance plugin with:
|
|
2069
|
+
* 1. PS-style color wheel (hue ring + saturation/lightness square) for
|
|
2070
|
+
* real-time theme color selection with dynamic token generation.
|
|
2071
|
+
* 2. Background image editor modal with drag-to-pan and scroll-to-zoom
|
|
2072
|
+
* inside a viewport-proportional preview rectangle.
|
|
2073
|
+
* 3. Opacity / blur sliders with zero-lag direct DOM manipulation: the
|
|
2074
|
+
* homepage background opacity (主界面) and the settings panel opacity
|
|
2075
|
+
* (设置界面透明度) are separate sliders.
|
|
2076
|
+
*
|
|
2077
|
+
* This file wires the plugin lifecycle (theme registration, wallpaper layer,
|
|
2078
|
+
* viewport watch, i18n, settings-section injection, boot restore, watchdog).
|
|
2079
|
+
* The heavy lifting lives in the sibling modules:
|
|
2080
|
+
* state.ts in-memory config mirror + getters
|
|
2081
|
+
* rpc.ts file-backed persistence over the /dsh-any-background channel
|
|
2082
|
+
* wallpaper.ts wallpaper DOM layer + inline token writes
|
|
2083
|
+
* utils/ color math, token generation, image compression
|
|
2084
|
+
* components/ ColorWheel, BgEditor, LiveSlider, ThemeSection
|
|
2085
|
+
* i18n.ts zh/en dictionaries
|
|
2086
|
+
* styles.ts shared inline styles
|
|
2087
|
+
*/
|
|
2088
|
+
const name = "dsh-any-background";
|
|
2089
|
+
const inject = [
|
|
2090
|
+
"slots",
|
|
2091
|
+
"locale",
|
|
2092
|
+
"theme",
|
|
2093
|
+
"connection"
|
|
2094
|
+
];
|
|
2095
|
+
const CUSTOM_ID = "custom-color";
|
|
1108
2096
|
function apply(ctx) {
|
|
1109
|
-
|
|
1110
|
-
rpcCallFn = (endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res);
|
|
2097
|
+
initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
|
|
1111
2098
|
const [initH, initS, initL] = rColor();
|
|
1112
2099
|
let customDispose = null;
|
|
1113
2100
|
const registerCustom = (h, s, l) => {
|
|
@@ -1122,7 +2109,7 @@ window.__ModuleLoader__.load({
|
|
|
1122
2109
|
} catch {
|
|
1123
2110
|
customDispose = null;
|
|
1124
2111
|
}
|
|
1125
|
-
ctx.theme.setTheme(CUSTOM_ID);
|
|
2112
|
+
if (ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
|
|
1126
2113
|
};
|
|
1127
2114
|
if (rHasColor()) registerCustom(initH, initS, initL);
|
|
1128
2115
|
ctx.effect(() => () => {
|
|
@@ -1132,34 +2119,53 @@ window.__ModuleLoader__.load({
|
|
|
1132
2119
|
if (typeof document !== "undefined") {
|
|
1133
2120
|
styleEl = document.createElement("style");
|
|
1134
2121
|
styleEl.dataset.plugin = "dsh-any-background";
|
|
1135
|
-
styleEl.textContent = `body[data-ds-dark-theme="
|
|
2122
|
+
styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}`;
|
|
1136
2123
|
document.head.appendChild(styleEl);
|
|
1137
2124
|
}
|
|
1138
2125
|
ctx.effect(() => () => {
|
|
1139
2126
|
styleEl?.parentNode?.removeChild(styleEl);
|
|
1140
2127
|
}, "dsh-any-background: gradient");
|
|
1141
2128
|
let rev = 0;
|
|
2129
|
+
let colorRev = 0;
|
|
1142
2130
|
const store = (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
|
|
1143
2131
|
init: () => ({
|
|
1144
2132
|
url: null,
|
|
1145
|
-
rev: -1
|
|
2133
|
+
rev: -1,
|
|
2134
|
+
colorRev: -1,
|
|
2135
|
+
color: null
|
|
1146
2136
|
}),
|
|
1147
|
-
actions: {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
2137
|
+
actions: {
|
|
2138
|
+
syncBg: (d, url, r) => {
|
|
2139
|
+
if (r <= d.rev) return;
|
|
2140
|
+
d.url = url;
|
|
2141
|
+
d.rev = r;
|
|
2142
|
+
},
|
|
2143
|
+
syncColor: (d, hsv, r) => {
|
|
2144
|
+
if (r <= d.colorRev) return;
|
|
2145
|
+
d.color = hsv;
|
|
2146
|
+
d.colorRev = r;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
1152
2149
|
});
|
|
1153
|
-
let bound;
|
|
2150
|
+
let bound = null;
|
|
1154
2151
|
const syncBg = () => {
|
|
1155
2152
|
rev++;
|
|
1156
2153
|
bound?.syncBg(rWp(), rev);
|
|
1157
2154
|
};
|
|
1158
|
-
applyWp(
|
|
2155
|
+
applyWp();
|
|
1159
2156
|
syncBg();
|
|
2157
|
+
watchParts();
|
|
1160
2158
|
loadPersisted().then(() => {
|
|
1161
|
-
|
|
2159
|
+
if (rHasColor()) {
|
|
2160
|
+
const [h, s, l] = rColor();
|
|
2161
|
+
registerCustom(h, s, l);
|
|
2162
|
+
}
|
|
2163
|
+
applyWp();
|
|
1162
2164
|
syncBg();
|
|
2165
|
+
if (rHasColor()) {
|
|
2166
|
+
colorRev++;
|
|
2167
|
+
bound?.syncColor(hslToHsv(...rColor()), colorRev);
|
|
2168
|
+
}
|
|
1163
2169
|
});
|
|
1164
2170
|
ctx.effect(() => () => {
|
|
1165
2171
|
teardownWp();
|
|
@@ -1169,14 +2175,14 @@ window.__ModuleLoader__.load({
|
|
|
1169
2175
|
const snapshot = ctx.theme.getTheme();
|
|
1170
2176
|
if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
|
|
1171
2177
|
}
|
|
1172
|
-
applyWp(
|
|
2178
|
+
applyWp();
|
|
1173
2179
|
}), "dsh-any-background: theme change");
|
|
1174
2180
|
let frame = 0;
|
|
1175
2181
|
const applySoon = () => {
|
|
1176
2182
|
if (frame !== 0) return;
|
|
1177
2183
|
frame = requestAnimationFrame(() => {
|
|
1178
2184
|
frame = 0;
|
|
1179
|
-
applyWp(
|
|
2185
|
+
applyWp();
|
|
1180
2186
|
});
|
|
1181
2187
|
};
|
|
1182
2188
|
const sentinel = document.createElement("div");
|
|
@@ -1213,31 +2219,43 @@ window.__ModuleLoader__.load({
|
|
|
1213
2219
|
sl
|
|
1214
2220
|
];
|
|
1215
2221
|
registerCustom(sh, ss, sl);
|
|
1216
|
-
applyWp(
|
|
2222
|
+
applyWp();
|
|
1217
2223
|
saveConfig();
|
|
2224
|
+
colorRev++;
|
|
2225
|
+
bound?.syncColor([
|
|
2226
|
+
nh,
|
|
2227
|
+
ns,
|
|
2228
|
+
nl
|
|
2229
|
+
], colorRev);
|
|
1218
2230
|
},
|
|
1219
2231
|
setWp: (u) => {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
applyWp(
|
|
2232
|
+
setWpUrl(u);
|
|
2233
|
+
setBgState({ ...DEFAULT_CONFIG.bgState });
|
|
2234
|
+
applyWp();
|
|
2235
|
+
syncBg();
|
|
2236
|
+
persistWallpaper(u);
|
|
2237
|
+
},
|
|
2238
|
+
setOps: (ops) => {
|
|
2239
|
+
cfg.opacities = ops;
|
|
2240
|
+
applyWp();
|
|
1223
2241
|
syncBg();
|
|
1224
|
-
|
|
2242
|
+
saveConfig();
|
|
1225
2243
|
},
|
|
1226
|
-
|
|
1227
|
-
cfg.
|
|
1228
|
-
applyWp(
|
|
2244
|
+
setBlurs: (blurs) => {
|
|
2245
|
+
cfg.blurs = blurs;
|
|
2246
|
+
applyWp();
|
|
1229
2247
|
syncBg();
|
|
1230
2248
|
saveConfig();
|
|
1231
2249
|
},
|
|
1232
2250
|
setWop: (v) => {
|
|
1233
2251
|
cfg.wallpaperOpacity = v;
|
|
1234
|
-
applyWp(
|
|
2252
|
+
applyWp();
|
|
1235
2253
|
syncBg();
|
|
1236
2254
|
saveConfig();
|
|
1237
2255
|
},
|
|
1238
2256
|
setBl: (v) => {
|
|
1239
2257
|
cfg.blur = v;
|
|
1240
|
-
applyWp(
|
|
2258
|
+
applyWp();
|
|
1241
2259
|
syncBg();
|
|
1242
2260
|
saveConfig();
|
|
1243
2261
|
},
|
|
@@ -1245,6 +2263,61 @@ window.__ModuleLoader__.load({
|
|
|
1245
2263
|
cfg.settingsOpacity = v;
|
|
1246
2264
|
applySettingsOverrides(v);
|
|
1247
2265
|
saveConfig();
|
|
2266
|
+
},
|
|
2267
|
+
extractColor: async () => {
|
|
2268
|
+
const url = rWp();
|
|
2269
|
+
if (!url) return false;
|
|
2270
|
+
const hsl = await extractWallpaperColor(url, rBgState());
|
|
2271
|
+
if (!hsl) return false;
|
|
2272
|
+
cfg.color = hsl;
|
|
2273
|
+
registerCustom(hsl[0], hsl[1], hsl[2]);
|
|
2274
|
+
applyWp();
|
|
2275
|
+
saveConfig();
|
|
2276
|
+
const hsv = hslToHsv(hsl[0], hsl[1], hsl[2]);
|
|
2277
|
+
colorRev++;
|
|
2278
|
+
bound?.syncColor(hsv, colorRev);
|
|
2279
|
+
return true;
|
|
2280
|
+
},
|
|
2281
|
+
exportTheme: () => {
|
|
2282
|
+
const payload = {
|
|
2283
|
+
version: 1,
|
|
2284
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2285
|
+
config: cfg,
|
|
2286
|
+
wallpaper: rWp()
|
|
2287
|
+
};
|
|
2288
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
|
|
2289
|
+
const url = URL.createObjectURL(blob);
|
|
2290
|
+
const a = document.createElement("a");
|
|
2291
|
+
a.href = url;
|
|
2292
|
+
a.download = "dsh-any-theme.json";
|
|
2293
|
+
a.click();
|
|
2294
|
+
URL.revokeObjectURL(url);
|
|
2295
|
+
},
|
|
2296
|
+
importTheme: async (file) => {
|
|
2297
|
+
try {
|
|
2298
|
+
const data = JSON.parse(await file.text());
|
|
2299
|
+
if (!data || typeof data !== "object") return false;
|
|
2300
|
+
const d = data;
|
|
2301
|
+
if (typeof d.config !== "object" || d.config === null) return false;
|
|
2302
|
+
adoptConfig(d.config);
|
|
2303
|
+
const wallpaper = typeof d.wallpaper === "string" && /^data:image\//.test(d.wallpaper) ? d.wallpaper : null;
|
|
2304
|
+
setWpUrl(wallpaper);
|
|
2305
|
+
persistConfig();
|
|
2306
|
+
persistWallpaper(wallpaper);
|
|
2307
|
+
if (rHasColor()) {
|
|
2308
|
+
const [h, s, l] = rColor();
|
|
2309
|
+
registerCustom(h, s, l);
|
|
2310
|
+
}
|
|
2311
|
+
applyWp();
|
|
2312
|
+
syncBg();
|
|
2313
|
+
if (rHasColor()) {
|
|
2314
|
+
colorRev++;
|
|
2315
|
+
bound?.syncColor(hslToHsv(...rColor()), colorRev);
|
|
2316
|
+
}
|
|
2317
|
+
return true;
|
|
2318
|
+
} catch {
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
1248
2321
|
}
|
|
1249
2322
|
};
|
|
1250
2323
|
};
|
|
@@ -1257,12 +2330,60 @@ window.__ModuleLoader__.load({
|
|
|
1257
2330
|
store,
|
|
1258
2331
|
inject: sectionInject
|
|
1259
2332
|
}, ThemeSection));
|
|
2333
|
+
const navLabel = () => ctx.locale.bind(NS)("nav");
|
|
2334
|
+
const applyNavIcon = () => {
|
|
2335
|
+
const nav = document.querySelector("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]")?.querySelector("nav");
|
|
2336
|
+
if (!nav) return;
|
|
2337
|
+
const target = navLabel();
|
|
2338
|
+
for (const cell of Array.from(nav.querySelectorAll("button"))) {
|
|
2339
|
+
const label = cell.querySelector("span");
|
|
2340
|
+
if (label && label.textContent?.trim() === target) {
|
|
2341
|
+
const svg = cell.querySelector("svg");
|
|
2342
|
+
if (svg && svg.dataset.dshAnyIcon !== "1") {
|
|
2343
|
+
const sun = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2344
|
+
sun.setAttribute("width", "16");
|
|
2345
|
+
sun.setAttribute("height", "16");
|
|
2346
|
+
sun.setAttribute("viewBox", "0 0 16 16");
|
|
2347
|
+
sun.setAttribute("fill", "none");
|
|
2348
|
+
sun.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
2349
|
+
sun.dataset.dshAnyIcon = "1";
|
|
2350
|
+
sun.innerHTML = SUN_PATHS;
|
|
2351
|
+
svg.replaceWith(sun);
|
|
2352
|
+
}
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
let navIconObserver = null;
|
|
2358
|
+
const watchNavIcon = () => {
|
|
2359
|
+
if (navIconObserver !== null || typeof MutationObserver === "undefined") return;
|
|
2360
|
+
navIconObserver = new MutationObserver((records) => {
|
|
2361
|
+
if (records.some((r) => {
|
|
2362
|
+
for (const n of r.addedNodes) {
|
|
2363
|
+
if (n.nodeType !== 1) continue;
|
|
2364
|
+
const el = n;
|
|
2365
|
+
if (el.matches?.("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]") || el.querySelector?.("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]")) return true;
|
|
2366
|
+
}
|
|
2367
|
+
return false;
|
|
2368
|
+
})) applyNavIcon();
|
|
2369
|
+
});
|
|
2370
|
+
navIconObserver.observe(document.body, {
|
|
2371
|
+
childList: true,
|
|
2372
|
+
subtree: true
|
|
2373
|
+
});
|
|
2374
|
+
applyNavIcon();
|
|
2375
|
+
};
|
|
2376
|
+
watchNavIcon();
|
|
2377
|
+
ctx.effect(() => () => {
|
|
2378
|
+
navIconObserver?.disconnect();
|
|
2379
|
+
navIconObserver = null;
|
|
2380
|
+
}, "dsh-any-background: nav icon watch");
|
|
1260
2381
|
const restoreSaved = () => {
|
|
1261
2382
|
if (rHasColor()) {
|
|
1262
2383
|
const [h, s, l] = rColor();
|
|
1263
2384
|
registerCustom(h, s, l);
|
|
1264
2385
|
}
|
|
1265
|
-
applyWp(
|
|
2386
|
+
applyWp();
|
|
1266
2387
|
};
|
|
1267
2388
|
const restoreTimers = [300, 1500].map((delay) => window.setTimeout(restoreSaved, delay));
|
|
1268
2389
|
ctx.effect(() => () => {
|
|
@@ -1280,11 +2401,14 @@ window.__ModuleLoader__.load({
|
|
|
1280
2401
|
ctx.theme.setTheme(CUSTOM_ID);
|
|
1281
2402
|
changed = true;
|
|
1282
2403
|
}
|
|
1283
|
-
if (changed) applyWp(
|
|
2404
|
+
if (changed) applyWp();
|
|
1284
2405
|
}, 1e3);
|
|
1285
2406
|
ctx.effect(() => () => {
|
|
1286
2407
|
window.clearInterval(watchdogId);
|
|
1287
2408
|
}, "dsh-any-background: theme watchdog");
|
|
2409
|
+
const onPageHide = () => flushSave();
|
|
2410
|
+
window.addEventListener("pagehide", onPageHide);
|
|
2411
|
+
ctx.effect(() => () => window.removeEventListener("pagehide", onPageHide), "dsh-any-background: pagehide flush");
|
|
1288
2412
|
}
|
|
1289
2413
|
//#endregion
|
|
1290
2414
|
exports.apply = apply;
|