deepseek-harness-background 0.2.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/LICENSE +21 -0
- package/README.md +129 -0
- package/README.zh.md +129 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +1116 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +540 -0
- package/package.json +88 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "deepseek-harness-background",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/background-css.ts
|
|
10
|
+
/**
|
|
11
|
+
* Background CSS — the stylesheet injected once into the document when the
|
|
12
|
+
* plugin first runs. It defines the behind-body wallpaper layer, the scrim,
|
|
13
|
+
* and the frosted-glass panels, all driven by CSS variables the painter writes
|
|
14
|
+
* onto `document.body`. Kept in a single idempotent `<style data-plugin-css>`
|
|
15
|
+
* tag (mirrors the reference `dsh-wallpaper-engine` bundle).
|
|
16
|
+
*/
|
|
17
|
+
/** Unique id stamped on the injected style element (dedup key). */
|
|
18
|
+
const BACKGROUND_CSS_TAG = "deepseek-harness-background/styles";
|
|
19
|
+
/** Body attribute the painter sets while a wallpaper is active. */
|
|
20
|
+
const ACTIVE_ATTR = "data-dsh-bg";
|
|
21
|
+
/**
|
|
22
|
+
* The backdrop CSS. The wallpaper is a fixed `z-index:-2` element (below the
|
|
23
|
+
* scrim at `-1`, below the UI). While active, the official surface tokens are
|
|
24
|
+
* made transparent (so the wallpaper shows through every column) and the
|
|
25
|
+
* opaque input / bubble surfaces turn into translucent frosted glass.
|
|
26
|
+
* Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`)
|
|
27
|
+
* and CSS-module SUFFIX conventions (`_bubble`) — the same stable anchors the
|
|
28
|
+
* reference engine uses, since hashed prefixes change across shell rebuilds.
|
|
29
|
+
*/
|
|
30
|
+
const BACKGROUND_CSS = `
|
|
31
|
+
.dsh-bg-layer {
|
|
32
|
+
position: fixed; inset: 0; z-index: -2; overflow: hidden; pointer-events: none;
|
|
33
|
+
}
|
|
34
|
+
.dsh-bg-layer .dsh-bg-image {
|
|
35
|
+
width: 100%; height: 100%; display: block; border: 0; background: transparent;
|
|
36
|
+
object-fit: var(--bg-object-fit, cover);
|
|
37
|
+
opacity: var(--bg-opacity, 1);
|
|
38
|
+
filter: blur(var(--bg-wallpaper-blur, 0px));
|
|
39
|
+
/* Blur thins the edges; scale the image up to hide the fringe. */
|
|
40
|
+
transform: scale(var(--bg-wallpaper-scale, 1));
|
|
41
|
+
transform-origin: center;
|
|
42
|
+
}
|
|
43
|
+
/* Scrim is theme-aware: a WHITE veil in the light scheme (it lifts the art
|
|
44
|
+
toward the near-white surface so dark text keeps contrast) and a BLACK veil
|
|
45
|
+
in the dark scheme (it dims the art so light text keeps contrast). */
|
|
46
|
+
.dsh-bg-scrim {
|
|
47
|
+
position: fixed; inset: 0; z-index: -1; pointer-events: none;
|
|
48
|
+
background: rgba(255, 255, 255, var(--bg-scrim, 0.25));
|
|
49
|
+
}
|
|
50
|
+
body[data-ds-dark-theme] .dsh-bg-scrim {
|
|
51
|
+
background: rgba(0, 0, 0, var(--bg-scrim, 0.25));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Active: make the app frame + sidebar background transparent so all columns
|
|
55
|
+
share the same wallpaper + scrim backdrop. */
|
|
56
|
+
body[data-dsh-bg] {
|
|
57
|
+
--dsw-alias-bg-base: transparent;
|
|
58
|
+
--dsw-specific-sidebar-fill: transparent;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Frosted glass over the opaque conversation surfaces (composer + bubbles).
|
|
62
|
+
The surface translucency is driven by --dsw-specific-input-major /
|
|
63
|
+
--dsw-specific-bubble, which the painter writes (theme-aware) so that a
|
|
64
|
+
fully-opaque panel restores the official surfaces. The blur here comes
|
|
65
|
+
from --bg-glass-blur; a faint top-weighted specular sheen makes the
|
|
66
|
+
translucent surfaces read as wet glass. */
|
|
67
|
+
body[data-dsh-bg] [data-composer-card],
|
|
68
|
+
body[data-dsh-bg] [class*="_bubble"] {
|
|
69
|
+
background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.05) 38%, rgba(255, 255, 255, 0.02));
|
|
70
|
+
-webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.6)) brightness(var(--bg-glass-brightness, 1.04)) contrast(1.01);
|
|
71
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.6)) brightness(var(--bg-glass-brightness, 1.04)) contrast(1.01);
|
|
72
|
+
box-shadow:
|
|
73
|
+
inset 0 1px 0 rgba(255, 255, 255, var(--bg-glass-highlight, 0.32)),
|
|
74
|
+
inset 0 -1px 0 rgba(255, 255, 255, 0.08),
|
|
75
|
+
inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
|
|
76
|
+
0 12px 40px rgba(0, 0, 0, var(--bg-glass-shadow, 0.12));
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
/**
|
|
80
|
+
* Inject the backdrop stylesheet once. Safe to call repeatedly; a second call
|
|
81
|
+
* is a no-op (dedup key on the style tag).
|
|
82
|
+
*/
|
|
83
|
+
function injectBackgroundCss() {
|
|
84
|
+
if (document.querySelector(`style[data-plugin-css="deepseek-harness-background/styles"]`)) return;
|
|
85
|
+
const tag = document.createElement("style");
|
|
86
|
+
tag.dataset.plugin = "deepseek-harness-background";
|
|
87
|
+
tag.dataset.pluginCss = BACKGROUND_CSS_TAG;
|
|
88
|
+
tag.textContent = BACKGROUND_CSS;
|
|
89
|
+
document.head.appendChild(tag);
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/settings.ts
|
|
93
|
+
/** Browser-facing base path of the background API (shared with the host routes). */
|
|
94
|
+
const BACKGROUND_API_PREFIX = "/api/bg-wallpaper";
|
|
95
|
+
/** Maximum scrim opacity (a nearly opaque veil keeps text legible over any art). */
|
|
96
|
+
const SCRIM_MAX = .95;
|
|
97
|
+
/** Default scrim strength applied over user art (a light readability veil). */
|
|
98
|
+
const DEFAULT_SCRIM = .25;
|
|
99
|
+
/** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
|
|
100
|
+
const DEFAULT_PANEL_OPACITY = .15;
|
|
101
|
+
/** Default background rendering mode. */
|
|
102
|
+
const DEFAULT_FIT = "cover";
|
|
103
|
+
/** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
|
|
104
|
+
const FIT_MODES = ["cover", "contain"];
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/client/backdrop.ts
|
|
107
|
+
/**
|
|
108
|
+
* Background painter — renders the user background behind the whole app frame:
|
|
109
|
+
* a fixed `z-index:-2` wallpaper element plus a `z-index:-1` scrim on
|
|
110
|
+
* `document.body`, driven by the `data-dsh-bg` body attribute and a set of
|
|
111
|
+
* CSS variables. It also applies the frosted-glass surface tokens so the
|
|
112
|
+
* wallpaper shows through translucent input / bubble panels.
|
|
113
|
+
*
|
|
114
|
+
* Retraction discipline mirrors the reference skin plugins: every property /
|
|
115
|
+
* DOM node this painter owns is restored to its prior state on dispose, and
|
|
116
|
+
* the `data-ds-dark-theme` observer is disconnected.
|
|
117
|
+
*/
|
|
118
|
+
/** Class names the layer and scrim elements carry. */
|
|
119
|
+
const LAYER_CLASS = "dsh-bg-layer";
|
|
120
|
+
const IMAGE_CLASS = "dsh-bg-image";
|
|
121
|
+
const SCRIM_CLASS = "dsh-bg-scrim";
|
|
122
|
+
/** Surface tokens this painter overwrites (restored on dispose). */
|
|
123
|
+
const SURFACE_TOKENS = ["--dsw-specific-input-major", "--dsw-specific-bubble"];
|
|
124
|
+
/** Fraction of the official white surface alpha kept at the most transparent panel setting. */
|
|
125
|
+
const GLASS_MIN_ALPHA = .05;
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the effective background image url for a section.
|
|
128
|
+
* @param settings - resolved background section.
|
|
129
|
+
* @returns the image url, or '' when no source is set.
|
|
130
|
+
*/
|
|
131
|
+
function backgroundImageUrl(settings, uploadBase) {
|
|
132
|
+
if (settings.uploadId) return `${uploadBase}/image/${settings.uploadId}`;
|
|
133
|
+
return settings.url;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Manage the behind-body wallpaper + scrim layers and the glass surface.
|
|
137
|
+
*/
|
|
138
|
+
var BackgroundPainter = class {
|
|
139
|
+
layer = null;
|
|
140
|
+
img = null;
|
|
141
|
+
scrim = null;
|
|
142
|
+
observer;
|
|
143
|
+
active = false;
|
|
144
|
+
settings;
|
|
145
|
+
savedTokens = /* @__PURE__ */ new Map();
|
|
146
|
+
savedVars = /* @__PURE__ */ new Map();
|
|
147
|
+
/** Remember + hijack a property only once so dispose restores the original. */
|
|
148
|
+
rememberOnce(prop) {
|
|
149
|
+
if (this.savedTokens.has(prop) || this.savedVars.has(prop)) return;
|
|
150
|
+
const value = document.body.style.getPropertyValue(prop);
|
|
151
|
+
if (SURFACE_TOKENS.includes(prop)) this.savedTokens.set(prop, value);
|
|
152
|
+
else this.savedVars.set(prop, value);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Apply the latest settings. Idempotent.
|
|
156
|
+
* @param settings - resolved background section.
|
|
157
|
+
* @param uploadBase - the API base for resolving uploads.
|
|
158
|
+
*/
|
|
159
|
+
apply(settings, uploadBase) {
|
|
160
|
+
this.settings = settings;
|
|
161
|
+
if (!settings.enabled) {
|
|
162
|
+
this.dispose();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
injectBackgroundCss();
|
|
166
|
+
this.rememberOnce("--dsw-alias-bg-base");
|
|
167
|
+
this.rememberOnce("--dsw-specific-sidebar-fill");
|
|
168
|
+
document.body.style.setProperty("--dsw-alias-bg-base", "transparent");
|
|
169
|
+
document.body.style.setProperty("--dsw-specific-sidebar-fill", "transparent");
|
|
170
|
+
const url = backgroundImageUrl(settings, uploadBase);
|
|
171
|
+
if (url === "") {
|
|
172
|
+
this.applySurfaceGlass(settings, true);
|
|
173
|
+
this.active = false;
|
|
174
|
+
this.removeLayers();
|
|
175
|
+
document.body.removeAttribute(ACTIVE_ATTR);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!this.layer) {
|
|
179
|
+
this.layer = document.createElement("div");
|
|
180
|
+
this.layer.className = LAYER_CLASS;
|
|
181
|
+
this.img = document.createElement("img");
|
|
182
|
+
this.img.className = IMAGE_CLASS;
|
|
183
|
+
this.img.alt = "";
|
|
184
|
+
this.layer.appendChild(this.img);
|
|
185
|
+
document.body.appendChild(this.layer);
|
|
186
|
+
}
|
|
187
|
+
if (this.img && this.img.src !== url) this.img.src = url;
|
|
188
|
+
if (!this.scrim) {
|
|
189
|
+
this.scrim = document.createElement("div");
|
|
190
|
+
this.scrim.className = SCRIM_CLASS;
|
|
191
|
+
document.body.appendChild(this.scrim);
|
|
192
|
+
}
|
|
193
|
+
document.body.setAttribute(ACTIVE_ATTR, "on");
|
|
194
|
+
this.active = true;
|
|
195
|
+
document.body.style;
|
|
196
|
+
this.setVar("--bg-object-fit", settings.fit);
|
|
197
|
+
this.setVar("--bg-opacity", String(settings.opacity));
|
|
198
|
+
this.setVar("--bg-wallpaper-blur", `${settings.wallpaperBlur}px`);
|
|
199
|
+
this.setVar("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
|
|
200
|
+
this.setVar("--bg-scrim", String(settings.scrim));
|
|
201
|
+
this.setVar("--bg-glass-blur", `${settings.blur}px`);
|
|
202
|
+
this.setVar("--bg-glass-saturate", String(1.15 + settings.blur * .03));
|
|
203
|
+
this.applySurfaceGlass(settings, false);
|
|
204
|
+
if (!this.observer) {
|
|
205
|
+
this.observer = new MutationObserver(() => this.applySurfaceGlass(this.settings, false));
|
|
206
|
+
this.observer.observe(document.body, {
|
|
207
|
+
attributes: true,
|
|
208
|
+
attributeFilter: ["data-ds-dark-theme"]
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Write one owned CSS variable onto body. */
|
|
213
|
+
setVar(name, value) {
|
|
214
|
+
this.rememberOnce(name);
|
|
215
|
+
document.body.style.setProperty(name, value);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Update one effect knob without re-running the full apply. This is the hot
|
|
219
|
+
* path for slider drags (many events per second): only the single CSS
|
|
220
|
+
* variable changes, the DOM layers are untouched.
|
|
221
|
+
* @param key - the knob to update.
|
|
222
|
+
* @param value - its new value in its canonical unit.
|
|
223
|
+
*/
|
|
224
|
+
setKnob(key, value) {
|
|
225
|
+
if (this.settings === void 0 || !this.settings.enabled) return;
|
|
226
|
+
switch (key) {
|
|
227
|
+
case "opacity":
|
|
228
|
+
this.setVar("--bg-opacity", String(value));
|
|
229
|
+
break;
|
|
230
|
+
case "scrim":
|
|
231
|
+
this.setVar("--bg-scrim", String(value));
|
|
232
|
+
break;
|
|
233
|
+
case "blur":
|
|
234
|
+
this.setVar("--bg-glass-blur", `${value}px`);
|
|
235
|
+
this.setVar("--bg-glass-saturate", String(1.15 + Number(value) * .03));
|
|
236
|
+
break;
|
|
237
|
+
case "wallpaperBlur":
|
|
238
|
+
this.setVar("--bg-wallpaper-blur", `${value}px`);
|
|
239
|
+
this.setVar("--bg-wallpaper-scale", (1 + Number(value) * .006).toFixed(4));
|
|
240
|
+
break;
|
|
241
|
+
case "fit": this.setVar("--bg-object-fit", String(value));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Apply the translucent-glass surface tokens (or restore the official ones
|
|
246
|
+
* when the panel is fully opaque / there is no background).
|
|
247
|
+
* @param settings - resolved section (for panelOpacity).
|
|
248
|
+
* @param forceRestore - true to restore official surfaces regardless (used
|
|
249
|
+
* when no source is set, so leftover glass tokens never linger).
|
|
250
|
+
*/
|
|
251
|
+
applySurfaceGlass(settings, forceRestore) {
|
|
252
|
+
const s = document.body.style;
|
|
253
|
+
const inDark = document.body.dataset.dsDarkTheme !== void 0;
|
|
254
|
+
for (const token of SURFACE_TOKENS) this.rememberOnce(token);
|
|
255
|
+
const restore = () => {
|
|
256
|
+
for (const token of SURFACE_TOKENS) {
|
|
257
|
+
const original = this.savedTokens.get(token);
|
|
258
|
+
if (original !== void 0 && original !== "") s.setProperty(token, original);
|
|
259
|
+
else s.removeProperty(token);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
if (forceRestore || settings === void 0 || settings.panelOpacity >= 1) {
|
|
263
|
+
restore();
|
|
264
|
+
this.setVar("--bg-glass-blur", "0px");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const alpha = GLASS_MIN_ALPHA + settings.panelOpacity * .85;
|
|
268
|
+
const clamped = Math.max(0, Math.min(.9, alpha));
|
|
269
|
+
const factor = inDark ? .4 : .8;
|
|
270
|
+
s.setProperty("--dsw-specific-input-major", `rgba(255, 255, 255, ${(clamped * factor).toFixed(3)})`);
|
|
271
|
+
s.setProperty("--dsw-specific-bubble", `rgba(255, 255, 255, ${(clamped * factor * .8).toFixed(3)})`);
|
|
272
|
+
}
|
|
273
|
+
/** Remove the wallpaper + scrim layers and clear the active attribute. */
|
|
274
|
+
removeLayers() {
|
|
275
|
+
if (this.layer) {
|
|
276
|
+
this.layer.remove();
|
|
277
|
+
this.layer = null;
|
|
278
|
+
this.img = null;
|
|
279
|
+
}
|
|
280
|
+
if (this.scrim) {
|
|
281
|
+
this.scrim.remove();
|
|
282
|
+
this.scrim = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Restore every owned property and remove the layers. */
|
|
286
|
+
dispose() {
|
|
287
|
+
this.observer?.disconnect();
|
|
288
|
+
this.observer = void 0;
|
|
289
|
+
this.removeLayers();
|
|
290
|
+
const s = document.body.style;
|
|
291
|
+
document.body.removeAttribute(ACTIVE_ATTR);
|
|
292
|
+
for (const [prop, value] of this.savedTokens) s.setProperty(prop, value);
|
|
293
|
+
for (const [prop, value] of this.savedVars) s.setProperty(prop, value);
|
|
294
|
+
this.savedTokens.clear();
|
|
295
|
+
this.savedVars.clear();
|
|
296
|
+
this.active = false;
|
|
297
|
+
this.settings = void 0;
|
|
298
|
+
}
|
|
299
|
+
/** @returns whether a wallpaper is currently rendered. */
|
|
300
|
+
isActive() {
|
|
301
|
+
return this.active;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
/** Module-level painter singleton shared by the settings row preview and the live apply. */
|
|
305
|
+
const backgroundPainter = new BackgroundPainter();
|
|
306
|
+
/**
|
|
307
|
+
* Apply the given settings through the shared painter (idempotent). Convenience
|
|
308
|
+
* used by the settings row for live previews and by the plugin apply to render
|
|
309
|
+
* the persisted section.
|
|
310
|
+
* @param settings - settings to apply (may be an unsaved draft for preview).
|
|
311
|
+
*/
|
|
312
|
+
function paintBackground(settings) {
|
|
313
|
+
backgroundPainter.apply(settings, BACKGROUND_API_PREFIX);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Update one effect knob on the live backdrop without re-running the full
|
|
317
|
+
* apply — the hot path for slider drags (see BackgroundPainter.setKnob).
|
|
318
|
+
* @param key - the knob to update.
|
|
319
|
+
* @param value - its new value.
|
|
320
|
+
*/
|
|
321
|
+
function paintBackgroundKnob(key, value) {
|
|
322
|
+
backgroundPainter.setKnob(key, value);
|
|
323
|
+
}
|
|
324
|
+
/** Preview variables the settings-surface preview card consumes. */
|
|
325
|
+
const PREVIEW_VARS = [
|
|
326
|
+
"--bg-opacity",
|
|
327
|
+
"--bg-scrim",
|
|
328
|
+
"--bg-object-fit",
|
|
329
|
+
"--bg-wallpaper-blur",
|
|
330
|
+
"--bg-wallpaper-scale",
|
|
331
|
+
"--bg-glass-blur",
|
|
332
|
+
"--bg-glass-saturate",
|
|
333
|
+
"--bg-preview-glass"
|
|
334
|
+
];
|
|
335
|
+
/**
|
|
336
|
+
* Push the effect knobs onto one element (the settings-surface preview card)
|
|
337
|
+
* so dragging a slider updates the card without touching the live backdrop.
|
|
338
|
+
* The card's CSS reads the same variable names as the painter writes on body.
|
|
339
|
+
* @param el - the preview element.
|
|
340
|
+
* @param settings - the draft settings to render (may be unsaved).
|
|
341
|
+
*/
|
|
342
|
+
function paintPreviewSurface(el, settings) {
|
|
343
|
+
const s = el.style;
|
|
344
|
+
s.setProperty("--bg-object-fit", settings.fit);
|
|
345
|
+
s.setProperty("--bg-opacity", String(settings.opacity));
|
|
346
|
+
s.setProperty("--bg-scrim", String(settings.scrim));
|
|
347
|
+
s.setProperty("--bg-wallpaper-blur", `${settings.wallpaperBlur}px`);
|
|
348
|
+
s.setProperty("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
|
|
349
|
+
const inDark = document.body.dataset.dsDarkTheme !== void 0;
|
|
350
|
+
if (settings.panelOpacity >= 1) {
|
|
351
|
+
s.setProperty("--bg-glass-blur", "0px");
|
|
352
|
+
s.setProperty("--bg-preview-glass", "var(--dsw-alias-bg-layer-1)");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
s.setProperty("--bg-glass-blur", `${settings.blur}px`);
|
|
356
|
+
s.setProperty("--bg-glass-saturate", String(1.15 + settings.blur * .03));
|
|
357
|
+
const alpha = GLASS_MIN_ALPHA + settings.panelOpacity * .85;
|
|
358
|
+
const clamped = Math.max(0, Math.min(.9, alpha));
|
|
359
|
+
const factor = inDark ? .4 : .8;
|
|
360
|
+
s.setProperty("--bg-preview-glass", `rgba(255, 255, 255, ${(clamped * factor * .8).toFixed(3)})`);
|
|
361
|
+
}
|
|
362
|
+
/** Remove every preview variable from an element (restores CSS defaults). */
|
|
363
|
+
function clearPreviewSurface(el) {
|
|
364
|
+
for (const name of PREVIEW_VARS) el.style.removeProperty(name);
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/client/locales.ts
|
|
368
|
+
/** Chinese copy. */
|
|
369
|
+
const zh = {
|
|
370
|
+
"background.title": "自定义背景",
|
|
371
|
+
"background.description": "为整个应用界面设置背景图片,可调不透明度、遮罩、面板透明与毛玻璃模糊",
|
|
372
|
+
"background.upload": "上传图片",
|
|
373
|
+
"background.uploading": "上传中…",
|
|
374
|
+
"background.uploadMinetypes": "支持 JPG / PNG / WebP / GIF",
|
|
375
|
+
"background.urlPlaceholder": "或粘贴图片 http(s) 链接后回车",
|
|
376
|
+
"background.applyUrl": "应用链接",
|
|
377
|
+
"background.enabled": "背景已启用",
|
|
378
|
+
"background.source": "图片",
|
|
379
|
+
"background.effects": "效果",
|
|
380
|
+
"background.noPreview": "上传图片或粘贴链接后,此处实时预览效果",
|
|
381
|
+
"background.previewGlass": "毛玻璃",
|
|
382
|
+
"background.opacity": "不透明度",
|
|
383
|
+
"background.scrim": "遮罩",
|
|
384
|
+
"background.panelOpacity": "面板不透明度",
|
|
385
|
+
"background.blur": "毛玻璃模糊",
|
|
386
|
+
"background.wallpaperBlur": "壁纸模糊",
|
|
387
|
+
"background.fit": "填充方式",
|
|
388
|
+
"background.cover": "铺满",
|
|
389
|
+
"background.contain": "完整",
|
|
390
|
+
"background.clear": "清除背景",
|
|
391
|
+
"background.uploadFailed": "上传失败,请重试",
|
|
392
|
+
"background.saveFailed": "保存失败,请重试"
|
|
393
|
+
};
|
|
394
|
+
/** English copy. */
|
|
395
|
+
const en = {
|
|
396
|
+
"background.title": "Custom Background",
|
|
397
|
+
"background.description": "Set a background image behind the whole app surface with adjustable opacity, scrim, panel transparency and glass blur",
|
|
398
|
+
"background.upload": "Upload image",
|
|
399
|
+
"background.uploading": "Uploading…",
|
|
400
|
+
"background.uploadMinetypes": "JPG / PNG / WebP / GIF",
|
|
401
|
+
"background.urlPlaceholder": "Or paste an image http(s) link and press Enter",
|
|
402
|
+
"background.applyUrl": "Apply URL",
|
|
403
|
+
"background.enabled": "Background on",
|
|
404
|
+
"background.source": "Image",
|
|
405
|
+
"background.effects": "Effects",
|
|
406
|
+
"background.noPreview": "Upload an image or paste a link to preview live here",
|
|
407
|
+
"background.previewGlass": "Glass",
|
|
408
|
+
"background.opacity": "Opacity",
|
|
409
|
+
"background.scrim": "Scrim",
|
|
410
|
+
"background.panelOpacity": "Panel opacity",
|
|
411
|
+
"background.blur": "Glass blur",
|
|
412
|
+
"background.wallpaperBlur": "Wallpaper blur",
|
|
413
|
+
"background.fit": "Fit",
|
|
414
|
+
"background.cover": "Cover",
|
|
415
|
+
"background.contain": "Contain",
|
|
416
|
+
"background.clear": "Clear background",
|
|
417
|
+
"background.uploadFailed": "Upload failed, try again",
|
|
418
|
+
"background.saveFailed": "Save failed, try again"
|
|
419
|
+
};
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
422
|
+
function r(e) {
|
|
423
|
+
var t, f, n = "";
|
|
424
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
425
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
426
|
+
var o = e.length;
|
|
427
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
428
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
429
|
+
return n;
|
|
430
|
+
}
|
|
431
|
+
function clsx() {
|
|
432
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
433
|
+
return n;
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/client/settings-client.ts
|
|
437
|
+
/**
|
|
438
|
+
* Observable settings client (getSnapshot/subscribe — the uSES currency).
|
|
439
|
+
* Methods are arrow properties so React's useSyncExternalStore can call them
|
|
440
|
+
* without an owning `this`.
|
|
441
|
+
*/
|
|
442
|
+
var SettingsClient = class {
|
|
443
|
+
snapshot = { status: "loading" };
|
|
444
|
+
listeners = /* @__PURE__ */ new Set();
|
|
445
|
+
/** Monotonic request id; the latest save wins even when POSTs interleave. */
|
|
446
|
+
saveSeq = 0;
|
|
447
|
+
/** Pending save promise (used by the row's flush-on-unmount). */
|
|
448
|
+
pending;
|
|
449
|
+
/** @returns the current sync snapshot (stable reference until the next change). */
|
|
450
|
+
getSnapshot = () => this.snapshot;
|
|
451
|
+
/** Observe snapshot replacements. @returns the disposer removing this listener. */
|
|
452
|
+
subscribe = (listener) => {
|
|
453
|
+
this.listeners.add(listener);
|
|
454
|
+
return () => {
|
|
455
|
+
this.listeners.delete(listener);
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
notify() {
|
|
459
|
+
for (const listener of this.listeners) listener();
|
|
460
|
+
}
|
|
461
|
+
/** Await any in-flight save (used before teardown so edits are not dropped). */
|
|
462
|
+
async flush() {
|
|
463
|
+
await this.pending;
|
|
464
|
+
}
|
|
465
|
+
/** Fetch the section from the host. */
|
|
466
|
+
async load() {
|
|
467
|
+
try {
|
|
468
|
+
const response = await fetch(`${BACKGROUND_API_PREFIX}/settings`);
|
|
469
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
470
|
+
const body = await response.json();
|
|
471
|
+
if (!body.ok || body.value === void 0) throw new Error("unexpected payload");
|
|
472
|
+
this.snapshot = {
|
|
473
|
+
status: "ready",
|
|
474
|
+
value: body.value
|
|
475
|
+
};
|
|
476
|
+
} catch {
|
|
477
|
+
this.snapshot = { status: "error" };
|
|
478
|
+
}
|
|
479
|
+
this.notify();
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Persist the section through the host route. Concurrent calls are
|
|
483
|
+
* serialized by a request sequence: an older save that resolves after a
|
|
484
|
+
* newer one must not clobber the newer snapshot with its stale value.
|
|
485
|
+
* @param section - the complete next section.
|
|
486
|
+
* @returns whether the host accepted the write.
|
|
487
|
+
*/
|
|
488
|
+
async save(section) {
|
|
489
|
+
const seq = ++this.saveSeq;
|
|
490
|
+
const run = async () => {
|
|
491
|
+
try {
|
|
492
|
+
const response = await fetch(`${BACKGROUND_API_PREFIX}/settings`, {
|
|
493
|
+
method: "POST",
|
|
494
|
+
headers: { "content-type": "application/json" },
|
|
495
|
+
body: JSON.stringify(section)
|
|
496
|
+
});
|
|
497
|
+
const body = await response.json();
|
|
498
|
+
if (!response.ok || !body.ok || body.value === void 0) return false;
|
|
499
|
+
if (seq !== this.saveSeq) return true;
|
|
500
|
+
this.snapshot = {
|
|
501
|
+
status: "ready",
|
|
502
|
+
value: body.value
|
|
503
|
+
};
|
|
504
|
+
this.notify();
|
|
505
|
+
return true;
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const promise = run();
|
|
511
|
+
this.pending = promise;
|
|
512
|
+
return promise;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Upload a local image file; on success stores its id into the snapshot and
|
|
516
|
+
* returns the resolved background url (or null on failure).
|
|
517
|
+
* @param file - the chosen image file.
|
|
518
|
+
* @returns the resolved image url, or null.
|
|
519
|
+
*/
|
|
520
|
+
async upload(file) {
|
|
521
|
+
try {
|
|
522
|
+
const response = await fetch(`${BACKGROUND_API_PREFIX}/upload`, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers: { "content-type": file.type },
|
|
525
|
+
body: file
|
|
526
|
+
});
|
|
527
|
+
const body = await response.json();
|
|
528
|
+
if (!response.ok || !body.ok || body.id === void 0 || body.url === void 0) return null;
|
|
529
|
+
return body.url ?? null;
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
/** Shared client for the painter and the settings row. */
|
|
536
|
+
const settingsClient = new SettingsClient();
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
|
|
539
|
+
const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;height:132px;position:relative;overflow:hidden}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.6));border:1px solid #ffffff38;border-radius:10px;justify-content:center;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;box-shadow:inset 0 1px #ffffff4d}.-ozqdq_previewGlassText{color:#fff;opacity:.8;text-shadow:0 1px 2px #00000059;white-space:nowrap;font-size:11px;line-height:1}.-ozqdq_previewEmpty{color:var(--dsw-alias-label-tertiary);justify-content:center;align-items:center;font-size:12px;line-height:1.5;display:flex;position:absolute;inset:0}.-ozqdq_section{flex-direction:column;gap:8px;display:flex}.-ozqdq_sectionLabel{color:var(--dsw-alias-label-tertiary);letter-spacing:.01em;font-size:12px;font-weight:500;line-height:18px}.-ozqdq_sourceRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.-ozqdq_urlRow{align-items:center;gap:8px;display:flex}.-ozqdq_urlInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);min-width:0;height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;flex:1;padding:0 10px;font-size:13px;line-height:1.5}.-ozqdq_urlInput::placeholder{color:var(--dsw-alias-label-dimmed)}.-ozqdq_urlInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.-ozqdq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.-ozqdq_enabledBadge{white-space:nowrap;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.-ozqdq_btn{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:14px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;line-height:18px;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}.-ozqdq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.-ozqdq_btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.-ozqdq_btn:disabled{opacity:.45;cursor:default}.-ozqdq_btnPrimary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.-ozqdq_btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.-ozqdq_btnDanger{color:var(--dsw-alias-label-error)}.-ozqdq_btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.-ozqdq_sliderRow{align-items:center;gap:10px;display:flex}.-ozqdq_sliderLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_slider{-webkit-appearance:none;appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:18px;margin:0}.-ozqdq_slider::-webkit-slider-runnable-track{background:linear-gradient(90deg, var(--dsw-alias-brand-primary) var(--bg-fill,0%), var(--dsw-alias-border-l3) var(--bg-fill,0%));border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-track{background:var(--dsw-alias-border-l3);border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:2px;height:4px}.-ozqdq_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%;margin-top:-5px}.-ozqdq_slider::-moz-range-thumb{border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%}.-ozqdq_slider:hover::-webkit-slider-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:hover::-moz-range-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:active::-webkit-slider-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:active::-moz-range-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:focus-visible{outline:none}.-ozqdq_slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_slider:focus-visible::-moz-range-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_sliderValue{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:0 0 52px;font-size:13px;line-height:1.5}.-ozqdq_segRow{align-items:center;gap:10px;display:flex}.-ozqdq_segLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_segGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex:1;gap:2px;min-width:0;padding:2px;display:inline-flex}.-ozqdq_segBtn{height:24px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;flex:1;padding:0 16px;font-size:12px;line-height:24px}.-ozqdq_segBtn:hover:not(.-ozqdq_segActive){color:var(--dsw-alias-label-primary)}.-ozqdq_segActive{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-weight:500}.-ozqdq_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5}.-ozqdq_clearRow{align-items:center;gap:10px;display:flex}@media (prefers-reduced-motion:reduce){.-ozqdq_btn{transition:none}}";
|
|
540
|
+
const tagId = "deepseek-harness-background/SettingsRow.module.css";
|
|
541
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
542
|
+
const tag = document.createElement("style");
|
|
543
|
+
tag.dataset.plugin = "deepseek-harness-background";
|
|
544
|
+
tag.dataset.pluginCss = tagId;
|
|
545
|
+
tag.textContent = css;
|
|
546
|
+
document.head.appendChild(tag);
|
|
547
|
+
}
|
|
548
|
+
var SettingsRow_module_css_default = {
|
|
549
|
+
"titleRow": "-ozqdq_titleRow",
|
|
550
|
+
"clearBtn": "-ozqdq_clearBtn",
|
|
551
|
+
"preview": "-ozqdq_preview",
|
|
552
|
+
"previewGlass": "-ozqdq_previewGlass",
|
|
553
|
+
"header": "-ozqdq_header",
|
|
554
|
+
"previewEmpty": "-ozqdq_previewEmpty",
|
|
555
|
+
"sectionLabel": "-ozqdq_sectionLabel",
|
|
556
|
+
"btnDanger": "-ozqdq_btnDanger",
|
|
557
|
+
"sliderRow": "-ozqdq_sliderRow",
|
|
558
|
+
"segGroup": "-ozqdq_segGroup",
|
|
559
|
+
"clearRow": "-ozqdq_clearRow",
|
|
560
|
+
"slider": "-ozqdq_slider",
|
|
561
|
+
"sliderValue": "-ozqdq_sliderValue",
|
|
562
|
+
"hint": "-ozqdq_hint",
|
|
563
|
+
"segLabel": "-ozqdq_segLabel",
|
|
564
|
+
"segBtn": "-ozqdq_segBtn",
|
|
565
|
+
"sliderLabel": "-ozqdq_sliderLabel",
|
|
566
|
+
"segRow": "-ozqdq_segRow",
|
|
567
|
+
"desc": "-ozqdq_desc",
|
|
568
|
+
"urlRow": "-ozqdq_urlRow",
|
|
569
|
+
"previewImg": "-ozqdq_previewImg",
|
|
570
|
+
"segActive": "-ozqdq_segActive",
|
|
571
|
+
"error": "-ozqdq_error",
|
|
572
|
+
"title": "-ozqdq_title",
|
|
573
|
+
"urlInput": "-ozqdq_urlInput",
|
|
574
|
+
"btn": "-ozqdq_btn",
|
|
575
|
+
"btnPrimary": "-ozqdq_btnPrimary",
|
|
576
|
+
"previewGlassText": "-ozqdq_previewGlassText",
|
|
577
|
+
"enabledBadge": "-ozqdq_enabledBadge",
|
|
578
|
+
"row": "-ozqdq_row",
|
|
579
|
+
"previewScrim": "-ozqdq_previewScrim",
|
|
580
|
+
"section": "-ozqdq_section",
|
|
581
|
+
"sourceRow": "-ozqdq_sourceRow"
|
|
582
|
+
};
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region src/client/SettingsRow.tsx
|
|
585
|
+
/**
|
|
586
|
+
* Background settings row — the editable form rendered inside the General
|
|
587
|
+
* settings section (`settings.general.item` slot), in the same visual area as
|
|
588
|
+
* the official Appearance row. Layout mirrors the reference wallpaper engine
|
|
589
|
+
* picker (preview surface + grouped sections), styled with the harness alias
|
|
590
|
+
* tokens so it reads as part of the settings chrome.
|
|
591
|
+
*
|
|
592
|
+
* Interaction model (fixes the previous jank):
|
|
593
|
+
* - Sliders STEP in 5% (or 2px/5px) increments, so dragging snaps between
|
|
594
|
+
* discrete values instead of firing dozens of sub-percent updates.
|
|
595
|
+
* - While dragging, only the CSS variables change: the slider holds its own
|
|
596
|
+
* state (no parent re-render) and the preview card repaints in isolation.
|
|
597
|
+
* React's onChange maps to the native `input` event and would fire on every
|
|
598
|
+
* step, so a commit is deferred to release (pointerup / keyup / blur) and
|
|
599
|
+
* debounced — one POST per gesture, never per tick.
|
|
600
|
+
* - A `lastSavedRef` distinguishes our own saves from external changes, so
|
|
601
|
+
* the snapshot sync can never clobber a draft the user is still editing.
|
|
602
|
+
*/
|
|
603
|
+
/** Defaults mirror the host schema (also the pre-load draft fallback). */
|
|
604
|
+
const DEFAULTS = {
|
|
605
|
+
enabled: false,
|
|
606
|
+
uploadId: "",
|
|
607
|
+
url: "",
|
|
608
|
+
opacity: 1,
|
|
609
|
+
scrim: DEFAULT_SCRIM,
|
|
610
|
+
panelOpacity: DEFAULT_PANEL_OPACITY,
|
|
611
|
+
blur: 16,
|
|
612
|
+
wallpaperBlur: 0,
|
|
613
|
+
fit: DEFAULT_FIT
|
|
614
|
+
};
|
|
615
|
+
/** Per-field slider geometry: 5% steps for ratios, fine px steps for radii so
|
|
616
|
+
* the two blur sliders feel smooth near small values. Bounds come from the
|
|
617
|
+
* shared settings constants so the schema boundary and the UI cannot drift. */
|
|
618
|
+
const SLIDER_SPEC = {
|
|
619
|
+
opacity: {
|
|
620
|
+
min: 0,
|
|
621
|
+
max: 1,
|
|
622
|
+
step: .05,
|
|
623
|
+
unit: "percent"
|
|
624
|
+
},
|
|
625
|
+
scrim: {
|
|
626
|
+
min: 0,
|
|
627
|
+
max: SCRIM_MAX,
|
|
628
|
+
step: .05,
|
|
629
|
+
unit: "percent"
|
|
630
|
+
},
|
|
631
|
+
panelOpacity: {
|
|
632
|
+
min: 0,
|
|
633
|
+
max: 1,
|
|
634
|
+
step: .05,
|
|
635
|
+
unit: "percent"
|
|
636
|
+
},
|
|
637
|
+
blur: {
|
|
638
|
+
min: 0,
|
|
639
|
+
max: 40,
|
|
640
|
+
step: 1,
|
|
641
|
+
unit: "px"
|
|
642
|
+
},
|
|
643
|
+
wallpaperBlur: {
|
|
644
|
+
min: 0,
|
|
645
|
+
max: 60,
|
|
646
|
+
step: 2,
|
|
647
|
+
unit: "px"
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
/** Derive the bare upload id from a resolve url like /api/bg-wallpaper/image/<id>. */
|
|
651
|
+
function stripId(url) {
|
|
652
|
+
const prefix = `${BACKGROUND_API_PREFIX}/image/`;
|
|
653
|
+
return url.startsWith(prefix) ? url.slice(prefix.length) : "";
|
|
654
|
+
}
|
|
655
|
+
/** Read the settings store (uSES-friendly). */
|
|
656
|
+
function readSnapshot() {
|
|
657
|
+
return settingsClient.getSnapshot();
|
|
658
|
+
}
|
|
659
|
+
/** Field-wise equality for distinguishing our own saves from external changes. */
|
|
660
|
+
function sameSettings(a, b) {
|
|
661
|
+
return a.enabled === b.enabled && a.uploadId === b.uploadId && a.url === b.url && a.opacity === b.opacity && a.scrim === b.scrim && a.panelOpacity === b.panelOpacity && a.blur === b.blur && a.wallpaperBlur === b.wallpaperBlur && a.fit === b.fit;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* A stepped range control. While dragging it re-renders only itself: the value
|
|
665
|
+
* is local state, `onLive(name, value)` repaints (CSS variables, no React
|
|
666
|
+
* state), and `onCommit()` fires once on release through a debounced save.
|
|
667
|
+
*/
|
|
668
|
+
function ControlSlider(props) {
|
|
669
|
+
const { name, label, value, onLive, onCommit } = props;
|
|
670
|
+
const spec = SLIDER_SPEC[name];
|
|
671
|
+
const [local, setLocal] = (0, react.useState)(value);
|
|
672
|
+
const localRef = (0, react.useRef)(value);
|
|
673
|
+
const dragging = (0, react.useRef)(false);
|
|
674
|
+
const onLiveRef = (0, react.useRef)(onLive);
|
|
675
|
+
const onCommitRef = (0, react.useRef)(onCommit);
|
|
676
|
+
onLiveRef.current = onLive;
|
|
677
|
+
onCommitRef.current = onCommit;
|
|
678
|
+
(0, react.useEffect)(() => {
|
|
679
|
+
if (!dragging.current && localRef.current !== value) {
|
|
680
|
+
localRef.current = value;
|
|
681
|
+
setLocal(value);
|
|
682
|
+
}
|
|
683
|
+
}, [value]);
|
|
684
|
+
const handleLive = (e) => {
|
|
685
|
+
dragging.current = true;
|
|
686
|
+
const v = Number(e.currentTarget.value);
|
|
687
|
+
localRef.current = v;
|
|
688
|
+
setLocal(v);
|
|
689
|
+
syncTrackFill(e.currentTarget);
|
|
690
|
+
onLiveRef.current(name, v);
|
|
691
|
+
};
|
|
692
|
+
const handleCommit = () => {
|
|
693
|
+
if (!dragging.current) return;
|
|
694
|
+
dragging.current = false;
|
|
695
|
+
onCommitRef.current();
|
|
696
|
+
};
|
|
697
|
+
const text = spec.unit === "percent" ? `${Math.round(local * 100)}%` : `${local}px`;
|
|
698
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
699
|
+
className: SettingsRow_module_css_default.sliderRow,
|
|
700
|
+
children: [
|
|
701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
702
|
+
className: SettingsRow_module_css_default.sliderLabel,
|
|
703
|
+
children: label
|
|
704
|
+
}),
|
|
705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
706
|
+
type: "range",
|
|
707
|
+
className: SettingsRow_module_css_default.slider,
|
|
708
|
+
ref: (el) => {
|
|
709
|
+
if (el) syncTrackFill(el);
|
|
710
|
+
},
|
|
711
|
+
min: spec.min,
|
|
712
|
+
max: spec.max,
|
|
713
|
+
step: spec.step,
|
|
714
|
+
value: String(local),
|
|
715
|
+
"aria-label": label,
|
|
716
|
+
onInput: handleLive,
|
|
717
|
+
onPointerUp: handleCommit,
|
|
718
|
+
onKeyUp: (e) => {
|
|
719
|
+
if (e.key.startsWith("Arrow") || e.key === "Home" || e.key === "End" || e.key === "PageUp" || e.key === "PageDown") handleCommit();
|
|
720
|
+
},
|
|
721
|
+
onBlur: handleCommit
|
|
722
|
+
}),
|
|
723
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
724
|
+
className: SettingsRow_module_css_default.sliderValue,
|
|
725
|
+
children: text
|
|
726
|
+
})
|
|
727
|
+
]
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/** Write the filled-track fraction (%) of a range input into its --bg-fill
|
|
731
|
+
* variable (webkit has no range-progress pseudo-element; moz draws its own). */
|
|
732
|
+
function syncTrackFill(el) {
|
|
733
|
+
const min = Number(el.min);
|
|
734
|
+
const max = Number(el.max);
|
|
735
|
+
const value = Number(el.value);
|
|
736
|
+
const pct = max === min ? 0 : (value - min) / (max - min) * 100;
|
|
737
|
+
el.style.setProperty("--bg-fill", `${pct}%`);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* The background settings row. A draft mirrors the persisted section; dragging
|
|
741
|
+
* repaints the preview card and the live backdrop through CSS variables, and a
|
|
742
|
+
* release commits once through the plugin's own host route.
|
|
743
|
+
*/
|
|
744
|
+
function BackgroundSettingsRow({ t }) {
|
|
745
|
+
const snapshot = (0, react.useSyncExternalStore)(settingsClient.subscribe, readSnapshot);
|
|
746
|
+
const [draft, setDraft] = (0, react.useState)(DEFAULTS);
|
|
747
|
+
const [uploading, setUploading] = (0, react.useState)(false);
|
|
748
|
+
const [urlText, setUrlText] = (0, react.useState)("");
|
|
749
|
+
const [error, setError] = (0, react.useState)("");
|
|
750
|
+
const fileInput = (0, react.useRef)(null);
|
|
751
|
+
const previewRef = (0, react.useRef)(null);
|
|
752
|
+
const draftRef = (0, react.useRef)(DEFAULTS);
|
|
753
|
+
const editingRef = (0, react.useRef)(false);
|
|
754
|
+
const lastSavedRef = (0, react.useRef)(null);
|
|
755
|
+
const dirtyRef = (0, react.useRef)(false);
|
|
756
|
+
const commitTimer = (0, react.useRef)(void 0);
|
|
757
|
+
const editingTimer = (0, react.useRef)(void 0);
|
|
758
|
+
/** Repaint the live backdrop + preview card from one draft (no React state). */
|
|
759
|
+
const paintLive = (0, react.useCallback)((next) => {
|
|
760
|
+
paintBackground(next);
|
|
761
|
+
if (previewRef.current) paintPreviewSurface(previewRef.current, next);
|
|
762
|
+
}, []);
|
|
763
|
+
/** Mark the row as being edited; clears after a quiet 600ms window. */
|
|
764
|
+
const markEditing = (0, react.useCallback)(() => {
|
|
765
|
+
editingRef.current = true;
|
|
766
|
+
clearTimeout(editingTimer.current);
|
|
767
|
+
editingTimer.current = window.setTimeout(() => {
|
|
768
|
+
editingRef.current = false;
|
|
769
|
+
}, 600);
|
|
770
|
+
}, []);
|
|
771
|
+
/** Slider live handler (stable identity for the memoized sliders). */
|
|
772
|
+
const onLive = (0, react.useCallback)((key, value) => {
|
|
773
|
+
const next = {
|
|
774
|
+
...draftRef.current,
|
|
775
|
+
[key]: value
|
|
776
|
+
};
|
|
777
|
+
draftRef.current = next;
|
|
778
|
+
markEditing();
|
|
779
|
+
if (key === "panelOpacity") paintBackground(next);
|
|
780
|
+
else paintBackgroundKnob(key, value);
|
|
781
|
+
if (previewRef.current) paintPreviewSurface(previewRef.current, next);
|
|
782
|
+
}, [markEditing]);
|
|
783
|
+
/** Persist one section through the host route and adopt it on success. */
|
|
784
|
+
const saveNow = (0, react.useCallback)(async (next) => {
|
|
785
|
+
dirtyRef.current = true;
|
|
786
|
+
const ok = await settingsClient.save(next);
|
|
787
|
+
if (ok) dirtyRef.current = false;
|
|
788
|
+
setError(ok ? "" : t("background.saveFailed"));
|
|
789
|
+
if (ok) {
|
|
790
|
+
lastSavedRef.current = next;
|
|
791
|
+
draftRef.current = next;
|
|
792
|
+
setDraft(next);
|
|
793
|
+
if (!next.uploadId) setUrlText(next.url);
|
|
794
|
+
}
|
|
795
|
+
}, [t]);
|
|
796
|
+
/** Debounced commit: one POST per drag gesture, from the latest draft. */
|
|
797
|
+
const onCommit = (0, react.useCallback)(() => {
|
|
798
|
+
clearTimeout(commitTimer.current);
|
|
799
|
+
commitTimer.current = window.setTimeout(() => {
|
|
800
|
+
saveNow(draftRef.current);
|
|
801
|
+
}, 120);
|
|
802
|
+
}, [saveNow]);
|
|
803
|
+
(0, react.useEffect)(() => {
|
|
804
|
+
if (snapshot.status !== "ready" || snapshot.value === void 0) return;
|
|
805
|
+
const saved = lastSavedRef.current;
|
|
806
|
+
if ((saved === null || !sameSettings(saved, snapshot.value)) && !editingRef.current) {
|
|
807
|
+
lastSavedRef.current = snapshot.value;
|
|
808
|
+
draftRef.current = snapshot.value;
|
|
809
|
+
setDraft(snapshot.value);
|
|
810
|
+
setUrlText(snapshot.value.uploadId ? "" : snapshot.value.url);
|
|
811
|
+
if (previewRef.current) paintPreviewSurface(previewRef.current, snapshot.value);
|
|
812
|
+
}
|
|
813
|
+
}, [snapshot]);
|
|
814
|
+
(0, react.useEffect)(() => () => {
|
|
815
|
+
clearTimeout(commitTimer.current);
|
|
816
|
+
clearTimeout(editingTimer.current);
|
|
817
|
+
if (dirtyRef.current) settingsClient.flush();
|
|
818
|
+
}, []);
|
|
819
|
+
const handleFile = async (file) => {
|
|
820
|
+
if (!file) return;
|
|
821
|
+
markEditing();
|
|
822
|
+
setUploading(true);
|
|
823
|
+
setError("");
|
|
824
|
+
try {
|
|
825
|
+
const url = await settingsClient.upload(file);
|
|
826
|
+
if (url === null) {
|
|
827
|
+
setError(t("background.uploadFailed"));
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const next = {
|
|
831
|
+
...draftRef.current,
|
|
832
|
+
uploadId: stripId(url),
|
|
833
|
+
url: "",
|
|
834
|
+
enabled: true
|
|
835
|
+
};
|
|
836
|
+
draftRef.current = next;
|
|
837
|
+
setDraft(next);
|
|
838
|
+
setUrlText("");
|
|
839
|
+
paintLive(next);
|
|
840
|
+
await saveNow(next);
|
|
841
|
+
} finally {
|
|
842
|
+
setUploading(false);
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
const handleUrlSubmit = () => {
|
|
846
|
+
const url = urlText.trim();
|
|
847
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
848
|
+
setError(t("background.saveFailed"));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
markEditing();
|
|
852
|
+
const next = {
|
|
853
|
+
...draftRef.current,
|
|
854
|
+
uploadId: "",
|
|
855
|
+
url,
|
|
856
|
+
enabled: true
|
|
857
|
+
};
|
|
858
|
+
draftRef.current = next;
|
|
859
|
+
setDraft(next);
|
|
860
|
+
paintLive(next);
|
|
861
|
+
saveNow(next);
|
|
862
|
+
};
|
|
863
|
+
const handleClear = () => {
|
|
864
|
+
const next = {
|
|
865
|
+
...draftRef.current,
|
|
866
|
+
uploadId: "",
|
|
867
|
+
url: "",
|
|
868
|
+
enabled: false
|
|
869
|
+
};
|
|
870
|
+
draftRef.current = next;
|
|
871
|
+
setDraft(next);
|
|
872
|
+
setUrlText("");
|
|
873
|
+
if (previewRef.current) clearPreviewSurface(previewRef.current);
|
|
874
|
+
paintBackground(next);
|
|
875
|
+
saveNow(next);
|
|
876
|
+
};
|
|
877
|
+
const handleFit = (mode) => {
|
|
878
|
+
markEditing();
|
|
879
|
+
const next = {
|
|
880
|
+
...draftRef.current,
|
|
881
|
+
fit: mode
|
|
882
|
+
};
|
|
883
|
+
draftRef.current = next;
|
|
884
|
+
setDraft(next);
|
|
885
|
+
paintLive(next);
|
|
886
|
+
saveNow(next);
|
|
887
|
+
};
|
|
888
|
+
const sourceUrl = draft.uploadId ? `${BACKGROUND_API_PREFIX}/image/${draft.uploadId}` : draft.url;
|
|
889
|
+
const hasImage = sourceUrl !== "";
|
|
890
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
891
|
+
className: SettingsRow_module_css_default.row,
|
|
892
|
+
children: [
|
|
893
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
894
|
+
className: SettingsRow_module_css_default.header,
|
|
895
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
896
|
+
className: SettingsRow_module_css_default.titleRow,
|
|
897
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
898
|
+
className: SettingsRow_module_css_default.title,
|
|
899
|
+
children: t("background.title")
|
|
900
|
+
}), hasImage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
901
|
+
type: "button",
|
|
902
|
+
className: clsx(SettingsRow_module_css_default.btn, SettingsRow_module_css_default.btnDanger, SettingsRow_module_css_default.clearBtn),
|
|
903
|
+
onClick: handleClear,
|
|
904
|
+
children: t("background.clear")
|
|
905
|
+
})]
|
|
906
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
907
|
+
className: SettingsRow_module_css_default.desc,
|
|
908
|
+
children: t("background.description")
|
|
909
|
+
})]
|
|
910
|
+
}),
|
|
911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
912
|
+
className: SettingsRow_module_css_default.preview,
|
|
913
|
+
ref: previewRef,
|
|
914
|
+
children: hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
915
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
916
|
+
className: SettingsRow_module_css_default.previewImg,
|
|
917
|
+
src: sourceUrl,
|
|
918
|
+
alt: "",
|
|
919
|
+
draggable: false
|
|
920
|
+
}),
|
|
921
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: SettingsRow_module_css_default.previewScrim }),
|
|
922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
923
|
+
className: SettingsRow_module_css_default.previewGlass,
|
|
924
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
925
|
+
className: SettingsRow_module_css_default.previewGlassText,
|
|
926
|
+
children: t("background.previewGlass")
|
|
927
|
+
})
|
|
928
|
+
})
|
|
929
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
930
|
+
className: SettingsRow_module_css_default.previewEmpty,
|
|
931
|
+
children: t("background.noPreview")
|
|
932
|
+
})
|
|
933
|
+
}),
|
|
934
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
935
|
+
className: SettingsRow_module_css_default.section,
|
|
936
|
+
children: [
|
|
937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
938
|
+
className: SettingsRow_module_css_default.sectionLabel,
|
|
939
|
+
children: t("background.source")
|
|
940
|
+
}),
|
|
941
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
942
|
+
className: SettingsRow_module_css_default.sourceRow,
|
|
943
|
+
children: [
|
|
944
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
945
|
+
type: "file",
|
|
946
|
+
ref: fileInput,
|
|
947
|
+
hidden: true,
|
|
948
|
+
accept: "image/jpeg,image/png,image/webp,image/gif",
|
|
949
|
+
onChange: (e) => {
|
|
950
|
+
handleFile(e.currentTarget.files?.[0]);
|
|
951
|
+
e.currentTarget.value = "";
|
|
952
|
+
}
|
|
953
|
+
}),
|
|
954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
955
|
+
type: "button",
|
|
956
|
+
className: clsx(SettingsRow_module_css_default.btn, SettingsRow_module_css_default.btnPrimary),
|
|
957
|
+
disabled: uploading,
|
|
958
|
+
onClick: () => fileInput.current?.click(),
|
|
959
|
+
children: uploading ? t("background.uploading") : t("background.upload")
|
|
960
|
+
}),
|
|
961
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
962
|
+
className: SettingsRow_module_css_default.hint,
|
|
963
|
+
children: t("background.uploadMinetypes")
|
|
964
|
+
}),
|
|
965
|
+
draft.uploadId && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
966
|
+
className: SettingsRow_module_css_default.enabledBadge,
|
|
967
|
+
children: ["✓ ", t("background.enabled")]
|
|
968
|
+
})
|
|
969
|
+
]
|
|
970
|
+
}),
|
|
971
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
972
|
+
className: SettingsRow_module_css_default.urlRow,
|
|
973
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
974
|
+
className: SettingsRow_module_css_default.urlInput,
|
|
975
|
+
value: urlText,
|
|
976
|
+
placeholder: t("background.urlPlaceholder"),
|
|
977
|
+
onKeyDown: (e) => {
|
|
978
|
+
if (e.key === "Enter") handleUrlSubmit();
|
|
979
|
+
},
|
|
980
|
+
onChange: (e) => setUrlText(e.currentTarget.value)
|
|
981
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
982
|
+
type: "button",
|
|
983
|
+
className: SettingsRow_module_css_default.btn,
|
|
984
|
+
disabled: !urlText.trim(),
|
|
985
|
+
onClick: handleUrlSubmit,
|
|
986
|
+
children: t("background.applyUrl")
|
|
987
|
+
})]
|
|
988
|
+
})
|
|
989
|
+
]
|
|
990
|
+
}),
|
|
991
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
992
|
+
className: SettingsRow_module_css_default.section,
|
|
993
|
+
children: [
|
|
994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
995
|
+
className: SettingsRow_module_css_default.sectionLabel,
|
|
996
|
+
children: t("background.effects")
|
|
997
|
+
}),
|
|
998
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
|
|
999
|
+
name: "opacity",
|
|
1000
|
+
label: t("background.opacity"),
|
|
1001
|
+
value: draft.opacity,
|
|
1002
|
+
onLive,
|
|
1003
|
+
onCommit
|
|
1004
|
+
}),
|
|
1005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
|
|
1006
|
+
name: "scrim",
|
|
1007
|
+
label: t("background.scrim"),
|
|
1008
|
+
value: draft.scrim,
|
|
1009
|
+
onLive,
|
|
1010
|
+
onCommit
|
|
1011
|
+
}),
|
|
1012
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
|
|
1013
|
+
name: "panelOpacity",
|
|
1014
|
+
label: t("background.panelOpacity"),
|
|
1015
|
+
value: draft.panelOpacity,
|
|
1016
|
+
onLive,
|
|
1017
|
+
onCommit
|
|
1018
|
+
}),
|
|
1019
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
|
|
1020
|
+
name: "blur",
|
|
1021
|
+
label: t("background.blur"),
|
|
1022
|
+
value: draft.blur,
|
|
1023
|
+
onLive,
|
|
1024
|
+
onCommit
|
|
1025
|
+
}),
|
|
1026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
|
|
1027
|
+
name: "wallpaperBlur",
|
|
1028
|
+
label: t("background.wallpaperBlur"),
|
|
1029
|
+
value: draft.wallpaperBlur,
|
|
1030
|
+
onLive,
|
|
1031
|
+
onCommit
|
|
1032
|
+
}),
|
|
1033
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1034
|
+
className: SettingsRow_module_css_default.segRow,
|
|
1035
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1036
|
+
className: SettingsRow_module_css_default.segLabel,
|
|
1037
|
+
children: t("background.fit")
|
|
1038
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1039
|
+
className: SettingsRow_module_css_default.segGroup,
|
|
1040
|
+
role: "group",
|
|
1041
|
+
"aria-label": t("background.fit"),
|
|
1042
|
+
children: FIT_MODES.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1043
|
+
type: "button",
|
|
1044
|
+
className: clsx(SettingsRow_module_css_default.segBtn, draft.fit === mode && SettingsRow_module_css_default.segActive),
|
|
1045
|
+
onClick: () => handleFit(mode),
|
|
1046
|
+
children: t(mode === "cover" ? "background.cover" : "background.contain")
|
|
1047
|
+
}, mode))
|
|
1048
|
+
})]
|
|
1049
|
+
})
|
|
1050
|
+
]
|
|
1051
|
+
}),
|
|
1052
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1053
|
+
className: SettingsRow_module_css_default.error,
|
|
1054
|
+
role: "alert",
|
|
1055
|
+
children: error
|
|
1056
|
+
}),
|
|
1057
|
+
hasImage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1058
|
+
className: SettingsRow_module_css_default.clearRow,
|
|
1059
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1060
|
+
type: "button",
|
|
1061
|
+
className: clsx(SettingsRow_module_css_default.btn, SettingsRow_module_css_default.btnDanger),
|
|
1062
|
+
onClick: handleClear,
|
|
1063
|
+
children: t("background.clear")
|
|
1064
|
+
})
|
|
1065
|
+
})
|
|
1066
|
+
]
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/client/index.ts
|
|
1071
|
+
/**
|
|
1072
|
+
* Service injections this entry declares: the `slots` (runtime) and `locale`
|
|
1073
|
+
* services only — the settings surface talks to the plugin's own host route
|
|
1074
|
+
* over plain same-origin fetch, so no settings/connection service is needed.
|
|
1075
|
+
*/
|
|
1076
|
+
const inject = ["slots", "locale"];
|
|
1077
|
+
/**
|
|
1078
|
+
* Client plugin body: paint the persisted background live and register the
|
|
1079
|
+
* General-settings row.
|
|
1080
|
+
* @param ctx - client cordis context (slots/locale injected).
|
|
1081
|
+
*/
|
|
1082
|
+
function apply(ctx) {
|
|
1083
|
+
const paint = () => {
|
|
1084
|
+
const snapshot = settingsClient.getSnapshot();
|
|
1085
|
+
const value = snapshot.status === "ready" ? snapshot.value : void 0;
|
|
1086
|
+
if (value === void 0 || !value.enabled) {
|
|
1087
|
+
backgroundPainter.dispose();
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
paintBackground(value);
|
|
1091
|
+
};
|
|
1092
|
+
const unsubscribe = settingsClient.subscribe(paint);
|
|
1093
|
+
settingsClient.load();
|
|
1094
|
+
ctx.effect(() => ctx.locale.register("ui-background", {
|
|
1095
|
+
zh,
|
|
1096
|
+
en
|
|
1097
|
+
}), "deepseek-harness-background: dictionaries");
|
|
1098
|
+
ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
1099
|
+
name: "settings.general.item",
|
|
1100
|
+
id: "deepseek-harness-background",
|
|
1101
|
+
order: 600,
|
|
1102
|
+
locale: "ui-background"
|
|
1103
|
+
}, BackgroundSettingsRow));
|
|
1104
|
+
ctx.effect(() => () => {
|
|
1105
|
+
unsubscribe();
|
|
1106
|
+
backgroundPainter.dispose();
|
|
1107
|
+
}, "deepseek-harness-background: background surface");
|
|
1108
|
+
}
|
|
1109
|
+
//#endregion
|
|
1110
|
+
exports.apply = apply;
|
|
1111
|
+
exports.inject = inject;
|
|
1112
|
+
return module.exports;
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
//# sourceMappingURL=client.js.map
|