dsh-any-background 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ A **DeepSeek Harness** appearance plugin that lets you fully customize the Web U
16
16
  - **Background Wallpaper** — Choose any image as your wallpaper. Drag to pan, scroll to zoom inside a viewport-proportional editor. What you see is what you get.
17
17
  - **Opacity Controls** — Separate sliders for main interface background opacity, settings panel opacity, and wallpaper opacity.
18
18
  - **Blur Effect** — Adjustable wallpaper blur (0–60 px) for a frosted-glass look.
19
- - **Persistent** — All settings (color, wallpaper, opacity, blur, editor position) are saved to `localStorage` and restored on next launch.
19
+ - **Persistent** — All settings (color, wallpaper, opacity, blur, editor position) are stored on the **filesystem** under `~/.dsh/.dsh-any-background-data/` via the node half, and restored on next launch. No more `localStorage` quota worries.
20
20
  - **Bilingual** — Full Chinese / English UI with automatic locale detection.
21
21
  - **Theme Watchdog** — A background watchdog re-asserts the custom theme if the host resets it, so your pick never silently disappears.
22
22
 
@@ -29,7 +29,7 @@ dsh-any-background/
29
29
  ├── cordis.yml # Patch overlay for dev usage (pnpm dsh web --patch)
30
30
  ├── tsdown.config.ts # Build config: node-half (ESM) + client-half (CJS browser bundle)
31
31
  ├── src/
32
- │ ├── index.ts # Node half — empty shell for Cordis loader mount
32
+ │ ├── index.ts # Node half — file-backed persistence (RPC file store)
33
33
  │ ├── invariant.ts # Invariant companion (registers package ownership)
34
34
  │ └── client/
35
35
  │ └── index.tsx # Browser half — ALL UI logic lives here
@@ -45,7 +45,24 @@ dsh-any-background/
45
45
 
46
46
  ## Implementation
47
47
 
48
- The plugin is a **pure client-side** Cordis plugin. The node half is an empty shell; all behavior lives in `src/client/index.tsx`.
48
+ The plugin is a Cordis plugin split into two halves:
49
+
50
+ - **Node half** (`src/index.ts`) — owns file-backed persistence. It manages the `.dsh-any-background-data/` store under the DSH data home (`~/.dsh/`) and exposes a small RPC surface over the shared `/api` channel.
51
+ - **Browser half** (`src/client/index.tsx`) — all UI logic lives here. The browser cannot touch the filesystem, so it reads/writes the store through the node half's RPC endpoints.
52
+
53
+ ### Persistence
54
+
55
+ Since the plugin surfaces in the browser, persisted data is stored on disk by the **node half**:
56
+
57
+ ```
58
+ ~/.dsh/.dsh-any-background-data/
59
+ ├── theme-config.json # color, main/settings/wallpaper opacity, blur, bg edit state
60
+ └── wallpaper.jpg # the chosen background image (deleted when removed)
61
+ ```
62
+
63
+ - On startup the client calls `read`, which returns the config and (if present) the wallpaper as a data URL.
64
+ - Every setting change is written back synchronously to `theme-config.json`; changing the wallpaper writes `wallpaper.jpg`, removing it deletes the file.
65
+ - The store is created automatically if missing; a missing or malformed config falls back to defaults (with a warning), and all writes are error-guarded to avoid data loss.
49
66
 
50
67
  ### Architecture
51
68
 
@@ -93,7 +110,7 @@ The plugin is a **pure client-side** Cordis plugin. The node half is an empty sh
93
110
  ### Background Wallpaper
94
111
 
95
112
  - A `<div>` with `position:fixed; z-index:-1` is prepended to `<body>`.
96
- - Image is compressed via Canvas API (max 1600px side, JPEG quality 0.75) and stored as base64 in `localStorage`.
113
+ - Image is compressed via Canvas API (max 1600px side, JPEG quality 0.75); the node half writes it to `~/.dsh/.dsh-any-background-data/wallpaper.jpg`, and the client keeps the data URL in memory for display.
97
114
  - The editor modal shows a viewport-proportional rectangle; drag to pan, scroll to zoom (0.1×–10×).
98
115
  - Committed position is stored as fractional center coordinates + natural image size, so the layout survives viewport changes.
99
116
  - Wallpaper opacity is applied directly to the `<div>` element; background color opacity is applied via inline token overrides.
@@ -117,13 +134,13 @@ The plugin is a **pure client-side** Cordis plugin. The node half is an empty sh
117
134
 
118
135
  ### Opacity System
119
136
 
120
- Three independent opacity layers, each with its own slider and `localStorage` key:
137
+ Three independent opacity layers, each with its own slider and a persisted config field:
121
138
 
122
- | Layer | localStorage key | Default | Mechanism |
123
- |-------|-----------------|---------|-----------|
124
- | Main interface | `dsh-any-background:opacity` | 85% | Inline CSS variable on `<body>` |
125
- | Settings panel | `dsh-any-background:settings-opacity` | 100% | CSS variable on `<html>` via `[aria-modal]` selector |
126
- | Wallpaper | `dsh-any-background:wallpaper-opacity` | 100% | Direct `style.opacity` on wallpaper `<div>` |
139
+ | Layer | Config field | Default | Mechanism |
140
+ |-------|--------------|---------|-----------|
141
+ | Main interface | `opacity` | 85% | Inline CSS variable on `<body>` |
142
+ | Settings panel | `settingsOpacity` | 100% | CSS variable on `<html>` via `[aria-modal]` selector |
143
+ | Wallpaper | `wallpaperOpacity` | 100% | Direct `style.opacity` on wallpaper `<div>` |
127
144
 
128
145
  #### Settings Opacity
129
146
 
@@ -203,24 +220,33 @@ editing `src/`, run the bundle script (needs Node + pnpm):
203
220
  git clone https://github.com/Tkingxiao/dsh-any-background.git
204
221
  cd dsh-any-background
205
222
 
206
- # 2. Install the build tool
223
+ # 2. Install the build tool (also pulls the @deepseek-ai/dsh-home-paths runtime dep)
207
224
  pnpm install
208
225
 
209
226
  # 3. Rebuild lib/
210
227
  pnpm run bundle
211
228
 
212
- # 4. Install the plugin into the web profile (from the local checkout)
213
- dsh plugin --profile web add -w .
229
+ # 4. Install the plugin into the web profile from the local checkout
230
+ # (`dsh plugin add` wraps `pnpm add <dir>`, so point it at this directory)
231
+ dsh plugin --profile web add .
214
232
 
215
233
  # 5. Launch
216
234
  dsh web
217
235
  ```
218
236
 
237
+ ## Compatibility
238
+
239
+ The plugin works on both the **Web UI** and the **desktop client**:
240
+
241
+ - **[`dsh web`](https://github.com/deepseek-ai/deepseek-harness)** — Web profile, full support.
242
+ - **[deepseek-harness-desktop](https://github.com/anywhere-labs/deepseek-harness-desktop)** — supported, but there is a known Electron packaging issue: the **left sidebar and the center area opacity are inverted** (the sidebar looks more transparent than the center and vice versa). This is a client-side packaging bug, not a plugin bug — we are waiting for the desktop client to be updated to fix it.
243
+
219
244
  ## Dependencies
220
245
 
221
246
  | Package | Purpose |
222
247
  |---------|---------|
223
248
  | `@deepseek-ai/cordis` | Plugin framework (Cordis) |
249
+ | `@deepseek-ai/dsh-home-paths` | Resolve the DSH data home for the persistence store |
224
250
  | `@deepseek-ai/dsh-client-runtime` | Client runtime + `defineStore` |
225
251
  | `@deepseek-ai/dsh-client-locale` | i18n (Chinese/English) |
226
252
  | `@deepseek-ai/dsh-client-ui-theme` | Theme service (register/setTheme/overrideTokens) |
package/README.zh.md CHANGED
@@ -16,7 +16,7 @@
16
16
  - **背景壁纸** — 选择任意图片作为壁纸。在视口比例的编辑器中拖动平移、滚轮缩放,所见即所得。
17
17
  - **透明度控制** — 主界面背景透明度、设置面板透明度、壁纸透明度各自独立滑块控制。
18
18
  - **模糊效果** — 可调壁纸模糊(0–60 px),打造毛玻璃质感。
19
- - **持久化存储** — 所有设置(颜色、壁纸、透明度、模糊、编辑器位置)保存到 `localStorage`,下次启动自动恢复。
19
+ - **持久化存储** — 所有设置(颜色、壁纸、透明度、模糊、编辑器位置)由 Node 端保存到 **文件系统** 的 `~/.dsh/.dsh-any-background-data/`,下次启动自动恢复。不再受 `localStorage` 容量限制。
20
20
  - **中英双语** — 完整的中英文界面,自动跟随语言设置。
21
21
  - **主题守护** — 后台看门狗定期重新激活自定义主题,即使宿主重置也不会丢失你的选择。
22
22
 
@@ -29,7 +29,7 @@ dsh-any-background/
29
29
  ├── cordis.yml # 开发用补丁覆盖层(pnpm dsh web --patch)
30
30
  ├── tsdown.config.ts # 构建配置:Node 端(ESM)+ 客户端(CJS 浏览器包)
31
31
  ├── src/
32
- │ ├── index.ts # Node 端 — 空壳,供 Cordis 加载器挂载
32
+ │ ├── index.ts # Node 端 — 文件持久化(RPC 文件存储)
33
33
  │ ├── invariant.ts # 不变量 companion(注册包所有权)
34
34
  │ └── client/
35
35
  │ └── index.tsx # 浏览器端 — 所有 UI 逻辑都在这里
@@ -45,7 +45,24 @@ dsh-any-background/
45
45
 
46
46
  ## 实现原理
47
47
 
48
- 本插件是一个**纯客户端** Cordis 插件。Node 端是空壳,所有行为都在 `src/client/index.tsx` 中实现。
48
+ 本插件是一个 Cordis 插件,分为两个部分:
49
+
50
+ - **Node 端**(`src/index.ts`)— 负责文件持久化。管理 DSH 数据主目录(`~/.dsh/`)下的 `.dsh-any-background-data/` 存储,并在共享 `/api` 通道上暴露一组小的 RPC 接口。
51
+ - **浏览器端**(`src/client/index.tsx`)— 所有 UI 逻辑都在这里。浏览器无法直接读写文件系统,因此通过 Node 端的 RPC 接口读写存储。
52
+
53
+ ### 持久化
54
+
55
+ 由于插件运行在浏览器中,持久化数据由 **Node 端** 写入磁盘:
56
+
57
+ ```
58
+ ~/.dsh/.dsh-any-background-data/
59
+ ├── theme-config.json # 主题色、主界面/设置面板/壁纸透明度、模糊、壁纸编辑状态
60
+ └── wallpaper.jpg # 选中的背景图片(移除时删除)
61
+ ```
62
+
63
+ - 启动时客户端调用 `read`,返回配置以及(如果存在)壁纸的 data URL。
64
+ - 每次设置变更都会同步写回 `theme-config.json`;更换壁纸写入 `wallpaper.jpg`,移除壁纸则删除文件。
65
+ - 存储目录不存在时会自动创建;配置缺失或损坏时回退默认值(并记录警告),所有写入都有错误保护以避免数据丢失。
49
66
 
50
67
  ### 架构
51
68
 
@@ -93,7 +110,7 @@ dsh-any-background/
93
110
  ### 背景壁纸
94
111
 
95
112
  - 一个 `position:fixed; z-index:-1` 的 `<div>` 被插入到 `<body>` 最前面。
96
- - 图片通过 Canvas API 压缩(最大边 1600px,JPEG 质量 0.75),以 base64 存入 `localStorage`。
113
+ - 图片通过 Canvas API 压缩(最大边 1600px,JPEG 质量 0.75);Node 端写入 `~/.dsh/.dsh-any-background-data/wallpaper.jpg`,客户端在内存中保留 data URL 用于显示。
97
114
  - 编辑器弹窗显示视口比例的矩形区域;拖动平移,滚轮缩放(0.1×–10×)。
98
115
  - 提交的位置以分数中心坐标 + 原始图片尺寸存储,布局在视口变化时保持一致。
99
116
  - 壁纸透明度直接作用于 `<div>` 元素;背景色透明度通过内联令牌覆盖实现。
@@ -117,13 +134,13 @@ dsh-any-background/
117
134
 
118
135
  ### 透明度体系
119
136
 
120
- 三个独立的透明度层,各有独立滑块和 `localStorage` 键:
137
+ 三个独立的透明度层,各有独立滑块和一个持久化的配置字段:
121
138
 
122
- | 层 | localStorage 键 | 默认值 | 实现方式 |
123
- |----|-----------------|--------|---------|
124
- | 主界面 | `dsh-any-background:opacity` | 85% | `<body>` 上的内联 CSS 变量 |
125
- | 设置面板 | `dsh-any-background:settings-opacity` | 100% | `<html>` 上的 CSS 变量,通过 `[aria-modal]` 选择器定位 |
126
- | 壁纸 | `dsh-any-background:wallpaper-opacity` | 100% | 壁纸 `<div>` 上的 `style.opacity` |
139
+ | 层 | 配置字段 | 默认值 | 实现方式 |
140
+ |----|----------|--------|---------|
141
+ | 主界面 | `opacity` | 85% | `<body>` 上的内联 CSS 变量 |
142
+ | 设置面板 | `settingsOpacity` | 100% | `<html>` 上的 CSS 变量,通过 `[aria-modal]` 选择器定位 |
143
+ | 壁纸 | `wallpaperOpacity` | 100% | 壁纸 `<div>` 上的 `style.opacity` |
127
144
 
128
145
  #### 设置透明度
129
146
 
@@ -202,24 +219,33 @@ npx @deepseek-ai/dsh web
202
219
  git clone https://github.com/Tkingxiao/dsh-any-background.git
203
220
  cd dsh-any-background
204
221
 
205
- # 2. 安装构建工具
222
+ # 2. 安装构建工具(同时拉取 @deepseek-ai/dsh-home-paths 运行时依赖)
206
223
  pnpm install
207
224
 
208
225
  # 3. 重新构建 lib/
209
226
  pnpm run bundle
210
227
 
211
228
  # 4. 从本地安装到 web profile
212
- dsh plugin --profile web add -w .
229
+ # (`dsh plugin add` 是 `pnpm add <dir>` 的封装,传入本目录即可)
230
+ dsh plugin --profile web add .
213
231
 
214
232
  # 5. 启动
215
233
  dsh web
216
234
  ```
217
235
 
236
+ ## 兼容性
237
+
238
+ 插件同时支持 **Web 界面** 和 **桌面客户端**:
239
+
240
+ - **[`dsh web`](https://github.com/deepseek-ai/deepseek-harness)** — Web profile,完整支持。
241
+ - **[deepseek-harness-desktop](https://github.com/anywhere-labs/deepseek-harness-desktop)** — 已支持,但存在一个已知的 Electron 打包问题:**左侧边栏与主区域的透明度相反**(侧边栏看起来比主区域更透明,反之亦然)。这是客户端侧打包问题,并非插件缺陷 —— 我们正在等待桌面客户端更新修复。
242
+
218
243
  ## 依赖
219
244
 
220
245
  | 包名 | 用途 |
221
246
  |------|------|
222
247
  | `@deepseek-ai/cordis` | 插件框架(Cordis) |
248
+ | `@deepseek-ai/dsh-home-paths` | 解析 DSH 数据主目录,用于持久化存储 |
223
249
  | `@deepseek-ai/dsh-client-runtime` | 客户端运行时 + `defineStore` |
224
250
  | `@deepseek-ai/dsh-client-locale` | 国际化(中/英) |
225
251
  | `@deepseek-ai/dsh-client-ui-theme` | 主题服务(register/setTheme/overrideTokens) |
package/lib/client.js CHANGED
@@ -12,16 +12,10 @@ window.__ModuleLoader__.load({
12
12
  const inject = [
13
13
  "slots",
14
14
  "locale",
15
- "theme"
15
+ "theme",
16
+ "connection"
16
17
  ];
17
18
  const NS = "settings.anyBg";
18
- const LS_COLOR = "dsh-any-background:color";
19
- const LS_WP = "dsh-any-background:wallpaper";
20
- const LS_OP = "dsh-any-background:opacity";
21
- const LS_WOP = "dsh-any-background:wallpaper-opacity";
22
- const LS_BL = "dsh-any-background:blur";
23
- const LS_BG = "dsh-any-background:bgState";
24
- const LS_SOP = "dsh-any-background:settings-opacity";
25
19
  const DEF_OP = .85;
26
20
  const DEF_BL = 0;
27
21
  const DEF_SOP = 1;
@@ -72,72 +66,105 @@ window.__ModuleLoader__.load({
72
66
  editorCancel: "Cancel",
73
67
  editorReset: "Reset"
74
68
  };
75
- function rLS(k) {
76
- try {
77
- const v = localStorage.getItem(k);
78
- return typeof v === "string" ? v : null;
79
- } catch {
80
- return null;
69
+ const DEFAULT_CONFIG = {
70
+ color: null,
71
+ opacity: .85,
72
+ settingsOpacity: 1,
73
+ wallpaperOpacity: 1,
74
+ blur: 0,
75
+ bgState: {
76
+ zoom: 1,
77
+ x: 0,
78
+ y: 0,
79
+ iw: 0,
80
+ ih: 0
81
81
  }
82
- }
83
- function wLS(k, v) {
82
+ };
83
+ let cfg = {
84
+ ...DEFAULT_CONFIG,
85
+ bgState: { ...DEFAULT_CONFIG.bgState }
86
+ };
87
+ let wpUrl = null;
88
+ let rpcCallFn = null;
89
+ const RPC_CHANNEL = "/api";
90
+ const RPC_NS = "dshAnyBackground";
91
+ const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
92
+ async function rpcCall(method, payload) {
93
+ if (!rpcCallFn) return void 0;
84
94
  try {
85
- v === null ? localStorage.removeItem(k) : localStorage.setItem(k, v);
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;
86
99
  } catch (e) {
87
- console.warn(`dsh-any-background: localStorage write failed for "${k}"`, e);
100
+ console.warn(`dsh-any-background: rpc "${method}" threw`, e);
101
+ return;
88
102
  }
89
103
  }
104
+ /** Move a possibly-absent partial config into the shape the UI reads. */
105
+ function adoptConfig(raw) {
106
+ const c = raw ?? {};
107
+ const color = Array.isArray(c.color) && c.color.length === 3 ? [
108
+ c.color[0],
109
+ c.color[1],
110
+ c.color[2]
111
+ ] : null;
112
+ const bg = c.bgState ?? {};
113
+ cfg = {
114
+ color,
115
+ opacity: typeof c.opacity === "number" ? c.opacity : DEFAULT_CONFIG.opacity,
116
+ settingsOpacity: typeof c.settingsOpacity === "number" ? c.settingsOpacity : DEFAULT_CONFIG.settingsOpacity,
117
+ wallpaperOpacity: typeof c.wallpaperOpacity === "number" ? c.wallpaperOpacity : DEFAULT_CONFIG.wallpaperOpacity,
118
+ blur: typeof c.blur === "number" ? c.blur : DEFAULT_CONFIG.blur,
119
+ bgState: {
120
+ zoom: typeof bg.zoom === "number" ? bg.zoom : 1,
121
+ x: typeof bg.x === "number" ? bg.x : 0,
122
+ y: typeof bg.y === "number" ? bg.y : 0,
123
+ iw: typeof bg.iw === "number" && bg.iw > 0 ? bg.iw : 0,
124
+ ih: typeof bg.ih === "number" && bg.ih > 0 ? bg.ih : 0
125
+ }
126
+ };
127
+ }
128
+ /** Persist the current in-memory config to the node half (fire-and-forget). */
129
+ function saveConfig() {
130
+ rpcCall("writeConfig", { config: cfg });
131
+ }
132
+ /** Load the persisted theme (config + wallpaper) from the node half. */
133
+ async function loadPersisted() {
134
+ 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;
139
+ }
140
+ }
141
+ function rHasColor() {
142
+ return cfg.color !== null;
143
+ }
90
144
  function rColor() {
91
- try {
92
- const v = JSON.parse(rLS(LS_COLOR) || "");
93
- if (Array.isArray(v) && v.length === 3) return v;
94
- } catch {}
95
- return [
145
+ return cfg.color ?? [
96
146
  220,
97
147
  .55,
98
148
  .25
99
149
  ];
100
150
  }
101
151
  function rWp() {
102
- const v = rLS(LS_WP);
103
- return v?.length ? v : null;
152
+ return wpUrl;
104
153
  }
105
154
  function rOp() {
106
- const v = rLS(LS_OP);
107
- if (!v) return DEF_OP;
108
- const n = +v;
109
- return isFinite(n) ? Math.min(1, Math.max(0, n)) : DEF_OP;
155
+ return typeof cfg.opacity === "number" ? Math.min(1, Math.max(0, cfg.opacity)) : DEF_OP;
110
156
  }
111
157
  function rWop() {
112
- const v = rLS(LS_WOP);
113
- if (!v) return 1;
114
- const n = +v;
115
- return isFinite(n) ? Math.min(1, Math.max(0, n)) : 1;
158
+ return typeof cfg.wallpaperOpacity === "number" ? Math.min(1, Math.max(0, cfg.wallpaperOpacity)) : 1;
116
159
  }
117
160
  function rBl() {
118
- const v = rLS(LS_BL);
119
- if (!v) return DEF_BL;
120
- const n = +v;
121
- return isFinite(n) ? Math.min(60, Math.max(0, n)) : DEF_BL;
161
+ return typeof cfg.blur === "number" ? Math.min(60, Math.max(0, cfg.blur)) : DEF_BL;
122
162
  }
123
163
  function rSop() {
124
- const v = rLS(LS_SOP);
125
- if (!v) return DEF_SOP;
126
- const n = +v;
127
- return isFinite(n) ? Math.min(1, Math.max(0, n)) : DEF_SOP;
164
+ return typeof cfg.settingsOpacity === "number" ? Math.min(1, Math.max(0, cfg.settingsOpacity)) : DEF_SOP;
128
165
  }
129
166
  function rBgState() {
130
- try {
131
- const v = JSON.parse(rLS(LS_BG) || "");
132
- if (v && typeof v.zoom === "number" && typeof v.iw === "number" && v.iw > 0 && v.ih > 0) return v;
133
- } catch {}
134
- return {
135
- zoom: 1,
136
- x: 0,
137
- y: 0,
138
- iw: 0,
139
- ih: 0
140
- };
167
+ return cfg.bgState;
141
168
  }
142
169
  function hsvToHsl(h, s, v) {
143
170
  const l = v * (1 - s / 2);
@@ -357,7 +384,7 @@ window.__ModuleLoader__.load({
357
384
  c.fillStyle = gv;
358
385
  c.fillRect(gx, gy, sz, sz);
359
386
  const hRad = (hue - 90) * Math.PI / 180;
360
- const hR = 188 / 2;
387
+ const hR = 94;
361
388
  const hmx = CX + Math.cos(hRad) * hR, hmy = CX + Math.sin(hRad) * hR;
362
389
  c.beginPath();
363
390
  c.arc(hmx, hmy, 8, 0, Math.PI * 2);
@@ -388,7 +415,7 @@ window.__ModuleLoader__.load({
388
415
  if (Math.abs(x - CX) <= SQ_HALF && Math.abs(y - CX) <= SQ_HALF) return "square";
389
416
  const dx = x - CX, dy = y - CX;
390
417
  const dist = Math.sqrt(dx * dx + dy * dy);
391
- if (dist >= RING_INNER - 4 && dist <= 110) return "ring";
418
+ if (dist >= 78 && dist <= 110) return "ring";
392
419
  return null;
393
420
  }
394
421
  function pickHue(x, y) {
@@ -446,7 +473,7 @@ window.__ModuleLoader__.load({
446
473
  const mx = ev.clientX - rr.left, my = ev.clientY - rr.top;
447
474
  if (region === "ring") {
448
475
  const d = Math.sqrt((mx - CX) ** 2 + (my - CX) ** 2);
449
- if (d >= RING_INNER - 10 && d <= 116) apply(pickHue(mx, my), colRef.current.sat, colRef.current.lit);
476
+ if (d >= 72 && d <= 116) apply(pickHue(mx, my), colRef.current.sat, colRef.current.lit);
450
477
  } else {
451
478
  const [s, l] = pickSL(mx, my);
452
479
  apply(colRef.current.hue, s, l);
@@ -913,8 +940,9 @@ window.__ModuleLoader__.load({
913
940
  fmt: (v) => `${v}%`,
914
941
  onInput: (v) => {
915
942
  const op = v / 100;
916
- wLS(LS_OP, String(op));
943
+ cfg.opacity = op;
917
944
  applyCustomTokens(op);
945
+ saveConfig();
918
946
  },
919
947
  onChange: (v) => setOp(v / 100)
920
948
  })
@@ -938,8 +966,9 @@ window.__ModuleLoader__.load({
938
966
  fmt: (v) => `${v}%`,
939
967
  onInput: (v) => {
940
968
  const op = v / 100;
941
- wLS(LS_SOP, String(op));
969
+ cfg.settingsOpacity = op;
942
970
  applySettingsOverrides(op);
971
+ saveConfig();
943
972
  },
944
973
  onChange: (v) => setSop(v / 100)
945
974
  })
@@ -1019,8 +1048,9 @@ window.__ModuleLoader__.load({
1019
1048
  fmt: (v) => `${v}%`,
1020
1049
  onInput: (v) => {
1021
1050
  const op = v / 100;
1022
- wLS(LS_WOP, String(op));
1051
+ cfg.wallpaperOpacity = op;
1023
1052
  if (wpEl) wpEl.style.opacity = String(op);
1053
+ saveConfig();
1024
1054
  },
1025
1055
  onChange: (v) => setWop(v / 100)
1026
1056
  })]
@@ -1036,8 +1066,9 @@ window.__ModuleLoader__.load({
1036
1066
  def: rBl(),
1037
1067
  fmt: (v) => `${v}px`,
1038
1068
  onInput: (v) => {
1039
- wLS(LS_BL, String(v));
1069
+ cfg.blur = v;
1040
1070
  if (wpEl) wpEl.style.filter = v > 0 ? `blur(${v}px)` : "none";
1071
+ saveConfig();
1041
1072
  },
1042
1073
  onChange: (v) => setBl(v)
1043
1074
  })]
@@ -1053,14 +1084,15 @@ window.__ModuleLoader__.load({
1053
1084
  t,
1054
1085
  onClose: () => setEditorOpen(false),
1055
1086
  onCommit: (z, x, y, iw, ih) => {
1056
- wLS(LS_BG, JSON.stringify({
1087
+ cfg.bgState = {
1057
1088
  zoom: z,
1058
1089
  x,
1059
1090
  y,
1060
1091
  iw,
1061
1092
  ih
1062
- }));
1093
+ };
1063
1094
  applyWp(ctxRef);
1095
+ saveConfig();
1064
1096
  setEditorOpen(false);
1065
1097
  }
1066
1098
  }) : null
@@ -1069,6 +1101,7 @@ window.__ModuleLoader__.load({
1069
1101
  }
1070
1102
  function apply(ctx) {
1071
1103
  ctxRef = ctx;
1104
+ rpcCallFn = (endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res);
1072
1105
  const [initH, initS, initL] = rColor();
1073
1106
  let customDispose = null;
1074
1107
  const registerCustom = (h, s, l) => {
@@ -1085,7 +1118,7 @@ window.__ModuleLoader__.load({
1085
1118
  }
1086
1119
  ctx.theme.setTheme(CUSTOM_ID);
1087
1120
  };
1088
- if (rLS(LS_COLOR)) registerCustom(initH, initS, initL);
1121
+ if (rHasColor()) registerCustom(initH, initS, initL);
1089
1122
  ctx.effect(() => () => {
1090
1123
  customDispose?.();
1091
1124
  }, "dsh-any-background: skin dispose");
@@ -1118,11 +1151,15 @@ window.__ModuleLoader__.load({
1118
1151
  };
1119
1152
  applyWp(ctx);
1120
1153
  syncBg();
1154
+ loadPersisted().then(() => {
1155
+ applyWp(ctx);
1156
+ syncBg();
1157
+ });
1121
1158
  ctx.effect(() => () => {
1122
1159
  teardownWp();
1123
1160
  }, "dsh-any-background: wp cleanup");
1124
1161
  ctx.effect(() => ctx.on("theme/change", () => {
1125
- if (rLS(LS_COLOR)) {
1162
+ if (rHasColor()) {
1126
1163
  const snapshot = ctx.theme.getTheme();
1127
1164
  if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
1128
1165
  }
@@ -1164,38 +1201,44 @@ window.__ModuleLoader__.load({
1164
1201
  lit: dv,
1165
1202
  setColor: (nh, ns, nl) => {
1166
1203
  const [sh, ss, sl] = hsvToHsl(nh, ns, nl);
1167
- wLS(LS_COLOR, JSON.stringify([
1204
+ cfg.color = [
1168
1205
  sh,
1169
1206
  ss,
1170
1207
  sl
1171
- ]));
1208
+ ];
1172
1209
  registerCustom(sh, ss, sl);
1173
1210
  applyWp(ctx);
1211
+ saveConfig();
1174
1212
  },
1175
1213
  setWp: (u) => {
1176
- wLS(LS_WP, u);
1177
- wLS(LS_BG, null);
1214
+ wpUrl = u;
1215
+ cfg.bgState = { ...DEFAULT_CONFIG.bgState };
1178
1216
  applyWp(ctx);
1179
1217
  syncBg();
1218
+ rpcCall("setWallpaper", { dataUrl: u });
1180
1219
  },
1181
1220
  setOp: (v) => {
1182
- wLS(LS_OP, String(v));
1221
+ cfg.opacity = v;
1183
1222
  applyWp(ctx);
1184
1223
  syncBg();
1224
+ saveConfig();
1185
1225
  },
1186
1226
  setWop: (v) => {
1187
- wLS(LS_WOP, String(v));
1227
+ cfg.wallpaperOpacity = v;
1188
1228
  applyWp(ctx);
1189
1229
  syncBg();
1230
+ saveConfig();
1190
1231
  },
1191
1232
  setBl: (v) => {
1192
- wLS(LS_BL, String(v));
1233
+ cfg.blur = v;
1193
1234
  applyWp(ctx);
1194
1235
  syncBg();
1236
+ saveConfig();
1195
1237
  },
1196
1238
  setSop: (v) => {
1197
- wLS(LS_SOP, String(v));
1239
+ cfg.settingsOpacity = v;
1198
1240
  applySettingsOverrides(v);
1241
+ saveConfig();
1199
1242
  }
1200
1243
  };
1201
1244
  };
@@ -1209,7 +1252,7 @@ window.__ModuleLoader__.load({
1209
1252
  inject: sectionInject
1210
1253
  }, ThemeSection));
1211
1254
  const restoreSaved = () => {
1212
- if (rLS(LS_COLOR)) {
1255
+ if (rHasColor()) {
1213
1256
  const [h, s, l] = rColor();
1214
1257
  registerCustom(h, s, l);
1215
1258
  }
@@ -1220,7 +1263,7 @@ window.__ModuleLoader__.load({
1220
1263
  restoreTimers.forEach((id) => window.clearTimeout(id));
1221
1264
  }, "dsh-any-background: boot restore");
1222
1265
  const watchdogId = window.setInterval(() => {
1223
- if (!rLS(LS_COLOR)) return;
1266
+ if (!rHasColor()) return;
1224
1267
  const snapshot = ctx.theme.getTheme();
1225
1268
  let changed = false;
1226
1269
  if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) {