@linxin666/dsh-client-ui-skin-center 0.3.2 → 0.3.3
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.i18n.yaml +2 -2
- package/README.md +8 -3
- package/README.zh.md +8 -3
- package/contracts/README.md +18 -0
- package/contracts/semantic-attrs-v1.md +1 -1
- package/contracts/skin-manifest-v2.schema.json +1 -1
- package/lib/client.js +289 -14
- package/lib/client.js.map +1 -1
- package/lib/index.js +114 -14
- package/package.json +2 -2
- package/skins/blue-fantasy/skin.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the 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-docs.mjs --write <dir>
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 04b1c524d8222926a64008329b4fb9454ab2f0c0
|
|
6
|
+
README.zh.md: 56cd3679ad66705e5543409086911be3bf723fbd
|
package/README.md
CHANGED
|
@@ -22,9 +22,9 @@ dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center
|
|
|
22
22
|
# From the repo (dev): dsh plugin --profile web add link:$(pwd)/packages/skins/skin-center
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`$(pwd)` is your clone of the dsh-web
|
|
25
|
+
`$(pwd)` is your clone of the dsh-web monorepo. Only the default skin (blue-fantasy) ships inside this package; every other skin installs on demand from dsh-market.com into `$DSH_HOME/skins/<id>/`, and community skins are plain directories in the same place (no install command, no restart — reopen the card or reload to pick them up). A fresh install activates blue-fantasy by default (host seed); an update whose active skin is no longer available falls back to the stock look. The package ships `skins/blue-fantasy` only; the remaining skins stay in the repository under `skins/` as the market-build and gallery source and are never published in the package.
|
|
26
26
|
|
|
27
|
-
skin-center is a self-contained bundle meeting the official DSH plugin standard (`dsh.bundle.patch` points to `cordis.patch.yml`); it can also be installed via git: `dsh plugin --profile web add github:<org>/dsh-web
|
|
27
|
+
skin-center is a self-contained bundle meeting the official DSH plugin standard (`dsh.bundle.patch` points to `cordis.patch.yml`); it can also be installed via git: `dsh plugin --profile web add github:<org>/dsh-web#<sha>` (the `prepare` script builds `lib/` in place). pnpm ≥10 requires authorizing `allowBuilds` before installing a git dependency; a local `link:` install has no such requirement.
|
|
28
28
|
|
|
29
29
|
## Configuration
|
|
30
30
|
|
|
@@ -40,7 +40,7 @@ skin-center is a self-contained bundle meeting the official DSH plugin standard
|
|
|
40
40
|
- All `/api/skin-center/*` routes are same-origin only: writes reject cross-site requests (Sec-Fetch-Site / Origin fence), and asset reads are contained inside each skin directory (path escapes fail closed).
|
|
41
41
|
- Skin CSS is sanitized (whitelist) before serving; `patches.css` (L3) is arbitrary CSS by design and disclosed as such — it runs with full page styling power and is not a security boundary.
|
|
42
42
|
- The custom-theme editor emits only fixed declarations from `CUSTOM_THEME_ALLOWED_TOKENS`, each verified against the official token registry. User input is normalized color/contrast data and never becomes a selector, URL or free-form CSS payload.
|
|
43
|
-
- `hooks.mjs` is trusted code that shares this repository's review and release; it is served same-origin only and its import/apply errors can never take the static skin down.
|
|
43
|
+
- `hooks.mjs` is trusted code that shares this repository's review and release; it is served same-origin only and its import/apply errors can never take the static skin down. Hooks run for built-in skins, and for user-directory skins installed from the official DSH Market whose `dsh-market.provenance.json` sha256-pins the on-disk `skin.json` and hooks entry to the market-served bytes (verified by `src/provenance.ts`, issue #1073); a missing or mismatched provenance — hand-dropped or tampered directories — keeps the hooks facet refused while the declarative parts still load.
|
|
44
44
|
|
|
45
45
|
## Known limitations
|
|
46
46
|
|
|
@@ -48,6 +48,10 @@ skin-center is a self-contained bundle meeting the official DSH plugin standard
|
|
|
48
48
|
- Plugins that do not output semantic attributes (and have no stable DOM anchors) receive L1 token coverage only.
|
|
49
49
|
- A skin video background keeps playing regardless of the wallpaper pause-on-hidden setting; pause-on-hidden applies to the Wallpaper Engine bridge only.
|
|
50
50
|
|
|
51
|
+
## Telemetry
|
|
52
|
+
|
|
53
|
+
The browser half sends one anonymous install heartbeat per UTC day to dsh-market.com: a random localStorage id plus this package's name, nothing else. The server stores only a salted hash of that id, never IP addresses, and exposes aggregate counts only. See [docs/telemetry.md](../../../docs/telemetry.md) for the full contract.
|
|
54
|
+
|
|
51
55
|
## Directory structure
|
|
52
56
|
|
|
53
57
|
```
|
|
@@ -57,6 +61,7 @@ skins/skin-center/
|
|
|
57
61
|
src/core/css-safety/ # lightningcss scoping + whitelist pipeline
|
|
58
62
|
src/index.ts # host entry: routes, tapIndex adapter, legacy bridge
|
|
59
63
|
src/skin-repo.ts # dual-source skin catalog (built-in + $DSH_HOME/skins)
|
|
64
|
+
src/provenance.ts # official-market install provenance verification (hooks trust)
|
|
60
65
|
src/routes-v2.ts # /api/skin-center/v2/* routes
|
|
61
66
|
src/tap-index-adapter.ts # the single tapIndex adapter (anti-FOUC)
|
|
62
67
|
src/active-state.ts # active-skin selection persistence
|
package/README.zh.md
CHANGED
|
@@ -22,9 +22,9 @@ dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center
|
|
|
22
22
|
# 仓库开发:dsh plugin --profile web add link:$(pwd)/packages/skins/skin-center
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`$(pwd)` 是 dsh-web
|
|
25
|
+
`$(pwd)` 是 dsh-web monorepo 的本地克隆。只有默认皮肤(蓝色幻想)随这个包发布;其余皮肤在 dsh-market.com 按需安装到 `$DSH_HOME/skins/<id>/`,社区皮肤同样是放进该目录的普通目录(均无安装命令、无需重启——重开卡片或刷新页面即收录)。新装默认激活蓝色幻想(宿主种子);升级后原激活皮肤已不可用时回退官方主题。包内只发布 `skins/blue-fantasy`;其余皮肤保留在仓库 `skins/` 下,作为市场构建与画廊的目录来源,绝不进入 npm 包。
|
|
26
26
|
|
|
27
|
-
皮肤中心是符合官方 DSH 插件标准的自包含 bundle(`dsh.bundle.patch` 指向 `cordis.patch.yml`);也可经 git 安装:`dsh plugin --profile web add github:<org>/dsh-web
|
|
27
|
+
皮肤中心是符合官方 DSH 插件标准的自包含 bundle(`dsh.bundle.patch` 指向 `cordis.patch.yml`);也可经 git 安装:`dsh plugin --profile web add github:<org>/dsh-web#<sha>`(`prepare` 脚本就地构建 `lib/`)。pnpm ≥10 安装 git 依赖前需授权 `allowBuilds`;本地 `link:` 安装无此要求。
|
|
28
28
|
|
|
29
29
|
## 配置
|
|
30
30
|
|
|
@@ -40,7 +40,7 @@ dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center
|
|
|
40
40
|
- 所有 `/api/skin-center/*` 路由仅接受同源请求:写操作拒绝跨站请求(Sec-Fetch-Site / Origin 围栏),资产读取限定在各皮肤目录之内(路径逃逸 fail-closed)。
|
|
41
41
|
- 皮肤 CSS 在服务前经白名单净化;`patches.css`(L3)按设计就是任意 CSS 并如实公示——它拥有完整页面样式能力,不构成安全边界。
|
|
42
42
|
- 自定义主题编辑器只会从 `CUSTOM_THEME_ALLOWED_TOKENS` 生成固定声明,且每个 token 都对照官方 token 注册表校验。用户输入只作为规范化后的颜色/对比度数据,不会成为选择器、URL 或自由 CSS 载荷。
|
|
43
|
-
- `hooks.mjs` 是与本仓库同审同发的受信代码,仅同源 serve,其 import/apply 错误永远不会拖垮静态皮肤。
|
|
43
|
+
- `hooks.mjs` 是与本仓库同审同发的受信代码,仅同源 serve,其 import/apply 错误永远不会拖垮静态皮肤。hooks 对内置皮肤放行;对用户目录皮肤,仅当其从官方 DSH 市场安装且 `dsh-market.provenance.json` 以 sha256 钉住磁盘上的 `skin.json` 与 hooks 入口字节时放行(由 `src/provenance.ts` 校验,issue #1073);provenance 缺失或不匹配——手工投放或被篡改的目录——hooks facet 保持拒绝,声明式部分仍正常加载。
|
|
44
44
|
|
|
45
45
|
## 已知限制
|
|
46
46
|
|
|
@@ -48,6 +48,10 @@ dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center
|
|
|
48
48
|
- 不输出语义属性(且无稳定 DOM 锚点)的插件只享受 L1 token 覆盖。
|
|
49
49
|
- 皮肤视频背景不受壁纸「隐藏时暂停」设置影响;该设置仅作用于 Wallpaper Engine 桥。
|
|
50
50
|
|
|
51
|
+
## 数据遥测
|
|
52
|
+
|
|
53
|
+
浏览器半区每个 UTC 日向 dsh-market.com 发送一次匿名安装心跳:仅含一个 localStorage 随机 ID 与本包名,无其他数据。服务端只存储该 ID 的加盐哈希,不存 IP,且只暴露聚合计数。完整契约见 [docs/telemetry.md](../../../docs/telemetry.md)。
|
|
54
|
+
|
|
51
55
|
## 目录结构
|
|
52
56
|
|
|
53
57
|
```
|
|
@@ -57,6 +61,7 @@ skins/skin-center/
|
|
|
57
61
|
src/core/css-safety/ # lightningcss 作用域限定 + 白名单管线
|
|
58
62
|
src/index.ts # host 入口:路由、tapIndex 适配器、旧版迁移桥
|
|
59
63
|
src/skin-repo.ts # 双来源皮肤目录册(内置 + $DSH_HOME/skins)
|
|
64
|
+
src/provenance.ts # 官方市场安装 provenance 校验(hooks 信任)
|
|
60
65
|
src/routes-v2.ts # /api/skin-center/v2/* 路由
|
|
61
66
|
src/tap-index-adapter.ts # 单一 tapIndex 适配器(防 FOUC)
|
|
62
67
|
src/active-state.ts # 活动皮肤选择持久化
|
package/contracts/README.md
CHANGED
|
@@ -37,6 +37,24 @@ manifests would be rejected by their own validator. Optional legal metadata
|
|
|
37
37
|
(`license`, `licenseUrl`, `noticeUrl`, `sourceUrl`, `attribution`) is
|
|
38
38
|
first-class in v2.
|
|
39
39
|
|
|
40
|
+
## Hooks trust model
|
|
41
|
+
|
|
42
|
+
`hooks.mjs` is trusted executable code that shares this repository's review
|
|
43
|
+
and release. It is served verbatim over `GET /skins/<id>/hooks.mjs` only for:
|
|
44
|
+
|
|
45
|
+
- **built-in skins** — shipped inside the skin-center npm package, same
|
|
46
|
+
review and release by definition;
|
|
47
|
+
- **official-market installs** — user-directory skins carrying a
|
|
48
|
+
`dsh-market.provenance.json` whose sha256 pins the on-disk `skin.json`
|
|
49
|
+
and hooks entry to the bytes the official DSH Market served (the market
|
|
50
|
+
content is built from this repository, so a hash match means the bytes
|
|
51
|
+
are the reviewed ones). Verification lives in `src/provenance.ts` and
|
|
52
|
+
fails closed: any missing, foreign-source or mismatched provenance keeps
|
|
53
|
+
the hooks facet refused with a catalog warning while the declarative
|
|
54
|
+
parts (skin.css / patches.css / assets) still load (issue #1073).
|
|
55
|
+
|
|
56
|
+
Locally dropped or third-party skin directories never run hooks.
|
|
57
|
+
|
|
40
58
|
## Loader-side rules (pinned here, enforced in M2)
|
|
41
59
|
|
|
42
60
|
- All skin CSS is force-scoped under `html[data-dsh-skin="<id>"]` by the
|
|
@@ -111,7 +111,7 @@ family / 插件区域:
|
|
|
111
111
|
| `git-graph` | dsh-git-graph | slot entry id `git-graph`;`[data-gitgraph-chip-anchor]` / `[data-gitgraph-dialog]` |
|
|
112
112
|
| `pet` | dsh-pet | `[data-dsh-pet-root]`;一级设置分区 settings.section id `pet`(只列内置与已安装宠物) |
|
|
113
113
|
| `remote-web-ui` | dsh-remote-web-ui | slot entry id `remote-web-ui` |
|
|
114
|
-
| `web-ui-settings` | dsh-web-
|
|
114
|
+
| `web-ui-settings` | dsh-web-settings | settings.section id `web-ui-plugins` |
|
|
115
115
|
| `skill-explorer` | dsh-skill-explorer | `[data-dsh-skill-explorer-view]` / `[data-dsh-skill-explorer-entry]` |
|
|
116
116
|
| `doctor` | dsh-doctor | web-ui.plugin.item 槽 entry id `doctor`(设置 → Web UI 插件 → Doctor 卡片);卡片内 `[data-dsh-plugin="doctor"]` |
|
|
117
117
|
| `aionui-panel` | dsh-aionui-panel(停更) | dock entry id `aionui-*` |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://schemas.linxin666.org/dsh-skin/v2.json",
|
|
4
4
|
"title": "DSH Skin Manifest v2",
|
|
5
|
-
"description": "Declarative manifest for a dsh-web
|
|
5
|
+
"description": "Declarative manifest for a dsh-web skin asset directory. Skins are pure assets; the skin-center package is the only loader/renderer. Validators must use this local schema copy and never fetch the $id URL.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
package/lib/client.js
CHANGED
|
@@ -570,6 +570,7 @@ window.__ModuleLoader__.load({
|
|
|
570
570
|
volumeValue = 100;
|
|
571
571
|
dimValue = 25;
|
|
572
572
|
blurValue = 0;
|
|
573
|
+
opacityValue = 100;
|
|
573
574
|
dirsValue = [];
|
|
574
575
|
listeners = /* @__PURE__ */ new Set();
|
|
575
576
|
scope;
|
|
@@ -598,13 +599,16 @@ window.__ModuleLoader__.load({
|
|
|
598
599
|
/** Detached frame-capture video; released on error/abort/loadeddata and on
|
|
599
600
|
* teardown so it never keeps buffering the source file. */
|
|
600
601
|
captureVideo = null;
|
|
602
|
+
/** Guard flag: suppresses readAll during applyThemeDefaults scope writes
|
|
603
|
+
* to prevent mid-write listener cascades from resetting values. */
|
|
604
|
+
seeding = false;
|
|
601
605
|
constructor(scope, options = {}) {
|
|
602
606
|
this.scope = scope;
|
|
603
607
|
this.options = options;
|
|
604
608
|
this.doc = options.doc ?? document;
|
|
605
609
|
this.readAll();
|
|
606
610
|
this.unsubscribe = scope.subscribe(() => {
|
|
607
|
-
if (this.disposed) return;
|
|
611
|
+
if (this.disposed || this.seeding) return;
|
|
608
612
|
this.readAll();
|
|
609
613
|
if (this.enabledValue && this.selectionValue && (!this.applied || this.applied.id !== this.selectionValue)) this.fetchAndSync();
|
|
610
614
|
else {
|
|
@@ -625,6 +629,7 @@ window.__ModuleLoader__.load({
|
|
|
625
629
|
});
|
|
626
630
|
this.mountObserver.observe(this.doc.body, { childList: true });
|
|
627
631
|
}
|
|
632
|
+
this.applyThemeDefaults();
|
|
628
633
|
if (this.enabledValue && this.selectionValue) this.fetchAndSync();
|
|
629
634
|
}
|
|
630
635
|
fetchAndSync() {
|
|
@@ -722,6 +727,7 @@ window.__ModuleLoader__.load({
|
|
|
722
727
|
fit = () => this.fitValue;
|
|
723
728
|
dim = () => this.dimValue;
|
|
724
729
|
wallpaperBlur = () => this.blurValue;
|
|
730
|
+
wallpaperOpacity = () => this.opacityValue;
|
|
725
731
|
pauseOnHidden = () => this.pauseOnHiddenValue;
|
|
726
732
|
sound = () => this.soundValue;
|
|
727
733
|
volume = () => this.volumeValue;
|
|
@@ -781,6 +787,12 @@ window.__ModuleLoader__.load({
|
|
|
781
787
|
this.publish();
|
|
782
788
|
this.scope.set("wallpaperBlur", this.blurValue);
|
|
783
789
|
}
|
|
790
|
+
setOpacity(value) {
|
|
791
|
+
this.opacityValue = clamp(value, 0, 100);
|
|
792
|
+
this.render();
|
|
793
|
+
this.publish();
|
|
794
|
+
this.scope.set("wallpaperOpacity", this.opacityValue);
|
|
795
|
+
}
|
|
784
796
|
setPauseOnHidden(value) {
|
|
785
797
|
this.pauseOnHiddenValue = value;
|
|
786
798
|
this.publish();
|
|
@@ -860,6 +872,32 @@ window.__ModuleLoader__.load({
|
|
|
860
872
|
this.doc.removeEventListener("keydown", this.onFirstGesture);
|
|
861
873
|
this.teardownLayers();
|
|
862
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* Seed dim and opacity with theme-tuned values when neither has been
|
|
877
|
+
* explicitly set (both are at schema defaults: dim=25, opacity=100).
|
|
878
|
+
* This runs once at construction so a fresh wallpaper install gets
|
|
879
|
+
* readable defaults for the active theme. Users who have already
|
|
880
|
+
* adjusted either slider keep their values (#1051).
|
|
881
|
+
*/
|
|
882
|
+
applyThemeDefaults() {
|
|
883
|
+
const snapshot = this.scope.getSnapshot();
|
|
884
|
+
const value = snapshot.value ?? {};
|
|
885
|
+
if (value.dim !== void 0 || value.wallpaperOpacity !== void 0) return;
|
|
886
|
+
if (snapshot.writable === false) return;
|
|
887
|
+
if ((this.options.themeGet !== void 0 ? this.options.themeGet() : this.doc.body?.hasAttribute("data-ds-dark-theme") ? "dark" : "light") === "light") {
|
|
888
|
+
this.dimValue = 0;
|
|
889
|
+
this.opacityValue = 40;
|
|
890
|
+
} else {
|
|
891
|
+
this.dimValue = 40;
|
|
892
|
+
this.opacityValue = 100;
|
|
893
|
+
}
|
|
894
|
+
this.seeding = true;
|
|
895
|
+
try {
|
|
896
|
+
this.scope.set("dim", this.dimValue);
|
|
897
|
+
this.scope.set("wallpaperOpacity", this.opacityValue);
|
|
898
|
+
} catch {}
|
|
899
|
+
this.seeding = false;
|
|
900
|
+
}
|
|
863
901
|
readAll() {
|
|
864
902
|
const value = this.scope.getSnapshot().value ?? {};
|
|
865
903
|
this.enabledValue = typeof value.enabled === "boolean" ? value.enabled : true;
|
|
@@ -872,6 +910,7 @@ window.__ModuleLoader__.load({
|
|
|
872
910
|
this.volumeValue = typeof value.volume === "number" && Number.isFinite(value.volume) ? clamp(value.volume, 0, 100) : 100;
|
|
873
911
|
this.dimValue = typeof value.dim === "number" && Number.isFinite(value.dim) ? clamp(value.dim, 0, 90) : 25;
|
|
874
912
|
this.blurValue = typeof value.wallpaperBlur === "number" && Number.isFinite(value.wallpaperBlur) ? clamp(value.wallpaperBlur, 0, 60) : 0;
|
|
913
|
+
this.opacityValue = typeof value.wallpaperOpacity === "number" && Number.isFinite(value.wallpaperOpacity) ? clamp(value.wallpaperOpacity, 0, 100) : 100;
|
|
875
914
|
this.dirsValue = Array.isArray(value.weLibraryDirs) ? value.weLibraryDirs.filter((d) => typeof d === "string" && d.trim() !== "") : [];
|
|
876
915
|
}
|
|
877
916
|
/** Resume a policy-blocked video on the first user gesture (#580). */
|
|
@@ -914,9 +953,16 @@ window.__ModuleLoader__.load({
|
|
|
914
953
|
this.rootNeutralizer.dataset.dshWallpaperRoot = "";
|
|
915
954
|
this.rootNeutralizer.textContent = `
|
|
916
955
|
[id="root"] { background: transparent; }
|
|
917
|
-
|
|
956
|
+
/* Wallpaper opacity backdrop (#1051): light-mode fades to white,
|
|
957
|
+
dark-mode fades to black. :has() tracks theme switches live. */
|
|
958
|
+
html[data-dsh-wallpaper-active] {
|
|
959
|
+
background-color: white !important;
|
|
960
|
+
background-image: none !important;
|
|
961
|
+
}
|
|
962
|
+
html[data-dsh-wallpaper-active]:has(body[data-ds-dark-theme]) {
|
|
963
|
+
background-color: black !important;
|
|
964
|
+
}
|
|
918
965
|
body[data-dsh-wallpaper-active],
|
|
919
|
-
html[data-dsh-skin][data-dsh-wallpaper-active],
|
|
920
966
|
html[data-dsh-skin][data-dsh-wallpaper-active] body,
|
|
921
967
|
html[data-dsh-skin] body[data-dsh-wallpaper-active],
|
|
922
968
|
body[data-dsh-wallpaper-active][data-ds-dark-theme],
|
|
@@ -985,6 +1031,7 @@ window.__ModuleLoader__.load({
|
|
|
985
1031
|
const blur = this.blurValue > 0 ? "blur(" + String(this.blurValue) + "px)" : "";
|
|
986
1032
|
this.mediaLayer.style.filter = blur;
|
|
987
1033
|
this.mediaLayer.style.transform = this.blurValue > 0 ? "scale(1.05)" : "";
|
|
1034
|
+
this.mediaLayer.style.opacity = this.opacityValue < 100 ? String(this.opacityValue / 100) : "";
|
|
988
1035
|
this.scrimLayer.style.background = "rgba(0, 0, 0, " + String(this.dimValue / 100) + ")";
|
|
989
1036
|
}
|
|
990
1037
|
/** Push the current sizing mode onto the mounted media element. */
|
|
@@ -1477,6 +1524,7 @@ window.__ModuleLoader__.load({
|
|
|
1477
1524
|
const fit = (0, react.useSyncExternalStore)(wallpaper.subscribe, wallpaper.fit);
|
|
1478
1525
|
const dim = (0, react.useSyncExternalStore)(wallpaper.subscribe, wallpaper.dim);
|
|
1479
1526
|
const blur = (0, react.useSyncExternalStore)(wallpaper.subscribe, wallpaper.wallpaperBlur);
|
|
1527
|
+
const opacity = (0, react.useSyncExternalStore)(wallpaper.subscribe, wallpaper.wallpaperOpacity);
|
|
1480
1528
|
const pauseOnHidden = (0, react.useSyncExternalStore)(wallpaper.subscribe, wallpaper.pauseOnHidden);
|
|
1481
1529
|
const sound = (0, react.useSyncExternalStore)(wallpaper.subscribe, wallpaper.sound);
|
|
1482
1530
|
const volume = (0, react.useSyncExternalStore)(wallpaper.subscribe, wallpaper.volume);
|
|
@@ -1485,6 +1533,7 @@ window.__ModuleLoader__.load({
|
|
|
1485
1533
|
const dirs = (0, react.useSyncExternalStore)(wallpaper.subscribe, wallpaper.dirs);
|
|
1486
1534
|
const [shownDim, setShownDim] = useLiveValue$1(dim);
|
|
1487
1535
|
const [shownBlur, setShownBlur] = useLiveValue$1(blur);
|
|
1536
|
+
const [shownOpacity, setShownOpacity] = useLiveValue$1(opacity);
|
|
1488
1537
|
const [shownVolume, setShownVolume] = useLiveValue$1(volume);
|
|
1489
1538
|
const [dirInput, setDirInput] = (0, react.useState)("");
|
|
1490
1539
|
const [picking, setPicking] = (0, react.useState)(false);
|
|
@@ -1723,6 +1772,30 @@ window.__ModuleLoader__.load({
|
|
|
1723
1772
|
wallpaper.setDim(value);
|
|
1724
1773
|
}
|
|
1725
1774
|
}),
|
|
1775
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1776
|
+
className: skin_center_module_css_default.backgroundHead,
|
|
1777
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1778
|
+
className: skin_center_module_css_default.backgroundLabel,
|
|
1779
|
+
children: t("wallpaperOpacity")
|
|
1780
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1781
|
+
className: skin_center_module_css_default.backgroundValue,
|
|
1782
|
+
"aria-hidden": "true",
|
|
1783
|
+
children: [shownOpacity, "%"]
|
|
1784
|
+
})]
|
|
1785
|
+
}),
|
|
1786
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderControl, {
|
|
1787
|
+
className: skin_center_module_css_default.backgroundRange,
|
|
1788
|
+
min: 0,
|
|
1789
|
+
max: 100,
|
|
1790
|
+
step: 5,
|
|
1791
|
+
value: opacity,
|
|
1792
|
+
ariaValuetext: shownOpacity + "%",
|
|
1793
|
+
ariaLabel: t("wallpaperOpacity"),
|
|
1794
|
+
onChanging: setShownOpacity,
|
|
1795
|
+
onChange: (value) => {
|
|
1796
|
+
wallpaper.setOpacity(value);
|
|
1797
|
+
}
|
|
1798
|
+
}),
|
|
1726
1799
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1727
1800
|
className: skin_center_module_css_default.backgroundHead,
|
|
1728
1801
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2599,6 +2672,36 @@ window.__ModuleLoader__.load({
|
|
|
2599
2672
|
};
|
|
2600
2673
|
/** The fields normalize/sanitize know about; unknown keys are dropped. */
|
|
2601
2674
|
const SKIN_BACKGROUND_FIELDS = Object.keys(SKIN_BACKGROUND_DEFAULTS);
|
|
2675
|
+
function clampInt(value, min, max) {
|
|
2676
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
2677
|
+
}
|
|
2678
|
+
const RANGES = {
|
|
2679
|
+
backgroundOpacity: [0, 100],
|
|
2680
|
+
backgroundBlurEmpty: [0, 20],
|
|
2681
|
+
backgroundBlurContent: [0, 20],
|
|
2682
|
+
inputCardBlur: [0, 20],
|
|
2683
|
+
bubbleOpacity: [0, 100]
|
|
2684
|
+
};
|
|
2685
|
+
function isRecord$1(value) {
|
|
2686
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Lenient normalization for stored/legacy data: unknown keys and wrongly
|
|
2690
|
+
* typed fields are dropped, numeric fields are clamped into range. Never
|
|
2691
|
+
* fails; a non-object input yields an empty config.
|
|
2692
|
+
*/
|
|
2693
|
+
function normalizeSkinBackground(value) {
|
|
2694
|
+
if (!isRecord$1(value)) return {};
|
|
2695
|
+
const out = {};
|
|
2696
|
+
if (typeof value.enabled === "boolean") out.enabled = value.enabled;
|
|
2697
|
+
for (const field of Object.keys(RANGES)) {
|
|
2698
|
+
const raw = value[field];
|
|
2699
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) continue;
|
|
2700
|
+
const [min, max] = RANGES[field];
|
|
2701
|
+
out[field] = clampInt(raw, min, max);
|
|
2702
|
+
}
|
|
2703
|
+
return out;
|
|
2704
|
+
}
|
|
2602
2705
|
/** Fill every absent field from {@link SKIN_BACKGROUND_DEFAULTS}. */
|
|
2603
2706
|
function resolveSkinBackground(value) {
|
|
2604
2707
|
return {
|
|
@@ -2653,7 +2756,7 @@ window.__ModuleLoader__.load({
|
|
|
2653
2756
|
/**
|
|
2654
2757
|
* Selector for a conversation message row inside the shell's center column.
|
|
2655
2758
|
* Official shell message rows carry `data-chat-anchor-key`; the
|
|
2656
|
-
* `data-pane="conversation"` attribute is stamped by the dsh-web-
|
|
2759
|
+
* `data-pane="conversation"` attribute is stamped by the dsh-web-all compat
|
|
2657
2760
|
* shim on the center column, where the _userRow / _compactionRow /
|
|
2658
2761
|
* _contextRow / _turnErrorRow suffixes are CSS-module message-row classes
|
|
2659
2762
|
* (hash prefix varies, suffix is stable).
|
|
@@ -2914,6 +3017,62 @@ window.__ModuleLoader__.load({
|
|
|
2914
3017
|
}
|
|
2915
3018
|
};
|
|
2916
3019
|
//#endregion
|
|
3020
|
+
//#region src/core/background-scope.ts
|
|
3021
|
+
/**
|
|
3022
|
+
* Safe reconciliation helpers for the legacy skin-background settings scope.
|
|
3023
|
+
*
|
|
3024
|
+
* The scope's resolved value contains schema defaults, while its raw user layer
|
|
3025
|
+
* contains only fields explicitly stored by the user. The v2 active-state
|
|
3026
|
+
* document is authoritative, so a scope update may merge only those raw fields
|
|
3027
|
+
* into the live background values.
|
|
3028
|
+
*/
|
|
3029
|
+
function isRecord(value) {
|
|
3030
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3031
|
+
}
|
|
3032
|
+
/** Return the known, explicitly stored skin-background fields from a user layer. */
|
|
3033
|
+
function extractSkinBackgroundUserLayer(value) {
|
|
3034
|
+
if (!isRecord(value)) return null;
|
|
3035
|
+
const normalized = normalizeSkinBackground(value);
|
|
3036
|
+
const knownFields = Object.keys(normalized);
|
|
3037
|
+
if (knownFields.length === 0) return null;
|
|
3038
|
+
const owned = {};
|
|
3039
|
+
for (const field of knownFields) owned[field] = normalized[field];
|
|
3040
|
+
return owned;
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Build a v2-safe patch from the raw user layer. Absent fields, including
|
|
3044
|
+
* schema-default fields absent from the user layer, never enter the patch.
|
|
3045
|
+
*/
|
|
3046
|
+
function skinBackgroundUserPatch(current, user) {
|
|
3047
|
+
const userConfig = extractSkinBackgroundUserLayer(user);
|
|
3048
|
+
if (userConfig === null) return null;
|
|
3049
|
+
const patch = {};
|
|
3050
|
+
for (const field of SKIN_BACKGROUND_FIELDS) {
|
|
3051
|
+
if (!Object.hasOwn(userConfig, field)) continue;
|
|
3052
|
+
const value = userConfig[field];
|
|
3053
|
+
if (value === void 0 || value === current[field]) continue;
|
|
3054
|
+
patch[field] = value;
|
|
3055
|
+
}
|
|
3056
|
+
return Object.keys(patch).length === 0 ? null : patch;
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* Accept a scope publication only when its namespace revision is new. The
|
|
3060
|
+
* caller records the returned revision even when there is no user patch, so a
|
|
3061
|
+
* later mirror publication with the same revision stays inert.
|
|
3062
|
+
*/
|
|
3063
|
+
function reconcileSkinBackgroundScope(current, snapshot, lastRevision) {
|
|
3064
|
+
if (snapshot.revision === void 0 || snapshot.revision === lastRevision) return {
|
|
3065
|
+
accepted: false,
|
|
3066
|
+
revision: lastRevision,
|
|
3067
|
+
patch: null
|
|
3068
|
+
};
|
|
3069
|
+
return {
|
|
3070
|
+
accepted: true,
|
|
3071
|
+
revision: snapshot.revision,
|
|
3072
|
+
patch: skinBackgroundUserPatch(current, snapshot.user)
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
//#endregion
|
|
2917
3076
|
//#region src/client/locales.ts
|
|
2918
3077
|
const en = {
|
|
2919
3078
|
title: "Skin Center",
|
|
@@ -2967,6 +3126,7 @@ window.__ModuleLoader__.load({
|
|
|
2967
3126
|
wallpaperFitFill: "Stretch",
|
|
2968
3127
|
wallpaperClear: "Turn off wallpaper",
|
|
2969
3128
|
wallpaperDim: "Wallpaper dimming",
|
|
3129
|
+
wallpaperOpacity: "Wallpaper opacity",
|
|
2970
3130
|
wallpaperBlur: "Wallpaper blur",
|
|
2971
3131
|
wallpaperPauseHidden: "Pause when window hidden",
|
|
2972
3132
|
wallpaperSound: "Wallpaper sound",
|
|
@@ -3008,7 +3168,7 @@ window.__ModuleLoader__.load({
|
|
|
3008
3168
|
customThemeSaveFailed: "Could not save custom theme changes."
|
|
3009
3169
|
};
|
|
3010
3170
|
const zh = {
|
|
3011
|
-
title: "
|
|
3171
|
+
title: "皮肤",
|
|
3012
3172
|
cardDescription: "在 GUI 内即时试穿任意皮肤,退出即完全还原;应用一键完成并自动刷新。",
|
|
3013
3173
|
enabled: "启用皮肤中心",
|
|
3014
3174
|
enabledHint: "关闭后停用试穿、应用与背景控件,重新打开即恢复。",
|
|
@@ -3059,6 +3219,7 @@ window.__ModuleLoader__.load({
|
|
|
3059
3219
|
wallpaperFitFill: "拉伸铺满",
|
|
3060
3220
|
wallpaperClear: "关闭壁纸",
|
|
3061
3221
|
wallpaperDim: "壁纸暗化",
|
|
3222
|
+
wallpaperOpacity: "壁纸不透明度",
|
|
3062
3223
|
wallpaperBlur: "壁纸模糊",
|
|
3063
3224
|
wallpaperPauseHidden: "窗口隐藏时暂停",
|
|
3064
3225
|
wallpaperSound: "壁纸声音",
|
|
@@ -4384,6 +4545,76 @@ window.__ModuleLoader__.load({
|
|
|
4384
4545
|
}
|
|
4385
4546
|
};
|
|
4386
4547
|
//#endregion
|
|
4548
|
+
//#region src/client/telemetry.ts
|
|
4549
|
+
const VISITOR_KEY = "dsh-web-ui-telemetry-visitor";
|
|
4550
|
+
const DAY_KEY_PREFIX = "dsh-web-ui-telemetry-day:";
|
|
4551
|
+
const ENDPOINT = "https://dsh-market.com/api/telemetry/event";
|
|
4552
|
+
/** The building package's version, when the bundle carries it. */
|
|
4553
|
+
function bakedVersion() {
|
|
4554
|
+
try {
|
|
4555
|
+
return "0.3.3";
|
|
4556
|
+
} catch {
|
|
4557
|
+
return;
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
/** Read or lazily create the anonymous visitor id; null when storage is unavailable. */
|
|
4561
|
+
function visitorId() {
|
|
4562
|
+
try {
|
|
4563
|
+
const existing = localStorage.getItem(VISITOR_KEY);
|
|
4564
|
+
if (existing && /^[A-Za-z0-9_-]{16,64}$/.test(existing)) return existing;
|
|
4565
|
+
const fresh = crypto.randomUUID().replaceAll("-", "");
|
|
4566
|
+
localStorage.setItem(VISITOR_KEY, fresh);
|
|
4567
|
+
return fresh;
|
|
4568
|
+
} catch {
|
|
4569
|
+
return null;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
/** Drop stale per-day dedup keys so localStorage does not grow forever. */
|
|
4573
|
+
function pruneDayKeys(today) {
|
|
4574
|
+
try {
|
|
4575
|
+
for (let index = localStorage.length - 1; index >= 0; index -= 1) {
|
|
4576
|
+
const key = localStorage.key(index);
|
|
4577
|
+
if (key !== null && key.startsWith(DAY_KEY_PREFIX) && key !== DAY_KEY_PREFIX + today) localStorage.removeItem(key);
|
|
4578
|
+
}
|
|
4579
|
+
} catch {}
|
|
4580
|
+
}
|
|
4581
|
+
/**
|
|
4582
|
+
* Fire the daily heartbeat for the given items at most once per UTC day per
|
|
4583
|
+
* browser. Never throws and never blocks the caller. Items without an explicit
|
|
4584
|
+
* version inherit the bundle's baked build version.
|
|
4585
|
+
*/
|
|
4586
|
+
function reportDailyHeartbeat(items) {
|
|
4587
|
+
try {
|
|
4588
|
+
if (items.length === 0) return;
|
|
4589
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4590
|
+
if (navigator.webdriver) return;
|
|
4591
|
+
if (localStorage.getItem(DAY_KEY_PREFIX + today) !== null) return;
|
|
4592
|
+
const visitor = visitorId();
|
|
4593
|
+
if (visitor === null) return;
|
|
4594
|
+
pruneDayKeys(today);
|
|
4595
|
+
const payloadItems = items.map((item) => {
|
|
4596
|
+
const out = { name: item.name };
|
|
4597
|
+
const version = item.version ?? bakedVersion();
|
|
4598
|
+
if (version !== void 0) out.version = version;
|
|
4599
|
+
if (item.channel !== void 0) out.channel = item.channel;
|
|
4600
|
+
return out;
|
|
4601
|
+
});
|
|
4602
|
+
const body = JSON.stringify({
|
|
4603
|
+
kind: "heartbeat",
|
|
4604
|
+
visitor,
|
|
4605
|
+
items: payloadItems
|
|
4606
|
+
});
|
|
4607
|
+
fetch(ENDPOINT, {
|
|
4608
|
+
method: "POST",
|
|
4609
|
+
headers: { "content-type": "application/json" },
|
|
4610
|
+
body,
|
|
4611
|
+
keepalive: true
|
|
4612
|
+
}).then((response) => {
|
|
4613
|
+
if (response.ok) localStorage.setItem(DAY_KEY_PREFIX + today, "1");
|
|
4614
|
+
}).catch(() => {});
|
|
4615
|
+
} catch {}
|
|
4616
|
+
}
|
|
4617
|
+
//#endregion
|
|
4387
4618
|
//#region src/client/index.ts
|
|
4388
4619
|
/** Locale namespace owned by this plugin. */
|
|
4389
4620
|
const NS = "skinCenter";
|
|
@@ -4397,12 +4628,36 @@ window.__ModuleLoader__.load({
|
|
|
4397
4628
|
"remote",
|
|
4398
4629
|
"workspaces"
|
|
4399
4630
|
];
|
|
4631
|
+
/** Self-report item for the install heartbeat. */
|
|
4632
|
+
const SELF_ITEM = [{ name: "@linxin666/dsh-client-ui-skin-center" }];
|
|
4633
|
+
/**
|
|
4634
|
+
* Beat the install heartbeat (docs/telemetry.md), enriching it with the
|
|
4635
|
+
* installed skin inventory (skin:<id> + version + channel) once the v2
|
|
4636
|
+
* catalog answers. Offline or pre-boot the beat stays package-only.
|
|
4637
|
+
*/
|
|
4638
|
+
function beatHeartbeat() {
|
|
4639
|
+
reportDailyHeartbeat(SELF_ITEM);
|
|
4640
|
+
fetch("/api/skin-center/v2/catalog").then((res) => res.ok ? res.json() : null).then((catalog) => {
|
|
4641
|
+
if (!catalog || !Array.isArray(catalog.skins)) return;
|
|
4642
|
+
const items = [...SELF_ITEM];
|
|
4643
|
+
for (const skin of catalog.skins) {
|
|
4644
|
+
const id = skin && skin.manifest && typeof skin.manifest.id === "string" ? skin.manifest.id : "";
|
|
4645
|
+
if (!id) continue;
|
|
4646
|
+
const item = { name: "skin:" + id };
|
|
4647
|
+
if (typeof skin.manifest.version === "string") item.version = skin.manifest.version;
|
|
4648
|
+
if (typeof skin.channel === "string") item.channel = skin.channel;
|
|
4649
|
+
items.push(item);
|
|
4650
|
+
}
|
|
4651
|
+
reportDailyHeartbeat(items.slice(0, 64));
|
|
4652
|
+
}).catch(() => {});
|
|
4653
|
+
}
|
|
4400
4654
|
/**
|
|
4401
4655
|
* Register the skin-center dictionaries, the body scope attribute, and the
|
|
4402
4656
|
* Skin Center as a first-level settings section.
|
|
4403
4657
|
* @param ctx - client root context.
|
|
4404
4658
|
*/
|
|
4405
4659
|
function apply(ctx) {
|
|
4660
|
+
beatHeartbeat();
|
|
4406
4661
|
ctx.effect(() => {
|
|
4407
4662
|
try {
|
|
4408
4663
|
return ctx.locale.register(NS, {
|
|
@@ -4448,19 +4703,37 @@ window.__ModuleLoader__.load({
|
|
|
4448
4703
|
const scopeConfig = () => {
|
|
4449
4704
|
const value = backgroundScope.getSnapshot().value;
|
|
4450
4705
|
if (value === void 0 || value === null) return null;
|
|
4451
|
-
if (!SKIN_BACKGROUND_FIELDS.some((field) => value[field] !== void 0)) return null;
|
|
4452
4706
|
return value;
|
|
4453
4707
|
};
|
|
4708
|
+
let v2Loaded = false;
|
|
4709
|
+
let lastScopeRevision = backgroundScope.getSnapshot().revision;
|
|
4454
4710
|
const background = new BackgroundController(scopeConfig(), persistBackground);
|
|
4455
|
-
|
|
4456
|
-
if (
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4711
|
+
const reconcileScope = () => {
|
|
4712
|
+
if (!v2Loaded) return;
|
|
4713
|
+
const snapshot = backgroundScope.getSnapshot();
|
|
4714
|
+
const result = reconcileSkinBackgroundScope(background.snapshot(), {
|
|
4715
|
+
revision: snapshot.revision,
|
|
4716
|
+
user: snapshot.user
|
|
4717
|
+
}, lastScopeRevision);
|
|
4718
|
+
if (!result.accepted) return;
|
|
4719
|
+
lastScopeRevision = result.revision;
|
|
4720
|
+
if (result.patch === null) return;
|
|
4721
|
+
const current = background.snapshot();
|
|
4722
|
+
background.init({
|
|
4723
|
+
...current,
|
|
4724
|
+
...result.patch
|
|
4725
|
+
});
|
|
4462
4726
|
persistBackground(background.snapshot());
|
|
4463
|
-
}
|
|
4727
|
+
};
|
|
4728
|
+
fetch(V2_ACTIVE_URL).then((res) => res.ok ? res.json() : null).then((body) => {
|
|
4729
|
+
v2Loaded = true;
|
|
4730
|
+
if (body?.background) background.init(body.background);
|
|
4731
|
+
reconcileScope();
|
|
4732
|
+
}).catch(() => {
|
|
4733
|
+
v2Loaded = true;
|
|
4734
|
+
reconcileScope();
|
|
4735
|
+
});
|
|
4736
|
+
ctx.effect(() => backgroundScope.subscribe(reconcileScope), "ui-skin-center: background scope sync");
|
|
4464
4737
|
ctx.effect(() => () => {
|
|
4465
4738
|
flushBackground();
|
|
4466
4739
|
background.dispose();
|
|
@@ -4509,6 +4782,7 @@ window.__ModuleLoader__.load({
|
|
|
4509
4782
|
fit: () => wallpaper.fit(),
|
|
4510
4783
|
dim: () => wallpaper.dim(),
|
|
4511
4784
|
wallpaperBlur: () => wallpaper.wallpaperBlur(),
|
|
4785
|
+
wallpaperOpacity: () => wallpaper.wallpaperOpacity(),
|
|
4512
4786
|
pauseOnHidden: () => wallpaper.pauseOnHidden(),
|
|
4513
4787
|
sound: () => wallpaper.sound(),
|
|
4514
4788
|
volume: () => wallpaper.volume(),
|
|
@@ -4524,6 +4798,7 @@ window.__ModuleLoader__.load({
|
|
|
4524
4798
|
setFit: (fit) => wallpaper.setFit(fit),
|
|
4525
4799
|
setDim: (value) => wallpaper.setDim(value),
|
|
4526
4800
|
setBlur: (value) => wallpaper.setBlur(value),
|
|
4801
|
+
setOpacity: (value) => wallpaper.setOpacity(value),
|
|
4527
4802
|
setPauseOnHidden: (value) => wallpaper.setPauseOnHidden(value),
|
|
4528
4803
|
setSound: (value) => wallpaper.setSound(value),
|
|
4529
4804
|
setVolume: (value) => wallpaper.setVolume(value),
|