@iasiv5/dsh-skins 1.5.3 → 1.5.5

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.en.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-skins — Skin pack for DSH Web
1
+ # dsh-skins — Skin pack for DSH
2
2
 
3
3
  [![Release](https://img.shields.io/github/v/release/iasiv5/dsh-skins?label=Release&sort=semver)](../../releases)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/iasiv5/dsh-skins/ci.yml?branch=main&label=CI)](../../actions/workflows/ci.yml)
@@ -8,7 +8,7 @@
8
8
 
9
9
  English · [中文](./README.md)
10
10
 
11
- Hot-swappable brand skins for DeepSeek Harness Web — with a one-click path back to the official interface.
11
+ Hot-swappable brand skins for DeepSeek Harness — installable on both the Web and Desktop profiles, with a one-click path back to the official interface.
12
12
 
13
13
  ![凡人修仙传 · 美人志 — factory wallpaper "Yuntai Gathering"](docs/assets/preview-meirenzhi-1.webp)
14
14
 
@@ -38,6 +38,8 @@ dsh plugin --profile web update @iasiv5/dsh-skins # update
38
38
  dsh plugin --profile web remove @iasiv5/dsh-skins # uninstall
39
39
  ```
40
40
 
41
+ **DSH Desktop app**: install from the app's built-in plugin page, or run `dsh plugin --profile desktop add @iasiv5/dsh-skins`, then restart the app.
42
+
41
43
  With the [dsh-m](https://github.com/iasiv5/dsh-m) plugin marketplace installed, you can also install / uninstall this plugin with one click inside the DSH Web UI.
42
44
 
43
45
  <details>
@@ -55,7 +57,7 @@ If the install fails, send me the command output verbatim and retry at most once
55
57
 
56
58
  </details>
57
59
 
58
- ## Four appearances
60
+ ## Three skins and the official look
59
61
 
60
62
  This section answers: which skins exist and what each feels like.
61
63
 
@@ -105,9 +107,9 @@ Every skin's key visuals are open to adjustment. Click the gear button on a skin
105
107
  |---|---|---|
106
108
  | **Wallpaper** | Pick from the skin's built-in artwork, or upload your own images into a personal library | Each skin's default artwork |
107
109
  | **Slogan** | The new-session guidance line, one Chinese and one English copy | The skin's factory slogan |
108
- | **Translucency** | 0–100%, one value driving three visual layers: panel tint, wallpaper scrim and blur. 0% is pure, fully visible wallpaper; 100% hides it completely | meirenzhi 35, openbmc / uefi-harness 55 |
110
+ | **Translucency** | 0–100%, one value driving three visual layers: panel tint, wallpaper scrim and blur. 0% is pure, fully visible wallpaper; 100% hides it completely | meirenzhi: Web 35%, Desktop 55%; openbmc / uefi-harness: 55% in both |
109
111
 
110
- > **On the Windows Desktop app (dsh Desktop)**: the shell chrome — sidebar, caption strip, right panel and window buttons — is the **opaque skin base color** and does not follow the translucency knob; the wallpaper is exposed inside the conversation area (center column), where the knob drives surface tint, scrim and blur. The Web keeps the original fully translucent form on every surface; the two environments are independent.
112
+ > **Desktop profile**: the shell is the solid skin base color and the wallpaper shows only inside the conversation area — see "The Desktop profile" below.
111
113
 
112
114
  ![Personalization panel — meirenzhi shown: built-in wallpaper grid, library upload, zh/en slogans and the translucency knob](docs/assets/preview-personalization.webp)
113
115
 
@@ -122,16 +124,25 @@ Every skin's key visuals are open to adjustment. Click the gear button on a skin
122
124
 
123
125
  - Any change in the panel applies immediately and is written automatically after a brief (~0.5s) debounce.
124
126
  - Two tabs sync within about a second; while offline, write controls disable themselves so edits are never lost silently.
125
- - Reset-to-default is the one guarded action: it lists the settings it will reset, and after you confirm it returns to factory values and persists automatically.
127
+ - Reset-to-default lists the settings it will reset; after confirmation it returns to factory values and persists automatically. On Desktop, destructive actions use an in-page confirmation to avoid the Electron input-focus issue after native JavaScript dialogs; Web keeps the browser's native confirm.
126
128
  - While the panel is open, clicking another skin's card moves the panel straight to that skin's settings.
127
129
 
128
130
  ### Where settings live, and whether they survive
129
131
 
130
- - Configuration and the library live under `$DSH_HOME/dsh-skins/`, physically isolated from the plugin install directory: plugin upgrades (via the dsh-m marketplace or `dsh plugin`) only replace the plugin install directory and cannot touch it.
132
+ - Web stores configuration and the library under `$DSH_HOME/dsh-skins/`; since v1.5.4 Desktop stores them under `$DSH_HOME/dsh-skins-desktop/`. On first launch Desktop copies existing settings and images from the legacy directory once, **without removing or rewriting the old directory**; the two stores then evolve independently. Both are separate from the plugin installation directory, which upgrades replace.
131
133
  - Only overrides are stored: untouched fields automatically follow the new version's defaults, and leftovers of retired skins or fields are cleaned up at load (the v1.4.0 tgcf removal rides this path).
132
134
  - A damaged state file triggers recovery mode: indexes are rebuilt and bad files quarantined — your images are never wiped.
133
135
  - Uploads pass magic-number validation and size caps; concurrent writes merge per field and never clobber each other.
134
136
 
137
+ ## The Desktop profile
138
+
139
+ In the **DSH Desktop app**, the shell chrome — sidebar, caption strip, right panel and window buttons — is the **opaque skin base color** and does not follow the translucency knob; the wallpaper is exposed inside the conversation area (center column), where the knob drives surface tint, scrim and blur. The Web profile keeps the original fully translucent form on every surface; the two environments are independent.
140
+
141
+ ![Desktop profile — solid chrome + wallpaper in the conversation area (meirenzhi · dark)](docs/assets/preview-desktop-1.webp)
142
+
143
+ - Verified with dsh Desktop `0.2.0-rc.2` (Windows); macOS / Linux Desktop currently follow the Web form and will be adapted in a later release.
144
+ - The right panel in fullscreen is the solid skin base color; the wallpaper is not shown through it.
145
+
135
146
  ## The skin switcher
136
147
 
137
148
  This section answers: what lives in the switcher and how to use it.
@@ -164,6 +175,9 @@ Update duty has moved to the dsh-m marketplace and the DSH CLI (ADR-0008); the s
164
175
  **Which DSH Web versions are supported?**
165
176
  Verified with DSH Web `0.2.0-rc.2`. Later rc builds of the same series are expected to work, but unverified versions carry no promise.
166
177
 
178
+ **Is the Desktop profile supported?**
179
+ Yes. The DSH Desktop app and the Web profile are installed independently and do not affect each other; the desktop form (solid chrome + wallpaper in the conversation area) is described under "The Desktop profile". Verified with dsh Desktop `0.2.0-rc.2` (Windows).
180
+
167
181
  **Does my appearance preference sync across browsers?**
168
182
  In the browser on the DSH host machine (loopback), the preference is persisted by the DSH Host and shared naturally. Other remote browsers keep the preference in their own `localStorage` — independent per browser, never overwritten.
169
183
 
@@ -175,7 +189,7 @@ Yes. Configuration and the library live in the `$DSH_HOME/dsh-skins/` data direc
175
189
  - Other skin plugins may also touch the body backdrop, brand slots or favicon; avoid enabling multiple visual skin plugins at the same time.
176
190
  - The `openbmc` brand slots reuse the OpenBMC project's official logo letterforms and brand gradient, used solely for identification; all rights remain with the OpenBMC project.
177
191
  - The `uefi-harness` mark is the UEFI Forum's official trademark (the red cube, from uefi.org's published uefi_logo_red.gif, embedded as vector paths traced via Wikimedia Commons "Logo of the UEFI Forum.svg"); it is used solely to identify the skin, and all rights remain with the UEFI Forum.
178
- - Windows Desktop known limits: after the right panel is closed/collapsed, the host keeps a reserved empty track (it shows the shell's dark base color and the conversation area does not expand) — host layout behavior, outside skin control; the fullscreen right panel is the solid skin base color without wallpaper blur (blurring would smear the panel's own contents, so it is intentionally omitted).
192
+ - Windows Desktop known limits: the fullscreen right panel is the solid skin base color; the wallpaper is not shown through it.
179
193
 
180
194
  ## License
181
195
 
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-readme-pairing.mjs --write
5
- README.md: f2874ac75adc7a7d2fbd8fc7970d7452ce2b0b15
6
- README.en.md: c00ca57444786f77a611dd385f15dfcced37eb99
5
+ README.md: 00a34bf2d930204da3aff8299779b931719ee5cd
6
+ README.en.md: d741fa3991323516fe07e4b81c4f6862c6800456
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-skins — DSH Web 多皮肤插件
1
+ # dsh-skins — DSH 多皮肤插件
2
2
 
3
3
  [![Release](https://img.shields.io/github/v/release/iasiv5/dsh-skins?label=Release&sort=semver)](../../releases)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/iasiv5/dsh-skins/ci.yml?branch=main&label=CI)](../../actions/workflows/ci.yml)
@@ -8,7 +8,7 @@
8
8
 
9
9
  [English](./README.en.md) · 中文
10
10
 
11
- 为 DeepSeek Harness Web 换上可热切换的品牌皮肤,随时一键回到官方界面。
11
+ 为 DeepSeek Harness 换上可热切换的品牌皮肤——Web 与 Desktop 两种 profile 均可安装使用,随时一键回到官方界面。
12
12
 
13
13
  ![凡人修仙传 · 美人志 — 内置默认壁纸「云台雅集」](docs/assets/preview-meirenzhi-1.webp)
14
14
 
@@ -38,6 +38,8 @@ dsh plugin --profile web update @iasiv5/dsh-skins # 更新
38
38
  dsh plugin --profile web remove @iasiv5/dsh-skins # 卸载
39
39
  ```
40
40
 
41
+ **DSH Desktop 应用**:在应用自带的插件页安装,或执行 `dsh plugin --profile desktop add @iasiv5/dsh-skins`,重启应用后生效。
42
+
41
43
  安装了 [dsh-m](https://github.com/iasiv5/dsh-m) 插件市场的话,也可以直接在 DSH Web 界面里一键安装 / 卸载本插件。
42
44
 
43
45
  <details>
@@ -55,7 +57,7 @@ dsh plugin --profile web remove @iasiv5/dsh-skins # 卸载
55
57
 
56
58
  </details>
57
59
 
58
- ## 四款外观
60
+ ## 三款皮肤与官方界面
59
61
 
60
62
  本节回答:有哪些皮肤、各是什么气质。
61
63
 
@@ -105,9 +107,9 @@ dsh plugin --profile web remove @iasiv5/dsh-skins # 卸载
105
107
  |---|---|---|
106
108
  | **壁纸** | 从皮肤自带的「内置精选」里挑,或上传自己的图片进「个人图库」 | 各皮肤的默认画作 |
107
109
  | **标语** | 新会话页的引导语,中文、英文各一份 | 皮肤出厂标语 |
108
- | **通透度** | 0–100% 单值联动三个视觉层:面板底色、壁纸遮罩、模糊。0% 纯壁纸完全透出,100% 完全遮蔽 | meirenzhi 35,openbmc / uefi-harness 55 |
110
+ | **通透度** | 0–100% 单值联动三个视觉层:面板底色、壁纸遮罩、模糊。0% 纯壁纸完全透出,100% 完全遮蔽 | 美人志:Web 35%、Desktop 55%;openbmc / uefi-harness:两端均 55% |
109
111
 
110
- > **Windows Desktop(dsh Desktop 应用)上的形态**:桌面端外壳——侧栏、顶栏条、右栏面板与窗口按钮——为**不透明的皮肤本色**,不随通透度变化;壁纸在会话区(内容列)显露,通透度联动会话区内的表面底色、遮罩与模糊。Web 端维持全表面透纱的原有形态,两端互不影响。
112
+ > **Desktop profile**:外壳为实色皮肤本色、壁纸只在会话区显露——详见下方「Desktop profile 上的形态」。
111
113
 
112
114
  ![个性化面板 — 以美人志为例:内置壁纸网格、图库上传、中英标语与通透度旋钮](docs/assets/preview-personalization.webp)
113
115
 
@@ -122,16 +124,25 @@ dsh plugin --profile web remove @iasiv5/dsh-skins # 卸载
122
124
 
123
125
  - 面板内任何修改立即生效,停顿约半秒自动写入配置。
124
126
  - 双标签页约 1 秒内自动同步;离线时写入控件自动禁用,不会静默丢改动。
125
- - 「恢复默认」是唯一带二次确认的操作:先列出将被重置的设置,确认后立即回出厂并自动保存。
127
+ - 「恢复默认」会先列出将被重置的设置,确认后立即回出厂并自动保存。Desktop 的破坏性操作使用页面内确认弹窗,避免 Electron 原生确认框关闭后输入区失焦;Web 保留浏览器原生确认框。
126
128
  - 面板展开时点击其他皮肤卡片,面板会随即切到该皮肤的设置。
127
129
 
128
130
  ### 配置存在哪、会不会丢
129
131
 
130
- - 配置与图库存于 `$DSH_HOME/dsh-skins/`,与插件安装目录物理隔离:插件升级(经 dsh-m 市场或 `dsh plugin`)只替换插件安装目录,碰不到它。
132
+ - Web profile 的配置与图库存于 `$DSH_HOME/dsh-skins/`;Desktop profile 自 v1.5.4 起存于 `$DSH_HOME/dsh-skins-desktop/`。Desktop 首次启动会从旧目录复制一次既有配置与图库,**不删除或改写旧目录**,此后两端独立。两处都与插件安装目录物理隔离:插件升级只替换插件安装目录。
131
133
  - 配置只存「改过什么」:未改动的字段自动跟随新版本的默认值,已下线皮肤或字段的残留会在加载时自动清理(v1.4.0 移除 tgcf 皮肤即走这条路径)。
132
134
  - 配置文件损坏时进入恢复模式:只重建索引、隔离坏文件,绝不清除你的图片。
133
135
  - 上传走魔数校验与尺寸上限;并发写入按字段合并,互不覆盖。
134
136
 
137
+ ## Desktop profile 上的形态
138
+
139
+ **DSH Desktop 应用**中,外壳——侧栏、顶栏条、右栏面板与窗口按钮——为**不透明的皮肤本色**,不随通透度变化;壁纸在会话区(内容列)显露,通透度联动会话区内的表面底色、遮罩与模糊。Web profile 维持全表面透纱的原有形态,两端互不影响。
140
+
141
+ ![Desktop profile — 实色外壳 + 会话区壁纸(美人志 · 深色)](docs/assets/preview-desktop-1.webp)
142
+
143
+ - 已在 dsh Desktop `0.2.0-rc.2`(Windows)验证;macOS / Linux Desktop 当前跟随 Web 形态,后续版本适配。
144
+ - 右栏面板全屏时为实色皮肤底、不透出壁纸;面板「收起/关闭」后的空轨道行为属宿主布局。
145
+
135
146
  ## 皮肤切换器
136
147
 
137
148
  本节回答:切换器里有什么、怎么用。
@@ -164,6 +175,9 @@ URL 也能切皮肤:`/?skin=official`、`/?skin=openbmc`、`/?skin=uefi-harnes
164
175
  **支持哪些 DSH Web 版本?**
165
176
  已使用 DSH Web `0.2.0-rc.2` 验证。同系列 rc 版本理论上兼容,未经验证的版本不作承诺。
166
177
 
178
+ **支持 Desktop profile 吗?**
179
+ 支持。DSH Desktop 应用与 Web profile 各自独立安装、互不影响;桌面形态(实色外壳 + 会话区壁纸)见「Desktop profile 上的形态」。已使用 dsh Desktop `0.2.0-rc.2`(Windows)验证。
180
+
167
181
  **外观偏好在不同的浏览器间同步吗?**
168
182
  在 DSH 所在机器的浏览器(loopback)上,偏好由 DSH Host 持久化,天然共享。其他远程浏览器由插件将偏好存入本地 `localStorage`,各浏览器独立、互不覆盖。
169
183
 
@@ -175,7 +189,7 @@ URL 也能切皮肤:`/?skin=official`、`/?skin=openbmc`、`/?skin=uefi-harnes
175
189
  - 其他皮肤插件也可能修改 body 背景、品牌位或 favicon;应避免同时启用多个视觉皮肤插件。
176
190
  - `openbmc` 的品牌位沿用 OpenBMC 项目官方 logo 的字标字形与品牌渐变色,仅作标识用途,权利归 OpenBMC 项目所有。
177
191
  - `uefi-harness` 的标志为 UEFI Forum 官方商标(红色立方体,源自 uefi.org 发布的 uefi_logo_red.gif,经 Wikimedia Commons「Logo of the UEFI Forum.svg」矢量描摹嵌入),仅作标识用途,权利归 UEFI Forum 所有。
178
- - Windows Desktop 已知边界:右栏面板「收起/关闭」后,宿主保留一条空轨道(显示外壳深色底,会话区不扩展)——宿主布局行为,非皮肤可控;全屏面板为实色皮肤底、不带壁纸模糊(会模糊面板内全部内容,故不做)。
192
+ - Windows Desktop 已知边界:全屏的右栏面板为实色皮肤底,不透出壁纸。
179
193
 
180
194
  ## License
181
195
 
package/lib/client.js CHANGED
@@ -125,17 +125,18 @@
125
125
  ]
126
126
  }
127
127
  };
128
- function getSkinSchema(skinId) {
128
+ function getSkinSchema(skinId, profile = "web") {
129
129
  const entry = SKINS[skinId];
130
130
  if (entry === void 0) return null;
131
- return { skinId, fields: entry.fields, builtinAssets: entry.builtinAssets };
131
+ const fields = profile === "desktop" && skinId === "meirenzhi" ? entry.fields.map((field) => field.key === "panelOpacity" ? { ...field, default: 55 } : field) : entry.fields;
132
+ return { skinId, fields, builtinAssets: entry.builtinAssets };
132
133
  }
133
134
  function getField(skinId, key) {
134
135
  return SKINS[skinId]?.fields.find((field) => field.key === key) ?? null;
135
136
  }
136
- function defaultsFor(skinId) {
137
+ function defaultsFor(skinId, profile = "web") {
137
138
  const values = {};
138
- for (const field of SKINS[skinId]?.fields ?? []) values[field.key] = field.default;
139
+ for (const field of getSkinSchema(skinId, profile)?.fields ?? []) values[field.key] = field.default;
139
140
  return values;
140
141
  }
141
142
  function isSameOverrideValue(a, b) {
@@ -245,8 +246,8 @@
245
246
  })) return { ok: false, code: "BAD_SHAPE" };
246
247
  return { ok: true };
247
248
  }
248
- function mergeValues(skinId, overrides, metaProvider) {
249
- const schema = getSkinSchema(skinId);
249
+ function mergeValues(skinId, overrides, metaProvider, profile = "web") {
250
+ const schema = getSkinSchema(skinId, profile);
250
251
  if (schema === null) return { values: {}, issues: [] };
251
252
  const provider = typeof metaProvider === "function" ? metaProvider : void 0;
252
253
  const values = {};
@@ -358,13 +359,13 @@
358
359
  });
359
360
  }
360
361
  function projectSkin(skin, rawOverrides, context = {}) {
361
- const schema = getSkinSchema(skin.id);
362
+ const schema = getSkinSchema(skin.id, context.profile);
362
363
  if (schema === null) {
363
364
  return { effects: null, issues: [], degraded: "failed" };
364
365
  }
365
366
  const project2 = skin.project;
366
367
  const attempt = (overrides) => {
367
- const { values, issues } = mergeValues(skin.id, overrides, context.metaProvider);
368
+ const { values, issues } = mergeValues(skin.id, overrides, context.metaProvider, context.profile);
368
369
  const resolvedAssets = {};
369
370
  for (const field of schema.fields) {
370
371
  if (field.type !== "image") continue;
@@ -387,7 +388,7 @@
387
388
  };
388
389
  const first = safeAttempt(rawOverrides ?? {});
389
390
  if (first.normalized !== null) return { effects: first.normalized, issues: first.issues, degraded: "none" };
390
- const second = safeAttempt(defaultsFor(skin.id));
391
+ const second = safeAttempt(defaultsFor(skin.id, context.profile));
391
392
  if (second.normalized !== null) return { effects: second.normalized, issues: first.issues, degraded: "defaults" };
392
393
  return { effects: null, issues: first.issues, degraded: "failed" };
393
394
  }
@@ -444,7 +445,7 @@
444
445
  }
445
446
  return lines.join("\n");
446
447
  }
447
- function createSkinRuntime() {
448
+ function createSkinRuntime({ profile = "web" } = {}) {
448
449
  const skins = /* @__PURE__ */ new Map();
449
450
  const order = [];
450
451
  let ctx = null;
@@ -519,6 +520,7 @@
519
520
  }
520
521
  function projectionContext() {
521
522
  return {
523
+ profile,
522
524
  assetResolver: personalization?.assetResolver,
523
525
  metaProvider: personalization?.metaProvider
524
526
  };
@@ -866,8 +868,13 @@ ${darkSelector}{${dark.join(";")}}`;
866
868
  "personalization.title": "个性化",
867
869
  "personalization.collapse": "收起个性化面板",
868
870
  "personalization.reset": "恢复默认",
869
- "personalization.resetJoin": "、",
870
- "personalization.resetConfirm": "以下设置将恢复为出厂默认:\n{fields}\n此操作立即生效,确认继续吗?",
871
+ "personalization.confirmTitle": "确认操作",
872
+ "personalization.confirm": "确认",
873
+ "personalization.cancel": "取消",
874
+ "personalization.resetJoin": "\n• ",
875
+ "personalization.resetConfirm": "以下设置将恢复为出厂默认:\n\n{fields}\n\n此操作立即生效,确认继续吗?",
876
+ "personalization.resetConfirmIntro": "以下设置将恢复为出厂默认:",
877
+ "personalization.resetConfirmOutro": "此操作立即生效,确认继续吗?",
871
878
  "personalization.light": "浅色",
872
879
  "personalization.dark": "深色",
873
880
  "personalization.builtin": "内置画作",
@@ -934,8 +941,13 @@ ${darkSelector}{${dark.join(";")}}`;
934
941
  "personalization.title": "Personalize",
935
942
  "personalization.collapse": "Collapse personalization panel",
936
943
  "personalization.reset": "Restore defaults",
937
- "personalization.resetJoin": ", ",
938
- "personalization.resetConfirm": "These settings will be reset to factory defaults:\n{fields}\nThis takes effect immediately. Continue?",
944
+ "personalization.confirmTitle": "Confirm action",
945
+ "personalization.confirm": "Confirm",
946
+ "personalization.cancel": "Cancel",
947
+ "personalization.resetJoin": "\n• ",
948
+ "personalization.resetConfirm": "These settings will be reset to factory defaults:\n\n{fields}\n\nThis takes effect immediately. Continue?",
949
+ "personalization.resetConfirmIntro": "These settings will be reset to factory defaults:",
950
+ "personalization.resetConfirmOutro": "This takes effect immediately. Continue?",
939
951
  "personalization.light": "Light",
940
952
  "personalization.dark": "Dark",
941
953
  "personalization.builtin": "Built-in artwork",
@@ -1098,6 +1110,17 @@ ${darkSelector}{${dark.join(";")}}`;
1098
1110
  ".dsh-skins-pz-danger{color:var(--dsw-alias-error-text,var(--dsw-static-red-500,#d33));border-color:var(--dsw-alias-error-text,var(--dsw-static-red-500,#d33))}",
1099
1111
  ".dsh-skins-pz-fields-locale{flex-direction:row;gap:6px}",
1100
1112
  ".dsh-skins-pz-fields-locale .dsh-skins-pz-input{flex:1;min-width:0}",
1113
+ // HTML <dialog>.showModal() stays inside the renderer's top layer: unlike
1114
+ // window.confirm(), it never opens the Windows/Electron JavaScript dialog
1115
+ // that can leave the Desktop window unfocused after dismissal.
1116
+ ".dsh-skins-pz-dialog{box-sizing:border-box;width:min(480px,calc(100vw - 32px));max-height:calc(100vh - 32px);padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-elevated,var(--dsw-alias-bg-overlay));color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv3,0 8px 24px rgba(0,0,0,.25));font:inherit}",
1117
+ ".dsh-skins-pz-dialog::backdrop{background:rgba(0,0,0,.45)}",
1118
+ ".dsh-skins-pz-dialog-message{margin:0 0 20px;white-space:pre-line;line-height:1.6;font-size:14px}",
1119
+ ".dsh-skins-pz-dialog-intro{margin:0;font-size:14px;line-height:1.6}.dsh-skins-pz-dialog-outro{margin:0 0 20px;font-size:14px;line-height:1.6}",
1120
+ ".dsh-skins-pz-dialog-tags{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 16px;padding:0;list-style:none}",
1121
+ ".dsh-skins-pz-dialog-tag{box-sizing:border-box;display:inline-flex;align-items:center;gap:7px;max-width:100%;padding:6px 10px;border:1px solid var(--dsw-alias-border-l2);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 42%,transparent);border-radius:999px;background:var(--dsw-alias-bg-layer-1);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 10%,var(--dsw-alias-bg-overlay));color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;font-weight:600;white-space:normal}",
1122
+ '.dsh-skins-pz-dialog-tag::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-brand-primary)}',
1123
+ ".dsh-skins-pz-dialog-actions{display:flex;justify-content:flex-end;gap:8px}",
1101
1124
  // Stacked (<904px) resets MUST come after the rules they override — the
1102
1125
  // array order is the cascade order among equal-specificity selectors.
1103
1126
  "@media (max-width:904px){.dsh-skins-pz-thumbs{grid-template-columns:repeat(4,1fr)}}"
@@ -1175,30 +1198,42 @@ ${darkSelector}{${dark.join(";")}}`;
1175
1198
  }) })
1176
1199
  ] });
1177
1200
  }
1178
- function WallpaperSection({ skinId, field, value, onValue, state, disabled }) {
1201
+ function WallpaperSection({ skinId, field, value, onValue, state, disabled, requestConfirm }) {
1179
1202
  const uploadRef = useRef(null);
1180
1203
  const [message, setMessage] = useState(null);
1181
1204
  const [deletingId, setDeletingId] = useState(null);
1205
+ const [clearing, setClearing] = useState(false);
1182
1206
  const [visible, setVisible] = useState(PAGE_SIZE);
1183
1207
  const schema = getSkinSchema(skinId);
1184
1208
  const builtins = schema?.builtinAssets ?? {};
1185
1209
  const liveAssets = builtinAssetsFor(skinId);
1186
1210
  const choices = field.builtinChoices ?? [];
1187
1211
  const library = state.library;
1188
- const removeAsset = async (asset) => {
1189
- if (deletingId !== null) return;
1190
- const references = state.references[asset.id] ?? [];
1191
- const names = references.map((entry) => `${labelFor(entry.skinId)} · ${entry.key}`).join("\n");
1192
- const confirmed = references.length === 0 || window.confirm(`${tr("personalization.library.deleteConfirm")}
1193
- ${names}`);
1194
- if (!confirmed) return;
1212
+ const deleteAsset = async (asset) => {
1195
1213
  setMessage(null);
1196
1214
  setDeletingId(asset.id);
1197
- const result = await configClient.deleteImage(asset.id);
1198
- setDeletingId(null);
1199
- setMessage(result.error ? tr("personalization.library.deleteFailed") : tr("personalization.library.deleted", { name: asset.displayName }));
1215
+ try {
1216
+ const result = await configClient.deleteImage(asset.id);
1217
+ setMessage(result.error ? tr("personalization.library.deleteFailed") : tr("personalization.library.deleted", { name: asset.displayName }));
1218
+ } catch {
1219
+ setMessage(tr("personalization.library.deleteFailed"));
1220
+ } finally {
1221
+ setDeletingId(null);
1222
+ }
1200
1223
  };
1201
- const clearLibrary = async () => {
1224
+ const removeAsset = (asset) => {
1225
+ if (deletingId !== null || clearing) return;
1226
+ const references = state.references[asset.id] ?? [];
1227
+ if (references.length === 0) return deleteAsset(asset);
1228
+ const names = references.map((entry) => `${labelFor(entry.skinId)} · ${entry.key}`).join("\n");
1229
+ requestConfirm(
1230
+ `${tr("personalization.library.deleteConfirm")}
1231
+ ${names}`,
1232
+ () => deleteAsset(asset)
1233
+ );
1234
+ };
1235
+ const clearLibrary = () => {
1236
+ if (clearing || deletingId !== null || library.length === 0) return;
1202
1237
  const pairs = /* @__PURE__ */ new Map();
1203
1238
  for (const asset of library) {
1204
1239
  for (const entry of state.references[asset.id] ?? []) {
@@ -1206,19 +1241,27 @@ ${names}`);
1206
1241
  }
1207
1242
  }
1208
1243
  const names = [...pairs.keys()].join("、");
1209
- const confirmed = window.confirm(tr("personalization.library.clearConfirm", {
1244
+ requestConfirm(tr("personalization.library.clearConfirm", {
1210
1245
  count: library.length,
1211
1246
  names: names === "" ? "—" : names
1212
- }));
1213
- if (!confirmed) return;
1214
- for (const asset of [...library]) {
1215
- const result = await configClient.deleteImage(asset.id);
1216
- if (result.error) {
1247
+ }), async () => {
1248
+ setClearing(true);
1249
+ try {
1250
+ for (const asset of [...library]) {
1251
+ const result = await configClient.deleteImage(asset.id);
1252
+ if (result.error) {
1253
+ setMessage(tr("personalization.library.clearFailed"));
1254
+ void configClient.refetch();
1255
+ return;
1256
+ }
1257
+ }
1258
+ } catch {
1217
1259
  setMessage(tr("personalization.library.clearFailed"));
1218
- configClient.refetch();
1219
- return;
1260
+ void configClient.refetch();
1261
+ } finally {
1262
+ setClearing(false);
1220
1263
  }
1221
- }
1264
+ });
1222
1265
  };
1223
1266
  return jsx("div", { className: "dsh-skins-pz-row", children: [
1224
1267
  jsx("span", { className: "dsh-skins-pz-label", children: tr(field.labelKey) }),
@@ -1258,7 +1301,7 @@ ${names}`);
1258
1301
  jsx("button", {
1259
1302
  type: "button",
1260
1303
  className: `dsh-skins-pz-del${deletingId === asset.id ? " dsh-skins-pz-del-busy" : ""}`,
1261
- disabled: disabled || deletingId !== null,
1304
+ disabled: disabled || deletingId !== null || clearing,
1262
1305
  "aria-label": `${tr("personalization.library.delete")}: ${asset.displayName}`,
1263
1306
  onClick: () => removeAsset(asset),
1264
1307
  children: deletingId === asset.id ? jsx("span", { className: "dsh-skins-spinner", "aria-hidden": "true" }) : "×"
@@ -1281,14 +1324,14 @@ ${names}`);
1281
1324
  jsx("button", {
1282
1325
  type: "button",
1283
1326
  className: "dsh-skins-pz-btn",
1284
- disabled: disabled || deletingId !== null,
1327
+ disabled: disabled || deletingId !== null || clearing,
1285
1328
  onClick: () => uploadRef.current?.click(),
1286
1329
  children: tr("personalization.library.upload")
1287
1330
  }),
1288
1331
  library.length > 0 ? jsx("button", {
1289
1332
  type: "button",
1290
1333
  className: "dsh-skins-pz-btn dsh-skins-pz-danger",
1291
- disabled: disabled || deletingId !== null,
1334
+ disabled: disabled || deletingId !== null || clearing,
1292
1335
  onClick: clearLibrary,
1293
1336
  children: tr("personalization.library.clear")
1294
1337
  }) : null
@@ -1363,8 +1406,39 @@ ${names}`);
1363
1406
  }
1364
1407
  return function PersonalizationPanel({ skinId, onCollapse }) {
1365
1408
  const state = useConfigState();
1366
- const schema = getSkinSchema(skinId);
1409
+ const desktopProfile = typeof location !== "undefined" && location.protocol === "dsh-app:" && location.hostname === "app";
1410
+ const profile = desktopProfile ? "desktop" : "web";
1411
+ const schema = getSkinSchema(skinId, profile);
1367
1412
  const headerRef = useRef(null);
1413
+ const dialogRef = useRef(null);
1414
+ const confirmationRef = useRef(null);
1415
+ const [confirmation, setConfirmation] = useState(null);
1416
+ const requestConfirm = (message, accept, { detailItems = null } = {}) => {
1417
+ if (!desktopProfile) {
1418
+ if (window.confirm(message)) void accept();
1419
+ return;
1420
+ }
1421
+ if (confirmationRef.current !== null) return;
1422
+ const next = { message, accept, detailItems };
1423
+ confirmationRef.current = next;
1424
+ setConfirmation(next);
1425
+ };
1426
+ const cancelConfirm = () => {
1427
+ confirmationRef.current = null;
1428
+ setConfirmation(null);
1429
+ };
1430
+ const acceptConfirm = () => {
1431
+ if (confirmationRef.current !== confirmation) return;
1432
+ confirmationRef.current = null;
1433
+ setConfirmation(null);
1434
+ const current = configClient.getState();
1435
+ if (current.status === "synced" && current.mode !== "recovery") {
1436
+ void confirmation.accept();
1437
+ }
1438
+ };
1439
+ useEffect(() => {
1440
+ if (confirmation !== null) dialogRef.current?.showModal?.();
1441
+ }, [confirmation]);
1368
1442
  const wideLayout = typeof window !== "undefined" && window.innerWidth >= 905;
1369
1443
  useEffect(() => {
1370
1444
  headerRef.current?.focus?.(wideLayout ? { preventScroll: true } : void 0);
@@ -1372,7 +1446,7 @@ ${names}`);
1372
1446
  if (schema === null) return null;
1373
1447
  const writesBlocked = state.status !== "synced" || state.mode === "recovery";
1374
1448
  const overrides = configClient.effectiveOverrides(skinId);
1375
- const { values } = mergeValues(skinId, overrides);
1449
+ const { values } = mergeValues(skinId, overrides, void 0, profile);
1376
1450
  const hasAnyOverride = Object.keys(overrides).length > 0;
1377
1451
  const fieldRows = schema.fields.map((field) => {
1378
1452
  const value = values[field.key];
@@ -1390,7 +1464,8 @@ ${names}`);
1390
1464
  ...common,
1391
1465
  key: field.key,
1392
1466
  skinId,
1393
- state
1467
+ state,
1468
+ requestConfirm
1394
1469
  });
1395
1470
  default:
1396
1471
  return null;
@@ -1462,12 +1537,12 @@ ${names}`);
1462
1537
  disabled: writesBlocked,
1463
1538
  onClick: () => {
1464
1539
  const affected = schema.fields.filter((field) => overrides[field.key] !== void 0).map((field) => tr(field.labelKey));
1465
- const confirmed = window.confirm(tr("personalization.resetConfirm", {
1466
- fields: affected.length > 0 ? affected.join(tr("personalization.resetJoin")) : "—"
1467
- }));
1468
- if (!confirmed) return;
1469
- for (const field of schema.fields) configClient.previewReset(skinId, field.key);
1470
- void configClient.flushNow();
1540
+ requestConfirm(tr("personalization.resetConfirm", {
1541
+ fields: affected.length > 0 ? `• ${affected.join(tr("personalization.resetJoin"))}` : "—"
1542
+ }), () => {
1543
+ for (const field of schema.fields) configClient.previewReset(skinId, field.key);
1544
+ void configClient.flushNow();
1545
+ }, { detailItems: affected });
1471
1546
  },
1472
1547
  children: tr("personalization.reset")
1473
1548
  }) : null,
@@ -1484,7 +1559,40 @@ ${names}`);
1484
1559
  statusCluster.length > 0 ? jsx("div", {
1485
1560
  className: "dsh-skins-pz-actions",
1486
1561
  children: jsx("div", { className: "dsh-skins-pz-cluster dsh-skins-pz-cluster-status", children: statusCluster })
1487
- }) : null
1562
+ }) : null,
1563
+ confirmation === null ? null : jsx("dialog", {
1564
+ ref: dialogRef,
1565
+ className: "dsh-skins-pz-dialog",
1566
+ "aria-labelledby": "dsh-skins-confirm-title",
1567
+ onCancel: (event) => {
1568
+ event.preventDefault();
1569
+ cancelConfirm();
1570
+ },
1571
+ children: [
1572
+ jsx("div", { id: "dsh-skins-confirm-title", className: "dsh-skins-pop-title", children: tr("personalization.confirmTitle") }),
1573
+ confirmation.detailItems === null ? jsx("p", { className: "dsh-skins-pz-dialog-message", children: confirmation.message }) : jsx("div", { children: [
1574
+ jsx("p", { className: "dsh-skins-pz-dialog-intro", children: tr("personalization.resetConfirmIntro") }),
1575
+ confirmation.detailItems.length > 0 ? jsx("ul", { className: "dsh-skins-pz-dialog-tags", children: confirmation.detailItems.map((label) => jsx("li", { className: "dsh-skins-pz-dialog-tag", children: label }, label)) }) : jsx("p", { className: "dsh-skins-pz-dialog-message", children: "—" }),
1576
+ jsx("p", { className: "dsh-skins-pz-dialog-outro", children: tr("personalization.resetConfirmOutro") })
1577
+ ] }),
1578
+ jsx("div", { className: "dsh-skins-pz-dialog-actions", children: [
1579
+ jsx("button", {
1580
+ type: "button",
1581
+ className: "dsh-skins-pz-btn",
1582
+ autoFocus: true,
1583
+ onClick: cancelConfirm,
1584
+ children: tr("personalization.cancel")
1585
+ }),
1586
+ jsx("button", {
1587
+ type: "button",
1588
+ className: "dsh-skins-pz-btn dsh-skins-pz-danger",
1589
+ disabled: writesBlocked,
1590
+ onClick: acceptConfirm,
1591
+ children: tr("personalization.confirm")
1592
+ })
1593
+ ] })
1594
+ ]
1595
+ })
1488
1596
  ] });
1489
1597
  };
1490
1598
  }
@@ -1781,7 +1889,7 @@ ${names}`);
1781
1889
  closeShell();
1782
1890
  };
1783
1891
  const onKey = (event) => {
1784
- if (event.key === "Escape") closeShell();
1892
+ if (event.key === "Escape" && !document.querySelector?.("dialog.dsh-skins-pz-dialog[open]")) closeShell();
1785
1893
  };
1786
1894
  document.addEventListener("pointerdown", onPointer, true);
1787
1895
  window.addEventListener("keydown", onKey);
@@ -2006,6 +2114,7 @@ ${names}`);
2006
2114
  function createConfigClient(options = {}) {
2007
2115
  const fetchImpl = options.fetchImpl ?? (typeof fetch === "function" ? fetch : null);
2008
2116
  const baseUrl = options.baseUrl ?? "/dsh-skins";
2117
+ const profile = options.profile === "desktop" ? "desktop" : "web";
2009
2118
  const timeoutMs = options.timeoutMs ?? FETCH_TIMEOUT_MS;
2010
2119
  const uploadTimeoutMs = options.uploadTimeoutMs ?? UPLOAD_TIMEOUT_MS;
2011
2120
  if (typeof fetchImpl !== "function") throw new Error("config client requires a fetch implementation");
@@ -2238,7 +2347,7 @@ ${names}`);
2238
2347
  const composite = `${skinId} ${key}`;
2239
2348
  const storedOverride = snapshot.skins[skinId]?.[key];
2240
2349
  const previewed = previews.get(composite);
2241
- if (isSameOverrideValue(value, defaultsFor(skinId)[key])) {
2350
+ if (isSameOverrideValue(value, defaultsFor(skinId, profile)[key])) {
2242
2351
  if (storedOverride === void 0 && previewed === void 0) return;
2243
2352
  if (previewed === null) return;
2244
2353
  return previewReset(skinId, key);
@@ -3460,7 +3569,8 @@ body[data-dsh-uefi-harness][data-ds-dark-theme] .lc-tip {
3460
3569
  const jsxRuntime = require2("react/jsx-runtime");
3461
3570
  const react = require2("react");
3462
3571
  const reactDom = require2("react-dom");
3463
- const runtime = createSkinRuntime();
3572
+ const profile = window.location?.protocol === "dsh-app:" && window.location?.hostname === "app" ? "desktop" : "web";
3573
+ const runtime = createSkinRuntime({ profile });
3464
3574
  const skinById = /* @__PURE__ */ new Map();
3465
3575
  for (const factory of [createOpenBmcHarness, createUefiHarness, createMeirenzhiSkin]) {
3466
3576
  const skin = factory(jsxRuntime);
@@ -3472,7 +3582,7 @@ body[data-dsh-uefi-harness][data-ds-dark-theme] .lc-tip {
3472
3582
  skinById.set(skin.id, skin);
3473
3583
  runtime.register(skin);
3474
3584
  }
3475
- const configClient = typeof fetch === "function" ? createConfigClient({ contextActive: () => runtime.active() }) : null;
3585
+ const configClient = typeof fetch === "function" ? createConfigClient({ profile, contextActive: () => runtime.active() }) : null;
3476
3586
  if (configClient !== null) {
3477
3587
  const assetResolver = (ref) => {
3478
3588
  if (ref.kind === "builtin") {
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/index.js
2
2
  import { homedir } from "node:os";
3
- import { join as join2 } from "node:path";
3
+ import { cpSync, existsSync as existsSync2, mkdtempSync, readdirSync as readdirSync2, renameSync as renameSync3, rmSync } from "node:fs";
4
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
4
5
 
5
6
  // src/host/personalization/store.js
6
7
  import { createHash, randomUUID } from "node:crypto";
@@ -277,10 +278,11 @@ var SKINS = {
277
278
  ]
278
279
  }
279
280
  };
280
- function getSkinSchema(skinId) {
281
+ function getSkinSchema(skinId, profile = "web") {
281
282
  const entry = SKINS[skinId];
282
283
  if (entry === void 0) return null;
283
- return { skinId, fields: entry.fields, builtinAssets: entry.builtinAssets };
284
+ const fields = profile === "desktop" && skinId === "meirenzhi" ? entry.fields.map((field) => field.key === "panelOpacity" ? { ...field, default: 55 } : field) : entry.fields;
285
+ return { skinId, fields, builtinAssets: entry.builtinAssets };
284
286
  }
285
287
  function getField(skinId, key) {
286
288
  return SKINS[skinId]?.fields.find((field) => field.key === key) ?? null;
@@ -288,9 +290,9 @@ function getField(skinId, key) {
288
290
  function listAssetFields(skinId) {
289
291
  return SKINS[skinId]?.fields.filter((field) => field.type === "image") ?? [];
290
292
  }
291
- function defaultsFor(skinId) {
293
+ function defaultsFor(skinId, profile = "web") {
292
294
  const values = {};
293
- for (const field of SKINS[skinId]?.fields ?? []) values[field.key] = field.default;
295
+ for (const field of getSkinSchema(skinId, profile)?.fields ?? []) values[field.key] = field.default;
294
296
  return values;
295
297
  }
296
298
  function isSameOverrideValue(a, b) {
@@ -483,6 +485,7 @@ function createPersonalizationStore(options = {}) {
483
485
  }
484
486
  const fs = options.fs ?? defaultFs();
485
487
  const now = options.now ?? Date.now;
488
+ const profile = options.profile === "desktop" ? "desktop" : "web";
486
489
  const stateFile = join(dataDir, STATE_FILE);
487
490
  const assetsDir = join(dataDir, ASSETS_DIR);
488
491
  const quarantineDir = join(dataDir, QUARANTINE_DIR);
@@ -611,7 +614,7 @@ function createPersonalizationStore(options = {}) {
611
614
  continue;
612
615
  }
613
616
  const section = state.skins[skinId];
614
- const defaults = defaultsFor(skinId);
617
+ const defaults = defaultsFor(skinId, profile);
615
618
  for (const key of Object.keys(section)) {
616
619
  if (!validateOverride(skinId, key, section[key], provider).ok) {
617
620
  delete section[key];
@@ -1181,14 +1184,46 @@ function mountPersonalizationRoutes(host, options) {
1181
1184
 
1182
1185
  // src/index.js
1183
1186
  var name = "@iasiv5/dsh-skins";
1187
+ function expandHomePath(path) {
1188
+ if (path === "~") return homedir();
1189
+ if (path.startsWith("~/") || path.startsWith("~\\")) return join2(homedir(), path.slice(2));
1190
+ return path;
1191
+ }
1184
1192
  function dshHome() {
1185
- return process.env.DSH_HOME?.trim() || join2(homedir(), ".dsh");
1193
+ const configured = process.env.DSH_HOME?.trim();
1194
+ return resolve(expandHomePath(configured || join2(homedir(), ".dsh")));
1195
+ }
1196
+ function hostProfileFor(root, cwd = process.cwd()) {
1197
+ const normalized = (path) => process.platform === "win32" ? resolve(path).toLowerCase() : resolve(path);
1198
+ return normalized(cwd) === normalized(join2(expandHomePath(root), "profiles", "desktop")) ? "desktop" : "web";
1199
+ }
1200
+ function dataDirForProfile(root, profile) {
1201
+ root = resolve(expandHomePath(root));
1202
+ const legacy = join2(root, "dsh-skins");
1203
+ if (profile !== "desktop") return legacy;
1204
+ const desktop = join2(root, "dsh-skins-desktop");
1205
+ if (existsSync2(desktop) || !existsSync2(legacy)) return desktop;
1206
+ const stage = mkdtempSync(join2(root, ".dsh-skins-desktop-migrate-"));
1207
+ try {
1208
+ for (const name2 of readdirSync2(legacy)) {
1209
+ cpSync(join2(legacy, name2), join2(stage, name2), { recursive: true, force: false, errorOnExist: true });
1210
+ }
1211
+ renameSync3(stage, desktop);
1212
+ } catch (error) {
1213
+ if (!existsSync2(desktop)) throw error;
1214
+ } finally {
1215
+ if (dirname2(resolve(stage)) !== resolve(root)) throw new Error("unsafe Desktop migration staging path");
1216
+ if (existsSync2(stage)) rmSync(stage, { recursive: true, force: true });
1217
+ }
1218
+ return desktop;
1186
1219
  }
1187
1220
  function apply(ctx) {
1188
1221
  ctx.inject(["webServer", "webRuntime"], (hostContext) => {
1189
1222
  const root = dshHome();
1223
+ const profile = hostProfileFor(root);
1190
1224
  const personalization = createPersonalizationStore({
1191
- dataDir: join2(root, "dsh-skins")
1225
+ dataDir: dataDirForProfile(root, profile),
1226
+ profile
1192
1227
  });
1193
1228
  hostContext.effect(() => {
1194
1229
  return mountPersonalizationRoutes(hostContext, {
@@ -1200,5 +1235,7 @@ function apply(ctx) {
1200
1235
  }
1201
1236
  export {
1202
1237
  apply,
1238
+ dataDirForProfile,
1239
+ hostProfileFor,
1203
1240
  name
1204
1241
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-skins",
3
3
  "description": "DSH Web 皮肤插件:多套品牌皮肤侧栏热切换。",
4
- "version": "1.5.3",
4
+ "version": "1.5.5",
5
5
  "homepage": "https://github.com/iasiv5/dsh-skins#readme",
6
6
  "repository": {
7
7
  "type": "git",