dsh-background-by-model 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -32,31 +32,30 @@ window.__ModuleLoader__.load({
32
32
  uiOpacity: "透明度",
33
33
  uiBlur: "模糊度",
34
34
  uiOpacityBg: "主背景",
35
- uiOpacitySide: "侧边栏",
35
+ uiOpacitySide: "左侧栏",
36
36
  uiOpacityCard: "对话框中选项面板",
37
37
  uiOpacityInput: "输入框与控件",
38
38
  uiSop: "设置界面透明度",
39
39
  uiChatRegion: "对话文本框",
40
40
  uiTrajectory: "轨迹页",
41
- uiPreview: "文件预览侧栏",
42
- uiPreviewHint: "点开文件后出现在右侧的那一栏。透明度默认跟随「主背景」,拖动本卡片后单独生效;模糊会在主背景模糊之上再叠加一层",
43
- uiScopeHint: "界面页的设置是全局的,所有模型共用",
44
- statusTitle: "当前生效",
45
- statusModel: "当前模型",
46
- statusUnknown: "未检测到当前模型",
41
+ uiPreview: "右侧栏",
47
42
  statusUnknownHint: "检测不到时一律使用第 1 条可用规则;切换一次模型或稍等片刻会自动重试",
48
- statusSourceDefault: "宿主默认",
49
43
  statusSourceDefaultHint: "只读到宿主默认模型,它不是本会话当前选择的模型;本会话的模型选择可用后会自动纠正",
50
44
  statusNoteNoService: "仍未拿到会话服务(会话控制器晚于本插件加载),正在自动重试",
51
45
  statusNoteNoSession: "会话服务已就绪,但还没有当前会话 id",
52
46
  statusNoteNoProjection: "拿不到本会话的模型选择投影(binding / projections 不可用)",
53
47
  statusNoteEmptySelection: "本会话的模型选择投影还没有值(切一次模型即可写入)",
54
- statusNone: "还没有可用规则(规则需要启用并选择图片)",
55
- statusHit: "命中",
56
- statusFallback: "未命中 · 使用兜底",
57
- statusRule: "规则",
58
48
  rulesTitle: "规则列表",
59
49
  rulesHint: "从上到下即为优先级。规则 1 同时兼任兜底:所有匹配串都没命中时使用它",
50
+ tryoutTitle: "匹配试跑",
51
+ tryoutPlaceholder: "输入模型名,如 deepseek-flash",
52
+ tryoutUseCurrent: "填入当前模型",
53
+ tryoutHit: "命中规则",
54
+ tryoutFallback: "无匹配 · 走兜底规则",
55
+ tryoutNone: "没有可用规则(规则需要启用并有图片)",
56
+ tryoutEmpty: "输入模型名即可试跑",
57
+ autoExtract: "选图后自动取主题色",
58
+ autoExtractHint: "只在规则还没有颜色时自动取色,绝不覆盖你手动选的颜色",
60
59
  ruleAdd: "新增规则",
61
60
  ruleFallbackBadge: "兜底",
62
61
  ruleMatchLabel: "匹配串",
@@ -104,7 +103,7 @@ window.__ModuleLoader__.load({
104
103
  extractColor: "从图片提取主题色",
105
104
  extracting: "取色中…",
106
105
  extractNoWp: "请先为该规则选择图片",
107
- extractDone: "已应用图片主色调",
106
+ extractDone: "已应用图片配色",
108
107
  extractFail: "未在这张图里找到鲜明的颜色,请换一张",
109
108
  eyedropper: "从图片取色",
110
109
  pickerTitle: "从图片取色",
@@ -138,31 +137,30 @@ window.__ModuleLoader__.load({
138
137
  uiOpacity: "Opacity",
139
138
  uiBlur: "Blur",
140
139
  uiOpacityBg: "Main background",
141
- uiOpacitySide: "Sidebar",
140
+ uiOpacitySide: "Left panel",
142
141
  uiOpacityCard: "Cards & panels",
143
142
  uiOpacityInput: "Input & controls",
144
143
  uiSop: "Settings interface opacity",
145
144
  uiChatRegion: "Conversation text frame",
146
145
  uiTrajectory: "Trajectory view",
147
- uiPreview: "File preview panel",
148
- uiPreviewHint: "The column that slides in on the right when you open a file. Its opacity follows the Main background until you drag this card; its blur stacks on top of the Main background blur",
149
- uiScopeHint: "Interface settings are global — every model shares them",
150
- statusTitle: "Active now",
151
- statusModel: "Current model",
152
- statusUnknown: "No model detected",
146
+ uiPreview: "Right panel",
153
147
  statusUnknownHint: "Without a detected model rule 1 is always used; switching the model once or waiting a moment retries automatically",
154
- statusSourceDefault: "host default",
155
148
  statusSourceDefaultHint: "Only the host-wide default model could be read — that is not necessarily this session's selection. It self-corrects once this session's model selection is available.",
156
149
  statusNoteNoService: "The sessions service is not available yet (the session controller loads after this plugin); retrying automatically",
157
150
  statusNoteNoSession: "The sessions service is up but reports no current session yet",
158
151
  statusNoteNoProjection: "This session's model-selection projection is unreachable (no binding / projections)",
159
152
  statusNoteEmptySelection: "The model-selection projection exists but carries no value yet (selecting a model once writes it)",
160
- statusNone: "No usable rule yet (a rule needs to be enabled and have an image)",
161
- statusHit: "matched",
162
- statusFallback: "no match · using the fallback",
163
- statusRule: "Rule",
164
153
  rulesTitle: "Rules",
165
154
  rulesHint: "Top to bottom is the priority. Rule 1 doubles as the fallback — it is used when no match string hits",
155
+ tryoutTitle: "Match test",
156
+ tryoutPlaceholder: "Type a model name, e.g. deepseek-flash",
157
+ tryoutUseCurrent: "Use current model",
158
+ tryoutHit: "matches rule",
159
+ tryoutFallback: "no match · fallback rule",
160
+ tryoutNone: "No usable rule (a rule needs to be enabled and have an image)",
161
+ tryoutEmpty: "Type a model name to test the list",
162
+ autoExtract: "Extract the theme color when an image is chosen",
163
+ autoExtractHint: "Only fills a rule that has no color yet — a color you picked is never overwritten",
166
164
  ruleAdd: "Add rule",
167
165
  ruleFallbackBadge: "fallback",
168
166
  ruleMatchLabel: "Match",
@@ -210,7 +208,7 @@ window.__ModuleLoader__.load({
210
208
  extractColor: "Extract from image",
211
209
  extracting: "Extracting…",
212
210
  extractNoWp: "Choose an image for this rule first",
213
- extractDone: "Image color applied",
211
+ extractDone: "Image colors applied",
214
212
  extractFail: "No vivid color found in this image, try another",
215
213
  eyedropper: "Eyedropper",
216
214
  pickerTitle: "Pick from image",
@@ -231,7 +229,7 @@ window.__ModuleLoader__.load({
231
229
  footerTag: "Per-model wallpaper plugin"
232
230
  };
233
231
  //#endregion
234
- //#region src/client/state.ts
232
+ //#region src/schema.ts
235
233
  const DEFAULT_BG_STATE = {
236
234
  zoom: 1,
237
235
  x: 0,
@@ -239,6 +237,7 @@ window.__ModuleLoader__.load({
239
237
  iw: 0,
240
238
  ih: 0
241
239
  };
240
+ /** Every accepted `bgMode`, in UI order. */
242
241
  const BG_MODES$1 = [
243
242
  "fit",
244
243
  "fill",
@@ -246,6 +245,137 @@ window.__ModuleLoader__.load({
246
245
  "tile",
247
246
  "center"
248
247
  ];
248
+ /**
249
+ * Main interface opacities (0..1), global (Interface page):
250
+ * `bg` main background (`--dsw-alias-bg-base`)
251
+ * `sidebar` sidebar (`--dsw-specific-sidebar-fill`)
252
+ * `card` cards/panels (`--dsw-alias-bg-layer-1/2/3`, `--dsw-specific-menu`)
253
+ * `input` input/control surfaces (`--dsw-specific-input-major`)
254
+ */
255
+ const PART_OPACITY_KEYS = [
256
+ "bg",
257
+ "sidebar",
258
+ "card",
259
+ "input"
260
+ ];
261
+ const DEFAULT_PART_OPACITIES = {
262
+ bg: .85,
263
+ sidebar: .93,
264
+ card: 1,
265
+ input: 1
266
+ };
267
+ /**
268
+ * Interface blur (px, 0..60), global (Interface page):
269
+ * `bg` main background (AppFrame grid)
270
+ * `sidebar` sidebar column
271
+ * `card` cards/panels (center + details columns, dialog option panels)
272
+ * `settings` settings panel
273
+ * `chat` conversation text region (message column of the chat view)
274
+ * `trajectory` trajectory view surface
275
+ * `rightbar` file-preview panel — the right sidebar a file click opens
276
+ * `input` input/control surfaces ([data-composer-card], [data-cordis-panel])
277
+ */
278
+ const PART_BLUR_KEYS = [
279
+ "bg",
280
+ "sidebar",
281
+ "card",
282
+ "settings",
283
+ "chat",
284
+ "trajectory",
285
+ "rightbar",
286
+ "input"
287
+ ];
288
+ const DEFAULT_PART_BLURS = {
289
+ bg: 0,
290
+ sidebar: 0,
291
+ card: 0,
292
+ settings: 0,
293
+ chat: 0,
294
+ trajectory: 0,
295
+ rightbar: 0,
296
+ input: 0
297
+ };
298
+ /** A default config with independent nested objects (never hand out the shared ones). */
299
+ function freshThemeConfig() {
300
+ return {
301
+ rules: [],
302
+ opacities: { ...DEFAULT_PART_OPACITIES },
303
+ blurs: { ...DEFAULT_PART_BLURS },
304
+ settingsOpacity: 1,
305
+ chatTextOpacity: 0,
306
+ trajectoryOpacity: 1,
307
+ rightbarOpacity: null,
308
+ autoExtract: true
309
+ };
310
+ }
311
+ /** Slot names reach the filesystem, so they are strictly whitelisted. */
312
+ const SLOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
313
+ function clamp$1(n, lo, hi, def) {
314
+ return typeof n === "number" && isFinite(n) ? Math.min(hi, Math.max(lo, n)) : def;
315
+ }
316
+ const clamp01 = (n, def) => clamp$1(n, 0, 1, def);
317
+ function normalizeBgState(s) {
318
+ const v = s ?? {};
319
+ return {
320
+ zoom: clamp$1(v.zoom, .1, 10, 1),
321
+ x: typeof v.x === "number" && isFinite(v.x) ? v.x : 0,
322
+ y: typeof v.y === "number" && isFinite(v.y) ? v.y : 0,
323
+ iw: typeof v.iw === "number" && v.iw > 0 ? v.iw : 0,
324
+ ih: typeof v.ih === "number" && v.ih > 0 ? v.ih : 0
325
+ };
326
+ }
327
+ /** One `[h, s, l]` triple, or null when the value is not a complete finite triple. */
328
+ function normalizeHsl(raw) {
329
+ if (!Array.isArray(raw) || raw.length !== 3) return null;
330
+ if (!raw.every((n) => typeof n === "number" && isFinite(n))) return null;
331
+ return [
332
+ clamp$1(raw[0], 0, 360, 220),
333
+ clamp$1(raw[1], 0, 1, .55),
334
+ clamp$1(raw[2], 0, 1, .25)
335
+ ];
336
+ }
337
+ /** Coerce one persisted rule, or null when it lacks a usable id/slot. */
338
+ function normalizeRule(raw) {
339
+ const r = raw ?? {};
340
+ const id = typeof r.id === "string" && r.id !== "" ? r.id : null;
341
+ const slot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
342
+ if (id === null || slot === null) return null;
343
+ const mode = BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit";
344
+ return {
345
+ id,
346
+ slot,
347
+ match: typeof r.match === "string" ? r.match : "",
348
+ enabled: r.enabled !== false,
349
+ color: normalizeHsl(r.color),
350
+ bgMode: mode,
351
+ wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
352
+ blur: clamp$1(r.blur, 0, 60, 0),
353
+ bgState: normalizeBgState(r.bgState)
354
+ };
355
+ }
356
+ /** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
357
+ function normalizeConfig(raw) {
358
+ const r = raw ?? {};
359
+ const rules = Array.isArray(r.rules) ? r.rules.map(normalizeRule).filter((x) => x !== null) : [];
360
+ const ops = r.opacities ?? {};
361
+ const bl = r.blurs ?? {};
362
+ const blurs = {};
363
+ for (const k of PART_BLUR_KEYS) blurs[k] = clamp$1(bl[k], 0, 60, DEFAULT_PART_BLURS[k]);
364
+ const opacities = {};
365
+ for (const k of PART_OPACITY_KEYS) opacities[k] = clamp01(ops[k], DEFAULT_PART_OPACITIES[k]);
366
+ return {
367
+ rules,
368
+ opacities,
369
+ blurs,
370
+ settingsOpacity: clamp01(r.settingsOpacity, 1),
371
+ chatTextOpacity: clamp01(r.chatTextOpacity, 0),
372
+ trajectoryOpacity: clamp01(r.trajectoryOpacity, 1),
373
+ rightbarOpacity: typeof r.rightbarOpacity === "number" && isFinite(r.rightbarOpacity) ? clamp01(r.rightbarOpacity, 1) : null,
374
+ autoExtract: r.autoExtract !== false
375
+ };
376
+ }
377
+ //#endregion
378
+ //#region src/client/state.ts
249
379
  const PALETTE = [
250
380
  [
251
381
  356,
@@ -288,41 +418,8 @@ window.__ModuleLoader__.load({
288
418
  .58
289
419
  ]
290
420
  ];
291
- const DEFAULT_CONFIG = {
292
- rules: [],
293
- opacities: {
294
- bg: .85,
295
- sidebar: .93,
296
- card: 1,
297
- input: 1
298
- },
299
- blurs: {
300
- bg: 0,
301
- sidebar: 0,
302
- card: 0,
303
- settings: 0,
304
- chat: 0,
305
- trajectory: 0,
306
- rightbar: 0,
307
- input: 0
308
- },
309
- settingsOpacity: 1,
310
- chatTextOpacity: 0,
311
- trajectoryOpacity: 1,
312
- rightbarOpacity: null
313
- };
314
- function freshConfig() {
315
- return {
316
- rules: [],
317
- opacities: { ...DEFAULT_CONFIG.opacities },
318
- blurs: { ...DEFAULT_CONFIG.blurs },
319
- settingsOpacity: DEFAULT_CONFIG.settingsOpacity,
320
- chatTextOpacity: DEFAULT_CONFIG.chatTextOpacity,
321
- trajectoryOpacity: DEFAULT_CONFIG.trajectoryOpacity,
322
- rightbarOpacity: DEFAULT_CONFIG.rightbarOpacity
323
- };
324
- }
325
- let cfg = freshConfig();
421
+ freshThemeConfig();
422
+ let cfg = freshThemeConfig();
326
423
  const images = /* @__PURE__ */ new Map();
327
424
  const displayUrls = /* @__PURE__ */ new Map();
328
425
  function revokeDisplay(slot) {
@@ -419,8 +516,6 @@ window.__ModuleLoader__.load({
419
516
  }
420
517
  return `r${Date.now()}`;
421
518
  }
422
- const clamp01 = (n, def) => typeof n === "number" && isFinite(n) ? Math.min(1, Math.max(0, n)) : def;
423
- const clamp$1 = (n, lo, hi, def) => typeof n === "number" && isFinite(n) ? Math.min(hi, Math.max(lo, n)) : def;
424
519
  function rHasColor() {
425
520
  return activeRule()?.color !== null && activeRule() !== null;
426
521
  }
@@ -451,108 +546,33 @@ window.__ModuleLoader__.load({
451
546
  function rOps() {
452
547
  const o = cfg.opacities ?? {};
453
548
  const out = {};
454
- for (const k of [
455
- "bg",
456
- "sidebar",
457
- "card",
458
- "input"
459
- ]) out[k] = clamp01(o[k], DEFAULT_CONFIG.opacities[k]);
549
+ for (const k of PART_OPACITY_KEYS) out[k] = clamp01(o[k], DEFAULT_PART_OPACITIES[k]);
460
550
  return out;
461
551
  }
462
552
  function rBlurs() {
463
553
  const b = cfg.blurs ?? {};
464
554
  const out = {};
465
- for (const k of [
466
- "bg",
467
- "sidebar",
468
- "card",
469
- "settings",
470
- "chat",
471
- "trajectory",
472
- "rightbar",
473
- "input"
474
- ]) out[k] = clamp$1(b[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
555
+ for (const k of PART_BLUR_KEYS) out[k] = clamp$1(b[k], 0, 60, DEFAULT_PART_BLURS[k]);
475
556
  return out;
476
557
  }
477
558
  function rSop() {
478
- return clamp01(cfg.settingsOpacity, DEFAULT_CONFIG.settingsOpacity);
559
+ return clamp01(cfg.settingsOpacity, 1);
479
560
  }
480
561
  function rChatTextOpacity() {
481
- return clamp01(cfg.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity);
562
+ return clamp01(cfg.chatTextOpacity, 0);
482
563
  }
483
564
  function rTrajectoryOpacity() {
484
- return clamp01(cfg.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity);
565
+ return clamp01(cfg.trajectoryOpacity, 1);
485
566
  }
486
567
  /** Own opacity of the file-preview panel; while unowned it IS the main background's. */
487
568
  function rRightbarOpacity() {
488
569
  const own = cfg.rightbarOpacity;
489
570
  return typeof own === "number" && isFinite(own) ? clamp01(own, 1) : rOps().bg;
490
571
  }
491
- function adoptBgState(s) {
492
- return {
493
- zoom: clamp$1(s.zoom, .1, 10, 1),
494
- x: typeof s.x === "number" && isFinite(s.x) ? s.x : 0,
495
- y: typeof s.y === "number" && isFinite(s.y) ? s.y : 0,
496
- iw: typeof s.iw === "number" && s.iw > 0 ? s.iw : 0,
497
- ih: typeof s.ih === "number" && s.ih > 0 ? s.ih : 0
498
- };
499
- }
500
- /** Coerce one persisted rule, or null when it lacks a usable id/slot. */
501
- function normalizeRule(raw) {
502
- const r = raw ?? {};
503
- const id = typeof r.id === "string" && r.id !== "" ? r.id : null;
504
- const slot = typeof r.slot === "string" && /^[A-Za-z0-9_-]{1,32}$/.test(r.slot) ? r.slot : null;
505
- if (id === null || slot === null) return null;
506
- const c = r.color;
507
- const color = Array.isArray(c) && c.length === 3 && c.every((n) => typeof n === "number" && isFinite(n)) ? [
508
- clamp$1(c[0], 0, 360, 220),
509
- clamp$1(c[1], 0, 1, .55),
510
- clamp$1(c[2], 0, 1, .25)
511
- ] : null;
512
- return {
513
- id,
514
- slot,
515
- match: typeof r.match === "string" ? r.match : "",
516
- enabled: r.enabled !== false,
517
- color,
518
- bgMode: BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit",
519
- wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
520
- blur: clamp$1(r.blur, 0, 60, 0),
521
- bgState: adoptBgState(r.bgState ?? {})
522
- };
523
- }
524
572
  /** Move a possibly-absent partial config into the shape the UI reads. */
525
573
  function adoptConfig(raw) {
526
- const c = raw ?? {};
527
- const rules = Array.isArray(c.rules) ? c.rules.map(normalizeRule).filter((r) => r !== null) : [];
528
- const ops = c.opacities ?? {};
529
- const bl = c.blurs ?? {};
530
- const blurs = {};
531
- for (const k of [
532
- "bg",
533
- "sidebar",
534
- "card",
535
- "settings",
536
- "chat",
537
- "trajectory",
538
- "rightbar",
539
- "input"
540
- ]) blurs[k] = clamp$1(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
541
- cfg = {
542
- rules,
543
- opacities: {
544
- bg: clamp01(ops.bg, DEFAULT_CONFIG.opacities.bg),
545
- sidebar: clamp01(ops.sidebar, DEFAULT_CONFIG.opacities.sidebar),
546
- card: clamp01(ops.card, DEFAULT_CONFIG.opacities.card),
547
- input: clamp01(ops.input, DEFAULT_CONFIG.opacities.input)
548
- },
549
- blurs,
550
- settingsOpacity: clamp01(c.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
551
- chatTextOpacity: clamp01(c.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
552
- trajectoryOpacity: clamp01(c.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity),
553
- rightbarOpacity: typeof c.rightbarOpacity === "number" && isFinite(c.rightbarOpacity) ? clamp01(c.rightbarOpacity, 1) : null
554
- };
555
- if (activeRuleId !== null && !rules.some((r) => r.id === activeRuleId)) {
574
+ cfg = normalizeConfig(raw);
575
+ if (activeRuleId !== null && !cfg.rules.some((r) => r.id === activeRuleId)) {
556
576
  activeRuleId = null;
557
577
  activeMatched = false;
558
578
  }
@@ -909,7 +929,7 @@ window.__ModuleLoader__.load({
909
929
  const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
910
930
  const v = max / 255;
911
931
  const s = max === 0 ? 0 : (max - min) / max;
912
- totalLum += v;
932
+ totalLum += (.2126 * r + .7152 * gg + .0722 * b) / 255;
913
933
  sampled++;
914
934
  if (s < .08 || v < .12 || v > .97) continue;
915
935
  const key = r >> 4 << 8 | gg >> 4 << 4 | b >> 4;
@@ -2189,12 +2209,6 @@ window.__ModuleLoader__.load({
2189
2209
  .dab-crash-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.5}
2190
2210
 
2191
2211
  /* ── model rules ─────────────────────────────────────────────────────────── */
2192
- .dab-status{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12.5px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
2193
- .dab-status-model{font-family:var(--dab-mono);font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-radius:8px;padding:3px 9px}
2194
- .dab-status-arrow{color:var(--dsw-alias-brand-primary);font-weight:700}
2195
- .dab-status-hit{color:var(--dsw-alias-brand-primary);font-weight:600}
2196
- .dab-status-none{color:var(--dsw-alias-state-error-primary)}
2197
- .dab-status-src{font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-label-tertiary));background:var(--dsw-alias-interactive-bg-hover);border-radius:6px;padding:2px 7px}
2198
2212
  .dab-rules{display:flex;flex-direction:column;gap:12px}
2199
2213
  .dab-rule{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;padding:14px;transition:border-color .25s,box-shadow .25s,opacity .25s}
2200
2214
  .dab-rule.is-active{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 16%,transparent)}
@@ -2698,11 +2712,6 @@ window.__ModuleLoader__.load({
2698
2712
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2699
2713
  className: "dab-desc",
2700
2714
  children: t("descInterface")
2701
- }),
2702
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2703
- className: "dab-hint",
2704
- style: { marginTop: 7 },
2705
- children: t("uiScopeHint")
2706
2715
  })
2707
2716
  ]
2708
2717
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -2800,12 +2809,7 @@ window.__ModuleLoader__.load({
2800
2809
  blurs[blurKey] = v;
2801
2810
  setBlurs(blurs);
2802
2811
  }
2803
- }),
2804
- isRightbar ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2805
- className: "dab-hint",
2806
- style: { marginTop: 9 },
2807
- children: t("uiPreviewHint")
2808
- }) : null
2812
+ })
2809
2813
  ]
2810
2814
  }, blurKey);
2811
2815
  })
@@ -3590,7 +3594,20 @@ window.__ModuleLoader__.load({
3590
3594
  const { t, useStore } = p;
3591
3595
  const store = useStore((s) => s);
3592
3596
  const rules = cfg.rules;
3593
- const activeIndex = store.activeRuleId === null ? -1 : rules.findIndex((r) => r.id === store.activeRuleId);
3597
+ const [text, setText] = (0, react.useState)("");
3598
+ const [touched, setTouched] = (0, react.useState)(false);
3599
+ (0, react.useEffect)(() => {
3600
+ if (!touched) setText(store.model);
3601
+ }, [store.model, touched]);
3602
+ const probe = text.trim();
3603
+ const result = (() => {
3604
+ if (probe === "") return null;
3605
+ const hit = matchRule(rules, probe);
3606
+ const rule = hit.rule;
3607
+ if (rule === null) return null;
3608
+ const n = rules.findIndex((r) => r.id === rule.id) + 1;
3609
+ return `${hit.matched ? t("tryoutHit") : t("tryoutFallback")} ${n}`;
3610
+ })();
3594
3611
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3595
3612
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
3596
3613
  className: "dab-head dab-rise",
@@ -3614,48 +3631,50 @@ window.__ModuleLoader__.load({
3614
3631
  className: "dab-card dab-rise",
3615
3632
  style: { "--d": 1 },
3616
3633
  children: [
3634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3635
+ className: "dab-swatch-title",
3636
+ children: t("tryoutTitle")
3637
+ }),
3617
3638
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3618
- className: "dab-status",
3619
- children: [
3620
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("statusModel") }),
3621
- store.model !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3622
- className: "dab-status-model",
3623
- children: store.model
3624
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3625
- className: "dab-status-none",
3626
- children: t("statusUnknown")
3627
- }),
3628
- store.model !== "" && store.modelSource === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3629
- className: "dab-status-src",
3630
- children: t("statusSourceDefault")
3631
- }) : null,
3632
- store.model !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3633
- className: "dab-status-arrow",
3634
- children: "→"
3635
- }) : null,
3636
- activeIndex >= 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3637
- className: store.matched ? "dab-status-hit" : "",
3638
- children: [
3639
- store.matched ? t("statusHit") : t("statusFallback"),
3640
- " · ",
3641
- t("statusRule"),
3642
- " ",
3643
- activeIndex + 1
3644
- ]
3645
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3646
- className: "dab-status-none",
3647
- children: t("statusNone")
3648
- })
3649
- ]
3639
+ style: {
3640
+ marginTop: 10,
3641
+ display: "flex",
3642
+ flexWrap: "wrap",
3643
+ gap: 8,
3644
+ alignItems: "center"
3645
+ },
3646
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3647
+ type: "text",
3648
+ className: "dab-urlinput",
3649
+ value: text,
3650
+ placeholder: t("tryoutPlaceholder"),
3651
+ onChange: (e) => {
3652
+ setTouched(true);
3653
+ setText(e.target.value);
3654
+ }
3655
+ }), store.model !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3656
+ type: "button",
3657
+ className: "dab-btn",
3658
+ onClick: () => {
3659
+ setTouched(true);
3660
+ setText(store.model);
3661
+ },
3662
+ children: t("tryoutUseCurrent")
3663
+ }) : null]
3650
3664
  }),
3651
- store.model === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3652
3666
  className: "dab-hint",
3653
3667
  style: { marginTop: 9 },
3668
+ children: probe === "" ? t("tryoutEmpty") : result === null ? t("tryoutNone") : result
3669
+ }),
3670
+ store.model === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3671
+ className: "dab-hint",
3672
+ style: { marginTop: 6 },
3654
3673
  children: t("statusUnknownHint")
3655
3674
  }) : null,
3656
3675
  store.model !== "" && store.modelSource === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3657
3676
  className: "dab-hint",
3658
- style: { marginTop: 9 },
3677
+ style: { marginTop: 6 },
3659
3678
  children: t("statusSourceDefaultHint")
3660
3679
  }) : null,
3661
3680
  MODEL_NOTE_KEYS[store.modelNote] !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -3678,6 +3697,30 @@ window.__ModuleLoader__.load({
3678
3697
  style: { marginBottom: 11 },
3679
3698
  children: t("rulesHint")
3680
3699
  }),
3700
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3701
+ className: "dab-chip-row",
3702
+ style: {
3703
+ marginBottom: 12,
3704
+ alignItems: "center"
3705
+ },
3706
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3707
+ type: "button",
3708
+ className: `dab-toggle${cfg.autoExtract ? " is-on" : ""}`,
3709
+ role: "switch",
3710
+ "aria-checked": cfg.autoExtract,
3711
+ title: t("autoExtract"),
3712
+ onClick: () => p.setAutoExtract(!cfg.autoExtract),
3713
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
3714
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3715
+ className: "dab-hint",
3716
+ children: t("autoExtract")
3717
+ })]
3718
+ }),
3719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3720
+ className: "dab-hint",
3721
+ style: { marginBottom: 11 },
3722
+ children: t("autoExtractHint")
3723
+ }),
3681
3724
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3682
3725
  className: "dab-rules",
3683
3726
  children: rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleCard, {
@@ -4498,6 +4541,36 @@ window.__ModuleLoader__.load({
4498
4541
  zh,
4499
4542
  en
4500
4543
  }), "dsh-background-by-model: i18n");
4544
+ /**
4545
+ * The automatic half of "extract from image": fill a rule's theme color from
4546
+ * the picture it just received, but only while the rule has no color at all —
4547
+ * a color the user picked (or a previous extraction produced) is never
4548
+ * overwritten, so this cannot fight a deliberate choice.
4549
+ *
4550
+ * The decode is asynchronous, so the "still empty?" question is asked AGAIN on
4551
+ * arrival: a color chosen while it ran must win over the one being computed.
4552
+ */
4553
+ const maybeAutoExtract = async (id) => {
4554
+ if (!cfg.autoExtract) return;
4555
+ const rule = ruleById(id);
4556
+ if (rule === null || rule.color !== null) return;
4557
+ const url = displayImageOf(rule.slot);
4558
+ if (url === null) return;
4559
+ let hsl = null;
4560
+ try {
4561
+ hsl = await extractWallpaperColor(url, rule.bgState);
4562
+ } catch {
4563
+ hsl = null;
4564
+ }
4565
+ if (hsl === null) return;
4566
+ const current = ruleById(id);
4567
+ if (current === null || current.color !== null) return;
4568
+ current.color = hsl;
4569
+ rulesRev++;
4570
+ saveConfig();
4571
+ if (id === activeRuleId) applyActive();
4572
+ else sync();
4573
+ };
4501
4574
  const sectionInject = (actions) => {
4502
4575
  bound = actions;
4503
4576
  sync();
@@ -4552,6 +4625,7 @@ window.__ModuleLoader__.load({
4552
4625
  dataUrl === null ? deleteImage(rule.slot) : writeImage(rule.slot, dataUrl);
4553
4626
  if (id === activeRuleId) applyActive();
4554
4627
  else sync();
4628
+ if (dataUrl !== null) maybeAutoExtract(id);
4555
4629
  },
4556
4630
  setRuleImageFromUrl: async (id, url) => {
4557
4631
  const rule = ruleById(id);
@@ -4564,6 +4638,7 @@ window.__ModuleLoader__.load({
4564
4638
  setImage(rule.slot, res.dataUrl ?? null);
4565
4639
  if (id === activeRuleId) applyActive();
4566
4640
  else sync();
4641
+ maybeAutoExtract(id);
4567
4642
  }
4568
4643
  return res;
4569
4644
  },
@@ -4603,6 +4678,11 @@ window.__ModuleLoader__.load({
4603
4678
  applyRightbarOverrides(rRightbarOpacity());
4604
4679
  saveConfig();
4605
4680
  },
4681
+ setAutoExtract: (v) => {
4682
+ cfg.autoExtract = v;
4683
+ saveConfig();
4684
+ sync();
4685
+ },
4606
4686
  exportTheme: () => {
4607
4687
  const images = {};
4608
4688
  for (const rule of cfg.rules) {