dsh-wx-skin 0.2.0 → 0.2.1
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 +36 -14
- package/lib/client.js +45 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +23 -6
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +8 -7
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,26 +53,26 @@
|
|
|
53
53
|
|
|
54
54
|
## 📦 安装
|
|
55
55
|
|
|
56
|
-
前置条件:已安装 [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
56
|
+
前置条件:已安装 [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness),并已初始化目标 profile(Web 用 `web`,官方桌面端用应用自带的 `desktop`)。
|
|
57
57
|
|
|
58
58
|
### 方式一:从 npm 安装(推荐)
|
|
59
59
|
|
|
60
60
|
插件已发布到 npm,一条命令装齐(在 **DSH 源码 checkout 目录**执行):
|
|
61
61
|
|
|
62
62
|
```sh
|
|
63
|
-
pnpm dsh plugin --profile web add dsh-wx-skin@0.2.
|
|
63
|
+
pnpm dsh plugin --profile web add dsh-wx-skin@0.2.1
|
|
64
64
|
# 若 `dsh` 已加入 PATH,也可直接:
|
|
65
|
-
dsh plugin --profile web add dsh-wx-skin@0.2.
|
|
65
|
+
dsh plugin --profile web add dsh-wx-skin@0.2.1
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
### 方式二:使用发布包(tarball)
|
|
69
69
|
|
|
70
|
-
下载 `dsh-wx-skin-0.2.
|
|
70
|
+
下载 `dsh-wx-skin-0.2.1.tgz`(或通过 GitHub Releases 获取):
|
|
71
71
|
|
|
72
72
|
```sh
|
|
73
73
|
pnpm dsh plugin --profile web add file:<tgz 的绝对路径>
|
|
74
74
|
# 例如:
|
|
75
|
-
pnpm dsh plugin --profile web add file:C:/Users/you/Downloads/dsh-wx-skin-0.2.
|
|
75
|
+
pnpm dsh plugin --profile web add file:C:/Users/you/Downloads/dsh-wx-skin-0.2.1.tgz
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
### 方式三:克隆源码构建(开发者)
|
|
@@ -87,6 +87,24 @@ npm run build
|
|
|
87
87
|
pnpm dsh plugin --profile web add link:<本目录绝对路径>
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
### 方式四:官方桌面端(Electron Desktop,`apps/desktop`)
|
|
91
|
+
|
|
92
|
+
桌面端拥有自己的 profile `$DSH_HOME/profiles/desktop`(与 web 的 `profiles/web` **互不共享可执行包**),并且官方约定 **CLI 不修改该 profile**——请在应用内安装:
|
|
93
|
+
|
|
94
|
+
1. 打开桌面端 → **设置 → 插件 → 安装**;
|
|
95
|
+
2. 在输入框粘贴本插件的**绝对路径**(安装向导里的「本地插件目录」形态),例如:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
E:/code/work/wxDshPlugin/dsh-wx-skin
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
> `link:` / `file:` 前缀与裸绝对路径都被接受(`parseInstallSpec` 把它们读成 path 形态);npm 包名 `dsh-wx-skin@0.2.1` 与 tarball 路径同样可用。
|
|
102
|
+
3. 从开发菜单执行「**重启应用与 Host**」(或退出并重新打开应用)——宿主半区的路由与 client bundle 的清单都在启动时确定。
|
|
103
|
+
|
|
104
|
+
安装后与 web 端一样:**设置 → 皮肤**。桌面端与 web 端共用同一份 `$DSH_HOME/dsh-wx-skin.settings.json`,所以两端看到的是同一张壁纸与同一个文件夹队列。
|
|
105
|
+
|
|
106
|
+
> 桌面端**不需要**额外的构建步骤:它加载的正是 web 客户端,插件 bundle 由桌面端自己的 Host(默认 `127.0.0.1:19387`)提供。
|
|
107
|
+
|
|
90
108
|
### 完成安装后
|
|
91
109
|
|
|
92
110
|
**重启 `dsh web`**,刷新 `http://127.0.0.1:3080`,打开 **设置 → 皮肤** 即可使用(侧栏底部另有「上一张 / 下一张」换图快捷键)。
|
|
@@ -134,16 +152,16 @@ pnpm dsh plugin --profile web remove dsh-wx-skin
|
|
|
134
152
|
- **生命周期**:`src/client/index.ts` 的 `apply(ctx)` 用 `ctx.effect(() => …)` 建立控制器并在其中注册两个插槽。harness 的客户端热重载(client-hmr)与插件行停用都是“先卸载旧 fiber(跑 disposer)、删掉该插件拥有的 `<style data-plugin>`,再重新 apply”,因此必须把清理交给 fiber:否则每次重载都会多出一份设置页与一组快捷键。挂载失败时记录日志并 `teardownSkinDom()` 清理半成品,绝不外抛。
|
|
135
153
|
- **作用域可选依赖**:`slots` 与 `uiWorkspace` 都用 `ctx.inject([...], scope => …)` 的**子作用域**形式声明,而不是插件的 `inject`——`uiWorkspace` 要等客户端↔宿主连接就绪才 provide(比 `apply` 晚),而 `inject: ['slots']` 会让整个浏览器半区(含背景层与持久化)一起等待,并多一个启动审计失败面。`settings.section` / `sidebar.footer.action` 的**声明**到达时才建立注册,ui-settings / ui-sidebar 热重载(声明 epoch 变化)会自动反向清理并按新声明重建。
|
|
136
154
|
- **背景层**:注入全屏 `div[data-wx-skin-layer]`(`position: fixed; z-index: 0; pointer-events: none`),并将应用根 `#root` 抬到 `z-index: 1`。⚠️ 实测 **`z-index: -1` 的 fixed 图层在 DSH shell 中不绘制**(落在 canvas 背景之下),这是早期版本"能选图但背景不显示"的根因,故采用 `z-index: 0` + `#root` 抬升方案。铺图方式由「适合度」决定:applier 把模式投影成 `--wx-skin-bg-size` / `--wx-skin-bg-repeat` / `--wx-skin-bg-position` 三个变量,图层用它们取 `background-size/repeat/position`(默认值即「填充」)。
|
|
137
|
-
- **半透明表面**:以独立 `<style>` + `!important`
|
|
155
|
+
- **半透明表面**:以独立 `<style>` + `!important` 覆盖二十五个表面 token(`--dsw-alias-bg-*`、`--dsw-specific-*`、`--dsw-menu-surface-fill`、`--dsw-alias-markdown-*`、`--dsw-alias-file-diff-*-bg/-gutter`),明暗两套值;透明度由 `--wx-skin-surface` 变量统一控制(不依赖 `color-mix()`,任意现代浏览器可用)。其中 0.1.7 把 `--dsw-specific-menu` 改成字面量 rgba,0.1.7-rc 又把所有下拉/右键菜单/列表框迁到 `MenuSurface` 原语的新令牌 `--dsw-menu-surface-fill`(菜单内的吸顶分组标题仍读旧令牌,两个都覆盖才不会出现菜单头尾深浅不一)、并把改动文件的 diff 行从 8%/12% 的 `color-mix` 换成不透明字面量——这三批都被重新绑回滑块。该样式表带 `data-plugin="dsh-wx-skin"` 归属标记(与官方 CSS loader 一致):client-modules 会把未标记的 `<style>` 认领给下一个物化的插件,client-hmr 又按此属性删除插件拥有的样式,不标记就可能被别的插件连带删除。
|
|
138
156
|
- **图片管线**:canvas 解码 → 编码为 JPEG data URL;原始分辨率 ≤4096px 时保持原样,更大或超出浏览器存储容量时静默缩小;编码结果做有效性校验,异常自动降档重编——**永不因图片大小报错**。
|
|
139
|
-
- **宿主持久化**:`src/index.ts` 通过 `webServer` 服务注册 `/dsh-wx-skin/load` 与 `/dsh-wx-skin/save`
|
|
157
|
+
- **宿主持久化**:`src/index.ts` 通过 `webServer` 服务注册 `/dsh-wx-skin/load` 与 `/dsh-wx-skin/save` 两条本机路由(注册键保持根绝对),把设置原子写入 DSH home(`$DSH_HOME` 或 `~/.dsh`)下的 `dsh-wx-skin.settings.json`;桌面端每次启动端口变化、localStorage 清空时,客户端自动从宿主副本恢复——背景图片跨会话不丢失。浏览器侧只使用**文档相对**引用(`dsh-wx-skin/load`),经 shell 注入的 `<base href="./">` 解析到当前挂载,因此在带前缀剥离代理的部署下同样命中;官方桌面端加载 `dsh-app://app/`,相对引用同样落在文档根。
|
|
140
158
|
- **文件夹轮播**:
|
|
141
159
|
- `POST /dsh-wx-skin/folder` 读盘扫描(可选递归),只收 `png/jpg/jpeg/webp/gif/bmp`,跳过隐藏项与符号链接,按 `rel` 自然排序后返回路径列表;上限 2000 张 / 20000 项 / 深度 6,超限返回 `truncated`。客户端把列表、当前序号、本轮已用路径一起持久化(只存路径)。
|
|
142
160
|
- `GET /dsh-wx-skin/image?p=<绝对路径>&v=<mtime>` 逐个回传图片字节,**只服务上一次成功扫描的根(或设置文件里的 `folderPath`)之内的文件**:`realpath` 归一 + 分隔符前缀比较(Windows 忽略大小写)+ 扩展名白名单 + 必须是文件;`v` 用 mtime 做缓存令牌,文件改动即换 URL。跨站请求(`Sec-Fetch-Site: cross-site`)直接拒绝,响应带 `X-Content-Type-Options: nosniff`。
|
|
143
161
|
- 切换算法在 `src/client/skin-folder.ts`(纯函数、注入 rng):**顺序模式按目录顺序 +1 / -1 并两端回绕**(选到第 10 张就继续 11 / 回到 9),随机模式在**未切换集合**中等概率抽取;集合为空时才开启新一轮,并排除当前这张以避免立即重复。本轮记录只服务随机模式,按路径而非下标保存,文件夹内容变化时自动丢弃失效项。
|
|
144
162
|
- 同一文件里的 `matchFolderImageByName` / `focusFolderImage` 负责「按文件名把对话框选中的图接回文件夹」:唯一同名才定位,同名歧义直接放弃(宁可显示编码图,也不默默显示错的那张);定位会同时清掉不再使用的 data URL / URL,持久化里不会残留两套来源。
|
|
145
163
|
- 路径词汇在 `src/client/skin-path.ts`(纯字符串):判断 URL / 绝对路径、「本地文件」行的路径属于哪个文件夹(`parentDirectory`)、以及宿主规范化路径与用户输入是否为同一文件(`samePath`,Windows 忽略大小写与分隔符)。
|
|
146
|
-
-
|
|
164
|
+
- 「选择文件夹」按钮调用**载体自己的**目录选择器,由 `src/client/skin-picker.ts` 的桥在两种载体间择优:官方桌面端由 preload 提供的 `globalThis.__DSH_DIRECTORY_PICKER__`(绑定应用窗口的 Electron 对话框,页面脚本执行前就已就绪)、其余部署回落到 harness 客户端服务 `uiWorkspace.pickDirectory()`。后者由 `ui-workspace` 提供,而它自己 inject 了 `remote.directoryPicker` 等依赖,即**只有客户端↔宿主连接就绪后才会 provide**——比本插件的 `apply` 晚。因此插件用 `ctx.inject(['uiWorkspace'], scope => …)` 把该服务当作**作用域可选依赖**(不是插件的 `inject`,插件不会因此等待),并让桥**懒读取**两者:载体能力变化时通知设置页重渲染,按钮始终可见,都不可用时置灰并提示;缺失或不可用时仍可用手填路径。
|
|
147
165
|
- 设置页里的「下一张」在未加载文件夹时禁用;侧栏「下一张」快捷键同样置灰(`data-empty`)且点击不产生任何副作用。
|
|
148
166
|
- **不依赖 `ctx.theme` 服务**:皮肤完全独立于 DSH 主题系统,关闭时样式惰性、默认主题不受影响。
|
|
149
167
|
- **设置页配色**:设置页自带一套语义变量 `--wx-panel-*`(浅色默认 + `body[data-ds-dark-theme]` 覆盖),页面区**刻意不引用 `--dsw-alias-*`**:alias 令牌会随主题反转(暗色下 `--dsw-alias-label-primary` 近白、`--dsw-alias-border-l1` 近透明),而页内控件的底色调过,二者叠加就会出现"浅底浅字"。每个变量都带与官方 `--dsw-static-*` 令牌等值的字面量回退,并由 `tests/panel-theme.spec.ts` 校验两套调色板的文字对比度(≥4.5:1)与结构。
|
|
@@ -199,17 +217,21 @@ dsh-wx-skin/
|
|
|
199
217
|
|
|
200
218
|
## 📋 兼容性与注意事项
|
|
201
219
|
|
|
202
|
-
- **DSH 版本**:适配基准为 DSH `0.1.7-
|
|
203
|
-
- **客户端模块系统**:闭包工厂产物 `window.__ModuleLoader__.load({id, factory})`;平台模块表种子词与官方 `packages/client/web/src/platform.ts` 的 `PLATFORM_MODULES` 逐字一致(`react` / `react/jsx-runtime` / `react-dom` / `react-dom/client` / `@deepseek-ai/cordis` / `dsh-client-store` / `ui-slots` / `ui-primitives` / `ui-dockkit`);0.1.7 起 bundle 引用为文档相对(`plugins/??<id>/client.js&rev=<rev>`)、`rev` 由 bundle 文件元数据(mtime/ctime/size)生成,均由 shell 侧生成,插件不自行拼 URL。
|
|
204
|
-
- **插槽注册**:客户端服务名 `slots`;`ctx.slots.inject(key, cb)` 等待槽**声明**并把回调作为子 fiber effect 运行;`ctx.slots.register({name, id, order, label}, Component)` 的 list 槽按 `order` 升序渲染、**不额外包 DOM**。本插件使用两个官方加法页位:`settings.section`(`kind: 'list'`、`scope: 'root'`、`replaceRisk: 'none'`,owner props `{ close }`;内容列由 shell 提供内边距与滚动)与 `sidebar.footer.action`(同 kind/scope,owner props `{ wide }`;单次注册承载整组快捷键,该行的既有占用者会独占整行,见上文)。
|
|
220
|
+
- **DSH 版本**:适配基准为 DSH `0.1.7-rc.2`(checkout tag `dsh-v0.1.7-rc.2`,commit `477b4f4205`,2026-09-24;上一基准为 `0.1.7-alpha.2` / `00102833df`)。逐项核对过的契约:
|
|
221
|
+
- **客户端模块系统**:闭包工厂产物 `window.__ModuleLoader__.load({id, factory})`;平台模块表种子词与官方 `packages/client/web/src/platform.ts` 的 `PLATFORM_MODULES` 逐字一致(`react` / `react/jsx-runtime` / `react-dom` / `react-dom/client` / `@deepseek-ai/cordis` / `dsh-client-store` / `ui-slots` / `ui-primitives` / `ui-dockkit`);0.1.7 起 bundle 引用为文档相对(`plugins/??<id>/client.js&rev=<rev>`)、`rev` 由 bundle 文件元数据(mtime/ctime/size)生成,均由 shell 侧生成,插件不自行拼 URL。0.1.7-rc.2 未改动该契约(banner/footer/intro、CSS Modules 注入与 `data-plugin` 归属标记仍与 `packages/client/tsdown.client.ts` 一致)。
|
|
222
|
+
- **插槽注册**:客户端服务名 `slots`;`ctx.slots.inject(key, cb)` 等待槽**声明**并把回调作为子 fiber effect 运行;`ctx.slots.register({name, id, order, label}, Component)` 的 list 槽按 `order` 升序渲染、**不额外包 DOM**。本插件使用两个官方加法页位:`settings.section`(`kind: 'list'`、`scope: 'root'`、`replaceRisk: 'none'`,owner props `{ close }`;内容列由 shell 提供内边距与滚动)与 `sidebar.footer.action`(同 kind/scope,owner props `{ wide }`;单次注册承载整组快捷键,该行的既有占用者会独占整行,见上文)。两个席位的 kind/scope/owner props 在 0.1.7-rc.2 均未变化(该版本给 `SidebarRootInjected.hooks` 加了 `shortcuts`、给 `settings.launcher` 加了 `settingsOpen`/`settingsShortcut`,都只影响本插件未使用的 owner)。
|
|
205
223
|
- **宿主契约**:`webServer.register({kind:'exact', path, handler})`;cordis `ctx.effect` / `ctx.inject` / `ctx.logger`;`uiWorkspace.pickDirectory(): Promise<string | null>`(无 chooser 时 reject,设置页已吞异常)。
|
|
206
|
-
- **样式契约**:`body[data-ds-dark-theme]` 明暗开关、`#root`
|
|
207
|
-
- **路由约定**:目标为应用自有路由的浏览器引用一律文档相对(对齐官方 `verify-client-route-resolution`
|
|
224
|
+
- **样式契约**:`body[data-ds-dark-theme]` 明暗开关、`#root` 挂载根、本文覆盖的全部二十五个 `--dsw-*` 表面令牌(含 0.1.7 改为字面量 rgba 的 `--dsw-specific-menu`、0.1.7-rc 新增的 `--dsw-menu-surface-fill` 与四个 `--dsw-alias-file-diff-*`)。设置面板 0.1.7-rc 起改为 portal 到 `body`(与 `#root` 同级)并以 `z-index: 1000` 覆盖,插件抬升 `#root` 到 `z-index: 1` 因此不影响它;设面板仍以 `--dsw-alias-bg-layer-2` 作底色,skin 覆盖后照常透出。**不再依赖** `sidebarCol` / `logoRow` / `newSession` 等 shell 内部类名。
|
|
225
|
+
- **路由约定**:目标为应用自有路由的浏览器引用一律文档相对(对齐官方 `verify-client-route-resolution` 门禁,该门禁的受管前缀与规则在 0.1.7-rc.2 未变;`<base href="./">` 仍由 `dsh-host-frontend-static` 注入),`tests/route-resolution.spec.ts` 在本地执行同类静态扫描。`--dsw-alias-bg-document-preview` 有意不覆盖(仅文档/PDF 预览正文使用,阅读需要不透明底)。
|
|
226
|
+
- **官方桌面端(`apps/desktop`)**:同一个 web 客户端跑在 Electron 壳里,插件**不需要独立构建**,但有两处载体差异已被专门处理:
|
|
227
|
+
- **文档来源是 `dsh-app://app`**(`apps/desktop/src/main.ts` 的 `applicationUrl`),不是 `http://127.0.0.1:<port>`。壳把整篇文档交由 Electron 的 `protocol.handle` 转发给已认证的 Host,并在转发时**删掉** `origin` 与 `sec-fetch-site`(`apps/desktop/src/web-document.ts` 的 `forwardWebRequest`)。因此宿主守卫按「本地载体」放行:`dsh-app://app` 与回环 authority 同列白名单——`Origin` 对脚本是禁止修改的请求头,网页无法借用桌面端来源,放行它不会放宽这道「跨站浏览器」围栏。`tests/host-routes.spec.ts` 的 `desktop carrier` 用例固定了这两种请求形状。
|
|
228
|
+
- **目录选择器**:桌面端由渲染进程 preload 在页面脚本执行前装好 `globalThis.__DSH_DIRECTORY_PICKER__`(`apps/desktop/src/preload-app.ts`,IPC 到 `dsh-desktop:directory-pick`),打开的是**绑定应用窗口**的 Electron 文件夹对话框——这正是 harness 自己的原生目录流程所驱动的那个(`ui-directory-picker-native` 的浏览器半区同样优先用它)。插件因此优先走它,只有在没有它的 Web 部署里才回落到 `uiWorkspace.pickDirectory()`;否则桌面端会去开宿主侧的 OS 选择器(桌面 Host 绑定回环,自适应选择器会解析成 `native`,于是同一次点击会开出第二个不受窗口约束的对话框)。
|
|
229
|
+
- **不变量**:文档基址等价(桌面端窗口加载 `dsh-app://app/`,相对引用与 web 端的 `<base href="./">` 同解,`<base>` 由 `dsh-host-frontend-static` 单独注入、桌面端不经该路径);`localStorage` 可用(`registerSchemesAsPrivileged` 声明 `standard`+`secure`);`body[data-ds-dark-theme]`、`#root`、两个槽席位与 `webServer` 服务都与 web 端同源;`DSH_HOME` 未被桌面端改写,所以两端共用同一份设置文件。macOS 上桌面端新加的窗口拖拽行(`data-window-drag`)不影响皮肤:背景层是 `body > :not(#root)`,命中原有的 `no-drag` 规则,且 `pointer-events: none`。
|
|
208
230
|
- **生命周期约定**:插件必须通过 `ctx.effect` 归还副作用。client-hmr 重载与插件行停用都会先卸载旧 fiber,只有 fiber 上的 disposer 会执行;`dsh-wx-skin` 的两个插槽注册(React 组件由 shell 卸载)、背景层、样式表和文档属性都在这条路径上回收。
|
|
209
231
|
- **入口的降级**:设置页依赖 ui-settings 声明 `settings.section`,快捷键依赖 ui-sidebar 声明 `sidebar.footer.action`。若某个部署没有对应外壳(槽永不声明),插件**不会**因此等待或报错:背景、设置数据与持久化照常工作,只是那处入口不出现(这也是不用 `inject: ['slots']` 的原因)。侧栏折叠为图标轨时快捷键自动变成两个 28px 图标。
|
|
210
232
|
- **存储**:本地图片以 data URL 存于 `localStorage`(浏览器缓存/回退),并镜像到宿主副本 `~/.dsh/dsh-wx-skin.settings.json`(跨端口持久化);超大图会自动缩放至可持久化尺寸。文件夹轮播只持久化路径列表与序号,不占图片配额。
|
|
211
233
|
- **格式**:仅接受位图(PNG / JPEG / WebP / GIF / BMP);SVG 等矢量格式不支持(canvas 管线只处理位图,文件夹扫描也按白名单过滤,安全可预测)。
|
|
212
|
-
- **文件夹读取范围**:图片路由只服务「上一次成功扫描的根」或设置文件里 `folderPath`
|
|
234
|
+
- **文件夹读取范围**:图片路由只服务「上一次成功扫描的根」或设置文件里 `folderPath` 之内的位图文件,来源限于本机载体(Web 的回环 authority,或官方桌面端的 `dsh-app://app` 文档)+ 非跨站才放行,无法被用作任意文件读取;宿主进程与文件夹必须在同一台机器(`dsh web` 的 GUI 与目录同机)。
|
|
213
235
|
- **宿主半区变更需重启**:新增/修改宿主路由(`src/index.ts`、`src/folder.ts`)后必须重启 `dsh web`;只改 `src/client/*` 时 client-hmr 会热重载,不必重启。
|
|
214
236
|
- **依赖 `#root`**:皮肤通过将应用根 `#root` 抬升到 `z-index: 1` 使背景层位于应用之下,请确保 shell 的挂载根仍为 `#root`(DSH 默认如此)。
|
|
215
237
|
|
package/lib/client.js
CHANGED
|
@@ -1720,11 +1720,18 @@ html[data-wx-skin-active] body {
|
|
|
1720
1720
|
--dsw-alias-bg-module-platform: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;
|
|
1721
1721
|
--dsw-alias-bg-multi-select: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;
|
|
1722
1722
|
/* DSH 0.1.7 turned this from the bg-layer-3 alias (which this sheet already
|
|
1723
|
-
took over) into a literal rgba, so every
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1723
|
+
took over) into a literal rgba, so every overlay still reading it (hover
|
|
1724
|
+
cards, dock panels, selectors, sticky menu headers) stopped following the
|
|
1725
|
+
透出 slider. Re-bound to the same hue at the skin's own surface opacity; the
|
|
1726
|
+
!important also outranks the darwin near-opaque override, which 0.1.7-rc
|
|
1727
|
+
moved out of the shell's base.css into ui-theme's design-platform.css
|
|
1728
|
+
(unchanged 0.94 values). */
|
|
1727
1729
|
--dsw-specific-menu: rgb(248 249 250 / var(--wx-skin-surface, 0.72)) !important;
|
|
1730
|
+
/* 0.1.7-rc moved every shared dropdown / context menu / listbox onto the
|
|
1731
|
+
MenuSurface primitive, whose material layer paints with THIS token instead
|
|
1732
|
+
of the one above — while sticky menu headers kept the one above. Both are
|
|
1733
|
+
bound here, or a menu's header and body would drift apart under the skin. */
|
|
1734
|
+
--dsw-menu-surface-fill: rgb(248 249 250 / var(--wx-skin-surface, 0.72)) !important;
|
|
1728
1735
|
--dsw-specific-sidebar-fill: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;
|
|
1729
1736
|
--dsw-specific-input-major: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;
|
|
1730
1737
|
--dsw-specific-selector: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;
|
|
@@ -1737,6 +1744,13 @@ html[data-wx-skin-active] body {
|
|
|
1737
1744
|
--dsw-alias-markdown-placeholder: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;
|
|
1738
1745
|
--dsw-alias-markdown-tag: rgb(241 243 245 / var(--wx-skin-surface, 0.72)) !important;
|
|
1739
1746
|
--dsw-alias-markdown-citation: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;
|
|
1747
|
+
/* 0.1.7-rc replaced the changed-files diff rows' 8% color-mix with these opaque
|
|
1748
|
+
literals; re-bound so a diff over the wallpaper still shows it through. The
|
|
1749
|
+
'-marker' companions are foreground tints, not surfaces, and stay untouched. */
|
|
1750
|
+
--dsw-alias-file-diff-added-bg: rgb(230 244 231 / var(--wx-skin-surface, 0.72)) !important;
|
|
1751
|
+
--dsw-alias-file-diff-added-gutter: rgb(237 247 237 / var(--wx-skin-surface, 0.72)) !important;
|
|
1752
|
+
--dsw-alias-file-diff-deleted-bg: rgb(252 230 226 / var(--wx-skin-surface, 0.72)) !important;
|
|
1753
|
+
--dsw-alias-file-diff-deleted-gutter: rgb(253 236 233 / var(--wx-skin-surface, 0.72)) !important;
|
|
1740
1754
|
}
|
|
1741
1755
|
|
|
1742
1756
|
/* ---- translucent surfaces: dark palette ---- */
|
|
@@ -1749,6 +1763,7 @@ html[data-wx-skin-active] body[data-ds-dark-theme] {
|
|
|
1749
1763
|
--dsw-alias-bg-module-platform: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;
|
|
1750
1764
|
--dsw-alias-bg-multi-select: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;
|
|
1751
1765
|
--dsw-specific-menu: rgb(48 49 54 / var(--wx-skin-surface, 0.72)) !important;
|
|
1766
|
+
--dsw-menu-surface-fill: rgb(48 49 54 / var(--wx-skin-surface, 0.72)) !important;
|
|
1752
1767
|
--dsw-specific-sidebar-fill: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;
|
|
1753
1768
|
--dsw-specific-input-major: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;
|
|
1754
1769
|
--dsw-specific-selector: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;
|
|
@@ -1761,6 +1776,10 @@ html[data-wx-skin-active] body[data-ds-dark-theme] {
|
|
|
1761
1776
|
--dsw-alias-markdown-placeholder: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;
|
|
1762
1777
|
--dsw-alias-markdown-tag: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;
|
|
1763
1778
|
--dsw-alias-markdown-citation: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;
|
|
1779
|
+
--dsw-alias-file-diff-added-bg: rgb(31 49 36 / var(--wx-skin-surface, 0.72)) !important;
|
|
1780
|
+
--dsw-alias-file-diff-added-gutter: rgb(19 32 22 / var(--wx-skin-surface, 0.72)) !important;
|
|
1781
|
+
--dsw-alias-file-diff-deleted-bg: rgb(60 31 27 / var(--wx-skin-surface, 0.72)) !important;
|
|
1782
|
+
--dsw-alias-file-diff-deleted-gutter: rgb(40 19 14 / var(--wx-skin-surface, 0.72)) !important;
|
|
1764
1783
|
}
|
|
1765
1784
|
|
|
1766
1785
|
/* ---- sidebar entry active indicator ---- */
|
|
@@ -2020,14 +2039,32 @@ html[data-wx-skin-active] button[data-wx-skin-entry] {
|
|
|
2020
2039
|
//#endregion
|
|
2021
2040
|
//#region src/client/skin-picker.ts
|
|
2022
2041
|
/**
|
|
2023
|
-
*
|
|
2042
|
+
* Read the Desktop carrier's picker from the page globals.
|
|
2043
|
+
*
|
|
2044
|
+
* A browser deployment never installs the global, so this doubles as the
|
|
2045
|
+
* plugin's carrier test: present ⇒ the Desktop preload owns this document.
|
|
2046
|
+
* @returns the picker, or undefined when this document is not the Desktop app.
|
|
2047
|
+
*/
|
|
2048
|
+
function desktopDirectoryPicker() {
|
|
2049
|
+
const bridge = globalThis.__DSH_DIRECTORY_PICKER__;
|
|
2050
|
+
if (bridge === null || typeof bridge !== "object") return void 0;
|
|
2051
|
+
const pick = bridge.pick;
|
|
2052
|
+
if (typeof pick !== "function") return void 0;
|
|
2053
|
+
const bound = pick.bind(bridge);
|
|
2054
|
+
return { pick: () => bound() };
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Build a bridge over a live service lookup and a live Desktop-global lookup.
|
|
2024
2058
|
* @param current - reads the current `uiWorkspace` (undefined while absent).
|
|
2059
|
+
* @param desktop - reads the Desktop carrier's picker (defaults to the page global).
|
|
2025
2060
|
* @returns the availability check and the open action.
|
|
2026
2061
|
*/
|
|
2027
|
-
function createPickerBridge(current) {
|
|
2062
|
+
function createPickerBridge(current, desktop = desktopDirectoryPicker) {
|
|
2028
2063
|
return {
|
|
2029
|
-
available: () => typeof current()?.pickDirectory === "function",
|
|
2064
|
+
available: () => desktop() !== void 0 || typeof current()?.pickDirectory === "function",
|
|
2030
2065
|
async open() {
|
|
2066
|
+
const carrier = desktop();
|
|
2067
|
+
if (carrier !== void 0) return await carrier.pick();
|
|
2031
2068
|
const service = current();
|
|
2032
2069
|
if (typeof service?.pickDirectory !== "function") return null;
|
|
2033
2070
|
return await service.pickDirectory();
|
|
@@ -2052,7 +2089,7 @@ html[data-wx-skin-active] button[data-wx-skin-entry] {
|
|
|
2052
2089
|
}, "dsh-wx-skin: picker release");
|
|
2053
2090
|
});
|
|
2054
2091
|
const options = {
|
|
2055
|
-
picker: createPickerBridge(() => service),
|
|
2092
|
+
picker: createPickerBridge(() => service, desktopDirectoryPicker),
|
|
2056
2093
|
onPickerChange: (listener) => {
|
|
2057
2094
|
refresh = listener;
|
|
2058
2095
|
return () => {
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["css","css"],"sources":["../src/client/skin-path.ts","../src/client/skin-folder.ts","../src/client/skin-store.ts","../src/client/entries.tsx","../src/client/image-pipeline.ts","../src/client/skin-host.ts","../src/client/SkinPanel.tsx","../src/client/skin-settings.tsx","../src/client/global-skin-css.ts","../src/client/skin-dom.ts","../src/client/skin-controller.tsx","../src/client/skin-picker.ts","../src/client/index.ts"],"sourcesContent":["/**\n * dsh-wx-skin — local-path / URL vocabulary shared by the settings page and the\n * folder state machine. Pure string work, no DOM and no `node:path`: the value\n * travels between a Windows host and the page, so both separator styles must be\n * understood on either side.\n *\n * Why this exists: the skin now describes its background by PATH — the folder\n * row shows the folder, the local-file row shows the image's absolute path (or\n * its URL) — and a pasted file path has to be split into the folder the host may\n * scan.\n * @module dsh-wx-skin/client/skin-path\n */\n\n/** Whether the value is a remote image URL the browser can paint directly. */\nexport function isRemoteImageUrl(value: string): boolean {\n return /^https?:\\/\\/\\S+$/i.test(value.trim())\n}\n\n/**\n * Whether the value looks like an absolute local path: a Windows drive\n * (`C:\\…`, `C:/…`), a UNC share (`\\\\server\\share\\…`), or a POSIX root (`/…`).\n * Relative paths are rejected — the host resolves against its own process cwd,\n * which the user cannot see.\n */\nexport function isAbsolutePath(value: string): boolean {\n const path = value.trim()\n if (/^[A-Za-z]:[\\\\/]/.test(path)) return true\n if (path.startsWith('\\\\\\\\') || path.startsWith('//')) return true\n return path.startsWith('/')\n}\n\n/** Last segment of a path or URL, separators and query stripped. */\nexport function pathBasename(value: string): string {\n const withoutQuery = value.split(/[?#]/)[0] ?? value\n const segments = withoutQuery.split(/[/\\\\]/).filter(segment => segment !== '')\n return segments[segments.length - 1] ?? ''\n}\n\n/**\n * Parent directory of an absolute path, in the path's own style.\n *\n * `D:\\pics\\a\\b.png` → `D:\\pics\\a`; `/pics/b.png` → `/pics`; `C:\\b.png` →\n * `C:\\`; `\\\\server\\share\\b.png` → `\\\\server\\share`. Returns null when the value\n * is not an absolute path or has no parent to scan (a bare volume root).\n * @param value - absolute path.\n * @returns the parent directory, or null when there is none.\n */\nexport function parentDirectory(value: string): string | null {\n const path = value.trim().replace(/[/\\\\]+$/, '')\n if (!isAbsolutePath(path)) return null\n const separator = path.includes('\\\\') ? '\\\\' : '/'\n const cut = Math.max(path.lastIndexOf('\\\\'), path.lastIndexOf('/'))\n // A POSIX root (`/b.png`) has its separator at index 0; anything else without\n // a separator is not absolute.\n if (cut < 0 || (cut === 0 && !path.startsWith('/'))) return null\n const parent = path.slice(0, cut)\n if (parent === '') return separator\n // `D:\\b.png` → `D:\\`; an UNC share (`\\\\server\\share\\b.png`) needs no suffix.\n if (/^[A-Za-z]:$/.test(parent)) return `${parent}${separator}`\n return parent\n}\n\n/**\n * Whether two absolute paths name the same file. Windows paths compare\n * case-insensitively and tolerate either separator; the host returns\n * canonicalized paths, the user types whatever Explorer showed.\n * @param left - first path.\n * @param right - second path.\n * @returns true when they may be the same file.\n */\nexport function samePath(left: string, right: string): boolean {\n const normalize = (value: string): string => {\n const unified = value.trim().replace(/\\\\/g, '/').replace(/\\/+$/, '')\n return /^[A-Za-z]:/.test(unified) || unified.startsWith('//') ? unified.toLowerCase() : unified\n }\n return normalize(left) === normalize(right)\n}\n","/**\n * dsh-wx-skin — folder slideshow state machine (pure, DOM-free).\n *\n * The host scans a folder once and returns its images; everything after that is\n * decided here: the slideshow order, which image the next click shows, and the\n * \"already shown\" bookkeeping. The rule the UI promises is a pass over the\n * folder: an image that has been shown is never picked again until every image\n * has been shown, at which point the pass restarts — and the restart does not\n * immediately repeat the image on screen (unless it is the only one).\n *\n * `advanceFolder` takes its randomness through `rng` so tests are deterministic.\n * @module dsh-wx-skin/client/skin-folder\n */\nimport type { SkinFolderImage, SkinSettings } from '../core/types.ts'\nimport { samePath } from './skin-path.ts'\n\n/**\n * Browser reference to the host route serving one image file. Document-relative\n * for the same reason as the paths in `skin-host.ts`: the shell's\n * `<base href=\"./\">` resolves it under whatever mount served the page, and the\n * URL is consumed by `new Image().src`, `preloadNext`, and the\n * `--wx-skin-bg-image` inline custom property (inline-style URLs resolve\n * against the document base URL too).\n */\nexport const IMAGE_ROUTE = 'dsh-wx-skin/image'\n\n/** Upper bound on the cached list (mirrors the host scan bound). */\nexport const MAX_CACHED_IMAGES = 2000\n\n/** Natural-order comparator on the root-relative path (`img2` before `img10`). */\nconst collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })\n\n/**\n * Index of the cached image whose file name matches `name`, or -1.\n *\n * This is how a file chosen through the browser's file dialog joins the\n * slideshow: that dialog hands over the name and nothing else (no directory), so\n * when the loaded folder already holds exactly one image with that name, the\n * cursor can be placed on it — 下一张 then continues in folder order from the\n * file the user actually picked.\n *\n * An ambiguous name (the same file name in two scanned subfolders) returns -1:\n * guessing which one was picked would silently show the wrong image.\n * @param images - cached folder images.\n * @param name - file name to look for (case-insensitive).\n * @returns the index, or -1 when absent or ambiguous.\n */\nexport function matchFolderImageByName(images: readonly SkinFolderImage[], name: string): number {\n const wanted = name.trim().toLowerCase()\n if (wanted === '') return -1\n let found = -1\n for (const [index, image] of images.entries()) {\n if (image.name.toLowerCase() !== wanted) continue\n if (found >= 0) return -1 // ambiguous\n found = index\n }\n return found\n}\n\n/** Return a copy of `images` in slideshow order. */\nexport function sortFolderImages(images: readonly SkinFolderImage[]): SkinFolderImage[] {\n return [...images].sort((left, right) => collator.compare(left.rel, right.rel))\n}\n\n/** The host URL for one cached image; `v` invalidates the browser cache when the file changes. */\nexport function imageUrl(image: SkinFolderImage): string {\n return `${IMAGE_ROUTE}?p=${encodeURIComponent(image.path)}&v=${encodeURIComponent(String(image.mtimeMs))}`\n}\n\n/** Slideshow counters for the panel's status line. */\nexport interface FolderStatus {\n /** 1-based position of the shown image, 0 when none is shown. */\n position: number\n /** Total cached images. */\n total: number\n /** Images already shown in the current pass. */\n usedThisPass: number\n}\n\n/** Read the panel-facing counters out of the settings. */\nexport function folderStatus(settings: SkinSettings): FolderStatus {\n const total = settings.folderImages.length\n const used = new Set(settings.folderImages.map(image => image.path))\n return {\n position: settings.currentIndex >= 0 && settings.currentIndex < total ? settings.currentIndex + 1 : 0,\n total,\n usedThisPass: settings.usedPaths.filter(path => used.has(path)).length,\n }\n}\n\n/** Back-history bound: a long slideshow must not grow the persisted blob forever. */\nexport const MAX_HISTORY = 64\n\n/** Append the image being left behind, keeping only the most recent entries. */\nfunction pushHistory(history: readonly string[], outgoing: string | undefined): string[] {\n const next = outgoing === undefined ? [...history] : [...history, outgoing]\n return next.slice(-MAX_HISTORY)\n}\n\n/**\n * Settings showing one folder image. The other background kinds are cleared\n * with it, so a stale data URL or URL cannot linger in the persisted blob (and\n * a later 下一张 cannot leave the page claiming two sources at once).\n */\nfunction showingFolderImage(\n settings: SkinSettings,\n index: number,\n records: Pick<SkinSettings, 'usedPaths' | 'history'>,\n): SkinSettings {\n return {\n ...settings,\n enabled: true,\n source: 'folder',\n imageDataUrl: null,\n imageName: null,\n url: null,\n currentIndex: index,\n usedPaths: records.usedPaths,\n history: records.history,\n }\n}\n\n/**\n * Advance the slideshow by one image.\n *\n * SEQUENTIAL walks the folder in order — the image after the current one, then\n * the next, wrapping at the end — so picking image 10 makes 下一张 show 11 (and\n * 上一张 show 9). The pass record is not used here: a walk in index order never\n * repeats an image until it wraps, which is the whole point of the mode.\n *\n * RANDOM draws uniformly from the images not shown in this pass; when none\n * remain the pass restarts (the used set is cleared) excluding the image on\n * screen, so a full cycle never repeats back-to-back.\n *\n * With an empty folder the settings are returned unchanged apart from clearing\n * the cursor, so callers can treat \"index did not move\" as \"no folder loaded\".\n * @param settings - current settings.\n * @param rng - uniform [0,1) source for random mode (injected by tests).\n * @returns the next settings value.\n */\nexport function advanceFolder(settings: SkinSettings, rng: () => number = Math.random): SkinSettings {\n const images = settings.folderImages\n if (images.length === 0) return { ...settings, currentIndex: -1, usedPaths: [], history: [] }\n\n if (settings.orderMode !== 'random') {\n const index = settings.currentIndex < 0 ? 0 : (settings.currentIndex + 1) % images.length\n return showingFolderImage(settings, index, { usedPaths: [], history: [] })\n }\n\n const indexOfPath = new Map(images.map((image, index) => [image.path, index] as const))\n const used = new Set<number>()\n for (const path of settings.usedPaths) {\n const index = indexOfPath.get(path)\n if (index !== undefined) used.add(index)\n }\n\n let candidates = images.map((_, index) => index).filter(index => !used.has(index))\n let nextUsed = used\n if (candidates.length === 0) {\n // The pass is complete: clear the record, but never re-pick what is on screen.\n nextUsed = new Set<number>()\n candidates = images.map((_, index) => index)\n .filter(index => images.length === 1 || index !== settings.currentIndex)\n }\n\n const picked = candidates[Math.min(candidates.length - 1, Math.max(0, Math.floor(rng() * candidates.length)))]\n /* v8 ignore next -- candidates is non-empty: either image indices exist or the single-image pass is non-empty. */\n if (picked === undefined) return settings\n\n const usedThisPass = [...nextUsed, picked].sort((left, right) => left - right)\n return showingFolderImage(settings, picked, {\n usedPaths: usedThisPass.map(index => (images[index] as SkinFolderImage).path),\n // The image being left behind is where 上一张 goes back to.\n history: pushHistory(settings.history, images[settings.currentIndex]?.path),\n })\n}\n\n/**\n * Whether 「上一张」 has anywhere to go: sequential mode walks the folder order,\n * so any folder with more than one image has a previous one; random mode needs a\n * recorded earlier image.\n * @param settings - current settings.\n * @returns true when the previous-image action would move.\n */\nexport function canGoPrevious(settings: SkinSettings): boolean {\n if (settings.folderImages.length === 0) return false\n if (settings.orderMode !== 'random') return settings.folderImages.length > 1\n return settings.history.length > 0\n}\n\n/**\n * Go back one image.\n *\n * Sequential steps one index back through the folder order, wrapping from the\n * first to the last (picking 10 then 上一张 shows 9). Random pops the recorded\n * previous image; with no history it returns the settings unchanged.\n *\n * Going back never rewrites the pass record: everything in the history was\n * already shown.\n * @param settings - current settings.\n * @returns the previous settings value.\n */\nexport function previousFolder(settings: SkinSettings): SkinSettings {\n const images = settings.folderImages\n if (!canGoPrevious(settings)) return settings\n\n if (settings.orderMode !== 'random') {\n const from = settings.currentIndex >= 0 ? settings.currentIndex : 0\n const index = (from - 1 + images.length) % images.length\n return showingFolderImage(settings, index, { usedPaths: [], history: [] })\n }\n\n const history = [...settings.history]\n const recorded = history.pop()\n const recordedIndex = recorded === undefined ? -1 : images.findIndex(image => samePath(image.path, recorded))\n // Nothing shown yet reads as \"before the first image\", so the wrap lands on the last.\n const from = settings.currentIndex >= 0 ? settings.currentIndex : 0\n const index = recordedIndex >= 0 ? recordedIndex : (from - 1 + images.length) % images.length\n\n const shown = images[index] as SkinFolderImage\n const used = new Set(settings.usedPaths)\n used.add(shown.path)\n return showingFolderImage(settings, index, {\n usedPaths: images.filter(image => used.has(image.path)).map(image => image.path),\n history,\n })\n}\n\n/**\n * Stable signature of a cached list (path + size + mtime), so a rescan can tell\n * \"nothing changed\" from \"files were added, removed, or edited\" without writing\n * settings on every poll.\n * @param images - cached images.\n * @returns a comparable string.\n */\nexport function folderSignature(images: readonly SkinFolderImage[]): string {\n return images.map(image => `${image.path}\\u0000${image.size}\\u0000${image.mtimeMs}`).join('\\n')\n}\n\n/**\n * Merge a fresh scan of the SAME folder in without disturbing the slideshow.\n *\n * This is what an automatic rescan uses: the shown image is kept (matched by\n * path, or — when its file disappeared — the slot it occupied), and the pass\n * record and back-history keep only paths that still exist. Images added since\n * the last scan simply join the list, so 「下一张」 reaches them.\n * The active source is deliberately untouched: a rescan while a URL background\n * is on screen must not switch the background back to the folder.\n * @param settings - current settings.\n * @param images - the freshly scanned images.\n * @returns settings whose list and records track the folder.\n */\nexport function refreshFolderState(\n settings: SkinSettings,\n images: readonly SkinFolderImage[],\n): SkinSettings {\n const sorted = sortFolderImages(images).slice(0, MAX_CACHED_IMAGES)\n const kept = new Set(sorted.map(image => image.path))\n const currentPath = settings.folderImages[settings.currentIndex]?.path\n const matched = currentPath === undefined ? -1 : sorted.findIndex(image => image.path === currentPath)\n const shownIndex = settings.currentIndex >= 0 && sorted.length > 0\n ? Math.min(settings.currentIndex, sorted.length - 1)\n : -1\n return {\n ...settings,\n folderImages: sorted,\n currentIndex: matched >= 0 ? matched : sorted.length === 0 ? -1 : shownIndex,\n usedPaths: settings.usedPaths.filter(path => kept.has(path)),\n history: settings.history.filter(path => kept.has(path)),\n }\n}\n\n/**\n * Show one cached folder image, clearing the other background kinds. This is the\n * \"the user picked THIS image\" commit shared by the local-file row and the\n * file-dialog flow.\n * @param settings - current settings.\n * @param index - index into `settings.folderImages`.\n * @returns the settings showing that image, or the input when the index is stale.\n */\nexport function focusFolderImage(settings: SkinSettings, index: number): SkinSettings {\n if (index < 0 || index >= settings.folderImages.length) return settings\n return showingFolderImage(settings, index, { usedPaths: [], history: [] })\n}\n\n/**\n * Fold a scan into the settings when the user loads (or reloads) a folder.\n *\n * The same folder is MERGED, so adding or removing files never resets the\n * slideshow — new files simply join the list. A different folder starts fresh.\n * Either way the other background sources are cleared, so localStorage keeps\n * only the (small) path cache.\n *\n * `focusPath` is the \"open THIS image\" case behind the local-file row: a file\n * was named (typed, pasted, or derived from a folder), so the shown image is\n * that file — which also fixes where 下一张/上一张 walk from.\n * @param settings - current settings.\n * @param folderPath - the folder that was just scanned.\n * @param recursive - whether that scan descended into subdirectories.\n * @param images - the scanned images, already in slideshow order.\n * @param focusPath - absolute path of the image to show, when the caller named one.\n * @returns settings pointing at the folder, with one image picked when any exist.\n */\nexport function loadFolderState(\n settings: SkinSettings,\n folderPath: string,\n recursive: boolean,\n images: readonly SkinFolderImage[],\n focusPath?: string,\n): SkinSettings {\n const sorted = sortFolderImages(images).slice(0, MAX_CACHED_IMAGES)\n const activated: SkinSettings = {\n ...settings,\n enabled: true,\n source: 'folder',\n imageDataUrl: null,\n imageName: null,\n url: null,\n folderPath,\n folderRecursive: recursive,\n folderImages: sorted,\n }\n if (focusPath !== undefined) {\n const focused = sorted.findIndex(image => samePath(image.path, focusPath))\n if (focused >= 0) return { ...activated, currentIndex: focused, usedPaths: [], history: [] }\n }\n if (folderPath === settings.folderPath) {\n const merged = refreshFolderState(activated, sorted)\n // An explicit load of a folder that had no images yet still shows one.\n return merged.currentIndex >= 0 || merged.folderImages.length === 0 ? merged : advanceFolder(merged)\n }\n const reset: SkinSettings = { ...activated, currentIndex: -1, usedPaths: [], history: [] }\n return reset.folderImages.length === 0 ? reset : advanceFolder(reset)\n}\n","/**\n * dsh-wx-skin — pure skin settings state: defaults, sanitization, persistence\n * (localStorage), and the CSS-variable projection consumed by the DOM applier.\n * No DOM access here — tests exercise these functions with a fake storage.\n * @module dsh-wx-skin/client/skin-store\n */\nimport type { SkinFitMode, SkinFolderImage, SkinOrderMode, SkinSettings, SkinSource } from '../core/types.ts'\nimport { MAX_CACHED_IMAGES, imageUrl } from './skin-folder.ts'\n\n/** localStorage key holding the durable settings JSON. */\nexport const STORAGE_KEY = 'dsh-wx-skin.settings'\n\n/** Document attribute that gates the global skin CSS (set on documentElement). */\nexport const ACTIVE_ATTR = 'data-wx-skin-active'\n\n/** Attribute on the injected background layer div. */\nexport const LAYER_ATTR = 'data-wx-skin-layer'\n\n/** Attribute on the slot-rendered sidebar entry button (styling + test hook). */\nexport const ENTRY_ATTR = 'data-wx-skin-entry'\n\n/** CSS custom properties the applier writes on documentElement. */\nexport const SKIN_CSS_VARS = [\n '--wx-skin-bg-image',\n '--wx-skin-bg-size',\n '--wx-skin-bg-repeat',\n '--wx-skin-bg-position',\n '--wx-skin-scrim',\n '--wx-skin-blur',\n '--wx-skin-surface',\n] as const\n\n/** Clamp a number into [min, max]. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nconst SOURCES: readonly SkinSource[] = ['image', 'url', 'folder', 'none']\n\nconst ORDER_MODES: readonly SkinOrderMode[] = ['sequential', 'random']\n\n/** Background fit modes, in the order the settings page offers them. */\nexport const FIT_MODES: readonly SkinFitMode[] = ['fill', 'fit', 'stretch', 'tile', 'center']\n\n/** The three CSS longhands one fit mode projects onto the background layer. */\nexport interface FitProjection {\n /** `background-size`. */\n size: string\n /** `background-repeat`. */\n repeat: string\n /** `background-position`. */\n position: string\n}\n\n/**\n * Map a fit mode onto the background layer's CSS — the desktop wallpaper\n * vocabulary: fill crops, fit letterboxes, stretch may distort, tile repeats at\n * the image's natural size, and center shows one natural-size image in the\n * middle.\n * @param fit - the chosen mode.\n * @returns the longhand values for `--wx-skin-bg-*`.\n */\nexport function fitProjection(fit: SkinFitMode): FitProjection {\n if (fit === 'fit') return { size: 'contain', repeat: 'no-repeat', position: 'center' }\n if (fit === 'stretch') return { size: '100% 100%', repeat: 'no-repeat', position: 'center' }\n if (fit === 'tile') return { size: 'auto', repeat: 'repeat', position: 'left top' }\n if (fit === 'center') return { size: 'auto', repeat: 'no-repeat', position: 'center' }\n return { size: 'cover', repeat: 'no-repeat', position: 'center' }\n}\n\nconst MAX_DIM = 0.8\nconst MAX_BLUR = 24\n/** Surface opacity bounds (0..1 — fully adjustable; readability is handled by the dim slider). */\nexport const MIN_SURFACE = 0\nexport const MAX_SURFACE = 1\nexport const DEFAULT_SURFACE = 0.72\n\n/** Fresh defaults for a clean install. */\nexport const DEFAULT_SETTINGS: SkinSettings = Object.freeze({\n enabled: false,\n source: 'none',\n imageDataUrl: null,\n imageName: null,\n url: null,\n dim: 0.35,\n blur: 0,\n surface: DEFAULT_SURFACE,\n fit: 'fill',\n folderPath: null,\n folderImages: [],\n folderRecursive: false,\n orderMode: 'sequential',\n currentIndex: -1,\n usedPaths: [],\n history: [],\n})\n\n/**\n * Coerce an unknown persisted value into a valid folder image list: entries\n * must carry a usable path/name/rel and finite numbers, duplicates collapse on\n * the path (the slideshow identity), and the list is capped like the host scan.\n */\nexport function sanitizeFolderImages(raw: unknown): SkinFolderImage[] {\n if (!Array.isArray(raw)) return []\n const seen = new Set<string>()\n const images: SkinFolderImage[] = []\n for (const entry of raw) {\n if (typeof entry !== 'object' || entry === null) continue\n const record = entry as Record<string, unknown>\n const path = typeof record.path === 'string' && record.path !== '' ? record.path : null\n if (path === null || seen.has(path)) continue\n seen.add(path)\n images.push({\n path,\n name: typeof record.name === 'string' && record.name !== '' ? record.name : path,\n rel: typeof record.rel === 'string' && record.rel !== '' ? record.rel : path,\n size: typeof record.size === 'number' && Number.isFinite(record.size) ? record.size : 0,\n mtimeMs: typeof record.mtimeMs === 'number' && Number.isFinite(record.mtimeMs) ? record.mtimeMs : 0,\n })\n if (images.length >= MAX_CACHED_IMAGES) break\n }\n return images\n}\n\n/** Coerce an unknown persisted value into a valid SkinSettings. */\nexport function sanitizeSettings(raw: unknown): SkinSettings {\n const o = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>\n const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null)\n const num = (v: unknown, fallback: number): number => {\n const n = typeof v === 'number' && Number.isFinite(v) ? v : fallback\n return n\n }\n const source: SkinSource = SOURCES.includes(o.source as SkinSource) ? (o.source as SkinSource) : 'none'\n const folderImages = sanitizeFolderImages(o.folderImages)\n const indexed = typeof o.currentIndex === 'number' && Number.isFinite(o.currentIndex) ? Math.trunc(o.currentIndex) : -1\n const keptPaths = new Set(folderImages.map(image => image.path))\n return {\n enabled: o.enabled === true,\n source,\n imageDataUrl: str(o.imageDataUrl),\n imageName: str(o.imageName),\n url: str(o.url),\n dim: clamp(num(o.dim, DEFAULT_SETTINGS.dim), 0, MAX_DIM),\n blur: clamp(num(o.blur, DEFAULT_SETTINGS.blur), 0, MAX_BLUR),\n surface: clamp(num(o.surface, DEFAULT_SETTINGS.surface), MIN_SURFACE, MAX_SURFACE),\n fit: FIT_MODES.includes(o.fit as SkinFitMode) ? (o.fit as SkinFitMode) : DEFAULT_SETTINGS.fit,\n folderPath: str(o.folderPath),\n folderImages,\n folderRecursive: o.folderRecursive === true,\n orderMode: ORDER_MODES.includes(o.orderMode as SkinOrderMode) ? (o.orderMode as SkinOrderMode) : 'sequential',\n // A stale index (or one pointing past a shrunk list) reads as \"nothing shown\".\n currentIndex: indexed >= 0 && indexed < folderImages.length ? indexed : -1,\n // Only paths that still exist in the cached list count as already shown.\n usedPaths: Array.isArray(o.usedPaths)\n ? o.usedPaths.filter((path): path is string => typeof path === 'string' && keptPaths.has(path))\n : [],\n // Only paths still present in the cached list can be gone back to.\n history: Array.isArray(o.history)\n ? o.history.filter((path): path is string => typeof path === 'string' && keptPaths.has(path))\n : [],\n }\n}\n\n/** Read + sanitize the persisted settings, or the defaults. */\nexport function loadSettings(storage: Pick<Storage, 'getItem'> = globalThis.localStorage): SkinSettings {\n try {\n const text = storage.getItem(STORAGE_KEY)\n if (text === null) return { ...DEFAULT_SETTINGS }\n return sanitizeSettings(JSON.parse(text))\n } catch {\n return { ...DEFAULT_SETTINGS }\n }\n}\n\n/** Persist settings; quota errors are swallowed (the caller surfaces a message). */\nexport function saveSettings(settings: SkinSettings, storage: Pick<Storage, 'setItem'> = globalThis.localStorage): boolean {\n try {\n storage.setItem(STORAGE_KEY, JSON.stringify(settings))\n return true\n } catch {\n return false\n }\n}\n\n/** Remove the persisted settings (restores defaults on next boot). */\nexport function clearSettings(storage: Pick<Storage, 'removeItem'> = globalThis.localStorage): void {\n try {\n storage.removeItem(STORAGE_KEY)\n } catch {\n // Ignore.\n }\n}\n\n/** Whether the settings equal the pristine defaults (used to skip seeding the\n * host copy from localStorage on the first run after upgrade). */\nexport function isDefaultSettings(settings: SkinSettings): boolean {\n return settings.enabled === DEFAULT_SETTINGS.enabled\n && settings.source === DEFAULT_SETTINGS.source\n && settings.imageDataUrl === DEFAULT_SETTINGS.imageDataUrl\n && settings.imageName === DEFAULT_SETTINGS.imageName\n && settings.url === DEFAULT_SETTINGS.url\n && settings.dim === DEFAULT_SETTINGS.dim\n && settings.blur === DEFAULT_SETTINGS.blur\n && settings.surface === DEFAULT_SETTINGS.surface\n && settings.fit === DEFAULT_SETTINGS.fit\n && settings.folderPath === DEFAULT_SETTINGS.folderPath\n && settings.folderImages.length === 0\n && settings.folderRecursive === DEFAULT_SETTINGS.folderRecursive\n && settings.orderMode === DEFAULT_SETTINGS.orderMode\n && settings.currentIndex === DEFAULT_SETTINGS.currentIndex\n && settings.usedPaths.length === 0\n && settings.history.length === 0\n}\n\n/**\n * The name of the image currently on screen: a short label for the panel plus\n * the full reference for its hover title.\n */\nexport interface CurrentImageLabel {\n /** Name shown in the panel (may be ellipsized by CSS). */\n label: string\n /** Full path / URL behind the hover title. */\n detail: string\n}\n\n/**\n * Last path segment of a URL, decoded. Never throws: a value that is not an\n * absolute URL falls back to splitting on `/`, `\\`, `?`, and `#` (a pasted\n * Windows path is a realistic mistake in the URL box), and a malformed\n * percent-escape is returned as written.\n */\nexport function urlBasename(url: string): string {\n const lastSegment = (value: string): string => {\n const path = value.split(/[?#]/)[0] ?? value\n const segments = path.split(/[/\\\\]/).filter(segment => segment !== '')\n return segments[segments.length - 1] ?? ''\n }\n let last = ''\n try {\n const parsed = new URL(url)\n last = lastSegment(parsed.pathname) === '' ? parsed.host : lastSegment(parsed.pathname)\n } catch {\n last = lastSegment(url)\n }\n if (last === '') return url\n try {\n return decodeURIComponent(last)\n } catch {\n return last\n }\n}\n\n/**\n * Resolve what the panel should call the current background.\n *\n * A disabled skin has nothing on screen, so it reports no name. Folder mode\n * uses the cached file name of the shown image — an index that no longer\n * matches the list reports nothing rather than the wrong name.\n * @param settings - current settings.\n * @returns the label/detail pair, or null when there is nothing to name.\n */\nexport function currentImageLabel(settings: SkinSettings): CurrentImageLabel | null {\n if (!settings.enabled) return null\n if (settings.source === 'folder') {\n const image = settings.folderImages[settings.currentIndex]\n if (image === undefined) return null\n return { label: image.name, detail: image.path }\n }\n if (settings.source === 'image') {\n // The data URL itself is useless as a tooltip; only the picked file name is.\n const name = settings.imageName ?? '本地图片'\n return { label: name, detail: name }\n }\n if (settings.source === 'url') {\n if (settings.url === null) return null\n return { label: urlBasename(settings.url), detail: settings.url }\n }\n return null\n}\n\n/**\n * Project the settings onto the CSS custom properties the skin layer reads.\n * Returns an empty map when the skin is disabled (the applier then retracts\n * every variable).\n */\nexport function cssVariables(settings: SkinSettings): Partial<Record<(typeof SKIN_CSS_VARS)[number], string>> {\n if (!settings.enabled) return {}\n const vars: Partial<Record<(typeof SKIN_CSS_VARS)[number], string>> = {}\n if (settings.source === 'image' && settings.imageDataUrl !== null) {\n vars['--wx-skin-bg-image'] = `url(\"${settings.imageDataUrl}\")`\n } else if (settings.source === 'url' && settings.url !== null) {\n vars['--wx-skin-bg-image'] = `url(\"${escapeCssUrl(settings.url)}\")`\n } else if (settings.source === 'folder') {\n // Served by the host from inside the picked folder; the URL changes with the\n // file's mtime, so the browser cache never shows a stale image.\n const image = settings.folderImages[settings.currentIndex]\n if (image !== undefined) vars['--wx-skin-bg-image'] = `url(\"${escapeCssUrl(imageUrl(image))}\")`\n }\n const projection = fitProjection(settings.fit)\n vars['--wx-skin-bg-size'] = projection.size\n vars['--wx-skin-bg-repeat'] = projection.repeat\n vars['--wx-skin-bg-position'] = projection.position\n vars['--wx-skin-scrim'] = `rgba(0, 0, 0, ${settings.dim.toFixed(3)})`\n vars['--wx-skin-blur'] = `${Math.round(settings.blur)}px`\n vars['--wx-skin-surface'] = settings.surface.toFixed(3)\n return vars\n}\n\n/** Escape quotes so a user-supplied URL cannot break out of `url(\"...\")`. */\nexport function escapeCssUrl(url: string): string {\n return url.replace(/[\"\\\\\\n\\r]/g, char => `\\\\${char}`)\n}\n","/**\n * dsh-wx-skin — the sidebar-foot slideshow actions, registered into the\n * harness's own additive slot instead of injecting DOM into the shell's sidebar.\n *\n * DSH declares `sidebar.footer.action` (`kind: 'list'`, `scope: 'root'`,\n * `replaceRisk: 'none'`) as the sanctioned seat for extra sidebar-foot actions:\n * the shell renders one row per registration, in ascending `order`, directly\n * inside its `.footerActions` flex row above Settings, and hands each occupant\n * the live column state as `{ wide }`. Registering there replaces the plugin's\n * old `[class*=\"sidebarCol\"]` / `[class*=\"logoRow\"]` / `button[class*=\"newSession\"]`\n * DOM hunt plus its two MutationObservers.\n *\n * These are pure ACTIONS (换图), not settings: the skin's settings live in the\n * harness Settings panel (`settings.section`, see skin-settings.tsx), which\n * owns its own room, padding and scrolling.\n *\n * TWO SHAPE CONSTRAINTS come from the seat's box model, and both are handled\n * here rather than by guessing at the shell's internals:\n *\n * 1. ONE registration, not two. The row is a flex row and its shipped occupant\n * (`cordis-panel`, ui-cordis) claims the whole line with\n * `width: 100%; flex: none`; separate sibling registrations would be laid out\n * after a line-consuming item and squeezed to zero width.\n * 2. The group asks the ROW to wrap (`flex-wrap: wrap` on its own parent, set and\n * restored by effect). Without it, a second full-line occupant cannot share\n * the line at all; with it every occupant keeps its own line.\n * @module dsh-wx-skin/client/entries\n */\nimport { useEffect, useRef, useSyncExternalStore } from 'react'\nimport type { SkinController, SkinView } from './skin-controller.tsx'\nimport { canGoPrevious, folderStatus } from './skin-folder.ts'\nimport { currentImageLabel } from './skin-store.ts'\nimport css from './skin.module.css'\n\n/** The official additive seat these actions register into. */\nexport const FOOTER_SLOT = 'sidebar.footer.action'\n\n/** Owner props every `sidebar.footer.action` occupant receives. */\nexport interface FooterEntryProps {\n /** Whether the sidebar renders wide content (false = 56px rail, icon only). */\n wide?: boolean\n}\n\nconst NEXT_ICON = '<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 8h9\"/><path d=\"M8.5 4.5 12 8l-3.5 3.5\"/></svg>'\nconst PREV_ICON = '<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M13 8H4\"/><path d=\"M7.5 4.5 4 8l3.5 3.5\"/></svg>'\n\n/** One action button: icon always, label only in the wide sidebar. */\nfunction EntryButton({\n wide, role, label, title, icon, empty, onClick,\n}: {\n wide: boolean\n role: 'prev' | 'next'\n label: string\n title: string\n icon: string\n empty?: boolean\n onClick: () => void\n}): JSX.Element {\n return (\n <button\n type=\"button\"\n data-wx-skin-entry=\"\"\n data-wx-skin-entry-role={role}\n data-empty={empty === true ? 'true' : undefined}\n aria-disabled={empty === true ? 'true' : undefined}\n aria-label={label}\n title={title}\n className={wide ? `${css.footerAction} ${css.footerActionWide}` : `${css.footerAction} ${css.footerActionRail}`}\n onClick={onClick}\n >\n <span className={css.entryIcon} dangerouslySetInnerHTML={{ __html: icon }} />\n {wide ? <span className={css.entryLabel}>{label}</span> : null}\n </button>\n )\n}\n\n/**\n * Build the sidebar-foot group: 「上一张」「下一张」 on the wide sidebar, the same\n * two icons on the collapsed rail. Both are inert while no folder is loaded.\n * @param controller - the skin controller the actions drive.\n * @returns the component to register into {@link FOOTER_SLOT}.\n */\nexport function makeSkinFooter(controller: SkinController): (props: FooterEntryProps) => JSX.Element {\n const subscribe = (listener: () => void): (() => void) => controller.subscribe(listener)\n const getSnapshot = (): SkinView => controller.view()\n\n return function SkinFooter({ wide = true }: FooterEntryProps): JSX.Element {\n const { settings } = useSyncExternalStore(subscribe, getSnapshot)\n const group = useRef<HTMLDivElement>(null)\n const status = folderStatus(settings)\n const name = currentImageLabel(settings)\n const here = name === null ? '' : ` · 当前 ${name.label}`\n\n // Let the shell's footer row wrap: its shipped occupant claims a full line\n // with `flex: none`, so without wrapping a second occupant would be clipped.\n // Only the element this group was placed in is touched, and it is restored\n // on unmount (client-hmr reload, row disable).\n useEffect(() => {\n const parent = group.current?.parentElement\n if (parent === null || parent === undefined) return\n const previous = parent.style.flexWrap\n parent.style.flexWrap = 'wrap'\n return () => { parent.style.flexWrap = previous }\n }, [])\n\n const previousEnabled = canGoPrevious(settings)\n const previousTitle = previousEnabled\n ? `上一张${here}`\n : status.total === 0\n ? '先加载图片文件夹'\n : settings.orderMode === 'random' ? '随机模式还没有可返回的上一张' : '文件夹里只有这一张图片'\n const nextEmpty = status.total === 0\n const nextTitle = nextEmpty\n ? '先加载图片文件夹'\n : `${settings.orderMode === 'random' ? '随机' : '顺序'}下一张 · 第 ${status.position}/${status.total} 张${here}`\n\n return (\n <div\n ref={group}\n className={`${css.footerGroup} ${wide ? css.footerGroupWide : css.footerGroupRail}`}\n data-wx-skin-footer=\"\"\n >\n <EntryButton\n wide={wide}\n role=\"prev\"\n label=\"上一张\"\n title={previousTitle}\n icon={PREV_ICON}\n empty={!previousEnabled}\n onClick={() => { controller.previous() }}\n />\n <EntryButton\n wide={wide}\n role=\"next\"\n label=\"下一张\"\n title={nextTitle}\n icon={NEXT_ICON}\n empty={nextEmpty}\n onClick={() => { controller.next() }}\n />\n </div>\n )\n }\n}\n","/**\n * dsh-wx-skin — local-image pipeline: read a picked file, downscale it to a\n * bounded canvas, and re-encode it to a data URL. The browser implementation\n * is injected through `PipelineEnv` so tests drive it with a fake source; the\n * pure `fileToDataUrl` logic is environment-agnostic.\n *\n * There is intentionally no input-size or output-size limit: any image the\n * canvas can decode is accepted (no \"too large\" rejection). The only bound is\n * the downscale to `maxEdge`, which keeps the resulting data URL small enough\n * to persist while preserving a sharp full-screen background.\n * @module dsh-wx-skin/client/image-pipeline\n */\n\n/** A decodable raster image held by the pipeline. */\nexport interface ImageSource {\n width: number\n height: number\n /** Draw the source scaled to w×h into a fresh canvas and encode it. */\n render(w: number, h: number, type: string, quality: number): string\n /** Release any held resources (object URLs, …). */\n release(): void\n}\n\n/** The browser environment the pipeline runs against. */\nexport interface PipelineEnv {\n /** Decode a file into an ImageSource. Throws when the file is not readable. */\n readImage(file: Blob): Promise<ImageSource>\n}\n\n/** Error the pipeline throws with a stable machine-readable code. */\nexport class ImagePipelineError extends Error {\n constructor(\n public readonly code: 'unsupported' | 'decode' | 'encode',\n message: string,\n ) {\n super(message)\n this.name = 'ImagePipelineError'\n }\n}\n\n/** Tuning knobs for the encode. */\nexport interface DownscaleOptions {\n /**\n * Optional explicit cap on the longest edge of the first encode (px).\n * Omitted by default — the image keeps its original resolution up to\n * {@link DISPLAY_MAX_EDGE}; larger images are downscaled for display (a\n * background never needs more than ~4K, and oversized canvases silently\n * produce empty data URLs, which is exactly the \"selected but not shown\"\n * failure). Picking a file is never limited by size.\n */\n maxEdge?: number\n /** JPEG quality (0..1). Default 0.82. */\n quality?: number\n /**\n * Data-URL length budget (chars). When the encoded result exceeds this,\n * the pipeline silently re-encodes at smaller sizes so the skin stays\n * persistable; it never throws. Default 4_000_000 (safe under the ~5 MB\n * localStorage quota).\n */\n storageBudget?: number\n}\n\n/**\n * Display cap for the encoded background: images larger than this (per side)\n * are downscaled so the canvas stays inside every browser's limits and the\n * data URL stays small enough to persist. 4096 px is sharper than any screen\n * needs for a background.\n */\nexport const DISPLAY_MAX_EDGE = 4096\n\n/** Raster formats the pipeline accepts (canvas can re-encode these safely). */\nexport const ALLOWED_TYPES = [\n 'image/png', 'image/jpeg', 'image/jpg', 'image/pjpeg', 'image/x-png',\n 'image/webp', 'image/gif', 'image/bmp', 'image/x-ms-bmp',\n] as const\n\n/** Default browser environment: object URL + <img> decode + canvas encode. */\nexport const browserEnv: PipelineEnv = {\n async readImage(file: Blob): Promise<ImageSource> {\n const url = URL.createObjectURL(file)\n try {\n const image = await loadImage(url)\n return {\n width: image.naturalWidth,\n height: image.naturalHeight,\n render(w, h, type, quality) {\n const canvas = document.createElement('canvas')\n canvas.width = w\n canvas.height = h\n const ctx = canvas.getContext('2d')\n if (ctx === null) throw new Error('canvas 2d context unavailable')\n ctx.drawImage(image, 0, 0, w, h)\n return canvas.toDataURL(type, quality)\n },\n release() {\n URL.revokeObjectURL(url)\n },\n }\n } catch (error) {\n URL.revokeObjectURL(url)\n throw error\n }\n },\n}\n\n/** Load an <img> from a src, resolving on decode. */\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const image = new Image()\n image.onload = () => resolve(image)\n image.onerror = () => reject(new Error('image load failed'))\n image.src = src\n })\n}\n\n/** Convert a Blob file into a JPEG data URL. Picking is never limited by file\n * size; the encode keeps the original resolution up to the display cap and\n * silently re-encodes smaller when the canvas would be oversized or the data\n * URL would not persist — it never throws a size error. */\nexport async function fileToDataUrl(\n file: Blob,\n env: PipelineEnv = browserEnv,\n options: DownscaleOptions = {},\n): Promise<string> {\n const quality = options.quality ?? 0.82\n const storageBudget = options.storageBudget ?? 4_000_000\n\n if (typeof (file as File).type === 'string' && (file as File).type !== '') {\n const type = (file as File).type.toLowerCase()\n if (type === 'image/svg+xml') {\n throw new ImagePipelineError('unsupported', '不支持 SVG 矢量图,请选择 PNG / JPEG / WebP / GIF / BMP 位图。')\n }\n if (!type.startsWith('image/')) {\n throw new ImagePipelineError('unsupported', `不支持的文件类型“${type}”,请选择 PNG / JPEG / WebP / GIF / BMP 图片。`)\n }\n // image/* beyond the raster whitelist (e.g. avif) proceeds to decode —\n // a codec the canvas cannot read surfaces as a clear \"decode\" error.\n }\n\n let source: ImageSource\n try {\n source = await env.readImage(file)\n } catch (error) {\n throw new ImagePipelineError('decode', '无法读取图片:文件已损坏或不是有效的图片。')\n }\n\n try {\n if (!Number.isFinite(source.width) || !Number.isFinite(source.height) || source.width <= 0 || source.height <= 0) {\n throw new ImagePipelineError('decode', '无法解析图片尺寸,文件可能已损坏。')\n }\n // First attempt: original resolution when it fits the display cap,\n // otherwise downscaled so the canvas stays inside browser limits.\n const cap = options.maxEdge ?? Math.min(Math.max(source.width, source.height), DISPLAY_MAX_EDGE)\n const first = sizeFor(source, cap)\n let data = encode(source, first.w, first.h, quality)\n // Fallback: never reject — when the encode came back invalid (oversized\n // canvases can silently yield empty data URLs) or too big to persist,\n // re-encode smaller. The last attempt always returns.\n if (!isValidDataUrl(data) || data.length > storageBudget) {\n for (const edge of [2560, 1920, 1280]) {\n if (Math.max(first.w, first.h) <= edge) continue\n const next = sizeFor(source, edge)\n data = encode(source, next.w, next.h, Math.max(0.6, quality - 0.1))\n if (isValidDataUrl(data) && data.length <= storageBudget) break\n }\n }\n return data\n } finally {\n try {\n source.release()\n } catch {\n // A release failure must not mask the primary outcome.\n }\n }\n}\n\n/** True when the encoder produced a real image payload (not an empty `data:,`). */\nfunction isValidDataUrl(data: string): boolean {\n return data.startsWith('data:image/') && data.length > 64\n}\n\n/** Bounded target size: original dimensions, or downscaled to fit `edge`. */\nfunction sizeFor(source: ImageSource, edge: number | undefined): { w: number; h: number } {\n if (edge === undefined) return { w: Math.max(1, Math.round(source.width)), h: Math.max(1, Math.round(source.height)) }\n const scale = Math.min(1, edge / Math.max(source.width, source.height))\n return {\n w: Math.max(1, Math.round(source.width * scale)),\n h: Math.max(1, Math.round(source.height * scale)),\n }\n}\n\n/** Draw + encode one attempt; encode failures are wrapped with a stable code. */\nfunction encode(source: ImageSource, w: number, h: number, quality: number): string {\n try {\n return source.render(w, h, 'image/jpeg', quality)\n } catch (error) {\n throw new ImagePipelineError('encode', '图片编码失败,请换一张图片。')\n }\n}\n","/**\n * dsh-wx-skin — host persistence client: loads/saves the durable settings\n * through the host half's loopback routes (/dsh-wx-skin/load,\n * /dsh-wx-skin/save), which store the copy in the DSH home directory —\n * independent of the current origin, so the background image survives the\n * desktop app's per-launch port changes.\n *\n * Module is kept free of DOM access and takes an injectable fetch so unit\n * tests exercise it without a page. Every function degrades to a safe result\n * (null / false) and never throws; the caller keeps its localStorage\n * fallback.\n * @module dsh-wx-skin/client/skin-host\n */\nimport type { SkinFolderImage, SkinSettings } from '../core/types.ts'\nimport { sanitizeFolderImages, sanitizeSettings } from './skin-store.ts'\n\n/**\n * Browser references to the host half's routes. They are DOCUMENT-RELATIVE on\n * purpose: the shell injects `<base href=\"./\">` (`@deepseek-ai/dsh-host-frontend-static`),\n * so the same literal reaches the listener under any mount — including a\n * prefix-stripping proxy, where a root-absolute `/dsh-wx-skin/load` would miss.\n * DSH's `verify-client-route-resolution` gate enforces this shape for its own\n * browser sources; this plugin follows the same rule\n * (`.agents/notes/implemented/architecture/2026-09-14-web-document-relative-app-routes.md`\n * in the DSH checkout). The host half's registration keys stay absolute\n * (`LOAD_PATH`/`SAVE_PATH`/`FOLDER_PATH` in the package root).\n */\nexport const LOAD_ROUTE = 'dsh-wx-skin/load'\nexport const SAVE_ROUTE = 'dsh-wx-skin/save'\nexport const FOLDER_ROUTE = 'dsh-wx-skin/folder'\n\n/** Read the durable settings from the host, or null when unavailable. */\nexport async function hostLoad(fetchImpl: typeof fetch = globalThis.fetch): Promise<SkinSettings | null> {\n try {\n const res = await fetchImpl(LOAD_ROUTE)\n if (!res.ok) return null\n const body = await res.json() as { ok?: unknown; settings?: unknown }\n // The host only ever writes an object; any other shape means no usable\n // durable copy → the caller falls back to localStorage/defaults.\n if (body?.ok !== true\n || body.settings === null\n || body.settings === undefined\n || typeof body.settings !== 'object') return null\n return sanitizeSettings(body.settings)\n } catch {\n return null\n }\n}\n\n/** Persist the settings to the host; false when it could not (never throws). */\nexport async function hostSave(settings: SkinSettings, fetchImpl: typeof fetch = globalThis.fetch): Promise<boolean> {\n try {\n const res = await fetchImpl(SAVE_ROUTE, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ settings }),\n })\n return res.ok\n } catch {\n return false\n }\n}\n\n/** One successful folder scan. */\nexport interface FolderScanSuccess {\n ok: true\n /** Canonical folder the host will serve images from. */\n root: string\n images: SkinFolderImage[]\n truncated: boolean\n}\n\n/** A rejected folder scan, already phrased for the panel. */\nexport interface FolderScanFailure {\n ok: false\n error: string\n}\n\n/** Result of asking the host to scan a folder. */\nexport type FolderScanResult = FolderScanSuccess | FolderScanFailure\n\n/** Message shown when the host process predates the folder route. */\nconst STALE_HOST = '宿主未加载文件夹接口,请重启 dsh web 后再试。'\nconst UNREACHABLE = '无法连接宿主接口(回环请求失败)。'\n\n/**\n * Ask the host to scan `path` and return its images in slideshow order.\n *\n * The host is authoritative: the folder route validates that the path is a\n * readable directory and applies the path-confinement and count bounds. A\n * response that is not the expected JSON means the route is missing — an older\n * host would answer with the SPA index through its fallback — so that case\n * reports the restart hint instead of a generic failure.\n * @param path - absolute folder path typed or picked by the user.\n * @param recursive - descend into subdirectories.\n * @param fetchImpl - injectable fetch (tests).\n * @returns the scanned list, or a failure carrying a display message.\n */\nexport async function hostListFolder(\n path: string,\n recursive: boolean,\n fetchImpl: typeof fetch = globalThis.fetch,\n): Promise<FolderScanResult> {\n let response: Response\n try {\n response = await fetchImpl(FOLDER_ROUTE, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ path, recursive }),\n })\n } catch {\n return { ok: false, error: UNREACHABLE }\n }\n\n let body: unknown\n try {\n body = await response.json()\n } catch {\n return { ok: false, error: STALE_HOST }\n }\n if (typeof body !== 'object' || body === null) return { ok: false, error: STALE_HOST }\n\n const record = body as Record<string, unknown>\n if (record.ok !== true) {\n const error = typeof record.error === 'string' && record.error !== '' ? record.error : '读取文件夹失败。'\n return { ok: false, error }\n }\n const images = sanitizeFolderImages(record.images)\n if (!Array.isArray(record.images) || (record.images.length > 0 && images.length === 0)) {\n return { ok: false, error: STALE_HOST }\n }\n return {\n ok: true,\n root: typeof record.root === 'string' ? record.root : path,\n images,\n truncated: record.truncated === true,\n }\n}\n","/**\n * dsh-wx-skin — the skin's settings page: enable toggle, background preview, the\n * folder row, the local-file row, the slideshow controls (顺序 / 随机, 上一张 /\n * 下一张), dim / blur / surface sliders, and reset.\n *\n * The page describes the background by PATH, which is also what the slideshow\n * walks:\n *\n * - 文件夹 is the folder being shown; loading one scans it and shows an image.\n * - 本地文件 is the image on screen — its absolute path for a folder image, the\n * URL for a URL background, or the picked file name when the browser dialog\n * gave no directory. Pasting an absolute path there loads THAT file: the\n * folder becomes the file's own folder and the slideshow walks it, so 下一张\n * from image 10 shows 11 and 上一张 shows 9 (sequential mode steps through the\n * folder order).\n *\n * The component is CONTROLLED: it renders the `settings` the controller owns and\n * reports every change through `commit`. Only transient UI state (drafts, busy\n * flags, messages) lives in local state.\n * @module dsh-wx-skin/client/skin-panel\n */\nimport { useEffect, useRef, useState, type ChangeEvent } from 'react'\nimport type { SkinFitMode, SkinFolderImage, SkinOrderMode, SkinSettings } from '../core/types.ts'\nimport { ImagePipelineError, fileToDataUrl } from './image-pipeline.ts'\nimport { canGoPrevious, focusFolderImage, folderStatus, loadFolderState, matchFolderImageByName } from './skin-folder.ts'\nimport { hostListFolder } from './skin-host.ts'\nimport { isAbsolutePath, isRemoteImageUrl, parentDirectory, pathBasename, samePath } from './skin-path.ts'\nimport type { PickerBridge } from './skin-picker.ts'\nimport { DEFAULT_SETTINGS, DEFAULT_SURFACE, MAX_SURFACE, MIN_SURFACE, currentImageLabel } from './skin-store.ts'\nimport css from './skin.module.css'\n\nexport interface SkinPanelProps {\n /** Settings currently applied (owned by the controller). */\n settings: SkinSettings\n /** Persist + apply a settings change (the controller wires storage, DOM, host sync). */\n commit: (next: SkinSettings) => void\n /**\n * Bridge to the harness native directory picker. Always present; the service\n * behind it may not be ready yet, which the button renders as unavailable.\n */\n picker?: PickerBridge\n /** Advance the slideshow by one image (the controller owns the algorithm + preload). */\n onNext: () => void\n /** Go back one image (the controller owns the algorithm + preload). */\n onPrevious: () => void\n}\n\n/** How the page offers the background fit modes, in menu order. */\nconst FIT_OPTIONS: ReadonlyArray<{ value: SkinFitMode, label: string, hint: string }> = [\n { value: 'fill', label: '填充', hint: '铺满窗口,超出的部分裁掉。' },\n { value: 'fit', label: '适应', hint: '完整显示整张图,按比例缩放并留白。' },\n { value: 'stretch', label: '拉伸', hint: '拉满窗口,比例可能变形。' },\n { value: 'tile', label: '平铺', hint: '按图片原始尺寸重复铺满。' },\n { value: 'center', label: '居中', hint: '按原始尺寸居中,四周留白。' },\n]\n\n/** The one-line explanation shown under the 适合度 row. */\nfunction fitHint(fit: SkinFitMode): string {\n return FIT_OPTIONS.find(option => option.value === fit)?.hint ?? ''\n}\n\n/** Describe the currently active source for the status badge. */\nfunction describe(settings: SkinSettings): string {\n if (!settings.enabled) return '未启用'\n if (settings.source === 'image') return '本地图片'\n if (settings.source === 'url') return '图片(URL)'\n if (settings.source === 'folder') {\n const status = folderStatus(settings)\n return status.total === 0 ? '文件夹(空)' : `文件夹 ${status.position}/${status.total}`\n }\n return '未启用'\n}\n\n/**\n * What the 本地文件 row shows for the current settings: the image's absolute\n * path, its URL, or the picked file's name when no directory was available.\n */\nfunction currentPathOf(settings: SkinSettings): string {\n if (settings.source === 'folder') return settings.folderImages[settings.currentIndex]?.path ?? ''\n if (settings.source === 'url') return settings.url ?? ''\n if (settings.source === 'image') return settings.imageName ?? ''\n return ''\n}\n\n/** The popover-free skin settings page. */\nexport function SkinPanel({ settings, commit, picker, onNext, onPrevious }: SkinPanelProps): JSX.Element {\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n /** A just-picked local image, processed but not yet applied — the user\n * clicks 「应用」 to commit it (explicit two-step flow). */\n const [staged, setStaged] = useState<string | null>(null)\n /** File name of `staged`, kept so the page can name the applied image. */\n const [stagedName, setStagedName] = useState<string | null>(null)\n /** Folder path box; follows the loaded folder unless the user is typing. */\n const [folderDraft, setFolderDraft] = useState<string | null>(null)\n const [folderBusy, setFolderBusy] = useState(false)\n const [folderError, setFolderError] = useState<string | null>(null)\n const [folderNotice, setFolderNotice] = useState<string | null>(null)\n /** Local-file row draft; null means \"mirror the shown image\". */\n const [pathDraft, setPathDraft] = useState<string | null>(null)\n const [pathNotice, setPathNotice] = useState<string | null>(null)\n const fileRef = useRef<HTMLInputElement | null>(null)\n\n const status = folderStatus(settings)\n /** Name of the image currently on screen (null when nothing is named). */\n const currentLabel = currentImageLabel(settings)\n /** Path / URL of the image on screen; the local-file row mirrors it. */\n const currentPath = currentPathOf(settings)\n const folderValue = folderDraft ?? settings.folderPath ?? ''\n const pathValue = pathDraft ?? currentPath\n /** The shown image is a data URL from the browser dialog: no directory known. */\n const pathIsFileNameOnly = settings.source === 'image' && currentPath !== ''\n\n // The rows mirror the committed state; a draft the user is typing survives\n // until the shown image / folder changes underneath it.\n useEffect(() => { setPathDraft(null) }, [currentPath])\n useEffect(() => { setFolderDraft(null) }, [settings.folderPath])\n\n const onPickFile = async (event: ChangeEvent<HTMLInputElement>): Promise<void> => {\n const file = event.target.files?.[0]\n event.target.value = ''\n if (file === undefined) return\n setBusy(true)\n setError(null)\n setPathNotice(null)\n try {\n const dataUrl = await fileToDataUrl(file)\n setStaged(dataUrl)\n setStagedName(file.name)\n } catch (err) {\n setStaged(null)\n setStagedName(null)\n setError(err instanceof ImagePipelineError ? err.message : String(err))\n } finally {\n setBusy(false)\n }\n }\n\n /**\n * Commit the staged local image to the skin.\n *\n * The browser's file dialog hands over the file NAME and no directory, so this\n * is where the picked file joins the slideshow when it can: if the loaded\n * folder holds exactly one image with that name, the background becomes that\n * file — the 本地文件 row then shows its full path and 下一张 continues in\n * folder order from it (pick bg129 → 下一张 bg130). Otherwise the encoded image\n * is used, with a hint explaining why no folder was attached.\n */\n const onApplyImage = (): void => {\n if (staged === null) return\n const matched = stagedName === null ? -1 : matchFolderImageByName(settings.folderImages, stagedName)\n if (settings.folderPath !== null && matched >= 0) {\n const image = settings.folderImages[matched] as SkinFolderImage\n commit(focusFolderImage(settings, matched))\n setStaged(null)\n setStagedName(null)\n setPathDraft(null)\n setPathNotice(`已按文件名定位到文件夹里的 ${image.name}(「下一张 / 上一张」从它继续)。`)\n return\n }\n commit({\n ...settings,\n enabled: true,\n source: 'image',\n imageDataUrl: staged,\n imageName: stagedName,\n url: null,\n })\n setStaged(null)\n setStagedName(null)\n setPathDraft(null)\n setPathNotice(stagedName === null\n ? null\n : `已应用本地图片 ${stagedName}(浏览器文件对话框不提供目录,未能关联文件夹;粘贴完整路径可自动关联)。`)\n }\n\n const onReset = (): void => {\n setStaged(null)\n setStagedName(null)\n setFolderDraft(null)\n setFolderError(null)\n setFolderNotice(null)\n setPathDraft(null)\n setPathNotice(null)\n setError(null)\n commit({ ...DEFAULT_SETTINGS })\n }\n\n /**\n * Apply a local-file row value: an `http(s)` address becomes a URL background;\n * an absolute path is loaded as the shown image, with its own folder scanned\n * so the slideshow walks that folder from this file.\n */\n const onApplyPath = async (): Promise<void> => {\n const value = pathValue.trim()\n setError(null)\n setPathNotice(null)\n if (value === '') {\n setError('请填写本地图片的绝对路径,或点「选择图片」;也可以填 http(s) 图片地址。')\n return\n }\n if (isRemoteImageUrl(value)) {\n commit({ ...settings, enabled: true, source: 'url', url: value, imageDataUrl: null, imageName: null })\n setStaged(null)\n setStagedName(null)\n setPathDraft(null)\n return\n }\n if (!isAbsolutePath(value)) {\n setError('请填写完整路径(如 D:\\\\Pictures\\\\a.png)或 http(s) 图片地址。')\n return\n }\n const folder = parentDirectory(value)\n if (folder === null) {\n setError('无法从该路径得到所在文件夹,请填写完整的图片路径。')\n return\n }\n setFolderBusy(true)\n try {\n const result = await hostListFolder(folder, settings.folderRecursive)\n if (!result.ok) {\n setFolderError(result.error)\n return\n }\n if (!result.images.some(image => samePath(image.path, value))) {\n setError('该文件不在支持的图片类型内(PNG / JPEG / WebP / GIF / BMP),或无法访问。')\n return\n }\n setFolderDraft(null)\n setFolderError(null)\n setPathDraft(null)\n commit(loadFolderState(settings, result.root, settings.folderRecursive, result.images, value))\n setFolderNotice(`已切到该文件所在文件夹(${result.images.length} 张图片)。`)\n } finally {\n setFolderBusy(false)\n }\n }\n\n /** Scan a folder (or the one currently typed) and start the slideshow. */\n const onLoadFolder = async (explicit?: string): Promise<void> => {\n const target = (explicit ?? folderValue).trim()\n if (target === '') {\n setFolderError('请填写文件夹的绝对路径,或点「选择文件夹」。')\n return\n }\n setFolderBusy(true)\n setFolderError(null)\n setFolderNotice(null)\n try {\n const result = await hostListFolder(target, settings.folderRecursive)\n if (!result.ok) {\n setFolderError(result.error)\n return\n }\n setFolderDraft(null)\n commit(loadFolderState(settings, result.root, settings.folderRecursive, result.images))\n setFolderNotice(result.images.length === 0\n ? '该文件夹里没有找到图片(支持 PNG / JPEG / WebP / GIF / BMP)。'\n : `已加载 ${result.images.length} 张图片${result.truncated ? '(已达上限,只取前 2000 张)' : ''}。`)\n } finally {\n setFolderBusy(false)\n }\n }\n\n /** Native picker when the harness exposes it; the path box stays as fallback. */\n const onPickFolder = async (): Promise<void> => {\n if (picker === undefined) return\n setFolderError(null)\n if (!picker.available()) {\n setFolderError('宿主目录选择器尚未就绪,请稍后再试,或直接粘贴文件夹路径。')\n return\n }\n try {\n const picked = await picker.open()\n if (picked === null) return // cancelled\n setFolderDraft(picked)\n await onLoadFolder(picked)\n } catch {\n setFolderError('无法打开系统目录选择器,请直接粘贴文件夹路径。')\n }\n }\n\n const onMode = (mode: SkinOrderMode): void => {\n commit({ ...settings, orderMode: mode })\n }\n\n // Only the image layer: size/position/repeat/backdrop come from `.preview`,\n // which contains the whole image (the preview is not the wallpaper's crop).\n const previewStyle = settings.enabled\n ? { backgroundImage: 'var(--wx-skin-bg-image, none)' }\n : undefined\n\n return (\n // The skin's settings page inside the harness Settings panel (registered\n // into `settings.section`): inline content, no popover chrome and no title\n // of its own — the shell's modal header and nav row already name the page.\n <div className={css.panel} data-wx-skin-panel=\"\">\n <label className={css.toggleRow}>\n <input\n type=\"checkbox\"\n checked={settings.enabled}\n onChange={e => { commit({ ...settings, enabled: e.target.checked }) }}\n />\n <span>启用皮肤</span>\n </label>\n\n <div className={css.preview} style={previewStyle}>\n {!settings.enabled && <span>未启用</span>}\n </div>\n <div className={css.currentBadge}>当前:{describe(settings)}</div>\n {currentLabel !== null && (\n <div className={css.imageName} data-wx-skin-name=\"\" title={currentLabel.detail}>\n 图片:{currentLabel.label}\n </div>\n )}\n\n <div className={css.section}>文件夹</div>\n <div className={css.urlRow}>\n <input\n className={css.urlInput}\n value={folderValue}\n placeholder=\"D:\\\\Pictures\\\\wallpapers\"\n aria-label=\"图片文件夹路径\"\n onChange={e => setFolderDraft(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter') void onLoadFolder() }}\n />\n <button\n type=\"button\"\n className={css.button}\n disabled={folderBusy}\n onClick={() => void onLoadFolder()}\n >\n {folderBusy ? '读取中…' : settings.folderImages.length === 0 ? '加载' : '重新加载'}\n </button>\n </div>\n <div className={css.folderRow}>\n {/* Always rendered: the harness picker service arrives after this page\n mounts, so hiding the button until then made the option look absent.\n It is disabled (with a hint) until the service is ready. */}\n <button\n type=\"button\"\n className={css.button}\n data-wx-skin-picker=\"\"\n data-ready={picker?.available() === true ? 'true' : 'false'}\n disabled={picker?.available() !== true}\n title={picker?.available() === true ? '选择图片文件夹' : '目录选择器尚未就绪'}\n onClick={() => void onPickFolder()}\n >\n 选择文件夹\n </button>\n <label className={css.inlineToggle}>\n <input\n type=\"checkbox\"\n checked={settings.folderRecursive}\n onChange={e => { commit({ ...settings, folderRecursive: e.target.checked }) }}\n />\n <span>包含子文件夹</span>\n </label>\n </div>\n {folderNotice !== null && <div className={css.noticeText}>{folderNotice}</div>}\n {folderError !== null && <div className={css.errorText}>{folderError}</div>}\n\n <div className={css.section}>本地文件</div>\n <div className={css.urlRow}>\n <input\n className={css.urlInput}\n data-wx-skin-path=\"\"\n value={pathValue}\n placeholder=\"D:\\\\Pictures\\\\a.png 或 https://…/a.jpg\"\n aria-label=\"本地图片路径\"\n onChange={e => setPathDraft(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter') void onApplyPath() }}\n />\n <button\n type=\"button\"\n className={css.button}\n data-wx-skin-path-apply=\"\"\n disabled={folderBusy}\n title=\"应用该路径:http(s) 地址作为图片 URL;本地绝对路径会切到它所在文件夹并按顺序继续\"\n onClick={() => void onApplyPath()}\n >\n 应用\n </button>\n </div>\n <div className={css.folderRow}>\n <button\n type=\"button\"\n className={css.buttonPrimary}\n data-wx-skin-local-pick=\"\"\n disabled={busy}\n onClick={() => fileRef.current?.click()}\n >\n {busy ? '处理中…' : '选择图片'}\n </button>\n <input\n ref={fileRef}\n className={css.fileInput}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/bmp\"\n onChange={(e) => void onPickFile(e)}\n />\n </div>\n {staged !== null && (\n <div className={css.stagedRow}>\n <div\n className={css.stagedThumb}\n style={{ backgroundImage: `url(\"${staged}\")`, backgroundSize: 'cover', backgroundPosition: 'center' }}\n />\n <span className={css.stagedLabel}>已选择 {stagedName ?? '图片'}</span>\n <button type=\"button\" className={css.buttonPrimary} data-wx-skin-apply-image=\"\" onClick={onApplyImage}>应用</button>\n </div>\n )}\n {pathIsFileNameOnly && (\n <div className={css.noticeText}>\n 浏览器文件对话框只提供文件名({pathBasename(currentPath)}),不提供目录;粘贴完整路径后点「应用」即可自动关联所在文件夹并参与轮播。\n </div>\n )}\n {pathNotice !== null && <div className={css.noticeText}>{pathNotice}</div>}\n {error !== null && <div className={css.errorText}>{error}</div>}\n\n <div className={css.section}>切换</div>\n <div className={css.folderRow}>\n <div className={css.segmented} role=\"group\" aria-label=\"切换方式\">\n <button\n type=\"button\"\n className={settings.orderMode === 'sequential' ? `${css.segBtn} ${css.segBtnActive}` : css.segBtn}\n onClick={() => onMode('sequential')}\n >\n 顺序\n </button>\n <button\n type=\"button\"\n className={settings.orderMode === 'random' ? `${css.segBtn} ${css.segBtnActive}` : css.segBtn}\n onClick={() => onMode('random')}\n >\n 随机\n </button>\n </div>\n <button\n type=\"button\"\n className={css.button}\n data-wx-skin-prev=\"\"\n disabled={!canGoPrevious(settings)}\n title={canGoPrevious(settings) ? '上一张' : '没有可返回的上一张'}\n onClick={onPrevious}\n >\n 上一张\n </button>\n <button\n type=\"button\"\n className={css.buttonPrimary}\n data-wx-skin-next=\"\"\n disabled={status.total === 0}\n onClick={onNext}\n >\n 下一张\n </button>\n </div>\n <div className={css.statusLine}>\n {status.total === 0\n ? '尚未加载文件夹'\n : settings.orderMode === 'random'\n ? `第 ${status.position}/${status.total} 张 · 本轮已用 ${status.usedThisPass}/${status.total}`\n : `第 ${status.position}/${status.total} 张`}\n {settings.orderMode === 'random' && settings.usedPaths.length > 0 && status.total > 0 && (\n <button type=\"button\" className={css.linkButton} onClick={() => commit({ ...settings, usedPaths: [] })}>\n 重置本轮\n </button>\n )}\n </div>\n\n <div className={css.section}>适合度</div>\n <div className={css.urlRow}>\n <select\n className={css.select}\n data-wx-skin-fit=\"\"\n aria-label=\"背景适合度\"\n value={settings.fit}\n onChange={e => {\n const next = FIT_OPTIONS.find(option => option.value === e.target.value)\n if (next !== undefined) commit({ ...settings, fit: next.value })\n }}\n >\n {FIT_OPTIONS.map(option => (\n <option key={option.value} value={option.value}>{option.label}</option>\n ))}\n </select>\n </div>\n <div className={css.hintText} data-wx-skin-fit-hint=\"\">{fitHint(settings.fit)}</div>\n\n <div className={css.section}>效果</div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>暗化 {Math.round(settings.dim * 100)}%</span>\n <input\n className={css.slider}\n type=\"range\"\n min={0}\n max={80}\n step={5}\n value={Math.round(settings.dim * 100)}\n onChange={e => { commit({ ...settings, dim: Number(e.target.value) / 100 }) }}\n />\n </div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>模糊 {Math.round(settings.blur)}px</span>\n <input\n className={css.slider}\n type=\"range\"\n min={0}\n max={24}\n step={1}\n value={Math.round(settings.blur)}\n onChange={e => { commit({ ...settings, blur: Number(e.target.value) }) }}\n />\n </div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>透出 {Math.round(settings.surface * 100)}%</span>\n <input\n className={css.slider}\n type=\"range\"\n min={MIN_SURFACE * 100}\n max={MAX_SURFACE * 100}\n step={1}\n value={Math.round(settings.surface * 100)}\n title=\"表面不透明度越低,背景图片越明显\"\n onChange={e => { commit({ ...settings, surface: Number(e.target.value) / 100 }) }}\n />\n </div>\n\n <div className={css.footer}>\n <button type=\"button\" className={css.button} onClick={onReset}>恢复默认</button>\n </div>\n </div>\n )\n}\n","/**\n * dsh-wx-skin — the skin's settings page, registered into the harness Settings\n * panel through the official additive seat `settings.section`.\n *\n * The shell owns the modal: its nav rail lists one row per registration\n * (ascending `order`, label resolved per render) and its options column renders\n * the selected page with its own padding and scrolling. That is exactly the room\n * a full skin editor needs — the plugin's own body-level popover used to hang\n * past the window edge when it was anchored to the sidebar foot, which is why\n * the settings moved here.\n *\n * The component is thin: it renders the controller's view snapshot into the\n * panel component (SkinPanel), which owns the controls. `close` (the one owner\n * prop a section receives) is unused — the modal's own close button and Escape\n * already leave settings, and no flow here needs to leave them for the user.\n * @module dsh-wx-skin/client/skin-settings\n */\nimport { useSyncExternalStore } from 'react'\nimport { SkinPanel } from './SkinPanel.tsx'\nimport type { SkinController, SkinView } from './skin-controller.tsx'\n\n/** The official additive seat for one Settings page. */\nexport const SETTINGS_SECTION = 'settings.section'\n\n/** Owner props every `settings.section` occupant receives. */\nexport interface SettingsSectionProps {\n /** Close the settings panel (the shell owns the open state). */\n close?: () => void\n}\n\n/**\n * Build the settings page component.\n * @param controller - the skin controller the page edits.\n * @returns the component to register into {@link SETTINGS_SECTION}.\n */\nexport function makeSkinSettings(controller: SkinController): (props: SettingsSectionProps) => JSX.Element {\n const subscribe = (listener: () => void): (() => void) => controller.subscribe(listener)\n const getSnapshot = (): SkinView => controller.view()\n\n return function SkinSettings(): JSX.Element {\n const { settings } = useSyncExternalStore(subscribe, getSnapshot)\n return (\n <SkinPanel\n settings={settings}\n commit={controller.commit}\n picker={controller.picker}\n onNext={controller.next}\n onPrevious={controller.previous}\n />\n )\n }\n}\n","/**\n * dsh-wx-skin — global skin stylesheet, injected once as a <style> tag by\n * mount. Everything is gated on the `data-wx-skin-active` attribute on\n * documentElement, which the DOM applier toggles; with the attribute absent\n * the sheet is inert and the default theme is untouched.\n *\n * Design: a full-viewport fixed layer paints the chosen image/color, and the\n * alias surface tokens are overridden with translucent values so the\n * background shows through while text stays readable. The light/dark palette\n * branches ride `body[data-ds-dark-theme]`, exactly like the theme's own\n * token sheets.\n *\n * Stacking: measured on the real DSH shell, a `position: fixed` layer with\n * `z-index: -1` never paints (it lands behind the canvas background), while a\n * positive z-index layer paints fine. So the layer sits at `z-index: 0` and\n * the app root `#root` is lifted to `z-index: 1` — the layer renders behind\n * the app and shows through its translucent surfaces.\n *\n * Translucency: every surface token is `rgb(<static palette literal> /\n * var(--wx-skin-surface))` — a user-adjustable surface opacity (default 0.72)\n * — with no `color-mix()` dependency, so it works on any modern browser.\n * @module dsh-wx-skin/client/global-skin-css\n */\n\nexport const GLOBAL_SKIN_CSS = String.raw`\n/* ---- background layer (paints behind the app, above the canvas) ---- */\nhtml[data-wx-skin-active] div[data-wx-skin-layer] {\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image: var(--wx-skin-bg-image, none);\n /* 适合度 (fit mode) projects onto these three; the fallbacks are 填充. */\n background-size: var(--wx-skin-bg-size, cover);\n background-repeat: var(--wx-skin-bg-repeat, no-repeat);\n background-position: var(--wx-skin-bg-position, center);\n filter: blur(var(--wx-skin-blur, 0px));\n}\n\n/* dim / scrim overlay between the image and the translucent surfaces */\nhtml[data-wx-skin-active] div[data-wx-skin-layer]::after {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--wx-skin-scrim, transparent);\n}\n\n/* keep the canvas neutral so nothing opaque hides the layer */\nhtml[data-wx-skin-active] body {\n background: transparent !important;\n}\n\n/* lift the app above the layer; its translucent surfaces let it show through */\nhtml[data-wx-skin-active] #root {\n position: relative !important;\n z-index: 1 !important;\n}\n\n/* ---- translucent surfaces: light palette ---- */\nhtml[data-wx-skin-active] body {\n --dsw-alias-bg-base: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-1: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-2: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-3: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-overlay: rgb(233 236 242 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-module-platform: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-multi-select: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n /* DSH 0.1.7 turned this from the bg-layer-3 alias (which this sheet already\n took over) into a literal rgba, so every menu/dropdown/dock/panel surface\n stopped following the 透出 slider. Re-bound to the same hue at the skin's own\n surface opacity; the !important also outranks the darwin near-opaque\n override in the shell's base.css. */\n --dsw-specific-menu: rgb(248 249 250 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-sidebar-fill: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-input-major: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-selector: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-tip: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble: rgb(237 243 254 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble-highlight: rgb(211 226 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block-banner: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-inline-code: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-placeholder: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-tag: rgb(241 243 245 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-citation: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;\n}\n\n/* ---- translucent surfaces: dark palette ---- */\nhtml[data-wx-skin-active] body[data-ds-dark-theme] {\n --dsw-alias-bg-base: rgb(21 21 23 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-1: rgb(35 35 36 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-2: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-3: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-overlay: rgb(97 102 107 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-module-platform: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-multi-select: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-menu: rgb(48 49 54 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-sidebar-fill: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-input-major: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-selector: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-tip: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble-highlight: rgb(67 69 74 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block-banner: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-inline-code: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-placeholder: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-tag: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-citation: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n}\n\n/* ---- sidebar entry active indicator ---- */\nhtml[data-wx-skin-active] button[data-wx-skin-entry] {\n color: var(--dsw-alias-state-business-primary, #4176e6);\n}\n`\n","/**\n * dsh-wx-skin — DOM applier: owns the injected global stylesheet, the\n * background layer element, and the projection of skin settings onto the\n * document (attribute + CSS variables). Pure DOM writes; retracts only what\n * it wrote, so the default theme is untouched when the skin is off.\n * @module dsh-wx-skin/client/skin-dom\n */\nimport type { SkinSettings } from '../core/types.ts'\nimport { GLOBAL_SKIN_CSS } from './global-skin-css.ts'\nimport { ACTIVE_ATTR, LAYER_ATTR, SKIN_CSS_VARS, cssVariables } from './skin-store.ts'\n\n/** Identity of the injected global <style> tag (idempotent injection key). */\nexport const GLOBAL_STYLE_ID = 'dsh-wx-skin/global'\n\n/**\n * Plugin id stamped on the skin's own DOM. Must equal the package name (the\n * module graph's row id): the harness's client-hmr removes a plugin's owned\n * `<style data-plugin>` tags on reload, and claimStyles attributes an untagged\n * tag to whichever plugin materializes next — so the skin's stylesheet has to\n * carry its own owner.\n */\nexport const PLUGIN_ID = 'dsh-wx-skin'\n\n/** Inject the global skin stylesheet once. Safe to call repeatedly. */\nexport function ensureGlobalCss(): void {\n const existing = document.querySelector<HTMLStyleElement>(`style[data-plugin-css=\"${GLOBAL_STYLE_ID}\"]`)\n if (existing !== null) {\n // A tag injected before ownership stamping (or by the module loader) is adopted here.\n if (!existing.hasAttribute('data-plugin')) existing.dataset.plugin = PLUGIN_ID\n return\n }\n const tag = document.createElement('style')\n tag.dataset.plugin = PLUGIN_ID\n tag.dataset.pluginCss = GLOBAL_STYLE_ID\n tag.textContent = GLOBAL_SKIN_CSS\n document.head.appendChild(tag)\n}\n\n/** Ensure the full-viewport background layer exists and return it. */\nexport function ensureLayer(): HTMLDivElement {\n let layer = document.querySelector<HTMLDivElement>(`div[${LAYER_ATTR}]`)\n if (layer === null) {\n layer = document.createElement('div')\n layer.setAttribute(LAYER_ATTR, '')\n document.body.appendChild(layer)\n }\n return layer\n}\n\n/**\n * Apply skin settings to the document: toggle the gating attribute on\n * documentElement and write/retract the four CSS variables every time (so a\n * disabled skin leaves no residue).\n */\nexport class SkinApplier {\n apply(settings: SkinSettings): void {\n const root = document.documentElement\n if (settings.enabled) root.setAttribute(ACTIVE_ATTR, '')\n else root.removeAttribute(ACTIVE_ATTR)\n const vars = cssVariables(settings)\n for (const name of SKIN_CSS_VARS) {\n const value = vars[name]\n if (value === undefined) root.style.removeProperty(name)\n else root.style.setProperty(name, value)\n }\n }\n}\n\n/**\n * Remove everything the skin mounted: the background layer, the global\n * stylesheet, and the document-level attributes/variables. Every step is a\n * query-then-remove, so this is idempotent and safe to call from a fiber\n * disposer, a failed mount, or both.\n *\n * The sidebar entries are NOT swept here: they are React components registered\n * into the harness's `sidebar.footer.action` slot, so the shell unmounts them\n * when the registration's declaration lifetime ends (and the registration\n * disposer runs on fiber unload). React stays the single owner of those nodes.\n */\nexport function teardownSkinDom(): void {\n document.querySelector<HTMLElement>(`div[${LAYER_ATTR}]`)?.remove()\n document.querySelector<HTMLElement>(`style[data-plugin-css=\"${GLOBAL_STYLE_ID}\"]`)?.remove()\n const root = document.documentElement\n root.removeAttribute(ACTIVE_ATTR)\n for (const name of SKIN_CSS_VARS) root.style.removeProperty(name)\n}\n","/**\n * dsh-wx-skin — skin controller: the single owner of the skin's state and side\n * effects. It holds the current settings, applies them to the document, drives\n * the slideshow, rescans the folder, persists to localStorage + the host half,\n * and owns the document listeners.\n *\n * The UI is NOT built here. Both halves are React components registered into\n * harness slots — the settings page into `settings.section` (skin-settings.tsx)\n * and the slideshow quick actions into `sidebar.footer.action` (entries.tsx) —\n * and this controller is the value they render: they subscribe to\n * `subscribe`/`view` (a snapshot object recreated only when something changes)\n * and call back into `commit` / `next` / `previous`.\n * @module dsh-wx-skin/client/skin-controller\n */\nimport type { SkinSettings } from '../core/types.ts'\nimport {\n advanceFolder, canGoPrevious, folderSignature, imageUrl, previousFolder, refreshFolderState,\n} from './skin-folder.ts'\nimport type { PickerBridge } from './skin-picker.ts'\nimport { isDefaultSettings, loadSettings, saveSettings } from './skin-store.ts'\nimport { hostListFolder, hostLoad, hostSave } from './skin-host.ts'\nimport { SkinApplier, ensureGlobalCss, ensureLayer, teardownSkinDom } from './skin-dom.ts'\n\n/** How often a loaded folder is re-scanned in the background (ms). */\nconst FOLDER_RESCAN_MS = 60_000\n\n/** Host capabilities handed in by the browser half entry. */\nexport interface SkinControllerOptions {\n /**\n * Live bridge to the harness directory picker. The service behind it appears\n * only after the client↔host connection is up, so `available()` is asked on\n * every render instead of being snapshotted here.\n */\n picker?: PickerBridge\n /**\n * Subscribe to picker availability changes; returns an unsubscriber. Lets the\n * settings page light its 「选择文件夹」 button up without a remount.\n */\n onPickerChange?: (listener: () => void) => (() => void) | void\n}\n\n/** Immutable view the slot components render. */\nexport interface SkinView {\n /** Settings currently applied. */\n settings: SkinSettings\n /** Whether the harness directory picker is usable right now. */\n pickerReady: boolean\n}\n\n/** The skin's reactive surface, consumed by the settings page and the footer actions. */\nexport interface SkinController {\n /** Live directory-picker bridge (identity is stable; readiness comes from `view`). */\n readonly picker: PickerBridge | undefined\n /** Subscribe to view changes; returns an unsubscriber. */\n subscribe(listener: () => void): () => void\n /** Current view (stable reference until something changes). */\n view(): SkinView\n /** Apply + persist a settings change (the single commit path for every UI). */\n commit(next: SkinSettings): void\n /** Advance the slideshow by one image (no-op with no folder loaded). */\n next(): void\n /** Go back one image (no-op when there is nothing to go back to). */\n previous(): void\n /** Persist pending writes and remove everything this controller mounted. */\n dispose(): void\n}\n\n/**\n * Create the skin controller and mount its persistent pieces (stylesheet,\n * background layer, document listeners, host settings adoption).\n * @param options - host capabilities handed in by the browser half entry.\n * @returns the controller; call `dispose` to remove every artifact.\n */\nexport function createSkinController(options: SkinControllerOptions = {}): SkinController {\n const disposers: Array<() => void> = []\n ensureGlobalCss()\n ensureLayer()\n\n const applier = new SkinApplier()\n let latest = loadSettings()\n applier.apply(latest)\n\n const listeners = new Set<() => void>()\n let snapshot: SkinView = { settings: latest, pickerReady: options.picker?.available() === true }\n\n /** Publish the current view; a change-free call notifies nobody. */\n const notify = (): void => {\n const pickerReady = options.picker?.available() === true\n if (snapshot.settings === latest && snapshot.pickerReady === pickerReady) return\n snapshot = { settings: latest, pickerReady }\n for (const listener of [...listeners]) {\n try {\n listener()\n } catch (error) {\n // A broken subscriber must not take the skin (or the shell) down.\n console.warn('[dsh-wx-skin] view subscriber failed:', error)\n }\n }\n }\n\n // Debounced persistence (slider drags must not rewrite the image data URL\n // on every tick). Writes both the localStorage cache/fallback and the\n // durable host copy (deduped so an unchanged settings blob is not re-POSTed).\n let lastSentHost: string | undefined\n const syncHost = (): void => {\n const serialized = JSON.stringify(latest)\n if (lastSentHost === serialized) return\n lastSentHost = serialized\n void hostSave(latest)\n }\n const persist = (): void => {\n saveSettings(latest)\n syncHost()\n }\n let saveTimer: number | undefined\n const flushSave = (): void => {\n if (saveTimer === undefined) return\n window.clearTimeout(saveTimer)\n saveTimer = undefined\n persist()\n }\n const scheduleSave = (): void => {\n if (saveTimer !== undefined) window.clearTimeout(saveTimer)\n saveTimer = window.setTimeout(() => { persist() }, 200)\n }\n\n /** Every settings change from any UI goes through here. */\n function commitSettings(next: SkinSettings): void {\n latest = next\n applier.apply(next)\n scheduleSave()\n notify()\n }\n\n /**\n * Warm the browser cache for the image the next click will show (sequential\n * mode is deterministic; random mode draws fresh, so it is not preloaded).\n */\n const preloadNext = (current: SkinSettings): void => {\n if (current.orderMode !== 'sequential' || current.folderImages.length < 2) return\n const projected = advanceFolder(current)\n const candidate = projected.folderImages[projected.currentIndex]\n const shown = current.folderImages[current.currentIndex]\n if (candidate === undefined || shown?.path === candidate.path) return\n const image = new Image()\n image.src = imageUrl(candidate)\n }\n\n /**\n * Advance the slideshow by one image. With no folder loaded there is nothing\n * to switch to: the footer action renders as unavailable instead.\n */\n function next(): void {\n if (latest.folderImages.length === 0) return\n const advanced = advanceFolder(latest)\n commitSettings(advanced)\n preloadNext(advanced)\n }\n\n /**\n * Go back one image (the recorded previous one, or the previous in folder\n * order under sequential mode). Same rule as 下一张: nothing loaded, nothing\n * happens.\n */\n function previous(): void {\n if (!canGoPrevious(latest)) return\n const back = previousFolder(latest)\n commitSettings(back)\n preloadNext(back)\n }\n\n /**\n * Re-scan the loaded folder so images added outside the app join the queue.\n *\n * The merge never switches the background or resets the pass (see\n * refreshFolderState), and the settings are only written when the list really\n * changed — a poll that finds nothing new costs one loopback readdir.\n */\n let rescanning = false\n async function rescanFolder(): Promise<void> {\n const folderPath = latest.folderPath\n if (rescanning || folderPath === null) return\n rescanning = true\n try {\n const result = await hostListFolder(folderPath, latest.folderRecursive)\n if (!result.ok) return // unrelated failure (e.g. host restarted): keep the cache\n if (folderSignature(result.images) === folderSignature(latest.folderImages)) return\n commitSettings(refreshFolderState(latest, result.images))\n } finally {\n rescanning = false\n }\n }\n\n // The harness picker service arrives later than this controller; re-publish\n // the view so the settings page's picker button flips to ready in place.\n const offPicker = options.onPickerChange?.(() => { notify() })\n\n // Adopt the durable host copy once the controller is wired (a later arrival is\n // applied through the same single commit path as any UI change), then sync the\n // folder list so files added while the app was closed show up immediately.\n void (async () => {\n try {\n const host = await hostLoad()\n if (host !== null) {\n commitSettings(host)\n } else {\n const local = loadSettings()\n if (!isDefaultSettings(local)) void hostSave(local)\n }\n } catch {\n // Swallow — the skin must never take the shell down.\n }\n void rescanFolder()\n })()\n\n // Persist pending writes when the page goes away.\n const onBeforeUnload = (): void => flushSave()\n window.addEventListener('beforeunload', onBeforeUnload)\n\n // Folder rescan triggers: the page regaining focus is the \"I just added files\n // in Explorer\" case; the interval covers a side-by-side window; the settings\n // page opening refreshes on demand through the same path.\n const onVisibilityChange = (): void => { if (document.visibilityState === 'visible') void rescanFolder() }\n const onFocus = (): void => { void rescanFolder() }\n document.addEventListener('visibilitychange', onVisibilityChange)\n window.addEventListener('focus', onFocus)\n const rescanTimer = window.setInterval(() => { void rescanFolder() }, FOLDER_RESCAN_MS)\n\n disposers.push(() => {\n window.removeEventListener('beforeunload', onBeforeUnload)\n document.removeEventListener('visibilitychange', onVisibilityChange)\n window.removeEventListener('focus', onFocus)\n window.clearInterval(rescanTimer)\n })\n\n return {\n picker: options.picker,\n subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n view(): SkinView {\n return snapshot\n },\n commit: commitSettings,\n next,\n previous,\n dispose(): void {\n flushSave()\n offPicker?.()\n listeners.clear()\n for (const dispose of disposers.splice(0)) dispose()\n teardownSkinDom()\n },\n }\n}\n","/**\n * dsh-wx-skin — the bridge to the harness's native directory picker.\n *\n * The picker lives behind the `uiWorkspace` client service, which this plugin\n * consumes as a SCOPED OPTIONAL dependency (`ctx.inject(['uiWorkspace'], …)`):\n * the service is provided only after the client↔host connection and the remote\n * namespaces are up, which is always later than a dependency-free plugin's\n * `apply`. A value snapshotted at mount time would therefore always be\n * undefined — the bug this module exists to prevent.\n *\n * The bridge reads the live service through `current()` on every call, so the\n * panel can render \"not ready yet\" and become clickable the moment the service\n * arrives, without re-mounting anything.\n * @module dsh-wx-skin/client/skin-picker\n */\n\n/** The slice of the harness `uiWorkspace` service this plugin uses. */\nexport interface DirectoryPickerLike {\n pickDirectory?: () => Promise<string | null>\n}\n\n/** What the panel talks to, independent of when the service shows up. */\nexport interface PickerBridge {\n /** Whether the native picker is usable right now. */\n available(): boolean\n /**\n * Open the native picker.\n * @returns the picked absolute directory, or null when the service is\n * unavailable or the user cancelled; rejects when the picker itself fails.\n */\n open(): Promise<string | null>\n}\n\n/**\n * Build a bridge over a live service lookup.\n * @param current - reads the current `uiWorkspace` (undefined while absent).\n * @returns the availability check and the open action.\n */\nexport function createPickerBridge(current: () => DirectoryPickerLike | undefined): PickerBridge {\n return {\n available: () => typeof current()?.pickDirectory === 'function',\n async open(): Promise<string | null> {\n const service = current()\n if (typeof service?.pickDirectory !== 'function') return null\n return await service.pickDirectory()\n },\n }\n}\n","/**\n * dsh-wx-skin — browser half entry. Builds the skin controller (settings,\n * background layer, body-level popover, slideshow, persistence) and registers\n * the three sidebar entries into the harness's own additive slot\n * (`sidebar.footer.action`), so the shell owns their placement, ordering, the\n * collapsed rail, and teardown. Everything is wired as fiber effects, so the\n * harness can tear it down again: client-hmr unloads the old fiber before\n * re-applying a rebuilt bundle (and any row disable does the same), and only a\n * registered disposer removes the entries, the popover, the layer, and the\n * stylesheet. All mount failures are logged, never thrown.\n *\n * The only host capabilities the skin borrows are SCOPED OPTIONAL dependencies,\n * never `inject` on this plugin:\n *\n * - `uiWorkspace.pickDirectory` (the harness's native directory picker) appears\n * only after the client↔host connection is up — always later than this\n * `apply`. A value snapshotted here would always be undefined, which is\n * exactly why the panel never showed a 「选择文件夹」 button. It is watched with\n * `ctx.inject(['uiWorkspace'], …)` and read lazily through a bridge.\n * - `slots` (the slot registry) plus ui-sidebar's `sidebar.footer.action`\n * declaration. Declaring `inject: ['slots']` on the plugin would make the\n * whole browser half — background layer and persistence included — wait on a\n * service and, in a deployment without it, trip the client boot audit. The\n * scoped form keeps the skin working when the sidebar is absent; only the\n * entries would not appear.\n * @module dsh-wx-skin/client\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { FOOTER_SLOT, makeSkinFooter } from './entries.tsx'\nimport { SETTINGS_SECTION, makeSkinSettings } from './skin-settings.tsx'\nimport { createSkinController, type SkinController, type SkinControllerOptions } from './skin-controller.tsx'\nimport { createPickerBridge, type DirectoryPickerLike } from './skin-picker.ts'\nimport { teardownSkinDom } from './skin-dom.ts'\n\n/** No hard service dependencies — the skin only touches the DOM and the slots. */\nexport const inject: string[] = []\n\n/**\n * Structural face of the harness slot service (`@deepseek-ai/dsh-client-ui-renderer`).\n * Declared locally on purpose: this plugin does not depend on harness client\n * packages, exactly like `DirectoryPickerLike` in skin-picker.ts.\n *\n * `inject(key, …)` waits for a slot DECLARATION (ui-sidebar's / ui-settings', in\n * this case) and runs the callback as a child fiber effect, so a shell reload\n * tears the registrations down and rebuilds them.\n */\ninterface SlotsLike {\n inject(key: string, callback: () => unknown): () => void\n register(options: { name: string, id: string, order?: number, label?: string }, component: unknown): () => void\n}\n\n/** Apply the browser half. */\nexport function apply(ctx: Context): void {\n ctx.effect(() => {\n let service: DirectoryPickerLike | undefined\n let refresh: (() => void) | undefined\n\n // Scoped optional dependency: the child fiber starts when the harness\n // picker appears, and its disposer runs if the service goes away again.\n ctx.inject(['uiWorkspace'], (scope: Context) => {\n // `get` rather than the typed property: the Context merge for this service\n // belongs to the harness package, which this plugin does not depend on.\n service = scope.get('uiWorkspace') as DirectoryPickerLike | undefined\n refresh?.()\n scope.effect(() => () => {\n service = undefined\n refresh?.()\n }, 'dsh-wx-skin: picker release')\n })\n\n const options: SkinControllerOptions = {\n picker: createPickerBridge(() => service),\n onPickerChange: (listener) => {\n refresh = listener\n return () => {\n if (refresh === listener) refresh = undefined\n }\n },\n }\n\n // A failing external plugin must never take the GUI down.\n let controller: SkinController\n try {\n controller = createSkinController(options)\n } catch (error) {\n ctx.logger.error('[dsh-wx-skin] mount failed')\n ctx.logger.error(error)\n // A half-mounted skin leaves no layer, stylesheet, or entry behind.\n teardownSkinDom()\n return () => {}\n }\n\n // Two official seats, one registration each:\n // - `settings.section` — the skin's settings PAGE inside the harness\n // Settings panel (nav row + its own room, padding and scrolling).\n // - `sidebar.footer.action` — the slideshow quick actions at the sidebar\n // foot. ONE registration carries the pair: that row is a flex row whose\n // shipped occupant claims a full line, so sibling registrations would be\n // squeezed to zero width (see entries.tsx).\n ctx.inject(['slots'], (scope: Context) => {\n const slots = scope.get('slots') as SlotsLike | undefined\n if (slots === undefined) return () => {}\n const stops = [\n slots.inject(SETTINGS_SECTION, () => slots.register(\n { name: SETTINGS_SECTION, id: 'skin', order: 30, label: '皮肤' },\n makeSkinSettings(controller),\n )),\n slots.inject(FOOTER_SLOT, () => slots.register(\n { name: FOOTER_SLOT, id: 'skin-slideshow', order: 10, label: '换图' },\n makeSkinFooter(controller),\n )),\n ]\n return () => {\n for (const stop of stops.splice(0)) stop()\n }\n })\n\n return () => { controller.dispose() }\n }, 'dsh-wx-skin: skin mount')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["css","css"],"sources":["../src/client/skin-path.ts","../src/client/skin-folder.ts","../src/client/skin-store.ts","../src/client/entries.tsx","../src/client/image-pipeline.ts","../src/client/skin-host.ts","../src/client/SkinPanel.tsx","../src/client/skin-settings.tsx","../src/client/global-skin-css.ts","../src/client/skin-dom.ts","../src/client/skin-controller.tsx","../src/client/skin-picker.ts","../src/client/index.ts"],"sourcesContent":["/**\n * dsh-wx-skin — local-path / URL vocabulary shared by the settings page and the\n * folder state machine. Pure string work, no DOM and no `node:path`: the value\n * travels between a Windows host and the page, so both separator styles must be\n * understood on either side.\n *\n * Why this exists: the skin now describes its background by PATH — the folder\n * row shows the folder, the local-file row shows the image's absolute path (or\n * its URL) — and a pasted file path has to be split into the folder the host may\n * scan.\n * @module dsh-wx-skin/client/skin-path\n */\n\n/** Whether the value is a remote image URL the browser can paint directly. */\nexport function isRemoteImageUrl(value: string): boolean {\n return /^https?:\\/\\/\\S+$/i.test(value.trim())\n}\n\n/**\n * Whether the value looks like an absolute local path: a Windows drive\n * (`C:\\…`, `C:/…`), a UNC share (`\\\\server\\share\\…`), or a POSIX root (`/…`).\n * Relative paths are rejected — the host resolves against its own process cwd,\n * which the user cannot see.\n */\nexport function isAbsolutePath(value: string): boolean {\n const path = value.trim()\n if (/^[A-Za-z]:[\\\\/]/.test(path)) return true\n if (path.startsWith('\\\\\\\\') || path.startsWith('//')) return true\n return path.startsWith('/')\n}\n\n/** Last segment of a path or URL, separators and query stripped. */\nexport function pathBasename(value: string): string {\n const withoutQuery = value.split(/[?#]/)[0] ?? value\n const segments = withoutQuery.split(/[/\\\\]/).filter(segment => segment !== '')\n return segments[segments.length - 1] ?? ''\n}\n\n/**\n * Parent directory of an absolute path, in the path's own style.\n *\n * `D:\\pics\\a\\b.png` → `D:\\pics\\a`; `/pics/b.png` → `/pics`; `C:\\b.png` →\n * `C:\\`; `\\\\server\\share\\b.png` → `\\\\server\\share`. Returns null when the value\n * is not an absolute path or has no parent to scan (a bare volume root).\n * @param value - absolute path.\n * @returns the parent directory, or null when there is none.\n */\nexport function parentDirectory(value: string): string | null {\n const path = value.trim().replace(/[/\\\\]+$/, '')\n if (!isAbsolutePath(path)) return null\n const separator = path.includes('\\\\') ? '\\\\' : '/'\n const cut = Math.max(path.lastIndexOf('\\\\'), path.lastIndexOf('/'))\n // A POSIX root (`/b.png`) has its separator at index 0; anything else without\n // a separator is not absolute.\n if (cut < 0 || (cut === 0 && !path.startsWith('/'))) return null\n const parent = path.slice(0, cut)\n if (parent === '') return separator\n // `D:\\b.png` → `D:\\`; an UNC share (`\\\\server\\share\\b.png`) needs no suffix.\n if (/^[A-Za-z]:$/.test(parent)) return `${parent}${separator}`\n return parent\n}\n\n/**\n * Whether two absolute paths name the same file. Windows paths compare\n * case-insensitively and tolerate either separator; the host returns\n * canonicalized paths, the user types whatever Explorer showed.\n * @param left - first path.\n * @param right - second path.\n * @returns true when they may be the same file.\n */\nexport function samePath(left: string, right: string): boolean {\n const normalize = (value: string): string => {\n const unified = value.trim().replace(/\\\\/g, '/').replace(/\\/+$/, '')\n return /^[A-Za-z]:/.test(unified) || unified.startsWith('//') ? unified.toLowerCase() : unified\n }\n return normalize(left) === normalize(right)\n}\n","/**\n * dsh-wx-skin — folder slideshow state machine (pure, DOM-free).\n *\n * The host scans a folder once and returns its images; everything after that is\n * decided here: the slideshow order, which image the next click shows, and the\n * \"already shown\" bookkeeping. The rule the UI promises is a pass over the\n * folder: an image that has been shown is never picked again until every image\n * has been shown, at which point the pass restarts — and the restart does not\n * immediately repeat the image on screen (unless it is the only one).\n *\n * `advanceFolder` takes its randomness through `rng` so tests are deterministic.\n * @module dsh-wx-skin/client/skin-folder\n */\nimport type { SkinFolderImage, SkinSettings } from '../core/types.ts'\nimport { samePath } from './skin-path.ts'\n\n/**\n * Browser reference to the host route serving one image file. Document-relative\n * for the same reason as the paths in `skin-host.ts`: the shell's\n * `<base href=\"./\">` resolves it under whatever mount served the page, and the\n * URL is consumed by `new Image().src`, `preloadNext`, and the\n * `--wx-skin-bg-image` inline custom property (inline-style URLs resolve\n * against the document base URL too).\n */\nexport const IMAGE_ROUTE = 'dsh-wx-skin/image'\n\n/** Upper bound on the cached list (mirrors the host scan bound). */\nexport const MAX_CACHED_IMAGES = 2000\n\n/** Natural-order comparator on the root-relative path (`img2` before `img10`). */\nconst collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })\n\n/**\n * Index of the cached image whose file name matches `name`, or -1.\n *\n * This is how a file chosen through the browser's file dialog joins the\n * slideshow: that dialog hands over the name and nothing else (no directory), so\n * when the loaded folder already holds exactly one image with that name, the\n * cursor can be placed on it — 下一张 then continues in folder order from the\n * file the user actually picked.\n *\n * An ambiguous name (the same file name in two scanned subfolders) returns -1:\n * guessing which one was picked would silently show the wrong image.\n * @param images - cached folder images.\n * @param name - file name to look for (case-insensitive).\n * @returns the index, or -1 when absent or ambiguous.\n */\nexport function matchFolderImageByName(images: readonly SkinFolderImage[], name: string): number {\n const wanted = name.trim().toLowerCase()\n if (wanted === '') return -1\n let found = -1\n for (const [index, image] of images.entries()) {\n if (image.name.toLowerCase() !== wanted) continue\n if (found >= 0) return -1 // ambiguous\n found = index\n }\n return found\n}\n\n/** Return a copy of `images` in slideshow order. */\nexport function sortFolderImages(images: readonly SkinFolderImage[]): SkinFolderImage[] {\n return [...images].sort((left, right) => collator.compare(left.rel, right.rel))\n}\n\n/** The host URL for one cached image; `v` invalidates the browser cache when the file changes. */\nexport function imageUrl(image: SkinFolderImage): string {\n return `${IMAGE_ROUTE}?p=${encodeURIComponent(image.path)}&v=${encodeURIComponent(String(image.mtimeMs))}`\n}\n\n/** Slideshow counters for the panel's status line. */\nexport interface FolderStatus {\n /** 1-based position of the shown image, 0 when none is shown. */\n position: number\n /** Total cached images. */\n total: number\n /** Images already shown in the current pass. */\n usedThisPass: number\n}\n\n/** Read the panel-facing counters out of the settings. */\nexport function folderStatus(settings: SkinSettings): FolderStatus {\n const total = settings.folderImages.length\n const used = new Set(settings.folderImages.map(image => image.path))\n return {\n position: settings.currentIndex >= 0 && settings.currentIndex < total ? settings.currentIndex + 1 : 0,\n total,\n usedThisPass: settings.usedPaths.filter(path => used.has(path)).length,\n }\n}\n\n/** Back-history bound: a long slideshow must not grow the persisted blob forever. */\nexport const MAX_HISTORY = 64\n\n/** Append the image being left behind, keeping only the most recent entries. */\nfunction pushHistory(history: readonly string[], outgoing: string | undefined): string[] {\n const next = outgoing === undefined ? [...history] : [...history, outgoing]\n return next.slice(-MAX_HISTORY)\n}\n\n/**\n * Settings showing one folder image. The other background kinds are cleared\n * with it, so a stale data URL or URL cannot linger in the persisted blob (and\n * a later 下一张 cannot leave the page claiming two sources at once).\n */\nfunction showingFolderImage(\n settings: SkinSettings,\n index: number,\n records: Pick<SkinSettings, 'usedPaths' | 'history'>,\n): SkinSettings {\n return {\n ...settings,\n enabled: true,\n source: 'folder',\n imageDataUrl: null,\n imageName: null,\n url: null,\n currentIndex: index,\n usedPaths: records.usedPaths,\n history: records.history,\n }\n}\n\n/**\n * Advance the slideshow by one image.\n *\n * SEQUENTIAL walks the folder in order — the image after the current one, then\n * the next, wrapping at the end — so picking image 10 makes 下一张 show 11 (and\n * 上一张 show 9). The pass record is not used here: a walk in index order never\n * repeats an image until it wraps, which is the whole point of the mode.\n *\n * RANDOM draws uniformly from the images not shown in this pass; when none\n * remain the pass restarts (the used set is cleared) excluding the image on\n * screen, so a full cycle never repeats back-to-back.\n *\n * With an empty folder the settings are returned unchanged apart from clearing\n * the cursor, so callers can treat \"index did not move\" as \"no folder loaded\".\n * @param settings - current settings.\n * @param rng - uniform [0,1) source for random mode (injected by tests).\n * @returns the next settings value.\n */\nexport function advanceFolder(settings: SkinSettings, rng: () => number = Math.random): SkinSettings {\n const images = settings.folderImages\n if (images.length === 0) return { ...settings, currentIndex: -1, usedPaths: [], history: [] }\n\n if (settings.orderMode !== 'random') {\n const index = settings.currentIndex < 0 ? 0 : (settings.currentIndex + 1) % images.length\n return showingFolderImage(settings, index, { usedPaths: [], history: [] })\n }\n\n const indexOfPath = new Map(images.map((image, index) => [image.path, index] as const))\n const used = new Set<number>()\n for (const path of settings.usedPaths) {\n const index = indexOfPath.get(path)\n if (index !== undefined) used.add(index)\n }\n\n let candidates = images.map((_, index) => index).filter(index => !used.has(index))\n let nextUsed = used\n if (candidates.length === 0) {\n // The pass is complete: clear the record, but never re-pick what is on screen.\n nextUsed = new Set<number>()\n candidates = images.map((_, index) => index)\n .filter(index => images.length === 1 || index !== settings.currentIndex)\n }\n\n const picked = candidates[Math.min(candidates.length - 1, Math.max(0, Math.floor(rng() * candidates.length)))]\n /* v8 ignore next -- candidates is non-empty: either image indices exist or the single-image pass is non-empty. */\n if (picked === undefined) return settings\n\n const usedThisPass = [...nextUsed, picked].sort((left, right) => left - right)\n return showingFolderImage(settings, picked, {\n usedPaths: usedThisPass.map(index => (images[index] as SkinFolderImage).path),\n // The image being left behind is where 上一张 goes back to.\n history: pushHistory(settings.history, images[settings.currentIndex]?.path),\n })\n}\n\n/**\n * Whether 「上一张」 has anywhere to go: sequential mode walks the folder order,\n * so any folder with more than one image has a previous one; random mode needs a\n * recorded earlier image.\n * @param settings - current settings.\n * @returns true when the previous-image action would move.\n */\nexport function canGoPrevious(settings: SkinSettings): boolean {\n if (settings.folderImages.length === 0) return false\n if (settings.orderMode !== 'random') return settings.folderImages.length > 1\n return settings.history.length > 0\n}\n\n/**\n * Go back one image.\n *\n * Sequential steps one index back through the folder order, wrapping from the\n * first to the last (picking 10 then 上一张 shows 9). Random pops the recorded\n * previous image; with no history it returns the settings unchanged.\n *\n * Going back never rewrites the pass record: everything in the history was\n * already shown.\n * @param settings - current settings.\n * @returns the previous settings value.\n */\nexport function previousFolder(settings: SkinSettings): SkinSettings {\n const images = settings.folderImages\n if (!canGoPrevious(settings)) return settings\n\n if (settings.orderMode !== 'random') {\n const from = settings.currentIndex >= 0 ? settings.currentIndex : 0\n const index = (from - 1 + images.length) % images.length\n return showingFolderImage(settings, index, { usedPaths: [], history: [] })\n }\n\n const history = [...settings.history]\n const recorded = history.pop()\n const recordedIndex = recorded === undefined ? -1 : images.findIndex(image => samePath(image.path, recorded))\n // Nothing shown yet reads as \"before the first image\", so the wrap lands on the last.\n const from = settings.currentIndex >= 0 ? settings.currentIndex : 0\n const index = recordedIndex >= 0 ? recordedIndex : (from - 1 + images.length) % images.length\n\n const shown = images[index] as SkinFolderImage\n const used = new Set(settings.usedPaths)\n used.add(shown.path)\n return showingFolderImage(settings, index, {\n usedPaths: images.filter(image => used.has(image.path)).map(image => image.path),\n history,\n })\n}\n\n/**\n * Stable signature of a cached list (path + size + mtime), so a rescan can tell\n * \"nothing changed\" from \"files were added, removed, or edited\" without writing\n * settings on every poll.\n * @param images - cached images.\n * @returns a comparable string.\n */\nexport function folderSignature(images: readonly SkinFolderImage[]): string {\n return images.map(image => `${image.path}\\u0000${image.size}\\u0000${image.mtimeMs}`).join('\\n')\n}\n\n/**\n * Merge a fresh scan of the SAME folder in without disturbing the slideshow.\n *\n * This is what an automatic rescan uses: the shown image is kept (matched by\n * path, or — when its file disappeared — the slot it occupied), and the pass\n * record and back-history keep only paths that still exist. Images added since\n * the last scan simply join the list, so 「下一张」 reaches them.\n * The active source is deliberately untouched: a rescan while a URL background\n * is on screen must not switch the background back to the folder.\n * @param settings - current settings.\n * @param images - the freshly scanned images.\n * @returns settings whose list and records track the folder.\n */\nexport function refreshFolderState(\n settings: SkinSettings,\n images: readonly SkinFolderImage[],\n): SkinSettings {\n const sorted = sortFolderImages(images).slice(0, MAX_CACHED_IMAGES)\n const kept = new Set(sorted.map(image => image.path))\n const currentPath = settings.folderImages[settings.currentIndex]?.path\n const matched = currentPath === undefined ? -1 : sorted.findIndex(image => image.path === currentPath)\n const shownIndex = settings.currentIndex >= 0 && sorted.length > 0\n ? Math.min(settings.currentIndex, sorted.length - 1)\n : -1\n return {\n ...settings,\n folderImages: sorted,\n currentIndex: matched >= 0 ? matched : sorted.length === 0 ? -1 : shownIndex,\n usedPaths: settings.usedPaths.filter(path => kept.has(path)),\n history: settings.history.filter(path => kept.has(path)),\n }\n}\n\n/**\n * Show one cached folder image, clearing the other background kinds. This is the\n * \"the user picked THIS image\" commit shared by the local-file row and the\n * file-dialog flow.\n * @param settings - current settings.\n * @param index - index into `settings.folderImages`.\n * @returns the settings showing that image, or the input when the index is stale.\n */\nexport function focusFolderImage(settings: SkinSettings, index: number): SkinSettings {\n if (index < 0 || index >= settings.folderImages.length) return settings\n return showingFolderImage(settings, index, { usedPaths: [], history: [] })\n}\n\n/**\n * Fold a scan into the settings when the user loads (or reloads) a folder.\n *\n * The same folder is MERGED, so adding or removing files never resets the\n * slideshow — new files simply join the list. A different folder starts fresh.\n * Either way the other background sources are cleared, so localStorage keeps\n * only the (small) path cache.\n *\n * `focusPath` is the \"open THIS image\" case behind the local-file row: a file\n * was named (typed, pasted, or derived from a folder), so the shown image is\n * that file — which also fixes where 下一张/上一张 walk from.\n * @param settings - current settings.\n * @param folderPath - the folder that was just scanned.\n * @param recursive - whether that scan descended into subdirectories.\n * @param images - the scanned images, already in slideshow order.\n * @param focusPath - absolute path of the image to show, when the caller named one.\n * @returns settings pointing at the folder, with one image picked when any exist.\n */\nexport function loadFolderState(\n settings: SkinSettings,\n folderPath: string,\n recursive: boolean,\n images: readonly SkinFolderImage[],\n focusPath?: string,\n): SkinSettings {\n const sorted = sortFolderImages(images).slice(0, MAX_CACHED_IMAGES)\n const activated: SkinSettings = {\n ...settings,\n enabled: true,\n source: 'folder',\n imageDataUrl: null,\n imageName: null,\n url: null,\n folderPath,\n folderRecursive: recursive,\n folderImages: sorted,\n }\n if (focusPath !== undefined) {\n const focused = sorted.findIndex(image => samePath(image.path, focusPath))\n if (focused >= 0) return { ...activated, currentIndex: focused, usedPaths: [], history: [] }\n }\n if (folderPath === settings.folderPath) {\n const merged = refreshFolderState(activated, sorted)\n // An explicit load of a folder that had no images yet still shows one.\n return merged.currentIndex >= 0 || merged.folderImages.length === 0 ? merged : advanceFolder(merged)\n }\n const reset: SkinSettings = { ...activated, currentIndex: -1, usedPaths: [], history: [] }\n return reset.folderImages.length === 0 ? reset : advanceFolder(reset)\n}\n","/**\n * dsh-wx-skin — pure skin settings state: defaults, sanitization, persistence\n * (localStorage), and the CSS-variable projection consumed by the DOM applier.\n * No DOM access here — tests exercise these functions with a fake storage.\n * @module dsh-wx-skin/client/skin-store\n */\nimport type { SkinFitMode, SkinFolderImage, SkinOrderMode, SkinSettings, SkinSource } from '../core/types.ts'\nimport { MAX_CACHED_IMAGES, imageUrl } from './skin-folder.ts'\n\n/** localStorage key holding the durable settings JSON. */\nexport const STORAGE_KEY = 'dsh-wx-skin.settings'\n\n/** Document attribute that gates the global skin CSS (set on documentElement). */\nexport const ACTIVE_ATTR = 'data-wx-skin-active'\n\n/** Attribute on the injected background layer div. */\nexport const LAYER_ATTR = 'data-wx-skin-layer'\n\n/** Attribute on the slot-rendered sidebar entry button (styling + test hook). */\nexport const ENTRY_ATTR = 'data-wx-skin-entry'\n\n/** CSS custom properties the applier writes on documentElement. */\nexport const SKIN_CSS_VARS = [\n '--wx-skin-bg-image',\n '--wx-skin-bg-size',\n '--wx-skin-bg-repeat',\n '--wx-skin-bg-position',\n '--wx-skin-scrim',\n '--wx-skin-blur',\n '--wx-skin-surface',\n] as const\n\n/** Clamp a number into [min, max]. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nconst SOURCES: readonly SkinSource[] = ['image', 'url', 'folder', 'none']\n\nconst ORDER_MODES: readonly SkinOrderMode[] = ['sequential', 'random']\n\n/** Background fit modes, in the order the settings page offers them. */\nexport const FIT_MODES: readonly SkinFitMode[] = ['fill', 'fit', 'stretch', 'tile', 'center']\n\n/** The three CSS longhands one fit mode projects onto the background layer. */\nexport interface FitProjection {\n /** `background-size`. */\n size: string\n /** `background-repeat`. */\n repeat: string\n /** `background-position`. */\n position: string\n}\n\n/**\n * Map a fit mode onto the background layer's CSS — the desktop wallpaper\n * vocabulary: fill crops, fit letterboxes, stretch may distort, tile repeats at\n * the image's natural size, and center shows one natural-size image in the\n * middle.\n * @param fit - the chosen mode.\n * @returns the longhand values for `--wx-skin-bg-*`.\n */\nexport function fitProjection(fit: SkinFitMode): FitProjection {\n if (fit === 'fit') return { size: 'contain', repeat: 'no-repeat', position: 'center' }\n if (fit === 'stretch') return { size: '100% 100%', repeat: 'no-repeat', position: 'center' }\n if (fit === 'tile') return { size: 'auto', repeat: 'repeat', position: 'left top' }\n if (fit === 'center') return { size: 'auto', repeat: 'no-repeat', position: 'center' }\n return { size: 'cover', repeat: 'no-repeat', position: 'center' }\n}\n\nconst MAX_DIM = 0.8\nconst MAX_BLUR = 24\n/** Surface opacity bounds (0..1 — fully adjustable; readability is handled by the dim slider). */\nexport const MIN_SURFACE = 0\nexport const MAX_SURFACE = 1\nexport const DEFAULT_SURFACE = 0.72\n\n/** Fresh defaults for a clean install. */\nexport const DEFAULT_SETTINGS: SkinSettings = Object.freeze({\n enabled: false,\n source: 'none',\n imageDataUrl: null,\n imageName: null,\n url: null,\n dim: 0.35,\n blur: 0,\n surface: DEFAULT_SURFACE,\n fit: 'fill',\n folderPath: null,\n folderImages: [],\n folderRecursive: false,\n orderMode: 'sequential',\n currentIndex: -1,\n usedPaths: [],\n history: [],\n})\n\n/**\n * Coerce an unknown persisted value into a valid folder image list: entries\n * must carry a usable path/name/rel and finite numbers, duplicates collapse on\n * the path (the slideshow identity), and the list is capped like the host scan.\n */\nexport function sanitizeFolderImages(raw: unknown): SkinFolderImage[] {\n if (!Array.isArray(raw)) return []\n const seen = new Set<string>()\n const images: SkinFolderImage[] = []\n for (const entry of raw) {\n if (typeof entry !== 'object' || entry === null) continue\n const record = entry as Record<string, unknown>\n const path = typeof record.path === 'string' && record.path !== '' ? record.path : null\n if (path === null || seen.has(path)) continue\n seen.add(path)\n images.push({\n path,\n name: typeof record.name === 'string' && record.name !== '' ? record.name : path,\n rel: typeof record.rel === 'string' && record.rel !== '' ? record.rel : path,\n size: typeof record.size === 'number' && Number.isFinite(record.size) ? record.size : 0,\n mtimeMs: typeof record.mtimeMs === 'number' && Number.isFinite(record.mtimeMs) ? record.mtimeMs : 0,\n })\n if (images.length >= MAX_CACHED_IMAGES) break\n }\n return images\n}\n\n/** Coerce an unknown persisted value into a valid SkinSettings. */\nexport function sanitizeSettings(raw: unknown): SkinSettings {\n const o = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>\n const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null)\n const num = (v: unknown, fallback: number): number => {\n const n = typeof v === 'number' && Number.isFinite(v) ? v : fallback\n return n\n }\n const source: SkinSource = SOURCES.includes(o.source as SkinSource) ? (o.source as SkinSource) : 'none'\n const folderImages = sanitizeFolderImages(o.folderImages)\n const indexed = typeof o.currentIndex === 'number' && Number.isFinite(o.currentIndex) ? Math.trunc(o.currentIndex) : -1\n const keptPaths = new Set(folderImages.map(image => image.path))\n return {\n enabled: o.enabled === true,\n source,\n imageDataUrl: str(o.imageDataUrl),\n imageName: str(o.imageName),\n url: str(o.url),\n dim: clamp(num(o.dim, DEFAULT_SETTINGS.dim), 0, MAX_DIM),\n blur: clamp(num(o.blur, DEFAULT_SETTINGS.blur), 0, MAX_BLUR),\n surface: clamp(num(o.surface, DEFAULT_SETTINGS.surface), MIN_SURFACE, MAX_SURFACE),\n fit: FIT_MODES.includes(o.fit as SkinFitMode) ? (o.fit as SkinFitMode) : DEFAULT_SETTINGS.fit,\n folderPath: str(o.folderPath),\n folderImages,\n folderRecursive: o.folderRecursive === true,\n orderMode: ORDER_MODES.includes(o.orderMode as SkinOrderMode) ? (o.orderMode as SkinOrderMode) : 'sequential',\n // A stale index (or one pointing past a shrunk list) reads as \"nothing shown\".\n currentIndex: indexed >= 0 && indexed < folderImages.length ? indexed : -1,\n // Only paths that still exist in the cached list count as already shown.\n usedPaths: Array.isArray(o.usedPaths)\n ? o.usedPaths.filter((path): path is string => typeof path === 'string' && keptPaths.has(path))\n : [],\n // Only paths still present in the cached list can be gone back to.\n history: Array.isArray(o.history)\n ? o.history.filter((path): path is string => typeof path === 'string' && keptPaths.has(path))\n : [],\n }\n}\n\n/** Read + sanitize the persisted settings, or the defaults. */\nexport function loadSettings(storage: Pick<Storage, 'getItem'> = globalThis.localStorage): SkinSettings {\n try {\n const text = storage.getItem(STORAGE_KEY)\n if (text === null) return { ...DEFAULT_SETTINGS }\n return sanitizeSettings(JSON.parse(text))\n } catch {\n return { ...DEFAULT_SETTINGS }\n }\n}\n\n/** Persist settings; quota errors are swallowed (the caller surfaces a message). */\nexport function saveSettings(settings: SkinSettings, storage: Pick<Storage, 'setItem'> = globalThis.localStorage): boolean {\n try {\n storage.setItem(STORAGE_KEY, JSON.stringify(settings))\n return true\n } catch {\n return false\n }\n}\n\n/** Remove the persisted settings (restores defaults on next boot). */\nexport function clearSettings(storage: Pick<Storage, 'removeItem'> = globalThis.localStorage): void {\n try {\n storage.removeItem(STORAGE_KEY)\n } catch {\n // Ignore.\n }\n}\n\n/** Whether the settings equal the pristine defaults (used to skip seeding the\n * host copy from localStorage on the first run after upgrade). */\nexport function isDefaultSettings(settings: SkinSettings): boolean {\n return settings.enabled === DEFAULT_SETTINGS.enabled\n && settings.source === DEFAULT_SETTINGS.source\n && settings.imageDataUrl === DEFAULT_SETTINGS.imageDataUrl\n && settings.imageName === DEFAULT_SETTINGS.imageName\n && settings.url === DEFAULT_SETTINGS.url\n && settings.dim === DEFAULT_SETTINGS.dim\n && settings.blur === DEFAULT_SETTINGS.blur\n && settings.surface === DEFAULT_SETTINGS.surface\n && settings.fit === DEFAULT_SETTINGS.fit\n && settings.folderPath === DEFAULT_SETTINGS.folderPath\n && settings.folderImages.length === 0\n && settings.folderRecursive === DEFAULT_SETTINGS.folderRecursive\n && settings.orderMode === DEFAULT_SETTINGS.orderMode\n && settings.currentIndex === DEFAULT_SETTINGS.currentIndex\n && settings.usedPaths.length === 0\n && settings.history.length === 0\n}\n\n/**\n * The name of the image currently on screen: a short label for the panel plus\n * the full reference for its hover title.\n */\nexport interface CurrentImageLabel {\n /** Name shown in the panel (may be ellipsized by CSS). */\n label: string\n /** Full path / URL behind the hover title. */\n detail: string\n}\n\n/**\n * Last path segment of a URL, decoded. Never throws: a value that is not an\n * absolute URL falls back to splitting on `/`, `\\`, `?`, and `#` (a pasted\n * Windows path is a realistic mistake in the URL box), and a malformed\n * percent-escape is returned as written.\n */\nexport function urlBasename(url: string): string {\n const lastSegment = (value: string): string => {\n const path = value.split(/[?#]/)[0] ?? value\n const segments = path.split(/[/\\\\]/).filter(segment => segment !== '')\n return segments[segments.length - 1] ?? ''\n }\n let last = ''\n try {\n const parsed = new URL(url)\n last = lastSegment(parsed.pathname) === '' ? parsed.host : lastSegment(parsed.pathname)\n } catch {\n last = lastSegment(url)\n }\n if (last === '') return url\n try {\n return decodeURIComponent(last)\n } catch {\n return last\n }\n}\n\n/**\n * Resolve what the panel should call the current background.\n *\n * A disabled skin has nothing on screen, so it reports no name. Folder mode\n * uses the cached file name of the shown image — an index that no longer\n * matches the list reports nothing rather than the wrong name.\n * @param settings - current settings.\n * @returns the label/detail pair, or null when there is nothing to name.\n */\nexport function currentImageLabel(settings: SkinSettings): CurrentImageLabel | null {\n if (!settings.enabled) return null\n if (settings.source === 'folder') {\n const image = settings.folderImages[settings.currentIndex]\n if (image === undefined) return null\n return { label: image.name, detail: image.path }\n }\n if (settings.source === 'image') {\n // The data URL itself is useless as a tooltip; only the picked file name is.\n const name = settings.imageName ?? '本地图片'\n return { label: name, detail: name }\n }\n if (settings.source === 'url') {\n if (settings.url === null) return null\n return { label: urlBasename(settings.url), detail: settings.url }\n }\n return null\n}\n\n/**\n * Project the settings onto the CSS custom properties the skin layer reads.\n * Returns an empty map when the skin is disabled (the applier then retracts\n * every variable).\n */\nexport function cssVariables(settings: SkinSettings): Partial<Record<(typeof SKIN_CSS_VARS)[number], string>> {\n if (!settings.enabled) return {}\n const vars: Partial<Record<(typeof SKIN_CSS_VARS)[number], string>> = {}\n if (settings.source === 'image' && settings.imageDataUrl !== null) {\n vars['--wx-skin-bg-image'] = `url(\"${settings.imageDataUrl}\")`\n } else if (settings.source === 'url' && settings.url !== null) {\n vars['--wx-skin-bg-image'] = `url(\"${escapeCssUrl(settings.url)}\")`\n } else if (settings.source === 'folder') {\n // Served by the host from inside the picked folder; the URL changes with the\n // file's mtime, so the browser cache never shows a stale image.\n const image = settings.folderImages[settings.currentIndex]\n if (image !== undefined) vars['--wx-skin-bg-image'] = `url(\"${escapeCssUrl(imageUrl(image))}\")`\n }\n const projection = fitProjection(settings.fit)\n vars['--wx-skin-bg-size'] = projection.size\n vars['--wx-skin-bg-repeat'] = projection.repeat\n vars['--wx-skin-bg-position'] = projection.position\n vars['--wx-skin-scrim'] = `rgba(0, 0, 0, ${settings.dim.toFixed(3)})`\n vars['--wx-skin-blur'] = `${Math.round(settings.blur)}px`\n vars['--wx-skin-surface'] = settings.surface.toFixed(3)\n return vars\n}\n\n/** Escape quotes so a user-supplied URL cannot break out of `url(\"...\")`. */\nexport function escapeCssUrl(url: string): string {\n return url.replace(/[\"\\\\\\n\\r]/g, char => `\\\\${char}`)\n}\n","/**\n * dsh-wx-skin — the sidebar-foot slideshow actions, registered into the\n * harness's own additive slot instead of injecting DOM into the shell's sidebar.\n *\n * DSH declares `sidebar.footer.action` (`kind: 'list'`, `scope: 'root'`,\n * `replaceRisk: 'none'`) as the sanctioned seat for extra sidebar-foot actions:\n * the shell renders one row per registration, in ascending `order`, directly\n * inside its `.footerActions` flex row above Settings, and hands each occupant\n * the live column state as `{ wide }`. Registering there replaces the plugin's\n * old `[class*=\"sidebarCol\"]` / `[class*=\"logoRow\"]` / `button[class*=\"newSession\"]`\n * DOM hunt plus its two MutationObservers.\n *\n * These are pure ACTIONS (换图), not settings: the skin's settings live in the\n * harness Settings panel (`settings.section`, see skin-settings.tsx), which\n * owns its own room, padding and scrolling.\n *\n * TWO SHAPE CONSTRAINTS come from the seat's box model, and both are handled\n * here rather than by guessing at the shell's internals:\n *\n * 1. ONE registration, not two. The row is a flex row and its shipped occupant\n * (`cordis-panel`, ui-cordis) claims the whole line with\n * `width: 100%; flex: none`; separate sibling registrations would be laid out\n * after a line-consuming item and squeezed to zero width.\n * 2. The group asks the ROW to wrap (`flex-wrap: wrap` on its own parent, set and\n * restored by effect). Without it, a second full-line occupant cannot share\n * the line at all; with it every occupant keeps its own line.\n * @module dsh-wx-skin/client/entries\n */\nimport { useEffect, useRef, useSyncExternalStore } from 'react'\nimport type { SkinController, SkinView } from './skin-controller.tsx'\nimport { canGoPrevious, folderStatus } from './skin-folder.ts'\nimport { currentImageLabel } from './skin-store.ts'\nimport css from './skin.module.css'\n\n/** The official additive seat these actions register into. */\nexport const FOOTER_SLOT = 'sidebar.footer.action'\n\n/** Owner props every `sidebar.footer.action` occupant receives. */\nexport interface FooterEntryProps {\n /** Whether the sidebar renders wide content (false = 56px rail, icon only). */\n wide?: boolean\n}\n\nconst NEXT_ICON = '<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 8h9\"/><path d=\"M8.5 4.5 12 8l-3.5 3.5\"/></svg>'\nconst PREV_ICON = '<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M13 8H4\"/><path d=\"M7.5 4.5 4 8l3.5 3.5\"/></svg>'\n\n/** One action button: icon always, label only in the wide sidebar. */\nfunction EntryButton({\n wide, role, label, title, icon, empty, onClick,\n}: {\n wide: boolean\n role: 'prev' | 'next'\n label: string\n title: string\n icon: string\n empty?: boolean\n onClick: () => void\n}): JSX.Element {\n return (\n <button\n type=\"button\"\n data-wx-skin-entry=\"\"\n data-wx-skin-entry-role={role}\n data-empty={empty === true ? 'true' : undefined}\n aria-disabled={empty === true ? 'true' : undefined}\n aria-label={label}\n title={title}\n className={wide ? `${css.footerAction} ${css.footerActionWide}` : `${css.footerAction} ${css.footerActionRail}`}\n onClick={onClick}\n >\n <span className={css.entryIcon} dangerouslySetInnerHTML={{ __html: icon }} />\n {wide ? <span className={css.entryLabel}>{label}</span> : null}\n </button>\n )\n}\n\n/**\n * Build the sidebar-foot group: 「上一张」「下一张」 on the wide sidebar, the same\n * two icons on the collapsed rail. Both are inert while no folder is loaded.\n * @param controller - the skin controller the actions drive.\n * @returns the component to register into {@link FOOTER_SLOT}.\n */\nexport function makeSkinFooter(controller: SkinController): (props: FooterEntryProps) => JSX.Element {\n const subscribe = (listener: () => void): (() => void) => controller.subscribe(listener)\n const getSnapshot = (): SkinView => controller.view()\n\n return function SkinFooter({ wide = true }: FooterEntryProps): JSX.Element {\n const { settings } = useSyncExternalStore(subscribe, getSnapshot)\n const group = useRef<HTMLDivElement>(null)\n const status = folderStatus(settings)\n const name = currentImageLabel(settings)\n const here = name === null ? '' : ` · 当前 ${name.label}`\n\n // Let the shell's footer row wrap: its shipped occupant claims a full line\n // with `flex: none`, so without wrapping a second occupant would be clipped.\n // Only the element this group was placed in is touched, and it is restored\n // on unmount (client-hmr reload, row disable).\n useEffect(() => {\n const parent = group.current?.parentElement\n if (parent === null || parent === undefined) return\n const previous = parent.style.flexWrap\n parent.style.flexWrap = 'wrap'\n return () => { parent.style.flexWrap = previous }\n }, [])\n\n const previousEnabled = canGoPrevious(settings)\n const previousTitle = previousEnabled\n ? `上一张${here}`\n : status.total === 0\n ? '先加载图片文件夹'\n : settings.orderMode === 'random' ? '随机模式还没有可返回的上一张' : '文件夹里只有这一张图片'\n const nextEmpty = status.total === 0\n const nextTitle = nextEmpty\n ? '先加载图片文件夹'\n : `${settings.orderMode === 'random' ? '随机' : '顺序'}下一张 · 第 ${status.position}/${status.total} 张${here}`\n\n return (\n <div\n ref={group}\n className={`${css.footerGroup} ${wide ? css.footerGroupWide : css.footerGroupRail}`}\n data-wx-skin-footer=\"\"\n >\n <EntryButton\n wide={wide}\n role=\"prev\"\n label=\"上一张\"\n title={previousTitle}\n icon={PREV_ICON}\n empty={!previousEnabled}\n onClick={() => { controller.previous() }}\n />\n <EntryButton\n wide={wide}\n role=\"next\"\n label=\"下一张\"\n title={nextTitle}\n icon={NEXT_ICON}\n empty={nextEmpty}\n onClick={() => { controller.next() }}\n />\n </div>\n )\n }\n}\n","/**\n * dsh-wx-skin — local-image pipeline: read a picked file, downscale it to a\n * bounded canvas, and re-encode it to a data URL. The browser implementation\n * is injected through `PipelineEnv` so tests drive it with a fake source; the\n * pure `fileToDataUrl` logic is environment-agnostic.\n *\n * There is intentionally no input-size or output-size limit: any image the\n * canvas can decode is accepted (no \"too large\" rejection). The only bound is\n * the downscale to `maxEdge`, which keeps the resulting data URL small enough\n * to persist while preserving a sharp full-screen background.\n * @module dsh-wx-skin/client/image-pipeline\n */\n\n/** A decodable raster image held by the pipeline. */\nexport interface ImageSource {\n width: number\n height: number\n /** Draw the source scaled to w×h into a fresh canvas and encode it. */\n render(w: number, h: number, type: string, quality: number): string\n /** Release any held resources (object URLs, …). */\n release(): void\n}\n\n/** The browser environment the pipeline runs against. */\nexport interface PipelineEnv {\n /** Decode a file into an ImageSource. Throws when the file is not readable. */\n readImage(file: Blob): Promise<ImageSource>\n}\n\n/** Error the pipeline throws with a stable machine-readable code. */\nexport class ImagePipelineError extends Error {\n constructor(\n public readonly code: 'unsupported' | 'decode' | 'encode',\n message: string,\n ) {\n super(message)\n this.name = 'ImagePipelineError'\n }\n}\n\n/** Tuning knobs for the encode. */\nexport interface DownscaleOptions {\n /**\n * Optional explicit cap on the longest edge of the first encode (px).\n * Omitted by default — the image keeps its original resolution up to\n * {@link DISPLAY_MAX_EDGE}; larger images are downscaled for display (a\n * background never needs more than ~4K, and oversized canvases silently\n * produce empty data URLs, which is exactly the \"selected but not shown\"\n * failure). Picking a file is never limited by size.\n */\n maxEdge?: number\n /** JPEG quality (0..1). Default 0.82. */\n quality?: number\n /**\n * Data-URL length budget (chars). When the encoded result exceeds this,\n * the pipeline silently re-encodes at smaller sizes so the skin stays\n * persistable; it never throws. Default 4_000_000 (safe under the ~5 MB\n * localStorage quota).\n */\n storageBudget?: number\n}\n\n/**\n * Display cap for the encoded background: images larger than this (per side)\n * are downscaled so the canvas stays inside every browser's limits and the\n * data URL stays small enough to persist. 4096 px is sharper than any screen\n * needs for a background.\n */\nexport const DISPLAY_MAX_EDGE = 4096\n\n/** Raster formats the pipeline accepts (canvas can re-encode these safely). */\nexport const ALLOWED_TYPES = [\n 'image/png', 'image/jpeg', 'image/jpg', 'image/pjpeg', 'image/x-png',\n 'image/webp', 'image/gif', 'image/bmp', 'image/x-ms-bmp',\n] as const\n\n/** Default browser environment: object URL + <img> decode + canvas encode. */\nexport const browserEnv: PipelineEnv = {\n async readImage(file: Blob): Promise<ImageSource> {\n const url = URL.createObjectURL(file)\n try {\n const image = await loadImage(url)\n return {\n width: image.naturalWidth,\n height: image.naturalHeight,\n render(w, h, type, quality) {\n const canvas = document.createElement('canvas')\n canvas.width = w\n canvas.height = h\n const ctx = canvas.getContext('2d')\n if (ctx === null) throw new Error('canvas 2d context unavailable')\n ctx.drawImage(image, 0, 0, w, h)\n return canvas.toDataURL(type, quality)\n },\n release() {\n URL.revokeObjectURL(url)\n },\n }\n } catch (error) {\n URL.revokeObjectURL(url)\n throw error\n }\n },\n}\n\n/** Load an <img> from a src, resolving on decode. */\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const image = new Image()\n image.onload = () => resolve(image)\n image.onerror = () => reject(new Error('image load failed'))\n image.src = src\n })\n}\n\n/** Convert a Blob file into a JPEG data URL. Picking is never limited by file\n * size; the encode keeps the original resolution up to the display cap and\n * silently re-encodes smaller when the canvas would be oversized or the data\n * URL would not persist — it never throws a size error. */\nexport async function fileToDataUrl(\n file: Blob,\n env: PipelineEnv = browserEnv,\n options: DownscaleOptions = {},\n): Promise<string> {\n const quality = options.quality ?? 0.82\n const storageBudget = options.storageBudget ?? 4_000_000\n\n if (typeof (file as File).type === 'string' && (file as File).type !== '') {\n const type = (file as File).type.toLowerCase()\n if (type === 'image/svg+xml') {\n throw new ImagePipelineError('unsupported', '不支持 SVG 矢量图,请选择 PNG / JPEG / WebP / GIF / BMP 位图。')\n }\n if (!type.startsWith('image/')) {\n throw new ImagePipelineError('unsupported', `不支持的文件类型“${type}”,请选择 PNG / JPEG / WebP / GIF / BMP 图片。`)\n }\n // image/* beyond the raster whitelist (e.g. avif) proceeds to decode —\n // a codec the canvas cannot read surfaces as a clear \"decode\" error.\n }\n\n let source: ImageSource\n try {\n source = await env.readImage(file)\n } catch (error) {\n throw new ImagePipelineError('decode', '无法读取图片:文件已损坏或不是有效的图片。')\n }\n\n try {\n if (!Number.isFinite(source.width) || !Number.isFinite(source.height) || source.width <= 0 || source.height <= 0) {\n throw new ImagePipelineError('decode', '无法解析图片尺寸,文件可能已损坏。')\n }\n // First attempt: original resolution when it fits the display cap,\n // otherwise downscaled so the canvas stays inside browser limits.\n const cap = options.maxEdge ?? Math.min(Math.max(source.width, source.height), DISPLAY_MAX_EDGE)\n const first = sizeFor(source, cap)\n let data = encode(source, first.w, first.h, quality)\n // Fallback: never reject — when the encode came back invalid (oversized\n // canvases can silently yield empty data URLs) or too big to persist,\n // re-encode smaller. The last attempt always returns.\n if (!isValidDataUrl(data) || data.length > storageBudget) {\n for (const edge of [2560, 1920, 1280]) {\n if (Math.max(first.w, first.h) <= edge) continue\n const next = sizeFor(source, edge)\n data = encode(source, next.w, next.h, Math.max(0.6, quality - 0.1))\n if (isValidDataUrl(data) && data.length <= storageBudget) break\n }\n }\n return data\n } finally {\n try {\n source.release()\n } catch {\n // A release failure must not mask the primary outcome.\n }\n }\n}\n\n/** True when the encoder produced a real image payload (not an empty `data:,`). */\nfunction isValidDataUrl(data: string): boolean {\n return data.startsWith('data:image/') && data.length > 64\n}\n\n/** Bounded target size: original dimensions, or downscaled to fit `edge`. */\nfunction sizeFor(source: ImageSource, edge: number | undefined): { w: number; h: number } {\n if (edge === undefined) return { w: Math.max(1, Math.round(source.width)), h: Math.max(1, Math.round(source.height)) }\n const scale = Math.min(1, edge / Math.max(source.width, source.height))\n return {\n w: Math.max(1, Math.round(source.width * scale)),\n h: Math.max(1, Math.round(source.height * scale)),\n }\n}\n\n/** Draw + encode one attempt; encode failures are wrapped with a stable code. */\nfunction encode(source: ImageSource, w: number, h: number, quality: number): string {\n try {\n return source.render(w, h, 'image/jpeg', quality)\n } catch (error) {\n throw new ImagePipelineError('encode', '图片编码失败,请换一张图片。')\n }\n}\n","/**\n * dsh-wx-skin — host persistence client: loads/saves the durable settings\n * through the host half's loopback routes (/dsh-wx-skin/load,\n * /dsh-wx-skin/save), which store the copy in the DSH home directory —\n * independent of the current origin, so the background image survives the\n * desktop app's per-launch port changes.\n *\n * Module is kept free of DOM access and takes an injectable fetch so unit\n * tests exercise it without a page. Every function degrades to a safe result\n * (null / false) and never throws; the caller keeps its localStorage\n * fallback.\n * @module dsh-wx-skin/client/skin-host\n */\nimport type { SkinFolderImage, SkinSettings } from '../core/types.ts'\nimport { sanitizeFolderImages, sanitizeSettings } from './skin-store.ts'\n\n/**\n * Browser references to the host half's routes. They are DOCUMENT-RELATIVE on\n * purpose: the shell injects `<base href=\"./\">` (`@deepseek-ai/dsh-host-frontend-static`),\n * so the same literal reaches the listener under any mount — including a\n * prefix-stripping proxy, where a root-absolute `/dsh-wx-skin/load` would miss.\n * DSH's `verify-client-route-resolution` gate enforces this shape for its own\n * browser sources; this plugin follows the same rule\n * (`.agents/notes/implemented/architecture/2026-09-14-web-document-relative-app-routes.md`\n * in the DSH checkout). The host half's registration keys stay absolute\n * (`LOAD_PATH`/`SAVE_PATH`/`FOLDER_PATH` in the package root).\n */\nexport const LOAD_ROUTE = 'dsh-wx-skin/load'\nexport const SAVE_ROUTE = 'dsh-wx-skin/save'\nexport const FOLDER_ROUTE = 'dsh-wx-skin/folder'\n\n/** Read the durable settings from the host, or null when unavailable. */\nexport async function hostLoad(fetchImpl: typeof fetch = globalThis.fetch): Promise<SkinSettings | null> {\n try {\n const res = await fetchImpl(LOAD_ROUTE)\n if (!res.ok) return null\n const body = await res.json() as { ok?: unknown; settings?: unknown }\n // The host only ever writes an object; any other shape means no usable\n // durable copy → the caller falls back to localStorage/defaults.\n if (body?.ok !== true\n || body.settings === null\n || body.settings === undefined\n || typeof body.settings !== 'object') return null\n return sanitizeSettings(body.settings)\n } catch {\n return null\n }\n}\n\n/** Persist the settings to the host; false when it could not (never throws). */\nexport async function hostSave(settings: SkinSettings, fetchImpl: typeof fetch = globalThis.fetch): Promise<boolean> {\n try {\n const res = await fetchImpl(SAVE_ROUTE, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ settings }),\n })\n return res.ok\n } catch {\n return false\n }\n}\n\n/** One successful folder scan. */\nexport interface FolderScanSuccess {\n ok: true\n /** Canonical folder the host will serve images from. */\n root: string\n images: SkinFolderImage[]\n truncated: boolean\n}\n\n/** A rejected folder scan, already phrased for the panel. */\nexport interface FolderScanFailure {\n ok: false\n error: string\n}\n\n/** Result of asking the host to scan a folder. */\nexport type FolderScanResult = FolderScanSuccess | FolderScanFailure\n\n/** Message shown when the host process predates the folder route. */\nconst STALE_HOST = '宿主未加载文件夹接口,请重启 dsh web 后再试。'\nconst UNREACHABLE = '无法连接宿主接口(回环请求失败)。'\n\n/**\n * Ask the host to scan `path` and return its images in slideshow order.\n *\n * The host is authoritative: the folder route validates that the path is a\n * readable directory and applies the path-confinement and count bounds. A\n * response that is not the expected JSON means the route is missing — an older\n * host would answer with the SPA index through its fallback — so that case\n * reports the restart hint instead of a generic failure.\n * @param path - absolute folder path typed or picked by the user.\n * @param recursive - descend into subdirectories.\n * @param fetchImpl - injectable fetch (tests).\n * @returns the scanned list, or a failure carrying a display message.\n */\nexport async function hostListFolder(\n path: string,\n recursive: boolean,\n fetchImpl: typeof fetch = globalThis.fetch,\n): Promise<FolderScanResult> {\n let response: Response\n try {\n response = await fetchImpl(FOLDER_ROUTE, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ path, recursive }),\n })\n } catch {\n return { ok: false, error: UNREACHABLE }\n }\n\n let body: unknown\n try {\n body = await response.json()\n } catch {\n return { ok: false, error: STALE_HOST }\n }\n if (typeof body !== 'object' || body === null) return { ok: false, error: STALE_HOST }\n\n const record = body as Record<string, unknown>\n if (record.ok !== true) {\n const error = typeof record.error === 'string' && record.error !== '' ? record.error : '读取文件夹失败。'\n return { ok: false, error }\n }\n const images = sanitizeFolderImages(record.images)\n if (!Array.isArray(record.images) || (record.images.length > 0 && images.length === 0)) {\n return { ok: false, error: STALE_HOST }\n }\n return {\n ok: true,\n root: typeof record.root === 'string' ? record.root : path,\n images,\n truncated: record.truncated === true,\n }\n}\n","/**\n * dsh-wx-skin — the skin's settings page: enable toggle, background preview, the\n * folder row, the local-file row, the slideshow controls (顺序 / 随机, 上一张 /\n * 下一张), dim / blur / surface sliders, and reset.\n *\n * The page describes the background by PATH, which is also what the slideshow\n * walks:\n *\n * - 文件夹 is the folder being shown; loading one scans it and shows an image.\n * - 本地文件 is the image on screen — its absolute path for a folder image, the\n * URL for a URL background, or the picked file name when the browser dialog\n * gave no directory. Pasting an absolute path there loads THAT file: the\n * folder becomes the file's own folder and the slideshow walks it, so 下一张\n * from image 10 shows 11 and 上一张 shows 9 (sequential mode steps through the\n * folder order).\n *\n * The component is CONTROLLED: it renders the `settings` the controller owns and\n * reports every change through `commit`. Only transient UI state (drafts, busy\n * flags, messages) lives in local state.\n * @module dsh-wx-skin/client/skin-panel\n */\nimport { useEffect, useRef, useState, type ChangeEvent } from 'react'\nimport type { SkinFitMode, SkinFolderImage, SkinOrderMode, SkinSettings } from '../core/types.ts'\nimport { ImagePipelineError, fileToDataUrl } from './image-pipeline.ts'\nimport { canGoPrevious, focusFolderImage, folderStatus, loadFolderState, matchFolderImageByName } from './skin-folder.ts'\nimport { hostListFolder } from './skin-host.ts'\nimport { isAbsolutePath, isRemoteImageUrl, parentDirectory, pathBasename, samePath } from './skin-path.ts'\nimport type { PickerBridge } from './skin-picker.ts'\nimport { DEFAULT_SETTINGS, DEFAULT_SURFACE, MAX_SURFACE, MIN_SURFACE, currentImageLabel } from './skin-store.ts'\nimport css from './skin.module.css'\n\nexport interface SkinPanelProps {\n /** Settings currently applied (owned by the controller). */\n settings: SkinSettings\n /** Persist + apply a settings change (the controller wires storage, DOM, host sync). */\n commit: (next: SkinSettings) => void\n /**\n * Bridge to the harness native directory picker. Always present; the service\n * behind it may not be ready yet, which the button renders as unavailable.\n */\n picker?: PickerBridge\n /** Advance the slideshow by one image (the controller owns the algorithm + preload). */\n onNext: () => void\n /** Go back one image (the controller owns the algorithm + preload). */\n onPrevious: () => void\n}\n\n/** How the page offers the background fit modes, in menu order. */\nconst FIT_OPTIONS: ReadonlyArray<{ value: SkinFitMode, label: string, hint: string }> = [\n { value: 'fill', label: '填充', hint: '铺满窗口,超出的部分裁掉。' },\n { value: 'fit', label: '适应', hint: '完整显示整张图,按比例缩放并留白。' },\n { value: 'stretch', label: '拉伸', hint: '拉满窗口,比例可能变形。' },\n { value: 'tile', label: '平铺', hint: '按图片原始尺寸重复铺满。' },\n { value: 'center', label: '居中', hint: '按原始尺寸居中,四周留白。' },\n]\n\n/** The one-line explanation shown under the 适合度 row. */\nfunction fitHint(fit: SkinFitMode): string {\n return FIT_OPTIONS.find(option => option.value === fit)?.hint ?? ''\n}\n\n/** Describe the currently active source for the status badge. */\nfunction describe(settings: SkinSettings): string {\n if (!settings.enabled) return '未启用'\n if (settings.source === 'image') return '本地图片'\n if (settings.source === 'url') return '图片(URL)'\n if (settings.source === 'folder') {\n const status = folderStatus(settings)\n return status.total === 0 ? '文件夹(空)' : `文件夹 ${status.position}/${status.total}`\n }\n return '未启用'\n}\n\n/**\n * What the 本地文件 row shows for the current settings: the image's absolute\n * path, its URL, or the picked file's name when no directory was available.\n */\nfunction currentPathOf(settings: SkinSettings): string {\n if (settings.source === 'folder') return settings.folderImages[settings.currentIndex]?.path ?? ''\n if (settings.source === 'url') return settings.url ?? ''\n if (settings.source === 'image') return settings.imageName ?? ''\n return ''\n}\n\n/** The popover-free skin settings page. */\nexport function SkinPanel({ settings, commit, picker, onNext, onPrevious }: SkinPanelProps): JSX.Element {\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n /** A just-picked local image, processed but not yet applied — the user\n * clicks 「应用」 to commit it (explicit two-step flow). */\n const [staged, setStaged] = useState<string | null>(null)\n /** File name of `staged`, kept so the page can name the applied image. */\n const [stagedName, setStagedName] = useState<string | null>(null)\n /** Folder path box; follows the loaded folder unless the user is typing. */\n const [folderDraft, setFolderDraft] = useState<string | null>(null)\n const [folderBusy, setFolderBusy] = useState(false)\n const [folderError, setFolderError] = useState<string | null>(null)\n const [folderNotice, setFolderNotice] = useState<string | null>(null)\n /** Local-file row draft; null means \"mirror the shown image\". */\n const [pathDraft, setPathDraft] = useState<string | null>(null)\n const [pathNotice, setPathNotice] = useState<string | null>(null)\n const fileRef = useRef<HTMLInputElement | null>(null)\n\n const status = folderStatus(settings)\n /** Name of the image currently on screen (null when nothing is named). */\n const currentLabel = currentImageLabel(settings)\n /** Path / URL of the image on screen; the local-file row mirrors it. */\n const currentPath = currentPathOf(settings)\n const folderValue = folderDraft ?? settings.folderPath ?? ''\n const pathValue = pathDraft ?? currentPath\n /** The shown image is a data URL from the browser dialog: no directory known. */\n const pathIsFileNameOnly = settings.source === 'image' && currentPath !== ''\n\n // The rows mirror the committed state; a draft the user is typing survives\n // until the shown image / folder changes underneath it.\n useEffect(() => { setPathDraft(null) }, [currentPath])\n useEffect(() => { setFolderDraft(null) }, [settings.folderPath])\n\n const onPickFile = async (event: ChangeEvent<HTMLInputElement>): Promise<void> => {\n const file = event.target.files?.[0]\n event.target.value = ''\n if (file === undefined) return\n setBusy(true)\n setError(null)\n setPathNotice(null)\n try {\n const dataUrl = await fileToDataUrl(file)\n setStaged(dataUrl)\n setStagedName(file.name)\n } catch (err) {\n setStaged(null)\n setStagedName(null)\n setError(err instanceof ImagePipelineError ? err.message : String(err))\n } finally {\n setBusy(false)\n }\n }\n\n /**\n * Commit the staged local image to the skin.\n *\n * The browser's file dialog hands over the file NAME and no directory, so this\n * is where the picked file joins the slideshow when it can: if the loaded\n * folder holds exactly one image with that name, the background becomes that\n * file — the 本地文件 row then shows its full path and 下一张 continues in\n * folder order from it (pick bg129 → 下一张 bg130). Otherwise the encoded image\n * is used, with a hint explaining why no folder was attached.\n */\n const onApplyImage = (): void => {\n if (staged === null) return\n const matched = stagedName === null ? -1 : matchFolderImageByName(settings.folderImages, stagedName)\n if (settings.folderPath !== null && matched >= 0) {\n const image = settings.folderImages[matched] as SkinFolderImage\n commit(focusFolderImage(settings, matched))\n setStaged(null)\n setStagedName(null)\n setPathDraft(null)\n setPathNotice(`已按文件名定位到文件夹里的 ${image.name}(「下一张 / 上一张」从它继续)。`)\n return\n }\n commit({\n ...settings,\n enabled: true,\n source: 'image',\n imageDataUrl: staged,\n imageName: stagedName,\n url: null,\n })\n setStaged(null)\n setStagedName(null)\n setPathDraft(null)\n setPathNotice(stagedName === null\n ? null\n : `已应用本地图片 ${stagedName}(浏览器文件对话框不提供目录,未能关联文件夹;粘贴完整路径可自动关联)。`)\n }\n\n const onReset = (): void => {\n setStaged(null)\n setStagedName(null)\n setFolderDraft(null)\n setFolderError(null)\n setFolderNotice(null)\n setPathDraft(null)\n setPathNotice(null)\n setError(null)\n commit({ ...DEFAULT_SETTINGS })\n }\n\n /**\n * Apply a local-file row value: an `http(s)` address becomes a URL background;\n * an absolute path is loaded as the shown image, with its own folder scanned\n * so the slideshow walks that folder from this file.\n */\n const onApplyPath = async (): Promise<void> => {\n const value = pathValue.trim()\n setError(null)\n setPathNotice(null)\n if (value === '') {\n setError('请填写本地图片的绝对路径,或点「选择图片」;也可以填 http(s) 图片地址。')\n return\n }\n if (isRemoteImageUrl(value)) {\n commit({ ...settings, enabled: true, source: 'url', url: value, imageDataUrl: null, imageName: null })\n setStaged(null)\n setStagedName(null)\n setPathDraft(null)\n return\n }\n if (!isAbsolutePath(value)) {\n setError('请填写完整路径(如 D:\\\\Pictures\\\\a.png)或 http(s) 图片地址。')\n return\n }\n const folder = parentDirectory(value)\n if (folder === null) {\n setError('无法从该路径得到所在文件夹,请填写完整的图片路径。')\n return\n }\n setFolderBusy(true)\n try {\n const result = await hostListFolder(folder, settings.folderRecursive)\n if (!result.ok) {\n setFolderError(result.error)\n return\n }\n if (!result.images.some(image => samePath(image.path, value))) {\n setError('该文件不在支持的图片类型内(PNG / JPEG / WebP / GIF / BMP),或无法访问。')\n return\n }\n setFolderDraft(null)\n setFolderError(null)\n setPathDraft(null)\n commit(loadFolderState(settings, result.root, settings.folderRecursive, result.images, value))\n setFolderNotice(`已切到该文件所在文件夹(${result.images.length} 张图片)。`)\n } finally {\n setFolderBusy(false)\n }\n }\n\n /** Scan a folder (or the one currently typed) and start the slideshow. */\n const onLoadFolder = async (explicit?: string): Promise<void> => {\n const target = (explicit ?? folderValue).trim()\n if (target === '') {\n setFolderError('请填写文件夹的绝对路径,或点「选择文件夹」。')\n return\n }\n setFolderBusy(true)\n setFolderError(null)\n setFolderNotice(null)\n try {\n const result = await hostListFolder(target, settings.folderRecursive)\n if (!result.ok) {\n setFolderError(result.error)\n return\n }\n setFolderDraft(null)\n commit(loadFolderState(settings, result.root, settings.folderRecursive, result.images))\n setFolderNotice(result.images.length === 0\n ? '该文件夹里没有找到图片(支持 PNG / JPEG / WebP / GIF / BMP)。'\n : `已加载 ${result.images.length} 张图片${result.truncated ? '(已达上限,只取前 2000 张)' : ''}。`)\n } finally {\n setFolderBusy(false)\n }\n }\n\n /** Native picker when the harness exposes it; the path box stays as fallback. */\n const onPickFolder = async (): Promise<void> => {\n if (picker === undefined) return\n setFolderError(null)\n if (!picker.available()) {\n setFolderError('宿主目录选择器尚未就绪,请稍后再试,或直接粘贴文件夹路径。')\n return\n }\n try {\n const picked = await picker.open()\n if (picked === null) return // cancelled\n setFolderDraft(picked)\n await onLoadFolder(picked)\n } catch {\n setFolderError('无法打开系统目录选择器,请直接粘贴文件夹路径。')\n }\n }\n\n const onMode = (mode: SkinOrderMode): void => {\n commit({ ...settings, orderMode: mode })\n }\n\n // Only the image layer: size/position/repeat/backdrop come from `.preview`,\n // which contains the whole image (the preview is not the wallpaper's crop).\n const previewStyle = settings.enabled\n ? { backgroundImage: 'var(--wx-skin-bg-image, none)' }\n : undefined\n\n return (\n // The skin's settings page inside the harness Settings panel (registered\n // into `settings.section`): inline content, no popover chrome and no title\n // of its own — the shell's modal header and nav row already name the page.\n <div className={css.panel} data-wx-skin-panel=\"\">\n <label className={css.toggleRow}>\n <input\n type=\"checkbox\"\n checked={settings.enabled}\n onChange={e => { commit({ ...settings, enabled: e.target.checked }) }}\n />\n <span>启用皮肤</span>\n </label>\n\n <div className={css.preview} style={previewStyle}>\n {!settings.enabled && <span>未启用</span>}\n </div>\n <div className={css.currentBadge}>当前:{describe(settings)}</div>\n {currentLabel !== null && (\n <div className={css.imageName} data-wx-skin-name=\"\" title={currentLabel.detail}>\n 图片:{currentLabel.label}\n </div>\n )}\n\n <div className={css.section}>文件夹</div>\n <div className={css.urlRow}>\n <input\n className={css.urlInput}\n value={folderValue}\n placeholder=\"D:\\\\Pictures\\\\wallpapers\"\n aria-label=\"图片文件夹路径\"\n onChange={e => setFolderDraft(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter') void onLoadFolder() }}\n />\n <button\n type=\"button\"\n className={css.button}\n disabled={folderBusy}\n onClick={() => void onLoadFolder()}\n >\n {folderBusy ? '读取中…' : settings.folderImages.length === 0 ? '加载' : '重新加载'}\n </button>\n </div>\n <div className={css.folderRow}>\n {/* Always rendered: the harness picker service arrives after this page\n mounts, so hiding the button until then made the option look absent.\n It is disabled (with a hint) until the service is ready. */}\n <button\n type=\"button\"\n className={css.button}\n data-wx-skin-picker=\"\"\n data-ready={picker?.available() === true ? 'true' : 'false'}\n disabled={picker?.available() !== true}\n title={picker?.available() === true ? '选择图片文件夹' : '目录选择器尚未就绪'}\n onClick={() => void onPickFolder()}\n >\n 选择文件夹\n </button>\n <label className={css.inlineToggle}>\n <input\n type=\"checkbox\"\n checked={settings.folderRecursive}\n onChange={e => { commit({ ...settings, folderRecursive: e.target.checked }) }}\n />\n <span>包含子文件夹</span>\n </label>\n </div>\n {folderNotice !== null && <div className={css.noticeText}>{folderNotice}</div>}\n {folderError !== null && <div className={css.errorText}>{folderError}</div>}\n\n <div className={css.section}>本地文件</div>\n <div className={css.urlRow}>\n <input\n className={css.urlInput}\n data-wx-skin-path=\"\"\n value={pathValue}\n placeholder=\"D:\\\\Pictures\\\\a.png 或 https://…/a.jpg\"\n aria-label=\"本地图片路径\"\n onChange={e => setPathDraft(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter') void onApplyPath() }}\n />\n <button\n type=\"button\"\n className={css.button}\n data-wx-skin-path-apply=\"\"\n disabled={folderBusy}\n title=\"应用该路径:http(s) 地址作为图片 URL;本地绝对路径会切到它所在文件夹并按顺序继续\"\n onClick={() => void onApplyPath()}\n >\n 应用\n </button>\n </div>\n <div className={css.folderRow}>\n <button\n type=\"button\"\n className={css.buttonPrimary}\n data-wx-skin-local-pick=\"\"\n disabled={busy}\n onClick={() => fileRef.current?.click()}\n >\n {busy ? '处理中…' : '选择图片'}\n </button>\n <input\n ref={fileRef}\n className={css.fileInput}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/bmp\"\n onChange={(e) => void onPickFile(e)}\n />\n </div>\n {staged !== null && (\n <div className={css.stagedRow}>\n <div\n className={css.stagedThumb}\n style={{ backgroundImage: `url(\"${staged}\")`, backgroundSize: 'cover', backgroundPosition: 'center' }}\n />\n <span className={css.stagedLabel}>已选择 {stagedName ?? '图片'}</span>\n <button type=\"button\" className={css.buttonPrimary} data-wx-skin-apply-image=\"\" onClick={onApplyImage}>应用</button>\n </div>\n )}\n {pathIsFileNameOnly && (\n <div className={css.noticeText}>\n 浏览器文件对话框只提供文件名({pathBasename(currentPath)}),不提供目录;粘贴完整路径后点「应用」即可自动关联所在文件夹并参与轮播。\n </div>\n )}\n {pathNotice !== null && <div className={css.noticeText}>{pathNotice}</div>}\n {error !== null && <div className={css.errorText}>{error}</div>}\n\n <div className={css.section}>切换</div>\n <div className={css.folderRow}>\n <div className={css.segmented} role=\"group\" aria-label=\"切换方式\">\n <button\n type=\"button\"\n className={settings.orderMode === 'sequential' ? `${css.segBtn} ${css.segBtnActive}` : css.segBtn}\n onClick={() => onMode('sequential')}\n >\n 顺序\n </button>\n <button\n type=\"button\"\n className={settings.orderMode === 'random' ? `${css.segBtn} ${css.segBtnActive}` : css.segBtn}\n onClick={() => onMode('random')}\n >\n 随机\n </button>\n </div>\n <button\n type=\"button\"\n className={css.button}\n data-wx-skin-prev=\"\"\n disabled={!canGoPrevious(settings)}\n title={canGoPrevious(settings) ? '上一张' : '没有可返回的上一张'}\n onClick={onPrevious}\n >\n 上一张\n </button>\n <button\n type=\"button\"\n className={css.buttonPrimary}\n data-wx-skin-next=\"\"\n disabled={status.total === 0}\n onClick={onNext}\n >\n 下一张\n </button>\n </div>\n <div className={css.statusLine}>\n {status.total === 0\n ? '尚未加载文件夹'\n : settings.orderMode === 'random'\n ? `第 ${status.position}/${status.total} 张 · 本轮已用 ${status.usedThisPass}/${status.total}`\n : `第 ${status.position}/${status.total} 张`}\n {settings.orderMode === 'random' && settings.usedPaths.length > 0 && status.total > 0 && (\n <button type=\"button\" className={css.linkButton} onClick={() => commit({ ...settings, usedPaths: [] })}>\n 重置本轮\n </button>\n )}\n </div>\n\n <div className={css.section}>适合度</div>\n <div className={css.urlRow}>\n <select\n className={css.select}\n data-wx-skin-fit=\"\"\n aria-label=\"背景适合度\"\n value={settings.fit}\n onChange={e => {\n const next = FIT_OPTIONS.find(option => option.value === e.target.value)\n if (next !== undefined) commit({ ...settings, fit: next.value })\n }}\n >\n {FIT_OPTIONS.map(option => (\n <option key={option.value} value={option.value}>{option.label}</option>\n ))}\n </select>\n </div>\n <div className={css.hintText} data-wx-skin-fit-hint=\"\">{fitHint(settings.fit)}</div>\n\n <div className={css.section}>效果</div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>暗化 {Math.round(settings.dim * 100)}%</span>\n <input\n className={css.slider}\n type=\"range\"\n min={0}\n max={80}\n step={5}\n value={Math.round(settings.dim * 100)}\n onChange={e => { commit({ ...settings, dim: Number(e.target.value) / 100 }) }}\n />\n </div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>模糊 {Math.round(settings.blur)}px</span>\n <input\n className={css.slider}\n type=\"range\"\n min={0}\n max={24}\n step={1}\n value={Math.round(settings.blur)}\n onChange={e => { commit({ ...settings, blur: Number(e.target.value) }) }}\n />\n </div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>透出 {Math.round(settings.surface * 100)}%</span>\n <input\n className={css.slider}\n type=\"range\"\n min={MIN_SURFACE * 100}\n max={MAX_SURFACE * 100}\n step={1}\n value={Math.round(settings.surface * 100)}\n title=\"表面不透明度越低,背景图片越明显\"\n onChange={e => { commit({ ...settings, surface: Number(e.target.value) / 100 }) }}\n />\n </div>\n\n <div className={css.footer}>\n <button type=\"button\" className={css.button} onClick={onReset}>恢复默认</button>\n </div>\n </div>\n )\n}\n","/**\n * dsh-wx-skin — the skin's settings page, registered into the harness Settings\n * panel through the official additive seat `settings.section`.\n *\n * The shell owns the modal: its nav rail lists one row per registration\n * (ascending `order`, label resolved per render) and its options column renders\n * the selected page with its own padding and scrolling. That is exactly the room\n * a full skin editor needs — the plugin's own body-level popover used to hang\n * past the window edge when it was anchored to the sidebar foot, which is why\n * the settings moved here.\n *\n * The component is thin: it renders the controller's view snapshot into the\n * panel component (SkinPanel), which owns the controls. `close` (the one owner\n * prop a section receives) is unused — the modal's own close button and Escape\n * already leave settings, and no flow here needs to leave them for the user.\n * @module dsh-wx-skin/client/skin-settings\n */\nimport { useSyncExternalStore } from 'react'\nimport { SkinPanel } from './SkinPanel.tsx'\nimport type { SkinController, SkinView } from './skin-controller.tsx'\n\n/** The official additive seat for one Settings page. */\nexport const SETTINGS_SECTION = 'settings.section'\n\n/** Owner props every `settings.section` occupant receives. */\nexport interface SettingsSectionProps {\n /** Close the settings panel (the shell owns the open state). */\n close?: () => void\n}\n\n/**\n * Build the settings page component.\n * @param controller - the skin controller the page edits.\n * @returns the component to register into {@link SETTINGS_SECTION}.\n */\nexport function makeSkinSettings(controller: SkinController): (props: SettingsSectionProps) => JSX.Element {\n const subscribe = (listener: () => void): (() => void) => controller.subscribe(listener)\n const getSnapshot = (): SkinView => controller.view()\n\n return function SkinSettings(): JSX.Element {\n const { settings } = useSyncExternalStore(subscribe, getSnapshot)\n return (\n <SkinPanel\n settings={settings}\n commit={controller.commit}\n picker={controller.picker}\n onNext={controller.next}\n onPrevious={controller.previous}\n />\n )\n }\n}\n","/**\n * dsh-wx-skin — global skin stylesheet, injected once as a <style> tag by\n * mount. Everything is gated on the `data-wx-skin-active` attribute on\n * documentElement, which the DOM applier toggles; with the attribute absent\n * the sheet is inert and the default theme is untouched.\n *\n * Design: a full-viewport fixed layer paints the chosen image/color, and the\n * alias surface tokens are overridden with translucent values so the\n * background shows through while text stays readable. The light/dark palette\n * branches ride `body[data-ds-dark-theme]`, exactly like the theme's own\n * token sheets.\n *\n * Stacking: measured on the real DSH shell, a `position: fixed` layer with\n * `z-index: -1` never paints (it lands behind the canvas background), while a\n * positive z-index layer paints fine. So the layer sits at `z-index: 0` and\n * the app root `#root` is lifted to `z-index: 1` — the layer renders behind\n * the app and shows through its translucent surfaces.\n *\n * Translucency: every surface token is `rgb(<static palette literal> /\n * var(--wx-skin-surface))` — a user-adjustable surface opacity (default 0.72)\n * — with no `color-mix()` dependency, so it works on any modern browser.\n * @module dsh-wx-skin/client/global-skin-css\n */\n\nexport const GLOBAL_SKIN_CSS = String.raw`\n/* ---- background layer (paints behind the app, above the canvas) ---- */\nhtml[data-wx-skin-active] div[data-wx-skin-layer] {\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image: var(--wx-skin-bg-image, none);\n /* 适合度 (fit mode) projects onto these three; the fallbacks are 填充. */\n background-size: var(--wx-skin-bg-size, cover);\n background-repeat: var(--wx-skin-bg-repeat, no-repeat);\n background-position: var(--wx-skin-bg-position, center);\n filter: blur(var(--wx-skin-blur, 0px));\n}\n\n/* dim / scrim overlay between the image and the translucent surfaces */\nhtml[data-wx-skin-active] div[data-wx-skin-layer]::after {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--wx-skin-scrim, transparent);\n}\n\n/* keep the canvas neutral so nothing opaque hides the layer */\nhtml[data-wx-skin-active] body {\n background: transparent !important;\n}\n\n/* lift the app above the layer; its translucent surfaces let it show through */\nhtml[data-wx-skin-active] #root {\n position: relative !important;\n z-index: 1 !important;\n}\n\n/* ---- translucent surfaces: light palette ---- */\nhtml[data-wx-skin-active] body {\n --dsw-alias-bg-base: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-1: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-2: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-3: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-overlay: rgb(233 236 242 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-module-platform: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-multi-select: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n /* DSH 0.1.7 turned this from the bg-layer-3 alias (which this sheet already\n took over) into a literal rgba, so every overlay still reading it (hover\n cards, dock panels, selectors, sticky menu headers) stopped following the\n 透出 slider. Re-bound to the same hue at the skin's own surface opacity; the\n !important also outranks the darwin near-opaque override, which 0.1.7-rc\n moved out of the shell's base.css into ui-theme's design-platform.css\n (unchanged 0.94 values). */\n --dsw-specific-menu: rgb(248 249 250 / var(--wx-skin-surface, 0.72)) !important;\n /* 0.1.7-rc moved every shared dropdown / context menu / listbox onto the\n MenuSurface primitive, whose material layer paints with THIS token instead\n of the one above — while sticky menu headers kept the one above. Both are\n bound here, or a menu's header and body would drift apart under the skin. */\n --dsw-menu-surface-fill: rgb(248 249 250 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-sidebar-fill: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-input-major: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-selector: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-tip: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble: rgb(237 243 254 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble-highlight: rgb(211 226 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block-banner: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-inline-code: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-placeholder: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-tag: rgb(241 243 245 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-citation: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;\n /* 0.1.7-rc replaced the changed-files diff rows' 8% color-mix with these opaque\n literals; re-bound so a diff over the wallpaper still shows it through. The\n '-marker' companions are foreground tints, not surfaces, and stay untouched. */\n --dsw-alias-file-diff-added-bg: rgb(230 244 231 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-file-diff-added-gutter: rgb(237 247 237 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-file-diff-deleted-bg: rgb(252 230 226 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-file-diff-deleted-gutter: rgb(253 236 233 / var(--wx-skin-surface, 0.72)) !important;\n}\n\n/* ---- translucent surfaces: dark palette ---- */\nhtml[data-wx-skin-active] body[data-ds-dark-theme] {\n --dsw-alias-bg-base: rgb(21 21 23 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-1: rgb(35 35 36 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-2: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-3: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-overlay: rgb(97 102 107 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-module-platform: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-multi-select: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-menu: rgb(48 49 54 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-menu-surface-fill: rgb(48 49 54 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-sidebar-fill: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-input-major: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-selector: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-tip: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble-highlight: rgb(67 69 74 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block-banner: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-inline-code: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-placeholder: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-tag: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-citation: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-file-diff-added-bg: rgb(31 49 36 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-file-diff-added-gutter: rgb(19 32 22 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-file-diff-deleted-bg: rgb(60 31 27 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-file-diff-deleted-gutter: rgb(40 19 14 / var(--wx-skin-surface, 0.72)) !important;\n}\n\n/* ---- sidebar entry active indicator ---- */\nhtml[data-wx-skin-active] button[data-wx-skin-entry] {\n color: var(--dsw-alias-state-business-primary, #4176e6);\n}\n`\n","/**\n * dsh-wx-skin — DOM applier: owns the injected global stylesheet, the\n * background layer element, and the projection of skin settings onto the\n * document (attribute + CSS variables). Pure DOM writes; retracts only what\n * it wrote, so the default theme is untouched when the skin is off.\n * @module dsh-wx-skin/client/skin-dom\n */\nimport type { SkinSettings } from '../core/types.ts'\nimport { GLOBAL_SKIN_CSS } from './global-skin-css.ts'\nimport { ACTIVE_ATTR, LAYER_ATTR, SKIN_CSS_VARS, cssVariables } from './skin-store.ts'\n\n/** Identity of the injected global <style> tag (idempotent injection key). */\nexport const GLOBAL_STYLE_ID = 'dsh-wx-skin/global'\n\n/**\n * Plugin id stamped on the skin's own DOM. Must equal the package name (the\n * module graph's row id): the harness's client-hmr removes a plugin's owned\n * `<style data-plugin>` tags on reload, and claimStyles attributes an untagged\n * tag to whichever plugin materializes next — so the skin's stylesheet has to\n * carry its own owner.\n */\nexport const PLUGIN_ID = 'dsh-wx-skin'\n\n/** Inject the global skin stylesheet once. Safe to call repeatedly. */\nexport function ensureGlobalCss(): void {\n const existing = document.querySelector<HTMLStyleElement>(`style[data-plugin-css=\"${GLOBAL_STYLE_ID}\"]`)\n if (existing !== null) {\n // A tag injected before ownership stamping (or by the module loader) is adopted here.\n if (!existing.hasAttribute('data-plugin')) existing.dataset.plugin = PLUGIN_ID\n return\n }\n const tag = document.createElement('style')\n tag.dataset.plugin = PLUGIN_ID\n tag.dataset.pluginCss = GLOBAL_STYLE_ID\n tag.textContent = GLOBAL_SKIN_CSS\n document.head.appendChild(tag)\n}\n\n/** Ensure the full-viewport background layer exists and return it. */\nexport function ensureLayer(): HTMLDivElement {\n let layer = document.querySelector<HTMLDivElement>(`div[${LAYER_ATTR}]`)\n if (layer === null) {\n layer = document.createElement('div')\n layer.setAttribute(LAYER_ATTR, '')\n document.body.appendChild(layer)\n }\n return layer\n}\n\n/**\n * Apply skin settings to the document: toggle the gating attribute on\n * documentElement and write/retract the four CSS variables every time (so a\n * disabled skin leaves no residue).\n */\nexport class SkinApplier {\n apply(settings: SkinSettings): void {\n const root = document.documentElement\n if (settings.enabled) root.setAttribute(ACTIVE_ATTR, '')\n else root.removeAttribute(ACTIVE_ATTR)\n const vars = cssVariables(settings)\n for (const name of SKIN_CSS_VARS) {\n const value = vars[name]\n if (value === undefined) root.style.removeProperty(name)\n else root.style.setProperty(name, value)\n }\n }\n}\n\n/**\n * Remove everything the skin mounted: the background layer, the global\n * stylesheet, and the document-level attributes/variables. Every step is a\n * query-then-remove, so this is idempotent and safe to call from a fiber\n * disposer, a failed mount, or both.\n *\n * The sidebar entries are NOT swept here: they are React components registered\n * into the harness's `sidebar.footer.action` slot, so the shell unmounts them\n * when the registration's declaration lifetime ends (and the registration\n * disposer runs on fiber unload). React stays the single owner of those nodes.\n */\nexport function teardownSkinDom(): void {\n document.querySelector<HTMLElement>(`div[${LAYER_ATTR}]`)?.remove()\n document.querySelector<HTMLElement>(`style[data-plugin-css=\"${GLOBAL_STYLE_ID}\"]`)?.remove()\n const root = document.documentElement\n root.removeAttribute(ACTIVE_ATTR)\n for (const name of SKIN_CSS_VARS) root.style.removeProperty(name)\n}\n","/**\n * dsh-wx-skin — skin controller: the single owner of the skin's state and side\n * effects. It holds the current settings, applies them to the document, drives\n * the slideshow, rescans the folder, persists to localStorage + the host half,\n * and owns the document listeners.\n *\n * The UI is NOT built here. Both halves are React components registered into\n * harness slots — the settings page into `settings.section` (skin-settings.tsx)\n * and the slideshow quick actions into `sidebar.footer.action` (entries.tsx) —\n * and this controller is the value they render: they subscribe to\n * `subscribe`/`view` (a snapshot object recreated only when something changes)\n * and call back into `commit` / `next` / `previous`.\n * @module dsh-wx-skin/client/skin-controller\n */\nimport type { SkinSettings } from '../core/types.ts'\nimport {\n advanceFolder, canGoPrevious, folderSignature, imageUrl, previousFolder, refreshFolderState,\n} from './skin-folder.ts'\nimport type { PickerBridge } from './skin-picker.ts'\nimport { isDefaultSettings, loadSettings, saveSettings } from './skin-store.ts'\nimport { hostListFolder, hostLoad, hostSave } from './skin-host.ts'\nimport { SkinApplier, ensureGlobalCss, ensureLayer, teardownSkinDom } from './skin-dom.ts'\n\n/** How often a loaded folder is re-scanned in the background (ms). */\nconst FOLDER_RESCAN_MS = 60_000\n\n/** Host capabilities handed in by the browser half entry. */\nexport interface SkinControllerOptions {\n /**\n * Live bridge to the harness directory picker. The service behind it appears\n * only after the client↔host connection is up, so `available()` is asked on\n * every render instead of being snapshotted here.\n */\n picker?: PickerBridge\n /**\n * Subscribe to picker availability changes; returns an unsubscriber. Lets the\n * settings page light its 「选择文件夹」 button up without a remount.\n */\n onPickerChange?: (listener: () => void) => (() => void) | void\n}\n\n/** Immutable view the slot components render. */\nexport interface SkinView {\n /** Settings currently applied. */\n settings: SkinSettings\n /** Whether the harness directory picker is usable right now. */\n pickerReady: boolean\n}\n\n/** The skin's reactive surface, consumed by the settings page and the footer actions. */\nexport interface SkinController {\n /** Live directory-picker bridge (identity is stable; readiness comes from `view`). */\n readonly picker: PickerBridge | undefined\n /** Subscribe to view changes; returns an unsubscriber. */\n subscribe(listener: () => void): () => void\n /** Current view (stable reference until something changes). */\n view(): SkinView\n /** Apply + persist a settings change (the single commit path for every UI). */\n commit(next: SkinSettings): void\n /** Advance the slideshow by one image (no-op with no folder loaded). */\n next(): void\n /** Go back one image (no-op when there is nothing to go back to). */\n previous(): void\n /** Persist pending writes and remove everything this controller mounted. */\n dispose(): void\n}\n\n/**\n * Create the skin controller and mount its persistent pieces (stylesheet,\n * background layer, document listeners, host settings adoption).\n * @param options - host capabilities handed in by the browser half entry.\n * @returns the controller; call `dispose` to remove every artifact.\n */\nexport function createSkinController(options: SkinControllerOptions = {}): SkinController {\n const disposers: Array<() => void> = []\n ensureGlobalCss()\n ensureLayer()\n\n const applier = new SkinApplier()\n let latest = loadSettings()\n applier.apply(latest)\n\n const listeners = new Set<() => void>()\n let snapshot: SkinView = { settings: latest, pickerReady: options.picker?.available() === true }\n\n /** Publish the current view; a change-free call notifies nobody. */\n const notify = (): void => {\n const pickerReady = options.picker?.available() === true\n if (snapshot.settings === latest && snapshot.pickerReady === pickerReady) return\n snapshot = { settings: latest, pickerReady }\n for (const listener of [...listeners]) {\n try {\n listener()\n } catch (error) {\n // A broken subscriber must not take the skin (or the shell) down.\n console.warn('[dsh-wx-skin] view subscriber failed:', error)\n }\n }\n }\n\n // Debounced persistence (slider drags must not rewrite the image data URL\n // on every tick). Writes both the localStorage cache/fallback and the\n // durable host copy (deduped so an unchanged settings blob is not re-POSTed).\n let lastSentHost: string | undefined\n const syncHost = (): void => {\n const serialized = JSON.stringify(latest)\n if (lastSentHost === serialized) return\n lastSentHost = serialized\n void hostSave(latest)\n }\n const persist = (): void => {\n saveSettings(latest)\n syncHost()\n }\n let saveTimer: number | undefined\n const flushSave = (): void => {\n if (saveTimer === undefined) return\n window.clearTimeout(saveTimer)\n saveTimer = undefined\n persist()\n }\n const scheduleSave = (): void => {\n if (saveTimer !== undefined) window.clearTimeout(saveTimer)\n saveTimer = window.setTimeout(() => { persist() }, 200)\n }\n\n /** Every settings change from any UI goes through here. */\n function commitSettings(next: SkinSettings): void {\n latest = next\n applier.apply(next)\n scheduleSave()\n notify()\n }\n\n /**\n * Warm the browser cache for the image the next click will show (sequential\n * mode is deterministic; random mode draws fresh, so it is not preloaded).\n */\n const preloadNext = (current: SkinSettings): void => {\n if (current.orderMode !== 'sequential' || current.folderImages.length < 2) return\n const projected = advanceFolder(current)\n const candidate = projected.folderImages[projected.currentIndex]\n const shown = current.folderImages[current.currentIndex]\n if (candidate === undefined || shown?.path === candidate.path) return\n const image = new Image()\n image.src = imageUrl(candidate)\n }\n\n /**\n * Advance the slideshow by one image. With no folder loaded there is nothing\n * to switch to: the footer action renders as unavailable instead.\n */\n function next(): void {\n if (latest.folderImages.length === 0) return\n const advanced = advanceFolder(latest)\n commitSettings(advanced)\n preloadNext(advanced)\n }\n\n /**\n * Go back one image (the recorded previous one, or the previous in folder\n * order under sequential mode). Same rule as 下一张: nothing loaded, nothing\n * happens.\n */\n function previous(): void {\n if (!canGoPrevious(latest)) return\n const back = previousFolder(latest)\n commitSettings(back)\n preloadNext(back)\n }\n\n /**\n * Re-scan the loaded folder so images added outside the app join the queue.\n *\n * The merge never switches the background or resets the pass (see\n * refreshFolderState), and the settings are only written when the list really\n * changed — a poll that finds nothing new costs one loopback readdir.\n */\n let rescanning = false\n async function rescanFolder(): Promise<void> {\n const folderPath = latest.folderPath\n if (rescanning || folderPath === null) return\n rescanning = true\n try {\n const result = await hostListFolder(folderPath, latest.folderRecursive)\n if (!result.ok) return // unrelated failure (e.g. host restarted): keep the cache\n if (folderSignature(result.images) === folderSignature(latest.folderImages)) return\n commitSettings(refreshFolderState(latest, result.images))\n } finally {\n rescanning = false\n }\n }\n\n // The harness picker service arrives later than this controller; re-publish\n // the view so the settings page's picker button flips to ready in place.\n const offPicker = options.onPickerChange?.(() => { notify() })\n\n // Adopt the durable host copy once the controller is wired (a later arrival is\n // applied through the same single commit path as any UI change), then sync the\n // folder list so files added while the app was closed show up immediately.\n void (async () => {\n try {\n const host = await hostLoad()\n if (host !== null) {\n commitSettings(host)\n } else {\n const local = loadSettings()\n if (!isDefaultSettings(local)) void hostSave(local)\n }\n } catch {\n // Swallow — the skin must never take the shell down.\n }\n void rescanFolder()\n })()\n\n // Persist pending writes when the page goes away.\n const onBeforeUnload = (): void => flushSave()\n window.addEventListener('beforeunload', onBeforeUnload)\n\n // Folder rescan triggers: the page regaining focus is the \"I just added files\n // in Explorer\" case; the interval covers a side-by-side window; the settings\n // page opening refreshes on demand through the same path.\n const onVisibilityChange = (): void => { if (document.visibilityState === 'visible') void rescanFolder() }\n const onFocus = (): void => { void rescanFolder() }\n document.addEventListener('visibilitychange', onVisibilityChange)\n window.addEventListener('focus', onFocus)\n const rescanTimer = window.setInterval(() => { void rescanFolder() }, FOLDER_RESCAN_MS)\n\n disposers.push(() => {\n window.removeEventListener('beforeunload', onBeforeUnload)\n document.removeEventListener('visibilitychange', onVisibilityChange)\n window.removeEventListener('focus', onFocus)\n window.clearInterval(rescanTimer)\n })\n\n return {\n picker: options.picker,\n subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n view(): SkinView {\n return snapshot\n },\n commit: commitSettings,\n next,\n previous,\n dispose(): void {\n flushSave()\n offPicker?.()\n listeners.clear()\n for (const dispose of disposers.splice(0)) dispose()\n teardownSkinDom()\n },\n }\n}\n","/**\n * dsh-wx-skin — the bridge to whichever carrier can open a folder chooser.\n *\n * Two carriers can, and the plugin prefers the one that owns the dialog:\n *\n * 1. The **official Desktop application** (`apps/desktop` in the DSH checkout)\n * serves its document from `dsh-app://app` and installs\n * `globalThis.__DSH_DIRECTORY_PICKER__` from its renderer preload\n * (`src/preload-app.ts`). That `pick()` opens the Electron folder dialog\n * bound to the application window, restores and focuses the window first,\n * and shares one dialog across concurrent requests. It is the very chooser\n * the harness's own native directory flow drives\n * (`@deepseek-ai/dsh-client-ui-directory-picker-native`'s client half prefers\n * the same global over `uiWorkspace.pickDirectory()`), and it is installed\n * before any page script runs, so nothing has to arrive.\n *\n * 2. **Web** exposes the harness `uiWorkspace` client service, which this plugin\n * consumes as a SCOPED OPTIONAL dependency (`ctx.inject(['uiWorkspace'], …)`):\n * the service is provided only after the client↔host connection and the\n * remote namespaces are up, which is always later than a dependency-free\n * plugin's `apply`. A value snapshotted at mount time would therefore always\n * be undefined — the bug this module exists to prevent.\n *\n * Both lookups are LIVE, so the panel can render \"not ready yet\" and become\n * clickable the moment either carrier can serve a pick, without re-mounting\n * anything.\n * @module dsh-wx-skin/client/skin-picker\n */\n\n/** The slice of the harness `uiWorkspace` service this plugin uses (the Web carrier). */\nexport interface DirectoryPickerLike {\n pickDirectory?: () => Promise<string | null>\n}\n\n/**\n * The renderer-preload picker the Desktop carrier installs under\n * `globalThis.__DSH_DIRECTORY_PICKER__` (an Electron IPC call to its main process).\n */\nexport interface DesktopDirectoryPicker {\n /**\n * Open the window-bound native folder dialog.\n * @returns the picked absolute directory, or null when the user cancelled.\n */\n pick: () => Promise<string | null>\n}\n\n/** What the panel talks to, independent of when (or whether) a carrier shows up. */\nexport interface PickerBridge {\n /** Whether some carrier's picker is usable right now. */\n available(): boolean\n /**\n * Open the best available picker.\n * @returns the picked absolute directory, or null when no carrier is usable\n * or the user cancelled; rejects when the picker itself fails.\n */\n open(): Promise<string | null>\n}\n\n/**\n * Read the Desktop carrier's picker from the page globals.\n *\n * A browser deployment never installs the global, so this doubles as the\n * plugin's carrier test: present ⇒ the Desktop preload owns this document.\n * @returns the picker, or undefined when this document is not the Desktop app.\n */\nexport function desktopDirectoryPicker(): DesktopDirectoryPicker | undefined {\n const bridge = (globalThis as { __DSH_DIRECTORY_PICKER__?: unknown }).__DSH_DIRECTORY_PICKER__\n if (bridge === null || typeof bridge !== 'object') return undefined\n const pick: unknown = (bridge as { pick?: unknown }).pick\n if (typeof pick !== 'function') return undefined\n // `bind` keeps the preload object as the receiver of its own method.\n const bound = (pick as (this: unknown) => Promise<string | null>).bind(bridge)\n return { pick: () => bound() }\n}\n\n/**\n * Build a bridge over a live service lookup and a live Desktop-global lookup.\n * @param current - reads the current `uiWorkspace` (undefined while absent).\n * @param desktop - reads the Desktop carrier's picker (defaults to the page global).\n * @returns the availability check and the open action.\n */\nexport function createPickerBridge(\n current: () => DirectoryPickerLike | undefined,\n desktop: () => DesktopDirectoryPicker | undefined = desktopDirectoryPicker,\n): PickerBridge {\n return {\n available: () => desktop() !== undefined || typeof current()?.pickDirectory === 'function',\n async open(): Promise<string | null> {\n // The Desktop's own window-bound dialog wins whenever it is installed:\n // driving the Host's OS chooser instead would open a second, unbound\n // dialog for the same click (the Desktop Host binds loopback, so its\n // adaptive chooser resolves to the native backend rather than refusing).\n const carrier = desktop()\n if (carrier !== undefined) return await carrier.pick()\n const service = current()\n if (typeof service?.pickDirectory !== 'function') return null\n return await service.pickDirectory()\n },\n }\n}\n","/**\n * dsh-wx-skin — browser half entry. Builds the skin controller (settings,\n * background layer, body-level popover, slideshow, persistence) and registers\n * the three sidebar entries into the harness's own additive slot\n * (`sidebar.footer.action`), so the shell owns their placement, ordering, the\n * collapsed rail, and teardown. Everything is wired as fiber effects, so the\n * harness can tear it down again: client-hmr unloads the old fiber before\n * re-applying a rebuilt bundle (and any row disable does the same), and only a\n * registered disposer removes the entries, the popover, the layer, and the\n * stylesheet. All mount failures are logged, never thrown.\n *\n * The only host capabilities the skin borrows are SCOPED OPTIONAL dependencies,\n * never `inject` on this plugin:\n *\n * - The native directory picker. Two carriers serve it, and `skin-picker.ts`\n * prefers the one that owns the dialog: the official Desktop application's\n * renderer-preload `globalThis.__DSH_DIRECTORY_PICKER__`, else\n * `uiWorkspace.pickDirectory` (the Web carrier's harness service, which\n * appears only after the client↔host connection is up — always later than\n * this `apply`. A value snapshotted here would always be undefined, which is\n * exactly why the panel never showed a 「选择文件夹」 button). The Web service is\n * watched with `ctx.inject(['uiWorkspace'], …)`; both are read lazily.\n * - `slots` (the slot registry) plus ui-sidebar's `sidebar.footer.action`\n * declaration. Declaring `inject: ['slots']` on the plugin would make the\n * whole browser half — background layer and persistence included — wait on a\n * service and, in a deployment without it, trip the client boot audit. The\n * scoped form keeps the skin working when the sidebar is absent; only the\n * entries would not appear.\n * @module dsh-wx-skin/client\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { FOOTER_SLOT, makeSkinFooter } from './entries.tsx'\nimport { SETTINGS_SECTION, makeSkinSettings } from './skin-settings.tsx'\nimport { createSkinController, type SkinController, type SkinControllerOptions } from './skin-controller.tsx'\nimport { createPickerBridge, desktopDirectoryPicker, type DirectoryPickerLike } from './skin-picker.ts'\nimport { teardownSkinDom } from './skin-dom.ts'\n\n/** No hard service dependencies — the skin only touches the DOM and the slots. */\nexport const inject: string[] = []\n\n/**\n * Structural face of the harness slot service (`@deepseek-ai/dsh-client-ui-renderer`).\n * Declared locally on purpose: this plugin does not depend on harness client\n * packages, exactly like `DirectoryPickerLike` in skin-picker.ts.\n *\n * `inject(key, …)` waits for a slot DECLARATION (ui-sidebar's / ui-settings', in\n * this case) and runs the callback as a child fiber effect, so a shell reload\n * tears the registrations down and rebuilds them.\n */\ninterface SlotsLike {\n inject(key: string, callback: () => unknown): () => void\n register(options: { name: string, id: string, order?: number, label?: string }, component: unknown): () => void\n}\n\n/** Apply the browser half. */\nexport function apply(ctx: Context): void {\n ctx.effect(() => {\n let service: DirectoryPickerLike | undefined\n let refresh: (() => void) | undefined\n\n // Scoped optional dependency: the child fiber starts when the harness\n // picker appears, and its disposer runs if the service goes away again.\n ctx.inject(['uiWorkspace'], (scope: Context) => {\n // `get` rather than the typed property: the Context merge for this service\n // belongs to the harness package, which this plugin does not depend on.\n service = scope.get('uiWorkspace') as DirectoryPickerLike | undefined\n refresh?.()\n scope.effect(() => () => {\n service = undefined\n refresh?.()\n }, 'dsh-wx-skin: picker release')\n })\n\n const options: SkinControllerOptions = {\n // Desktop carrier first (its preload dialog is present from the start),\n // then the Web carrier's service, which arrives later.\n picker: createPickerBridge(() => service, desktopDirectoryPicker),\n onPickerChange: (listener) => {\n refresh = listener\n return () => {\n if (refresh === listener) refresh = undefined\n }\n },\n }\n\n // A failing external plugin must never take the GUI down.\n let controller: SkinController\n try {\n controller = createSkinController(options)\n } catch (error) {\n ctx.logger.error('[dsh-wx-skin] mount failed')\n ctx.logger.error(error)\n // A half-mounted skin leaves no layer, stylesheet, or entry behind.\n teardownSkinDom()\n return () => {}\n }\n\n // Two official seats, one registration each:\n // - `settings.section` — the skin's settings PAGE inside the harness\n // Settings panel (nav row + its own room, padding and scrolling).\n // - `sidebar.footer.action` — the slideshow quick actions at the sidebar\n // foot. ONE registration carries the pair: that row is a flex row whose\n // shipped occupant claims a full line, so sibling registrations would be\n // squeezed to zero width (see entries.tsx).\n ctx.inject(['slots'], (scope: Context) => {\n const slots = scope.get('slots') as SlotsLike | undefined\n if (slots === undefined) return () => {}\n const stops = [\n slots.inject(SETTINGS_SECTION, () => slots.register(\n { name: SETTINGS_SECTION, id: 'skin', order: 30, label: '皮肤' },\n makeSkinSettings(controller),\n )),\n slots.inject(FOOTER_SLOT, () => slots.register(\n { name: FOOTER_SLOT, id: 'skin-slideshow', order: 10, label: '换图' },\n makeSkinFooter(controller),\n )),\n ]\n return () => {\n for (const stop of stops.splice(0)) stop()\n }\n })\n\n return () => { controller.dispose() }\n }, 'dsh-wx-skin: skin mount')\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -23,7 +23,24 @@ export const IMAGE_PATH = '/dsh-wx-skin/image';
|
|
|
23
23
|
const MAX_SAVE_BYTES = 16 * 1024 * 1024;
|
|
24
24
|
/** The folder-scan request body is a path plus a flag. */
|
|
25
25
|
const MAX_FOLDER_BODY_BYTES = 64 * 1024;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Origins a request may carry and still reach the host routes.
|
|
28
|
+
*
|
|
29
|
+
* - The browser GUI is served by this Host from a loopback authority, so its
|
|
30
|
+
* same-origin requests carry `http://127.0.0.1:<port>` (or `localhost`, or the
|
|
31
|
+
* IPv6 literal).
|
|
32
|
+
* - The official Desktop application (`apps/desktop` in the DSH checkout) serves
|
|
33
|
+
* its whole document from `dsh-app://app` and relays it here through
|
|
34
|
+
* Electron's `protocol.handle` → `forwardWebRequest`, which DELETES the
|
|
35
|
+
* request's `origin` and `sec-fetch-site` before the Host sees them. Accepting
|
|
36
|
+
* the origin anyway keeps these routes working if that relay ever stops
|
|
37
|
+
* stripping them, and it cannot widen what the guard fences off: `Origin` is a
|
|
38
|
+
* forbidden header name for scripts, so no web page can borrow the Desktop
|
|
39
|
+
* origin, and the loopback entries are exactly as forgeable by a local
|
|
40
|
+
* non-browser client as this one is. The guard is a cross-site browser fence,
|
|
41
|
+
* not a local-process one — a local process already reads files directly.
|
|
42
|
+
*/
|
|
43
|
+
const ACCEPTED_ORIGINS = ['http://127.0.0.1', 'http://localhost', 'http://[::1]', 'dsh-app://app'];
|
|
27
44
|
/** `node:fs` bound to the scan's injected face. */
|
|
28
45
|
const FS_IO = {
|
|
29
46
|
readdirSync: dir => readdirSync(dir, { withFileTypes: true }),
|
|
@@ -43,8 +60,8 @@ function resolveDshHome() {
|
|
|
43
60
|
function settingsPath() {
|
|
44
61
|
return join(resolveDshHome(), 'dsh-wx-skin.settings.json');
|
|
45
62
|
}
|
|
46
|
-
/** Whether a request may reach the host routes:
|
|
47
|
-
function
|
|
63
|
+
/** Whether a request may reach the host routes: a local carrier's document only. */
|
|
64
|
+
function isLocalCarrierRequest(req) {
|
|
48
65
|
// A cross-site <img>/CSS request carries no Origin, but it does carry
|
|
49
66
|
// Sec-Fetch-Site — the one signal that separates it from a same-page fetch or
|
|
50
67
|
// a direct address-bar visit.
|
|
@@ -54,7 +71,7 @@ function isLoopbackRequest(req) {
|
|
|
54
71
|
const origin = req.headers.origin;
|
|
55
72
|
if (origin === undefined)
|
|
56
73
|
return true; // same-origin fetch omits Origin
|
|
57
|
-
return
|
|
74
|
+
return ACCEPTED_ORIGINS.some((base) => origin === base || origin.startsWith(`${base}:`));
|
|
58
75
|
}
|
|
59
76
|
/** Send a JSON response. */
|
|
60
77
|
function sendJson(res, status, body) {
|
|
@@ -115,14 +132,14 @@ function confinementRoot(settingsFile) {
|
|
|
115
132
|
return undefined;
|
|
116
133
|
}
|
|
117
134
|
}
|
|
118
|
-
/** Common method +
|
|
135
|
+
/** Common method + local-carrier guard for every host route. */
|
|
119
136
|
function guard(req, res, method) {
|
|
120
137
|
const accepted = typeof method === 'string' ? [method] : method;
|
|
121
138
|
if (req.method === undefined || !accepted.includes(req.method)) {
|
|
122
139
|
sendJson(res, 405, { ok: false, error: 'method not allowed' });
|
|
123
140
|
return false;
|
|
124
141
|
}
|
|
125
|
-
if (!
|
|
142
|
+
if (!isLocalCarrierRequest(req)) {
|
|
126
143
|
sendJson(res, 403, { ok: false, error: 'forbidden' });
|
|
127
144
|
return false;
|
|
128
145
|
}
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
* GET /dsh-wx-skin/image → image bytes (slideshow background)
|
|
17
17
|
*
|
|
18
18
|
* The schema lives in the browser half (sanitizeSettings); the host stores
|
|
19
|
-
* and returns the raw JSON blob it receives. Routes
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
19
|
+
* and returns the raw JSON blob it receives. Routes answer only a local
|
|
20
|
+
* carrier's document (the browser GUI on this Host's loopback authority, or the
|
|
21
|
+
* official Desktop application on `dsh-app://app`) and the file is written
|
|
22
|
+
* atomically (temp + rename) so concurrent clients cannot corrupt it. Image
|
|
23
|
+
* bytes are served only from inside the folder the user picked (the last scan in
|
|
24
|
+
* this process, else `settings.folderPath`), so the route can never be turned
|
|
25
|
+
* into a general file reader. When the webServer service is unavailable the host
|
|
26
|
+
* half stays a no-op and the client falls back to its localStorage behavior.
|
|
26
27
|
* @module dsh-wx-skin
|
|
27
28
|
*/
|
|
28
29
|
import type { Context } from '@deepseek-ai/cordis';
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAOlD,eAAO,MAAM,IAAI,gBAAgB,CAAA;AAEjC,6DAA6D;AAC7D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,sBAAsB,CAAA;AAC5C,eAAO,MAAM,SAAS,sBAAsB,CAAA;AAC5C,eAAO,MAAM,WAAW,wBAAwB,CAAA;AAChD,eAAO,MAAM,UAAU,uBAAuB,CAAA;AA2I9C;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA0JxC"}
|
package/package.json
CHANGED