dsh-font-enhancer 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,13 +12,14 @@ Chinese/English font, font-size, line-height, weight, italic, and text color.
12
12
  Features **unified** global font/color overrides, **theme** save/switch, **框选
13
13
  (pick-region)** element selection, and **random** font/color generators.
14
14
 
15
- > ✅ **v1.2.0 同时支持 DSH **网页端**(浏览器访问 Web GUI,默认 `http://127.0.0.1:43120`)
16
- > 和 **桌面端**(Electron 内置窗口)。** 两端均可正常使用框选区域、字体/颜色设置
17
- > 等全部功能。
15
+ > ✅ **v1.3.0 同时支持 DSH **网页端**(浏览器访问 Web GUI)和 **桌面端**(Electron 内置窗口)。**
16
+ > 新增**一键下载安装可爱字体**(霞鹜文楷/得意黑/站酷快乐体等,带实时下载进度)、**添加自提供安装包**、
17
+ > **🔄 刷新按钮**、保存方案记住统一状态等功能。搜「字体」即可在插件市场找到本插件。
18
18
  >
19
- > ✅ v1.2.0 supports both the DSH **Web GUI** (browser access, default
20
- > `http://127.0.0.1:43120`) and the **Desktop Electron window**. All features
21
- > work in both environments.
19
+ > ✅ v1.3.0 supports both the DSH **Web GUI** (browser) and the **Desktop Electron window**.
20
+ > New: one-click install of cute fonts (LXGW WenKai / Smiley Sans / ZCOOL KuaiLe etc. with live
21
+ > download progress), add your own font file, refresh button, themes remember unified overrides.
22
+ > Search "font / 字体" on the plugin market to find it.
22
23
 
23
24
  ---
24
25
 
@@ -84,9 +85,8 @@ Or copy the folder into `node_modules/` and add the package name to the profile
84
85
  ### 版本 Versions
85
86
  | 版本 | 说明 |
86
87
  |------|------|
87
- | `v1.2.0` | **网页端+桌面端双端可用**。修复桌面端 Electron 窗口浮球不可见问题。 |
88
- | `v1.1.0` | 增强版:新增框选区域(pick-region)+ 采集调试 + 随机配色。**仅网页端可用**。 |
89
- | `v1.0.0` | 旧版:内置区域样式 + 统一控制 + 主题。相对简单,兼容性更好。 |
88
+ | `v1.3.0` | **最新**:网页端+桌面端双端可用。一键下载安装可爱字体(带进度)、添加自提供安装包、刷新按钮、保存方案记住统一状态、字重斜体。搜「字体」可找到。 |
89
+ | `v1.0.0` | 旧版:内置区域样式 + 统一控制 + 主题。 |
90
90
 
91
91
  ---
92
92
 
package/lib/client.js CHANGED
@@ -54,7 +54,7 @@ window.__ModuleLoader__.load({
54
54
  // old region colors (e.g. the first cyan/blue scheme) are dropped and the
55
55
  // latest DEFAULT_REGIONS() palette is loaded instead. This makes palette
56
56
  // refreshes survive a restart without needing the user to clear localStorage.
57
- var SCHEMA_VERSION = 21;
57
+ var SCHEMA_VERSION = 22;
58
58
  // 21: DEFAULT_REGIONS 清空 —— 用户从零开始逐项框选/添加、逐项测试颜色稳定生效
59
59
 
60
60
  // ---------- helpers ----------
@@ -118,28 +118,30 @@ window.__ModuleLoader__.load({
118
118
  return d;
119
119
  }
120
120
  function DEFAULT_REGIONS() {
121
- // 默认区域 = 用户采集到的「稳定 DOM 路径」(operation.log REC_NEW),跨重启有效。
122
- // 每项先用统一红色 #FF0000 验证是否稳定定位+变色,确认后再逐项调整颜色。
123
- // 注意:部分类名是 CSS-Modules 哈希(.mMKm6W_*/.xUkysG_*),若 DSH 升级 re-hash 会失效,需重新采集。
121
+ // 默认区域 = 用 data-slot 稳定属性定位(不受 DSH 前端 CSS-Modules re-hash 影响)。
122
+ // 校准自 DSH 2.0.4 真实 DOM(2026-09-01 operation.log CALIBRATE 探针数据)。
123
+ // 每项先给一个独特颜色供交叉验证,确认稳定后固化。
124
+ // 注意:CSS-Modules 哈希类名(如 .mMKm6W_*)可能随 DSH 升级变化,尽量用 data-slot 定位。
124
125
  function mk(o) { var d = defaultRegion(); return Object.assign(d, o); }
125
126
  return [
126
- // 探针方案:每个区域一个独特颜色,用户重启后对照界面变色位置→告诉我颜色对应哪个区域,我据此固化正确默认。
127
- // logo(红)
128
- mk({ name: "logo", selector: '[data-slot="sidebar"] .xUkysG_logoRow, [data-slot="sidebar"] [class*="logo"]', enabled: true, useColor: true, color: "#FF0000", group: "其他" }),
129
- // 按钮(橙)
130
- mk({ name: "按钮", selector: '[data-slot="sidebar"] .xUkysG_newSession, [data-slot="sidebar"] .dsh-atb-entry, [data-slot="sidebar"] .dsw-web-sessions-badge, [data-slot="sidebar"] .zOa2rq_trigger', enabled: true, useColor: true, color: "#FF8C00", group: "左侧栏" }),
131
- // 对话栏上方(金)
132
- mk({ name: "对话栏上方", selector: '[data-slot="conversation"] ._4RFuWq_titleRow, [data-slot="conversation"] ._4RFuWq_tabs', enabled: true, useColor: true, color: "#FFD700", group: "对话区" }),
133
- // 工作区(亮绿)
134
- mk({ name: "工作区", selector: '[data-slot="sidebar"] .xUkysG_regionArea', enabled: true, useColor: true, color: "#00FF00", group: "左侧栏" }),
135
- // 对话区(深天蓝)
136
- mk({ name: "对话区", selector: '[data-slot="conversation.chat.node"] .sDVxEG_body, [data-slot*="conversation.chat.node"] [data-slot="markdown"]', enabled: true, useColor: true, color: "#00BFFF", group: "对话区" }),
137
- // 对话信息显示(蓝紫)
138
- mk({ name: "对话信息显示", selector: '[data-slot="conversation.chat.node"] ._row_luwio_16, [data-slot="conversation.chat.node"] .QWj8lG_actions', enabled: true, useColor: true, color: "#8A2BE2", group: "对话区" }),
127
+ // 左侧栏整体(红)
128
+ mk({ name: "左侧栏", selector: '[data-slot="sidebar"]', enabled: true, useColor: true, color: "#FF0000", group: "左侧栏" }),
129
+ // 对话区消息(深天蓝)— 用 chat.node + markdown 双重定位
130
+ mk({ name: "对话区", selector: '[data-slot="markdown"], [data-slot="conversation.chat.node"] > div', enabled: true, useColor: true, color: "#00BFFF", group: "对话区" }),
131
+ // 对话栏上方(金)— header 区域
132
+ mk({ name: "对话栏上方", selector: '[data-slot="conversation.session.header"]', enabled: true, useColor: true, color: "#FFD700", group: "对话区" }),
139
133
  // 输入区(品红)
140
- mk({ name: "输入区", selector: '[data-slot*="conversation.composer"] .krUYjW_card, [data-slot*="conversation.composer"] textarea, [data-slot*="conversation.composer"] [contenteditable="true"]', enabled: true, useColor: true, color: "#FF00FF", group: "对话区" }),
141
- // 对话栏底部区域(青)
142
- mk({ name: "对话栏底部区域", selector: '[data-slot="conversation.composer.dock"] .q2FAPq_root, [data-slot="conversation.composer.dock"] span button', enabled: true, useColor: true, color: "#00FFFF", group: "对话区" })
134
+ mk({ name: "输入区", selector: '[data-slot="conversation.composer"]', enabled: true, useColor: true, color: "#FF00FF", group: "对话区" }),
135
+ // 对话栏底部区域(青)— dock
136
+ mk({ name: "对话栏底部区域", selector: '[data-slot="conversation.input.dock"]', enabled: true, useColor: true, color: "#00FFFF", group: "对话区" }),
137
+ // 工作区 / 项目列表(亮绿)
138
+ mk({ name: "工作区", selector: '[data-slot="sidebar.workspaces"]', enabled: true, useColor: true, color: "#00FF00", group: "左侧栏" }),
139
+ // 对话信息显示(蓝紫)— 对话底部 footer/actions 区域
140
+ mk({ name: "对话信息显示", selector: '[data-slot="conversation.session.header.utilities"], [data-slot="conversation.session.header.actions"]', enabled: true, useColor: true, color: "#8A2BE2", group: "对话区" }),
141
+ // 品牌/Logo(橙)
142
+ mk({ name: "品牌标识", selector: '[data-slot="sidebar.brand.mark"], [data-slot="sidebar.brand.name"]', enabled: true, useColor: true, color: "#FF8C00", group: "左侧栏" }),
143
+ // 底部操作栏(灰)
144
+ mk({ name: "底部操作栏", selector: '[data-slot="sidebar.footer.action"]', enabled: true, useColor: true, color: "#808080", group: "左侧栏" })
143
145
  ];
144
146
  }
145
147
  function normThemes(o) {
@@ -149,7 +151,7 @@ window.__ModuleLoader__.load({
149
151
  });
150
152
  }
151
153
  function normUnified(o) {
152
- var u = { uc: { enabled: false, color: "#FFFFFF" }, uf: { enabled: false, en: "Times New Roman", zh: "KaiTi", size: 15 } };
154
+ var u = { uc: { enabled: false, color: "#FFFFFF" }, uf: { enabled: false, en: "Times New Roman", zh: "KaiTi", size: 15, weight: 400, italic: false } };
153
155
  if (!o || !o.unified || typeof o.unified !== "object") return u;
154
156
  var s = o.unified;
155
157
  if (s.uc && typeof s.uc === "object") {
@@ -161,24 +163,27 @@ window.__ModuleLoader__.load({
161
163
  if (typeof s.uf.en === "string") u.uf.en = s.uf.en;
162
164
  if (typeof s.uf.zh === "string") u.uf.zh = s.uf.zh;
163
165
  u.uf.size = clamp(s.uf.size, 12, 22);
166
+ u.uf.weight = clamp(s.uf.weight, 100, 900);
167
+ u.uf.italic = s.uf.italic === true;
164
168
  }
165
169
  return u;
166
170
  }
167
171
  function loadSettings() {
168
172
  try {
169
173
  var raw = localStorage.getItem(STORE_KEY);
170
- if (!raw) return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), bx: null, by: null };
174
+ if (!raw) return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), customFonts: [], bx: null, by: null };
171
175
  var o = JSON.parse(raw);
172
176
  var themes = normThemes(o);
173
177
  var unified = normUnified(o);
178
+ var customFonts = Array.isArray(o.customFonts) ? o.customFonts : [];
174
179
  if (o && (typeof o.ver !== "number" || o.ver < SCHEMA_VERSION)) {
175
180
  // Version bump: write back fresh region palette but KEEP themes &
176
- // unified settings so users don't lose their saved themes.
177
- var fresh = { regions: DEFAULT_REGIONS(), themes: themes, unified: unified, ver: SCHEMA_VERSION };
181
+ // unified & customFonts settings so users don't lose their saved themes.
182
+ var fresh = { regions: DEFAULT_REGIONS(), themes: themes, unified: unified, customFonts: customFonts, ver: SCHEMA_VERSION };
178
183
  if (typeof o.bx === "number") fresh.bx = o.bx;
179
184
  if (typeof o.by === "number") fresh.by = o.by;
180
185
  localStorage.setItem(STORE_KEY, JSON.stringify(fresh));
181
- return { regions: DEFAULT_REGIONS(), themes: themes, unified: unified, bx: typeof o.bx === "number" ? o.bx : null, by: typeof o.by === "number" ? o.by : null };
186
+ return { regions: DEFAULT_REGIONS(), themes: themes, unified: unified, customFonts: customFonts, bx: typeof o.bx === "number" ? o.bx : null, by: typeof o.by === "number" ? o.by : null };
182
187
  }
183
188
  if (o && Array.isArray(o.regions)) {
184
189
  var loaded = o.regions.map(normRegion).filter(function (x) { return x.group !== "已识别"; });
@@ -192,6 +197,7 @@ window.__ModuleLoader__.load({
192
197
  regions: regions,
193
198
  themes: themes,
194
199
  unified: unified,
200
+ customFonts: customFonts,
195
201
  bx: typeof o.bx === "number" ? o.bx : null,
196
202
  by: typeof o.by === "number" ? o.by : null
197
203
  };
@@ -207,9 +213,9 @@ window.__ModuleLoader__.load({
207
213
  g.lh = clamp(o.lh, 1.3, 2.2);
208
214
  g.weight = clamp(o.weight, 100, 900);
209
215
  g.italic = o.italic === true;
210
- return { regions: [g], themes: themes, unified: unified, bx: typeof o.bx === "number" ? o.bx : null, by: typeof o.by === "number" ? o.by : null };
216
+ return { regions: [g], themes: themes, unified: unified, customFonts: customFonts, bx: typeof o.bx === "number" ? o.bx : null, by: typeof o.by === "number" ? o.by : null };
211
217
  } catch (e) {
212
- return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), bx: null, by: null };
218
+ return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), customFonts: [], bx: null, by: null };
213
219
  }
214
220
  }
215
221
  function saveSettings(data) {
@@ -269,8 +275,8 @@ window.__ModuleLoader__.load({
269
275
  en: u.uf.enabled ? u.uf.en : r.en,
270
276
  zh: u.uf.enabled ? u.uf.zh : r.zh,
271
277
  size: u.uf.enabled ? u.uf.size : r.size,
272
- weight: r.weight,
273
- italic: r.italic,
278
+ weight: u.uf.enabled ? u.uf.weight : r.weight,
279
+ italic: u.uf.enabled ? u.uf.italic : r.italic,
274
280
  lh: r.lh,
275
281
  useColor: u.uc.enabled || r.useColor,
276
282
  color: u.uc.enabled ? u.uc.color : r.color
@@ -393,6 +399,26 @@ window.__ModuleLoader__.load({
393
399
  "",
394
400
  "/* ── hint ── */",
395
401
  "#dsh-fe-hint{margin-top:12px;font-size:10px;color:#6b7280;line-height:1.6;padding:10px 12px;background:rgba(255,255,255,.02);border-radius:10px;border:1px solid rgba(255,255,255,.04)}",
402
+ "#dsh-fe-customfont{margin:12px 0 10px;padding:10px 12px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:12px}",
403
+ "#dsh-fe-customfont .dsh-fe-cf-label{font-size:10px;color:#6b7280;font-weight:600;letter-spacing:.03em;text-transform:uppercase;margin-bottom:8px}",
404
+ "#dsh-fe-customfont .dsh-fe-cf-list{display:flex;flex-direction:column;gap:4px}",
405
+ "#dsh-fe-customfont .dsh-fe-cf-item{display:flex;align-items:center;gap:6px;padding:4px 6px;border-radius:6px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.04)}",
406
+ "#dsh-fe-customfont .dsh-fe-cf-name{flex:1;min-width:0;font-size:11px;color:#cfd4dc}",
407
+ "#dsh-fe-customfont .dsh-fe-cf-link{font-size:10px;color:#6aa6ff;text-decoration:none;flex-shrink:0}",
408
+ "#dsh-fe-customfont .dsh-fe-cf-link:hover{text-decoration:underline}",
409
+ "#dsh-fe-customfont .dsh-fe-cf-item button{flex-shrink:0;padding:2px 8px;border-radius:5px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.06);color:#cfd4dc;cursor:pointer;font:inherit;font-size:10px}",
410
+ "#dsh-fe-customfont .dsh-fe-cf-item button:hover{background:rgba(255,255,255,.12)}",
411
+ "#dsh-fe-customfont .dsh-fe-cf-tip{font-size:10px;color:#6b7280;margin-top:6px;line-height:1.5}",
412
+ "#dsh-fe-customfont .dsh-fe-cf-file{margin-top:10px;padding-top:10px;border-top:1px dashed rgba(255,255,255,.08)}",
413
+ "#dsh-fe-customfont .dsh-fe-cf-file .dsh-fe-cf-label{margin-bottom:6px}",
414
+ "#dsh-fe-customfont .dsh-fe-cf-file-row{display:flex;gap:6px;align-items:center}",
415
+ "#dsh-fe-customfont .dsh-fe-cf-file-row input[type=file]{flex:1;min-width:0;font-size:11px;color:#cfd4dc}",
416
+ "#dsh-fe-customfont .dsh-fe-cf-file-row button{flex-shrink:0;padding:5px 10px;border-radius:7px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.06);color:#cfd4dc;cursor:pointer;font:inherit;font-size:12px}",
417
+ "#dsh-fe-customfont .dsh-fe-cf-file-row button:hover{background:rgba(255,255,255,.12)}",
418
+ "#dsh-fe-guide{margin-top:12px;padding:12px 14px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.05);border-radius:12px;font-size:12px;line-height:1.7;color:#cfd4dc}",
419
+ "#dsh-fe-guide h4{margin:0 0 8px;font-size:14px;color:#f0f2f5}",
420
+ "#dsh-fe-guide p{margin:0 0 10px}",
421
+ "#dsh-fe-guide b{color:#8ab8ff}",
396
422
  "",
397
423
  "/* ── toast ── */",
398
424
  "#dsh-fe-toast{position:fixed;left:50%;bottom:72px;z-index:2147483601;transform:translateX(-50%);padding:8px 18px;border-radius:10px;font:12px/1.4 KaiTi,'Microsoft YaHei',sans-serif;background:rgba(0,0,0,.85);border:1px solid rgba(255,255,255,.12);box-shadow:0 4px 20px rgba(0,0,0,.5);pointer-events:none;opacity:0;transition:opacity .25s;white-space:nowrap;color:#e6e6e6}",
@@ -404,6 +430,7 @@ window.__ModuleLoader__.load({
404
430
 
405
431
  // ---------- state ----------
406
432
  var state = loadSettings();
433
+ state.customFonts = state.customFonts || [];
407
434
  var regions = state.regions;
408
435
  var root = null;
409
436
  var panel = null;
@@ -474,7 +501,22 @@ window.__ModuleLoader__.load({
474
501
  return Array.isArray(state.themes) ? state.themes : [];
475
502
  }
476
503
  function persist() {
477
- saveSettings({ regions: regions, themes: getThemes(), unified: state.unified, bx: state.bx, by: state.by });
504
+ saveSettings({ regions: regions, themes: getThemes(), unified: state.unified, customFonts: state.customFonts || [], bx: state.bx, by: state.by });
505
+ }
506
+ // Return merged font lists: built-ins + user custom fonts (deduped).
507
+ function mergedEnFonts() {
508
+ var list = EN_FONTS.slice();
509
+ (state.customFonts || []).forEach(function (f) {
510
+ if (f && f.en && !list.some(function (x) { return x[1] === f.en; })) list.push([f.en, f.en]);
511
+ });
512
+ return list;
513
+ }
514
+ function mergedZhFonts() {
515
+ var list = ZH_FONTS.slice();
516
+ (state.customFonts || []).forEach(function (f) {
517
+ if (f && f.zh && !list.some(function (x) { return x[1] === f.zh; })) list.push([f.zh, f.zh]);
518
+ });
519
+ return list;
478
520
  }
479
521
 
480
522
  // ── Point-and-pick custom region: hover to preview, click to select, name ─
@@ -819,6 +861,12 @@ window.__ModuleLoader__.load({
819
861
  color: r.color, useColor: r.useColor === true
820
862
  };
821
863
  });
864
+ // 记住全局统一的勾选状态和值,应用主题时能完整复原
865
+ var u = state.unified || {};
866
+ recipe.__unified = {
867
+ uc: u.uc ? { enabled: u.uc.enabled === true, color: u.uc.color } : { enabled: false, color: "#FFFFFF" },
868
+ uf: u.uf ? { enabled: u.uf.enabled === true, en: u.uf.en, zh: u.uf.zh, size: u.uf.size, weight: u.uf.weight, italic: u.uf.italic === true } : { enabled: false, en: "Times New Roman", zh: "KaiTi", size: 15, weight: 400, italic: false }
869
+ };
822
870
  var th = getThemes();
823
871
  // 只新增或覆盖同名主题,绝不删除其他主题
824
872
  var found = false;
@@ -877,16 +925,31 @@ window.__ModuleLoader__.load({
877
925
  r.color = rc.color; r.useColor = rc.useColor === true;
878
926
  }
879
927
  });
880
- // 应用主题时自动关闭统一颜色和统一字体,以免覆盖
881
- if (state.unified.uc.enabled) {
882
- state.unified.uc.enabled = false;
883
- var ucEl = root.querySelector("#dsh-fe-u-color");
884
- if (ucEl) ucEl.checked = false;
885
- }
886
- if (state.unified.uf.enabled) {
887
- state.unified.uf.enabled = false;
888
- var ufEl = root.querySelector("#dsh-fe-u-font");
889
- if (ufEl) ufEl.checked = false;
928
+ // 应用主题时恢复保存的统一状态(勾选 + 值),实现完整复原
929
+ if (t.recipe && t.recipe.__unified) {
930
+ var sun = t.recipe.__unified;
931
+ state.unified = state.unified || {};
932
+ if (sun.uc) { state.unified.uc = { enabled: sun.uc.enabled === true, color: sun.uc.color || "#FFFFFF" }; }
933
+ if (sun.uf) {
934
+ state.unified.uf = {
935
+ enabled: sun.uf.enabled === true, en: sun.uf.en || "Times New Roman",
936
+ zh: sun.uf.zh || "KaiTi", size: sun.uf.size != null ? sun.uf.size : 15,
937
+ weight: sun.uf.weight != null ? sun.uf.weight : 400, italic: sun.uf.italic === true
938
+ };
939
+ }
940
+ // 同步界面上的统一控件
941
+ var ucEl2 = root.querySelector("#dsh-fe-u-color"); var ucVal2 = root.querySelector("#dsh-fe-u-color-val");
942
+ var ufEl2 = root.querySelector("#dsh-fe-u-font");
943
+ var ufEn2 = root.querySelector("#dsh-fe-u-en"); var ufZh2 = root.querySelector("#dsh-fe-u-zh");
944
+ var ufSz2 = root.querySelector("#dsh-fe-u-size"); var ufWt2 = root.querySelector("#dsh-fe-u-weight"); var ufIt2 = root.querySelector("#dsh-fe-u-italic");
945
+ if (ucEl2) ucEl2.checked = state.unified.uc.enabled; if (ucVal2) ucVal2.value = state.unified.uc.color;
946
+ if (ufEl2) ufEl2.checked = state.unified.uf.enabled; if (ufEn2) ufEn2.value = state.unified.uf.en;
947
+ if (ufZh2) ufZh2.value = state.unified.uf.zh; if (ufSz2) ufSz2.value = state.unified.uf.size;
948
+ if (ufWt2) ufWt2.value = state.unified.uf.weight; if (ufIt2) ufIt2.checked = state.unified.uf.italic;
949
+ } else {
950
+ // 旧主题没有 __unified:回退为关闭统一,避免误覆盖
951
+ if (state.unified.uc && state.unified.uc.enabled) { state.unified.uc.enabled = false; var ucEl3 = root.querySelector("#dsh-fe-u-color"); if (ucEl3) ucEl3.checked = false; }
952
+ if (state.unified.uf && state.unified.uf.enabled) { state.unified.uf.enabled = false; var ufEl3 = root.querySelector("#dsh-fe-u-font"); if (ufEl3) ufEl3.checked = false; }
890
953
  }
891
954
  renderCards();
892
955
  applyAll(regions);
@@ -911,8 +974,8 @@ window.__ModuleLoader__.load({
911
974
  body =
912
975
  '<div class="dsh-fe-card-body">' +
913
976
  '<div class="dsh-fe-body-col">' +
914
- "<label>英文字体</label><select data-rid='" + r.id + "' data-fld='en'>" + optionsHtml(EN_FONTS, r.en) + "</select>" +
915
- "<label>中文字体</label><select data-rid='" + r.id + "' data-fld='zh'>" + optionsHtml(ZH_FONTS, r.zh) + "</select>" +
977
+ "<label>英文字体</label><select data-rid='" + r.id + "' data-fld='en'>" + optionsHtml(mergedEnFonts(), r.en) + "</select>" +
978
+ "<label>中文字体</label><select data-rid='" + r.id + "' data-fld='zh'>" + optionsHtml(mergedZhFonts(), r.zh) + "</select>" +
916
979
  "</div>" +
917
980
  '<div class="dsh-fe-body-col">' +
918
981
  '<div class="dsh-fe-row"><div><label>字号</label><input type="number" min="12" max="22" step="1" data-rid="' + r.id + '" data-fld="size" value="' + r.size + '"></div>' +
@@ -973,13 +1036,15 @@ window.__ModuleLoader__.load({
973
1036
  '<button id="dsh-fe-toggle" type="button" title="字体区域设置 Font settings">Aa</button>' +
974
1037
  '<div id="dsh-fe-panel" style="display:none">' +
975
1038
  '<div class="dsh-fe-header"><div class="dsh-fe-hl"><h4>字体区域设置</h4><div class="dsh-fe-sub">每个区域独立调字体/颜色</div></div><button id="dsh-fe-close" type="button" title="关闭">✕</button></div>' +
1039
+ '<div style="font-size:11px;color:#8b93a1;padding:4px 4px 8px;line-height:1.5">⬇️ 下滑至底部有 <b style="color:#8ab8ff">快速上手说明</b>;换字体/颜色、框选、一键装可爱字体都在这里。</div>' +
976
1040
  '<div class="dsh-fe-themeblock">' +
977
1041
  '<select id="dsh-fe-theme-sel"></select>' +
978
1042
  '<input type="text" id="dsh-fe-theme-name" placeholder="主题名" style="width:60px;padding:4px 6px;border-radius:6px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:11px">' +
979
- '<button type="button" data-act="themeSave" title="保存当前设置为选中主题">💾存</button>' +
1043
+ '<button type="button" data-act="themeSave" title="把当前所有区域的字体/颜色保存为一个方案(填入上方名称)">💾 保存方案</button>' +
980
1044
  '<button type="button" data-act="themeRename" title="重命名所选主题为输入框的名字">✏改</button>' +
981
1045
  '<button type="button" data-act="themeApply" title="应用选中主题">应用</button>' +
982
1046
  '<button type="button" data-act="themeDel" title="删除选中主题">🗑</button>' +
1047
+ '<button type="button" data-act="themeClearAll" title="删除所有已保存的主题" style="color:#e08080">🗑 清空全部</button>' +
983
1048
  "</div>" +
984
1049
  '<div class="dsh-fe-unified" id="dsh-fe-unified">' +
985
1050
  '<div class="dsh-fe-u-row">' +
@@ -991,24 +1056,57 @@ window.__ModuleLoader__.load({
991
1056
  "</div>" +
992
1057
  '<div class="dsh-fe-u-row">' +
993
1058
  '<label><input type="checkbox" id="dsh-fe-u-font" data-u="uf" data-u-fld="enabled">统一字体</label>' +
994
- '<select id="dsh-fe-u-en" data-u="uf" data-u-fld="en" style="min-width:80px">' + optionsHtml(EN_FONTS, "Times New Roman") + "</select>" +
995
- '<select id="dsh-fe-u-zh" data-u="uf" data-u-fld="zh" style="min-width:70px">' + optionsHtml(ZH_FONTS, "KaiTi") + "</select>" +
1059
+ '<select id="dsh-fe-u-en" data-u="uf" data-u-fld="en" style="min-width:80px">' + optionsHtml(mergedEnFonts(), "Times New Roman") + "</select>" +
1060
+ '<select id="dsh-fe-u-zh" data-u="uf" data-u-fld="zh" style="min-width:70px">' + optionsHtml(mergedZhFonts(), "KaiTi") + "</select>" +
996
1061
  '<input type="number" id="dsh-fe-u-size" data-u="uf" data-u-fld="size" value="15" min="12" max="22" step="1" style="width:44px" title="字号">' +
1062
+ '<input type="number" id="dsh-fe-u-weight" data-u="uf" data-u-fld="weight" value="400" min="100" max="900" step="100" style="width:48px" title="字重(100-900)">' +
1063
+ '<label title="统一斜体"><input type="checkbox" id="dsh-fe-u-italic" data-u="uf" data-u-fld="italic">斜体</label>' +
997
1064
  "</div>" +
998
1065
  "</div>" +
999
1066
  '<div class="dsh-fe-toolbar">' +
1000
1067
  '<button type="button" data-act="pickMode" title="在界面框选文字元素,合并成一个区域并命名">🖱 框选区域</button>' +
1001
- '<button type="button" data-act="recordToggle" title="开启后框选/加元素会记录元素稳定定位到文件,用于排查定位区域是否跨重启失效">🔴 采集区域</button>' +
1002
1068
  '<button type="button" data-act="randomAllFonts" title="给所有启用区域随机分配英/中文字体">🎲 随机字体</button>' +
1003
1069
  '<button type="button" class="dsh-fe-btn-accent" data-act="randomAllColors" title="给所有启用区域随机分配颜色">🎨 随机颜色</button>' +
1004
1070
  "</div>" +
1005
1071
  '<div class="dsh-fe-toolbar dsh-fe-toolbar2">' +
1006
1072
  '<button type="button" data-act="replacePicked" title="删除全部默认区域,只保留你框选的区域">⟳ 框选取代默认</button>' +
1007
1073
  '<button type="button" data-act="restoreDefault" title="删除新增的框选区域,恢复内置默认区域">↺ 恢复默认区域</button>' +
1008
- '<button type="button" class="dsh-fe-btn-accent" data-act="saveConfig" title="保存当前框选区域配置方案">💾 保存方案</button>' +
1074
+ '<button type="button" data-act="reloadUI" title="刷新界面(新安装的字体立即生效,无需关闭面板)">🔄 刷新</button>' +
1009
1075
  "</div>" +
1010
1076
  '<div id="dsh-fe-cards"></div>' +
1011
- '<div id="dsh-fe-hint">勾选区域前的复选框展开该区域设置。🎲字体只随机英/中文字体,🎨颜色随机颜色。调好的配置可保存为主题随时切换。<br><br>💡 插件卡顿/卡住时:① 点击 DSH 左下角"重载"按钮;② 或点一下右下角 Aa 悬浮球把它折叠,即可刷新插件样式。</div>' +
1077
+ '<div class="dsh-fe-customfont">' +
1078
+ '<div class="dsh-fe-cf-label">🎨 推荐可爱/奇特字体(一键下载安装 → 添加启用)</div>' +
1079
+ '<div class="dsh-fe-cf-list">' +
1080
+ '<div class="dsh-fe-cf-item"><span class="dsh-fe-cf-name">霞鹜文楷(中文手写)</span><button type="button" data-act="installFont" data-font-id="LXGW WenKai" data-font-zh="霞鹜文楷">⬇ 一键安装</button></div>' +
1081
+ '<div class="dsh-fe-cf-item"><span class="dsh-fe-cf-name">得意黑(中文动感)</span><button type="button" data-act="installFont" data-font-id="Smiley Sans" data-font-zh="Smiley Sans">⬇ 一键安装</button></div>' +
1082
+ '<div class="dsh-fe-cf-item"><span class="dsh-fe-cf-name">站酷快乐体(中文圆润)</span><button type="button" data-act="installFont" data-font-id="ZCOOL KuaiLe" data-font-zh="站酷快乐体">⬇ 一键安装</button></div>' +
1083
+ '<div class="dsh-fe-cf-item"><span class="dsh-fe-cf-name">Comic Neue(英文手写·已内置)</span><button type="button" data-act="installFont" data-font-id="Comic Neue" data-font-en="Comic Neue">⬇ 一键安装</button></div>' +
1084
+ '<div class="dsh-fe-cf-item"><span class="dsh-fe-cf-name">Baloo 2(英文圆润)</span><button type="button" data-act="installFont" data-font-id="Baloo 2" data-font-en="Baloo 2">⬇ 一键安装</button></div>' +
1085
+ '<div class="dsh-fe-cf-item"><span class="dsh-fe-cf-name">Fredoka(英文柔和)</span><button type="button" data-act="installFont" data-font-id="Fredoka" data-font-en="Fredoka">⬇ 一键安装</button></div>' +
1086
+ '<div class="dsh-fe-cf-item"><span class="dsh-fe-cf-name">Nunito(英文圆角)</span><button type="button" data-act="installFont" data-font-id="Nunito" data-font-en="Nunito">⬇ 一键安装</button></div>' +
1087
+ '<div class="dsh-fe-cf-item"><span class="dsh-fe-cf-name">Caveat(英文手写)</span><button type="button" data-act="installFont" data-font-id="Caveat" data-font-en="Caveat">⬇ 一键安装</button></div>' +
1088
+ "</div>" +
1089
+ '<div class="dsh-fe-cf-file">' +
1090
+ '<div class="dsh-fe-cf-label">📁 添加自提供安装包(清单里没有的字体)</div>' +
1091
+ '<div class="dsh-fe-cf-file-row">' +
1092
+ '<input type="file" id="dsh-fe-font-file" accept=".ttf,.otf,application/x-font-ttf,application/x-font-opentype" style="flex:1;min-width:0;color:#cfd4dc;font-size:12px">' +
1093
+ '<button type="button" data-act="installCustomFile" style="flex-shrink:0;padding:5px 10px;border-radius:7px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.06);color:#cfd4dc;cursor:pointer;font:inherit;font-size:12px">安装并添加</button>' +
1094
+ "</div>" +
1095
+ '<div class="dsh-fe-cf-tip">选择电脑上已有的字体安装包(.ttf/.otf),点「安装并添加」,自动装到系统并加入字体下拉。</div>' +
1096
+ "</div>" +
1097
+ '<div class="dsh-fe-cf-tip">💡 点「⬇ 一键安装」自动下载并装到系统,安装成功后<strong>自动添加到字体下拉</strong>,无需手动操作。安装路径会显示在提示里。中文字体较大(几MB),请耐心等待下载完成。</div>' +
1098
+ "</div>" +
1099
+ '<div id="dsh-fe-guide" class="dsh-fe-guide">' +
1100
+ "<h4>📖 快速上手(30 秒学会)</h4>" +
1101
+ "<p><b>① 改某个区域的字体/颜色</b><br>勾选区域卡片前的 ☑ → 展开设置 → 左边选字体,右边调字号/字重/斜体/颜色,马上生效。</p>" +
1102
+ "<p><b>② 一键统一所有区域</b><br>「全局统一」区勾选 <b>统一颜色</b> 或 <b>统一字体</b>,一个设置全界面生效。</p>" +
1103
+ "<p><b>③ 框选自己想要调整的区域</b><br>点 <b>🖱 框选区域</b> → 在界面上点选文字元素(可多选)→ 起个名字 → 确定,就能单独调它。</p>" +
1104
+ "<p><b>④ 保存方案,随时切换</b><br>调好后填个名字 → 点 <b>💾 保存方案</b>。以后下拉选它 → 点 <b>应用</b> 一键还原。</p>" +
1105
+ "<p><b>⑤ 用可爱字体(一键安装)</b><br>在下方字体列表点 <b>⬇ 一键安装</b> → 等下载完 → 点 <b>🔄 刷新</b>,新字体就在下拉里了。</p>" +
1106
+ "<p><b>⑥ 添加自己下载的字体</b><br>点「📁 添加自提供安装包」→ 选电脑上的 .ttf/.otf 文件 → 点 <b>安装并添加</b>。</p>" +
1107
+ "<p><b>⑦ 卡住了?</b><br>点 <b>🔄 刷新</b>,或点右下角 Aa 悬浮球收起来再展开。</p>" +
1108
+ "<p style='color:#e0a060'><b>⚠️ 网页端提示</b>:如果用的是手机/浏览器打开网页端,装完新字体后需要<b>刷新网页</b>(F5)才能显示新字体;桌面端窗口无需刷新。</p>" +
1109
+ "</div>" +
1012
1110
  '<div id="dsh-fe-toast"></div>' +
1013
1111
  "</div>";
1014
1112
  // Mount to the outermost <html> element, NOT document.body — DSH gives
@@ -1074,10 +1172,14 @@ window.__ModuleLoader__.load({
1074
1172
  var ufEn = root.querySelector("#dsh-fe-u-en");
1075
1173
  var ufZh = root.querySelector("#dsh-fe-u-zh");
1076
1174
  var ufSz = root.querySelector("#dsh-fe-u-size");
1175
+ var ufWt = root.querySelector("#dsh-fe-u-weight");
1176
+ var ufIt = root.querySelector("#dsh-fe-u-italic");
1077
1177
  if (ufEl) ufEl.checked = u.uf.enabled;
1078
1178
  if (ufEn) ufEn.value = u.uf.en;
1079
1179
  if (ufZh) ufZh.value = u.uf.zh;
1080
1180
  if (ufSz) ufSz.value = u.uf.size;
1181
+ if (ufWt) ufWt.value = u.uf.weight;
1182
+ if (ufIt) ufIt.checked = u.uf.italic;
1081
1183
 
1082
1184
  // Direct event listener for unified color picker (bypass delegation)
1083
1185
  var ucPicker = root.querySelector("#dsh-fe-u-color-val");
@@ -1237,6 +1339,8 @@ window.__ModuleLoader__.load({
1237
1339
  else if (fld === "color") state.unified[sect].color = el.value;
1238
1340
  else if (fld === "en" || fld === "zh") state.unified[sect][fld] = el.value;
1239
1341
  else if (fld === "size") state.unified[sect].size = clamp(el.value, 12, 22);
1342
+ else if (fld === "weight") state.unified[sect].weight = clamp(el.value, 100, 900);
1343
+ else if (fld === "italic") state.unified[sect].italic = el.checked;
1240
1344
  else return;
1241
1345
  applyAll(regions);
1242
1346
  persist();
@@ -1259,6 +1363,7 @@ window.__ModuleLoader__.load({
1259
1363
  if (!btn) return;
1260
1364
  var act = btn.getAttribute("data-act");
1261
1365
  var rid = btn.getAttribute("data-rid");
1366
+ var el = btn;
1262
1367
 
1263
1368
  if (act === "resetAll") {
1264
1369
  regions = DEFAULT_REGIONS();
@@ -1278,6 +1383,113 @@ window.__ModuleLoader__.load({
1278
1383
  if (act === "themeRename") { themeRename(); return; }
1279
1384
  if (act === "themeApply") { applyTheme(); return; }
1280
1385
  if (act === "themeDel") { deleteTheme(); return; }
1386
+ if (act === "themeClearAll") { state.themes = []; renderThemeSel(); persist(); showToast("已清空全部主题", "ok"); return; }
1387
+ if (act === "reloadUI") {
1388
+ // 强制 Chromium 加载新安装的字体,并刷新下拉列表(不关闭面板)
1389
+ var names = [];
1390
+ (state.customFonts || []).forEach(function (f) {
1391
+ if (f.en) names.push(f.en);
1392
+ if (f.zh) names.push(f.zh);
1393
+ });
1394
+ if (document.fonts && document.fonts.load) {
1395
+ names.forEach(function (n) {
1396
+ try { document.fonts.load('16px "' + n + '"').catch(function(){}); } catch (e) {}
1397
+ });
1398
+ }
1399
+ renderCards();
1400
+ renderThemeSel();
1401
+ applyAll(regions);
1402
+ showToast("🔄 已刷新,新字体应已生效", "ok");
1403
+ return;
1404
+ }
1405
+ if (act === "installFont") {
1406
+ var fid = el.getAttribute("data-font-id") || "";
1407
+ var fzh = el.getAttribute("data-font-zh") || "";
1408
+ var fen = el.getAttribute("data-font-en") || "";
1409
+ if (!fid) { showToast("字体信息缺失", "err"); return; }
1410
+ var btn = el; btn.textContent = "⏳ 下载中 0%"; btn.disabled = true;
1411
+ var done = false;
1412
+ // 轮询下载进度,更新按钮百分比
1413
+ var poll = setInterval(function () {
1414
+ if (done) return;
1415
+ try {
1416
+ fetch("/dsh-font-enhancer/font-progress?fontId=" + encodeURIComponent(fid), { cache: "no-store" })
1417
+ .then(function (r) { return r.json(); })
1418
+ .then(function (pg) {
1419
+ if (done || !pg) return;
1420
+ if (pg.state === "downloading" && pg.pct >= 0) { btn.textContent = "⏳ 下载中 " + pg.pct + "%"; }
1421
+ else if (pg.state === "done") { clearInterval(poll); }
1422
+ else if (pg.state === "error") { clearInterval(poll); }
1423
+ })
1424
+ .catch(function () {});
1425
+ } catch (e) {}
1426
+ }, 400);
1427
+ try {
1428
+ fetch("/dsh-font-enhancer/install-font", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fontId: fid }) })
1429
+ .then(function (r) { return r.json(); })
1430
+ .then(function (res) {
1431
+ done = true; clearInterval(poll);
1432
+ if (res && res.ok) {
1433
+ if (fzh && !state.customFonts.some(function (x) { return x.zh === fzh; })) state.customFonts.push({ zh: fzh });
1434
+ if (fen && !state.customFonts.some(function (x) { return x.en === fen; })) state.customFonts.push({ en: fen });
1435
+ persist(); renderCards(); renderThemeSel();
1436
+ var dest = res.dest ? ",路径:" + res.dest : "";
1437
+ btn.textContent = "✅ 已安装"; btn.disabled = true;
1438
+ showToast("✅ 字体已" + (res.already ? "存在" : "安装") + ":" + (fzh || fen) + ",已自动添加" + dest, "ok");
1439
+ } else {
1440
+ btn.textContent = "⬇ 重试安装"; btn.disabled = false;
1441
+ showToast("安装失败:" + (res && res.error || "未知错误"), "err");
1442
+ }
1443
+ })
1444
+ .catch(function () { done = true; clearInterval(poll); btn.textContent = "⬇ 重试安装"; btn.disabled = false; showToast("安装失败,请检查网络", "err"); });
1445
+ } catch (e) { done = true; clearInterval(poll); btn.textContent = "⬇ 重试安装"; btn.disabled = false; showToast("安装失败:" + e, "err"); }
1446
+ return;
1447
+ }
1448
+ if (act === "addCustomFont") {
1449
+ var en = el.getAttribute("data-font-en") || "";
1450
+ var zh = el.getAttribute("data-font-zh") || "";
1451
+ if (!en && !zh) { showToast("字体信息缺失", "err"); return; }
1452
+ if (en) { if (!state.customFonts.some(function (f) { return f.en === en; })) state.customFonts.push({ en: en }); }
1453
+ if (zh) { if (!state.customFonts.some(function (f) { return f.zh === zh; })) state.customFonts.push({ zh: zh }); }
1454
+ persist(); renderCards(); renderThemeSel(); showToast("已添加字体" + (en || zh), "ok");
1455
+ return;
1456
+ }
1457
+ if (act === "installCustomFile") {
1458
+ var fileInput = root.querySelector("#dsh-fe-font-file");
1459
+ var file = fileInput && fileInput.files && fileInput.files[0];
1460
+ if (!file) { showToast("请先选择字体文件(.ttf/.otf)", "err"); return; }
1461
+ var okExt = /\.(ttf|otf)$/i.test(file.name);
1462
+ if (!okExt) { showToast("请选择 .ttf 或 .otf 字体文件", "err"); return; }
1463
+ var btn2 = el; btn2.textContent = "⏳ 上传中..."; btn2.disabled = true;
1464
+ var reader = new FileReader();
1465
+ reader.onload = function (e) {
1466
+ var base64 = String(e.target.result).split(",")[1] || "";
1467
+ try {
1468
+ fetch("/dsh-font-enhancer/upload-font", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fileName: file.name, data: base64 }) })
1469
+ .then(function (r) { return r.json(); })
1470
+ .then(function (res) {
1471
+ btn2.textContent = "安装并添加"; btn2.disabled = false;
1472
+ if (res && res.ok) {
1473
+ // 自动添加字体(用文件名去掉扩展名作为字体名)
1474
+ var fam = (res.fontName || file.name.replace(/\.(ttf|otf)$/i, "")).trim();
1475
+ if (fam && !state.customFonts.some(function (x) { return x.en === fam; }) && !state.customFonts.some(function (x) { return x.zh === fam; })) {
1476
+ state.customFonts.push({ en: fam, zh: fam });
1477
+ }
1478
+ persist(); renderCards(); renderThemeSel();
1479
+ var dest = res.dest ? ",路径:" + res.dest : "";
1480
+ showToast("✅ 字体已安装并添加:" + fam + dest, "ok");
1481
+ if (fileInput) fileInput.value = "";
1482
+ } else {
1483
+ showToast("安装失败:" + (res && res.error || "未知错误"), "err");
1484
+ }
1485
+ })
1486
+ .catch(function () { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("上传失败,请重试", "err"); });
1487
+ } catch (e2) { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("安装失败:" + e2, "err"); }
1488
+ };
1489
+ reader.onerror = function () { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("读取文件失败", "err"); };
1490
+ reader.readAsDataURL(file);
1491
+ return;
1492
+ }
1281
1493
 
1282
1494
  var r = rid ? findRegion(rid) : null;
1283
1495
  if (!r) return;
Binary file
package/lib/index.js CHANGED
@@ -3,13 +3,37 @@
3
3
  // host registers a loopback-only settings/health API so the bundle has a real cordis
4
4
  // activate (inject=['webServer']); visual work lives in lib/client.js, mounted by the
5
5
  // client-modules system once the host activates. Works on both web and desktop profiles.
6
- import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "node:fs";
7
- import { join, dirname } from "node:path";
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, copyFileSync } from "node:fs";
7
+ import { join, dirname, basename } from "node:path";
8
8
  import { homedir } from "node:os";
9
+ import { fileURLToPath } from "node:url";
10
+ import { get as httpsGet } from "node:https";
9
11
 
10
12
  var SETTINGS_PREFIX = "/dsh-font-enhancer";
11
13
  var SETTINGS_STORE = join(homedir(), ".dsh", "data", "dsh-font-enhancer", "settings.json");
12
14
  var OPERATION_LOG = join(homedir(), ".dsh", "data", "dsh-font-enhancer", "operation.log");
15
+ // Fonts directory: sibling of this file (lib/fonts), resolved via import.meta.url
16
+ // so it works no matter where the package is installed.
17
+ var FONTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "fonts");
18
+ // User-scoped Windows fonts dir (no admin needed): %LOCALAPPDATA%\Microsoft\Windows\Fonts
19
+ var USER_FONTS_DIR = join(homedir(), "AppData", "Local", "Microsoft", "Windows", "Fonts");
20
+
21
+ // Curated font catalog: name -> direct .ttf download URL (OFL/free-commercial).
22
+ // Chinese fonts are too large to bundle, so we fetch on demand into the user
23
+ // fonts dir and register with Windows. Using the same file each install keeps
24
+ // idempotence (copy is skipped if the target already exists).
25
+ var FONT_CATALOG = {
26
+ "LXGW WenKai": { url: "https://github.com/lxgw/LxgwWenKai/releases/download/v1.330/LXGWWenKai-Regular.ttf", file: "LXGWWenKai-Regular.ttf", zh: "霞鹜文楷" },
27
+ "Smiley Sans": { url: "https://github.com/atelier-anchor/smiley-sans/releases/download/v2.0.1/SmileySans-Oblique.ttf", file: "SmileySans-Oblique.ttf", zh: "得意黑" },
28
+ "ZCOOL KuaiLe": { url: "https://github.com/google/fonts/raw/main/ofl/zcoolkuaile/ZCOOLKuaiLe-Regular.ttf", file: "ZCOOLKuaiLe-Regular.ttf", zh: "站酷快乐体" },
29
+ "Comic Neue": { url: "", file: "ComicNeue.ttf", en: "Comic Neue", bundled: true },
30
+ "Baloo 2": { url: "https://github.com/google/fonts/raw/main/ofl/baloo2/Baloo2-Regular.ttf", file: "Baloo2-Regular.ttf", en: "Baloo 2" },
31
+ "Fredoka": { url: "https://github.com/google/fonts/raw/main/ofl/fredoka/Fredoka-Regular.ttf", file: "Fredoka-Regular.ttf", en: "Fredoka" },
32
+ "Nunito": { url: "https://github.com/google/fonts/raw/main/ofl/nunito/Nunito-Regular.ttf", file: "Nunito-Regular.ttf", en: "Nunito" },
33
+ "Caveat": { url: "https://github.com/google/fonts/raw/main/ofl/caveat/Caveat-Regular.ttf", file: "Caveat-Regular.ttf", en: "Caveat" }
34
+ };
35
+ // In-memory download progress tracker: fontId -> { received, total, pct, state }
36
+ var DOWNLOAD_PROGRESS = {};
13
37
 
14
38
  export var name = "dsh-font-enhancer";
15
39
  export var inject = ["webServer"];
@@ -37,6 +61,58 @@ function writeSettings(data) {
37
61
  } catch (e) { return false; }
38
62
  }
39
63
 
64
+ // Download a URL to a local file (follows redirects via https.get default),
65
+ // tracking progress into DOWNLOAD_PROGRESS[fontId] for the client to poll.
66
+ function downloadTo(url, dest, fontId) {
67
+ return new Promise(function (resolve, reject) {
68
+ try {
69
+ httpsGet(url, function (res) {
70
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
71
+ res.resume();
72
+ resolve(downloadTo(res.headers.location, dest, fontId));
73
+ return;
74
+ }
75
+ if (res.statusCode !== 200) { res.resume(); if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "error", error: "HTTP " + res.statusCode }; reject(new Error("HTTP " + res.statusCode)); return; }
76
+ var total = parseInt(res.headers["content-length"] || "0", 10) || 0;
77
+ var received = 0;
78
+ var chunks = [];
79
+ res.on("data", function (c) {
80
+ chunks.push(c);
81
+ received += c.length;
82
+ if (fontId) DOWNLOAD_PROGRESS[fontId] = total > 0
83
+ ? { state: "downloading", received: received, total: total, pct: Math.floor(received / total * 100) }
84
+ : { state: "downloading", received: received, total: 0, pct: -1 };
85
+ });
86
+ res.on("end", function () {
87
+ try { writeFileSync(dest, Buffer.concat(chunks)); if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "done", pct: 100, received: received, total: total, dest: dest }; resolve(dest); }
88
+ catch (e) { if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "error", error: String(e) }; reject(e); }
89
+ });
90
+ res.on("error", function (e) { if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "error", error: String(e) }; reject(e); });
91
+ }).on("error", function (e) { if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "error", error: String(e) }; reject(e); });
92
+ } catch (e) { if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "error", error: String(e) }; reject(e); }
93
+ });
94
+ }
95
+
96
+ // Install a font to the per-user Windows fonts dir. No admin needed for
97
+ // %LOCALAPPDATA%\Microsoft\Windows\Fonts. Returns true if newly copied,
98
+ // false if it already exists (idempotent).
99
+ function installFont(file, url, fontId) {
100
+ return new Promise(function (resolve) {
101
+ try {
102
+ mkdirSync(USER_FONTS_DIR, { recursive: true });
103
+ var dest = join(USER_FONTS_DIR, file);
104
+ if (existsSync(dest)) { if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "done", already: true, pct: 100, dest: dest }; resolve({ ok: true, already: true, dest: dest }); return; }
105
+ if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "started", received: 0, total: 0, pct: 0 };
106
+ downloadTo(url, dest, fontId).then(function () {
107
+ resolve({ ok: true, already: false, dest: dest });
108
+ }).catch(function (e) {
109
+ try { if (existsSync(dest)) { try { require("node:fs").unlinkSync(dest); } catch (eu) {} } } catch (eu2) {}
110
+ resolve({ ok: false, error: String(e && e.message || e) });
111
+ });
112
+ } catch (e) { if (fontId) DOWNLOAD_PROGRESS[fontId] = { state: "error", error: String(e) }; resolve({ ok: false, error: String(e && e.message || e) }); }
113
+ });
114
+ }
115
+
40
116
  export function apply(ctx) {
41
117
  ctx.logger.info("dsh-font-enhancer: host half mounted (settings api + client)");
42
118
  ctx.effect(function () {
@@ -92,6 +168,103 @@ export function apply(ctx) {
92
168
  res.end(JSON.stringify({ ok: true }));
93
169
  });
94
170
  }});
95
- return function () { h1(); h2(); h3(); h4(); h5(); };
171
+ // Font file serving: serve .woff2/.ttf/.otf from the plugin's fonts dir.
172
+ // The prefix route matches any path below /dsh-font-enhancer/fonts/.
173
+ var h6 = ctx.webServer.register({ kind: "prefix", path: SETTINGS_PREFIX + "/fonts", handler: function (req, res) {
174
+ if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
175
+ var rel = decodeURIComponent(req.url.slice(SETTINGS_PREFIX.length + "/fonts".length));
176
+ if (!rel || rel.includes("..") || rel.includes("\0")) { res.writeHead(400); res.end(); return; }
177
+ var target = join(FONTS_DIR, rel.replace(/^\/+/, ""));
178
+ if (!existsSync(target)) { res.writeHead(404); res.end(); return; }
179
+ var ext = target.slice(target.lastIndexOf(".")).toLowerCase();
180
+ var mime = ext === ".woff2" ? "font/woff2" : ext === ".ttf" ? "font/ttf" : ext === ".otf" ? "font/otf" : ext === ".woff" ? "font/woff" : "application/octet-stream";
181
+ res.writeHead(200, { "content-type": mime, "cache-control": "public, max-age=86400" });
182
+ res.end(readFileSync(target));
183
+ }});
184
+ // Font install route: POST /dsh-font-enhancer/install-font { fontId }
185
+ // Downloads the font file from the catalog and installs to user fonts dir.
186
+ var h7 = ctx.webServer.register({ kind: "exact", path: SETTINGS_PREFIX + "/install-font", handler: function (req, res) {
187
+ if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
188
+ var chunks = [];
189
+ req.on("data", function (c) { chunks.push(c); });
190
+ req.on("end", async function () {
191
+ var body;
192
+ try { body = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch (e) { body = {}; }
193
+ var fontId = body.fontId || "";
194
+ var info = FONT_CATALOG[fontId];
195
+ if (!info) { res.writeHead(404, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "unknown font: " + fontId })); return; }
196
+ if (info.bundled) {
197
+ // bundled font: copy from plugin's fonts dir to user fonts dir
198
+ try {
199
+ mkdirSync(USER_FONTS_DIR, { recursive: true });
200
+ var src = join(FONTS_DIR, info.file);
201
+ var dest = join(USER_FONTS_DIR, info.file);
202
+ if (!existsSync(dest) && existsSync(src)) { copyFileSync(src, dest); }
203
+ res.writeHead(200, { "content-type": "application/json" });
204
+ res.end(JSON.stringify({ ok: true, already: existsSync(dest) && existsSync(src) ? false : true, dest: dest, fontId: fontId }));
205
+ return;
206
+ } catch (e) { res.writeHead(500, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: false, error: String(e) })); return; }
207
+ }
208
+ if (!info.url) { res.writeHead(400, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "no download url for " + fontId })); return; }
209
+ try {
210
+ var result = await installFont(info.file, info.url, fontId);
211
+ res.writeHead(result.ok ? 200 : 500, { "content-type": "application/json" });
212
+ res.end(JSON.stringify(result));
213
+ } catch (e) {
214
+ res.writeHead(500, { "content-type": "application/json" });
215
+ res.end(JSON.stringify({ ok: false, error: String(e) }));
216
+ }
217
+ });
218
+ }});
219
+ // Font progress route: GET /dsh-font-enhancer/font-progress?fontId=xxx
220
+ var h9 = ctx.webServer.register({ kind: "exact", path: SETTINGS_PREFIX + "/font-progress", handler: function (req, res) {
221
+ if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
222
+ var q = req.url.indexOf("?") >= 0 ? req.url.slice(req.url.indexOf("?") + 1) : "";
223
+ var m = /fontId=([^&]+)/.exec(q);
224
+ var fontId = m ? decodeURIComponent(m[1]) : "";
225
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
226
+ if (fontId && DOWNLOAD_PROGRESS[fontId]) { res.end(JSON.stringify(DOWNLOAD_PROGRESS[fontId])); }
227
+ else { res.end(JSON.stringify({ state: "none" })); }
228
+ }});
229
+ if (h7) { /* keep reference */ }
230
+ // Upload-font route: POST /dsh-font-enhancer/upload-font { fileName, data(base64) }
231
+ // Receives a user-provided .ttf/.otf, writes it to the per-user fonts dir.
232
+ var h8 = ctx.webServer.register({ kind: "exact", path: SETTINGS_PREFIX + "/upload-font", handler: function (req, res) {
233
+ if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
234
+ var chunks = [];
235
+ req.on("data", function (c) { chunks.push(c); });
236
+ req.on("end", function () {
237
+ var body;
238
+ try { body = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch (e) { body = {}; }
239
+ var fileName = body.fileName || "";
240
+ var data = body.data || "";
241
+ if (!/\.(ttf|otf)$/i.test(fileName) || !data) {
242
+ res.writeHead(400, { "content-type": "application/json" });
243
+ res.end(JSON.stringify({ ok: false, error: "need .ttf/.otf file data" }));
244
+ return;
245
+ }
246
+ try {
247
+ var buf = Buffer.from(data, "base64");
248
+ if (!buf || buf.length < 4) { res.writeHead(400, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "invalid file data" })); return; }
249
+ // basic signature check: TTF starts with 00 01 00 00, OTF with OTTO
250
+ var sig = buf.toString("ascii", 0, 4);
251
+ var ext = fileName.slice(fileName.lastIndexOf(".")).toLowerCase();
252
+ var okSig = (ext === ".ttf" && buf[0] === 0x00 && buf[1] === 0x01 && buf[2] === 0x00 && buf[3] === 0x00) || (ext === ".otf" && sig === "OTTO");
253
+ if (!okSig) { res.writeHead(400, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "file is not a valid TTF/OTF font" })); return; }
254
+ mkdirSync(USER_FONTS_DIR, { recursive: true });
255
+ // sanitize filename to avoid path traversal
256
+ var safeName = basename(fileName).replace(/[^A-Za-z0-9._-]/g, "_");
257
+ var dest = join(USER_FONTS_DIR, safeName);
258
+ writeFileSync(dest, buf);
259
+ var fontName = safeName.replace(/\.(ttf|otf)$/i, "");
260
+ res.writeHead(200, { "content-type": "application/json" });
261
+ res.end(JSON.stringify({ ok: true, dest: dest, fontName: fontName, file: safeName }));
262
+ } catch (e) {
263
+ res.writeHead(500, { "content-type": "application/json" });
264
+ res.end(JSON.stringify({ ok: false, error: String(e) }));
265
+ }
266
+ });
267
+ }});
268
+ return function () { h1(); h2(); h3(); h4(); h5(); h6(); try { h7(); } catch (e) {} try { h8(); } catch (e) {} try { h9(); } catch (e) {} };
96
269
  }, "dsh-font-enhancer: settings routes");
97
270
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-font-enhancer",
3
- "version": "1.2.0",
4
- "description": "DIY 你的 DSH 界面:按区域自定义字体/字号/颜色,支持全局统一设置、主题保存切换、框选区域、随机配色与字体。DIY your DSH UI fonts & colors: per-region font/size/color styling, pick-region element selection, unified overrides, theme save & switch, random font/color.",
3
+ "version": "1.3.0",
4
+ "description": "DSH 字体样式 DIY 插件(搜「字体」找到我):按区域自定义中英文字体/字号/字重/斜体/颜色,框选元素创建自定义区域,全局统一控制,主题保存/切换,随机配色,支持自定义字体。DIY your DSH UI fonts & colors: per-region custom font/size/weight/italic/color, pick elements to create custom regions, unified overrides, theme save/switch, random color, custom fonts.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "sideEffects": false,
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "lib/**/*.js",
18
+ "lib/fonts/*.ttf",
18
19
  "cordis.patch.yml",
19
20
  "README.md",
20
21
  "LICENSE"
@@ -24,14 +25,31 @@
24
25
  "dsh-plugin",
25
26
  "deepseek-harness",
26
27
  "font",
28
+ "字体",
27
29
  "typography",
30
+ "排版",
31
+ "DIY",
28
32
  "theme",
33
+ "主题",
34
+ "color",
35
+ "颜色",
29
36
  "ui",
37
+ "区域",
38
+ "styling",
39
+ "样式",
30
40
  "chinese-font",
41
+ "中文字体",
31
42
  "english-font",
32
- "color-theme",
33
43
  "font-picker",
34
- "text-styling"
44
+ "font-size",
45
+ "字重",
46
+ "斜体",
47
+ "theme-switch",
48
+ "主题切换",
49
+ "text-styling",
50
+ "文字样式",
51
+ "custom-font",
52
+ "自定义字体"
35
53
  ],
36
54
  "repository": {
37
55
  "type": "git",