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/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/index.tsx
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
- uiOpacityHint: "拖动滑块调整主页界面背景的透明度",
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: "Main interface opacity",
53
- uiOpacityHint: "Drag to adjust homepage interface background opacity",
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
- opacity: .85,
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
- let rpcCallFn = null;
89
- const RPC_CHANNEL = "/dsh-any-background";
90
- const RPC_NS = "dshAnyBackground";
91
- const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
92
- async function rpcCall(method, payload) {
93
- if (!rpcCallFn) return void 0;
94
- try {
95
- const res = await rpcCallFn(rpcEndpoint(method), payload);
96
- if (res && res.ok === true) return res.value;
97
- console.warn(`dsh-any-background: rpc "${method}" failed`, res?.error);
98
- return;
99
- } catch (e) {
100
- console.warn(`dsh-any-background: rpc "${method}" threw`, e);
101
- return;
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
- opacity: typeof c.opacity === "number" ? c.opacity : DEFAULT_CONFIG.opacity,
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
- /** Persist the current in-memory config to the node half (fire-and-forget). */
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
- if (data.config) adoptConfig(data.config);
137
- if (typeof data.wallpaper === "string") wpUrl = data.wallpaper;
138
- else if (data.wallpaper === null) wpUrl = null;
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
- function rHasColor() {
142
- return cfg.color !== null;
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 sidebar tokens are
296
- * re-emitted at the requested alpha; every other token (layers, labels,
297
- * borders, brand) is written verbatim. No reads: nothing can observe a stale
298
- * or reset theme value and leave the homepage on the system color.
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(op) {
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, op);
310
- else if (name === "--dsw-specific-sidebar-fill") v = toRgba(value, sideOp);
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
- function applyWp(ctx) {
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(rOp());
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
- const WHEEL_SIZE = 220;
362
- const CX = WHEEL_SIZE / 2;
363
- const RING_OUTER = 106;
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
- function pickSL(x, y) {
433
- const gx = CX - SQ_HALF, gy = CX - SQ_HALF, sz = SQ_HALF * 2;
434
- return [Math.max(0, Math.min(1, (x - gx) / sz)), Math.max(.02, Math.min(.98, 1 - (y - gy) / sz))];
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 rect = containerRef.current.getBoundingClientRect();
776
- const mx = e.clientX - rect.left, my = e.clientY - rect.top;
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
- function LiveSlider({ min, max, step, def, fmt, onInput, onChange }) {
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: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
871
- type: "range",
872
- min,
873
- max,
874
- step,
875
- defaultValue: def,
876
- style: ST.slider,
877
- onInput: (e) => {
878
- const v = Number(e.target.value);
879
- onInput(v);
880
- if (valRef.current) valRef.current.textContent = fmt(v);
881
- },
882
- onChange: (e) => onChange(Number(e.target.value))
883
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
884
- ref: valRef,
885
- style: ST.sliderVal,
886
- children: fmt(def)
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
- function ThemeSection(props) {
891
- const { t, hue, sat, lit, setColor, url, setWp, setOp, setWop, setBl, setSop, useStore } = props;
892
- const storeUrl = useStore((s) => s.url);
893
- const fileRef = (0, react.useRef)(null);
894
- const [editorOpen, setEditorOpen] = (0, react.useState)(false);
895
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
896
- style: ST.root,
897
- children: [
898
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
899
- style: ST.h2,
900
- children: t("nav")
901
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
902
- style: ST.sub,
903
- children: t("subtitle")
904
- })] }),
905
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
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.label,
908
- children: t("colorTitle")
1678
+ style: ST.crashTitle,
1679
+ children: this.props.t("crashTitle")
909
1680
  }),
910
1681
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
911
- style: ST.center,
912
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorWheel, {
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)("div", {
920
- style: ST.colorHint,
921
- children: t("colorHint")
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
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { style: ST.hr }),
925
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
926
- style: ST.label,
927
- children: t("uiTitle")
928
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
929
- style: ST.sliders,
930
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
931
- style: ST.sliderBlock,
932
- children: [
933
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
934
- style: ST.sliderLabel,
935
- children: t("uiOpacity")
936
- }),
937
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
938
- style: ST.smallHint,
939
- children: t("uiOpacityHint")
940
- }),
941
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
942
- min: 0,
943
- max: 100,
944
- step: 1,
945
- def: Math.round(rOp() * 100),
946
- fmt: (v) => `${v}%`,
947
- onInput: (v) => {
948
- const op = v / 100;
949
- cfg.opacity = op;
950
- applyCustomTokens(op);
951
- saveConfig();
952
- },
953
- onChange: (v) => setOp(v / 100)
954
- })
955
- ]
956
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
957
- style: ST.sliderBlock,
958
- children: [
959
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
960
- style: ST.sliderLabel,
961
- children: t("uiSop")
962
- }),
963
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
964
- style: ST.smallHint,
965
- children: t("uiSopHint")
966
- }),
967
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
968
- min: 0,
969
- max: 100,
970
- step: 1,
971
- def: Math.round(rSop() * 100),
972
- fmt: (v) => `${v}%`,
973
- onInput: (v) => {
974
- const op = v / 100;
975
- cfg.settingsOpacity = op;
976
- applySettingsOverrides(op);
977
- saveConfig();
978
- },
979
- onChange: (v) => setSop(v / 100)
980
- })
981
- ]
982
- })]
983
- })] }),
984
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { style: ST.hr }),
985
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
986
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
987
- style: ST.label,
988
- children: t("bgTitle")
989
- }),
990
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
991
- style: ST.center,
992
- children: storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
993
- src: storeUrl,
994
- alt: "",
995
- style: ST.preview,
996
- onClick: () => setEditorOpen(true)
997
- }) : null
998
- }),
999
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1000
- style: ST.btnGroup,
1001
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
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: () => fileRef.current?.click(),
1008
- children: t("bgChoose")
1808
+ onClick: exportTheme,
1809
+ children: t("exportTheme")
1009
1810
  }),
1010
- storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1011
1812
  type: "button",
1012
1813
  style: ST.btn,
1013
- onClick: () => setEditorOpen(true),
1014
- children: t("bgEdit")
1015
- }) : null,
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: fileRef,
1818
+ ref: importRef,
1027
1819
  type: "file",
1028
- accept: "image/*",
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
- readImg(f, (d) => {
1034
- if (d) setWp(d);
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.jsxs)("div", {
1043
- style: ST.sliders,
1044
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1045
- style: ST.sliderBlock,
1046
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1047
- style: ST.sliderLabel,
1048
- children: t("wpOpacity")
1049
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
1050
- min: 0,
1051
- max: 100,
1052
- step: 1,
1053
- def: Math.round(rWop() * 100),
1054
- fmt: (v) => `${v}%`,
1055
- onInput: (v) => {
1056
- const op = v / 100;
1057
- cfg.wallpaperOpacity = op;
1058
- if (wpEl) wpEl.style.opacity = String(op);
1059
- saveConfig();
1060
- },
1061
- onChange: (v) => setWop(v / 100)
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
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1064
- style: ST.sliderBlock,
1065
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1066
- style: ST.sliderLabel,
1067
- children: t("bgBlur")
1068
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
1069
- min: 0,
1070
- max: 60,
1071
- step: 1,
1072
- def: rBl(),
1073
- fmt: (v) => `${v}px`,
1074
- onInput: (v) => {
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
- onChange: (v) => setBl(v)
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
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1084
- style: ST.hint,
1085
- children: t("bgHint")
1086
- })
1087
- ] }),
1088
- editorOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
1089
- url: storeUrl,
1090
- t,
1091
- onClose: () => setEditorOpen(false),
1092
- onCommit: (z, x, y, iw, ih) => {
1093
- cfg.bgState = {
1094
- zoom: z,
1095
- x,
1096
- y,
1097
- iw,
1098
- ih
1099
- };
1100
- applyWp(ctxRef);
1101
- saveConfig();
1102
- setEditorOpen(false);
1103
- }
1104
- }) : null
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
- ctxRef = ctx;
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="${CUSTOM_ID}"]::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}`;
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: { syncBg: (d, url, r) => {
1148
- if (r <= d.rev) return;
1149
- d.url = url;
1150
- d.rev = r;
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(ctx);
2155
+ applyWp();
1159
2156
  syncBg();
2157
+ watchParts();
1160
2158
  loadPersisted().then(() => {
1161
- applyWp(ctx);
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(ctx);
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(ctx);
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(ctx);
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
- wpUrl = u;
1221
- cfg.bgState = { ...DEFAULT_CONFIG.bgState };
1222
- applyWp(ctx);
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
- rpcCall("setWallpaper", { dataUrl: u });
2242
+ saveConfig();
1225
2243
  },
1226
- setOp: (v) => {
1227
- cfg.opacity = v;
1228
- applyWp(ctx);
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(ctx);
2252
+ applyWp();
1235
2253
  syncBg();
1236
2254
  saveConfig();
1237
2255
  },
1238
2256
  setBl: (v) => {
1239
2257
  cfg.blur = v;
1240
- applyWp(ctx);
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(ctx);
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(ctx);
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;