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 +9 -9
- package/lib/client.js +260 -48
- package/lib/fonts/ComicNeue.ttf +0 -0
- package/lib/index.js +176 -3
- package/package.json +22 -4
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.
|
|
16
|
-
>
|
|
17
|
-
>
|
|
15
|
+
> ✅ **v1.3.0 同时支持 DSH **网页端**(浏览器访问 Web GUI)和 **桌面端**(Electron 内置窗口)。**
|
|
16
|
+
> 新增**一键下载安装可爱字体**(霞鹜文楷/得意黑/站酷快乐体等,带实时下载进度)、**添加自提供安装包**、
|
|
17
|
+
> **🔄 刷新按钮**、保存方案记住统一状态等功能。搜「字体」即可在插件市场找到本插件。
|
|
18
18
|
>
|
|
19
|
-
> ✅ v1.
|
|
20
|
-
>
|
|
21
|
-
>
|
|
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.
|
|
88
|
-
| `v1.
|
|
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 =
|
|
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
|
-
// 默认区域 =
|
|
122
|
-
//
|
|
123
|
-
//
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
141
|
-
//
|
|
142
|
-
mk({ name: "对话栏底部区域", selector: '[data-slot="conversation.
|
|
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 (
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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(
|
|
915
|
-
"<label>中文字体</label><select data-rid='" + r.id + "' data-fld='zh'>" + optionsHtml(
|
|
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="
|
|
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(
|
|
995
|
-
'<select id="dsh-fe-u-zh" data-u="uf" data-u-fld="zh" style="min-width:70px">' + optionsHtml(
|
|
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"
|
|
1074
|
+
'<button type="button" data-act="reloadUI" title="刷新界面(新安装的字体立即生效,无需关闭面板)">🔄 刷新</button>' +
|
|
1009
1075
|
"</div>" +
|
|
1010
1076
|
'<div id="dsh-fe-cards"></div>' +
|
|
1011
|
-
'<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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
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",
|