dsh-font-enhancer 1.4.5 → 1.4.9

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/lib/client.js CHANGED
@@ -1,1735 +1,1741 @@
1
- window.__ModuleLoader__.load({
2
- id: "dsh-font-enhancer",
3
- factory: function (require) {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
-
7
- // ---- Word-style font lists (label, css-family) ----
8
- var EN_FONTS = [
9
- ["Times New Roman", "Times New Roman"],
10
- ["Arial", "Arial"],
11
- ["Calibri", "Calibri"],
12
- ["Cambria", "Cambria"],
13
- ["Georgia", "Georgia"],
14
- ["Verdana", "Verdana"],
15
- ["Tahoma", "Tahoma"],
16
- ["Segoe UI", "Segoe UI"],
17
- ["Garamond", "Garamond"],
18
- ["Book Antiqua", "Book Antiqua"],
19
- ["Palatino Linotype", "Palatino Linotype"],
20
- ["Trebuchet MS", "Trebuchet MS"],
21
- ["Lucida Sans Unicode", "Lucida Sans Unicode"],
22
- ["Comic Sans MS", "Comic Sans MS"],
23
- ["Consolas (mono)", "Consolas"],
24
- ["Courier New (mono)", "Courier New"],
25
- ["Impact", "Impact"],
26
- ["Bahnschrift", "Bahnschrift"]
27
- ];
28
- // Simplified-Chinese fonts shipped with Word / Windows.
29
- // 楷体 (KaiTi) is first so it is both the default and the first option.
30
- var ZH_FONTS = [
31
- ["楷体 (KaiTi)", "KaiTi"],
32
- ["微软雅黑 (Microsoft YaHei)", "Microsoft YaHei"],
33
- ["宋体 (SimSun)", "SimSun"],
34
- ["黑体 (SimHei)", "SimHei"],
35
- ["新宋体 (NSimSun)", "NSimSun"],
36
- ["仿宋 (FangSong)", "FangSong"],
37
- ["微软雅黑 Light", "Microsoft YaHei Light"],
38
- ["等线 (DengXian)", "DengXian"],
39
- ["等线 Light", "DengXian Light"],
40
- ["华文细黑 (STXihei)", "STXihei"],
41
- ["华文宋体 (STSong)", "STSong"],
42
- ["华文中宋 (STZhongsong)", "STZhongsong"],
43
- ["华文楷体 (STKaiti)", "STKaiti"],
44
- ["华文行楷 (STXingkai)", "STXingkai"],
45
- ["隶书 (LiSu)", "LiSu"],
46
- ["幼圆 (YouYuan)", "YouYuan"],
47
- ["方正舒体 (FZShuTi)", "FZShuTi"],
48
- ["方正姚体 (FZYaoti)", "FZYaoti"]
49
- ];
50
-
51
- var STORE_KEY = "dsh-font-enhancer";
52
- // Bump this every time the built-in default region palette changes. A stored
53
- // settings payload whose `ver` is older than this is treated as stale: the
54
- // old region colors (e.g. the first cyan/blue scheme) are dropped and the
55
- // latest DEFAULT_REGIONS() palette is loaded instead. This makes palette
56
- // refreshes survive a restart without needing the user to clear localStorage.
57
- var SCHEMA_VERSION = 22;
58
- // 21: DEFAULT_REGIONS 清空 —— 用户从零开始逐项框选/添加、逐项测试颜色稳定生效
59
-
60
- // ---------- helpers ----------
61
- function esc(s) {
62
- return String(s)
63
- .replace(/&/g, "&")
64
- .replace(/</g, "&lt;")
65
- .replace(/>/g, "&gt;")
66
- .replace(/"/g, "&quot;");
67
- }
68
- function clamp(v, lo, hi) {
69
- v = Number(v);
70
- if (!isFinite(v)) v = lo;
71
- return Math.max(lo, Math.min(hi, v));
72
- }
73
- function uid() {
74
- return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
75
- }
76
- function pickWeight() {
77
- return [400, 500, 600, 700, 800][Math.floor(Math.random() * 5)];
78
- }
79
- function randFont(list) {
80
- return list[Math.floor(Math.random() * list.length)][1];
81
- }
82
-
83
- // ---------- region model ----------
84
- function defaultRegion() {
85
- return {
86
- id: uid(),
87
- name: "新区域",
88
- selector: "body",
89
- enabled: true,
90
- en: "Times New Roman",
91
- zh: "KaiTi",
92
- size: 15,
93
- lh: 1.6,
94
- weight: 400,
95
- italic: false,
96
- color: "#FFFFFF",
97
- useColor: false,
98
- group: "自定义",
99
- _open: false
100
- };
101
- }
102
- function normRegion(o) {
103
- var d = defaultRegion();
104
- if (!o || typeof o !== "object") return d;
105
- if (typeof o.id === "string") d.id = o.id;
106
- d.name = typeof o.name === "string" ? o.name : d.name;
107
- d.selector = typeof o.selector === "string" ? o.selector : d.selector;
108
- d.enabled = o.enabled !== false;
109
- if (typeof o.en === "string") d.en = o.en;
110
- if (typeof o.zh === "string") d.zh = o.zh;
111
- d.size = clamp(o.size, 12, 22);
112
- d.lh = clamp(o.lh, 1.3, 2.2);
113
- d.weight = clamp(o.weight, 100, 900);
114
- d.italic = o.italic === true;
115
- if (typeof o.color === "string") d.color = o.color;
116
- d.useColor = o.useColor === true;
117
- if (typeof o.group === "string") d.group = o.group;
118
- return d;
119
- }
120
- function DEFAULT_REGIONS() {
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 定位。
125
- function mk(o) { var d = defaultRegion(); return Object.assign(d, o); }
126
- return [
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: "对话区" }),
133
- // 输入区(品红)
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: "左侧栏" })
145
- ];
146
- }
147
- function normThemes(o) {
148
- if (!o || !Array.isArray(o.themes)) return [];
149
- return o.themes.filter(function (t) {
150
- return t && typeof t.name === "string" && t.recipe && typeof t.recipe === "object";
151
- });
152
- }
153
- function normUnified(o) {
154
- var u = { uc: { enabled: false, color: "#FFFFFF" }, uf: { enabled: false, en: "Times New Roman", zh: "KaiTi", size: 15, weight: 400, italic: false } };
155
- if (!o || !o.unified || typeof o.unified !== "object") return u;
156
- var s = o.unified;
157
- if (s.uc && typeof s.uc === "object") {
158
- u.uc.enabled = s.uc.enabled === true;
159
- if (typeof s.uc.color === "string") u.uc.color = s.uc.color;
160
- }
161
- if (s.uf && typeof s.uf === "object") {
162
- u.uf.enabled = s.uf.enabled === true;
163
- if (typeof s.uf.en === "string") u.uf.en = s.uf.en;
164
- if (typeof s.uf.zh === "string") u.uf.zh = s.uf.zh;
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;
168
- }
169
- return u;
170
- }
171
- function loadSettings() {
172
- try {
173
- var raw = localStorage.getItem(STORE_KEY);
174
- if (!raw) return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), customFonts: [], bx: null, by: null };
175
- var o = JSON.parse(raw);
176
- var themes = normThemes(o);
177
- var unified = normUnified(o);
178
- var customFonts = Array.isArray(o.customFonts) ? o.customFonts : [];
179
- if (o && (typeof o.ver !== "number" || o.ver < SCHEMA_VERSION)) {
180
- // Version bump: write back fresh region palette but KEEP themes &
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 };
183
- if (typeof o.bx === "number") fresh.bx = o.bx;
184
- if (typeof o.by === "number") fresh.by = o.by;
185
- localStorage.setItem(STORE_KEY, JSON.stringify(fresh));
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 };
187
- }
188
- if (o && Array.isArray(o.regions)) {
189
- var loaded = o.regions.map(normRegion).filter(function (x) { return x.group !== "已识别"; });
190
- var hasPicked = loaded.some(function (x) { return x.group === "框选"; });
191
- var regions = hasPicked
192
- // Once the user has box-picked regions, those SUPERSEDE the built-in
193
- // preset regions — the panel shows only what the user picked/owns.
194
- ? loaded.filter(function (x) { return x.group === "框选"; })
195
- : loaded;
196
- return {
197
- regions: regions,
198
- themes: themes,
199
- unified: unified,
200
- customFonts: customFonts,
201
- bx: typeof o.bx === "number" ? o.bx : null,
202
- by: typeof o.by === "number" ? o.by : null
203
- };
204
- }
205
- var g = defaultRegion();
206
- g.selector = "body";
207
- g.name = "全局界面(整站兜底)";
208
- g.enabled = true;
209
- g._open = true;
210
- if (typeof o.en === "string") g.en = o.en;
211
- if (typeof o.zh === "string") g.zh = o.zh;
212
- g.size = clamp(o.size, 12, 22);
213
- g.lh = clamp(o.lh, 1.3, 2.2);
214
- g.weight = clamp(o.weight, 100, 900);
215
- g.italic = o.italic === true;
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 };
217
- } catch (e) {
218
- return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), customFonts: [], bx: null, by: null };
219
- }
220
- }
221
- function saveSettings(data) {
222
- try {
223
- data.ver = SCHEMA_VERSION;
224
- localStorage.setItem(STORE_KEY, JSON.stringify(data));
225
- } catch (e) {}
226
- }
227
-
228
- // ---------- CSS generation ----------
229
- // No blanket descendant diffusion. Every region is scoped to its own
230
- // text-bearing nodes only (the element itself + its direct inline/block
231
- // text wrappers). This is what stops one region's rule from painting the
232
- // children that belong to a SIBLING region (the previous withDesc() made
233
- // every region apply "sel *" over all descendants, so body/sidebarCol
234
- // recoloured every nested label and the regions bled into each other).
235
- function withText(sel) {
236
- return sel
237
- .split(",")
238
- .map(function (s) {
239
- s = s.trim();
240
- // body * 会匹配所有元素(包括输入框),用 !important 覆盖字号可能
241
- // 破坏交互区域布局。body 只染 body 本身和 SVG text 元素就够了。
242
- if (s === "body") return "body, body text";
243
- return s + ", " + s + " *, " + s + " text";
244
- })
245
- .join(", ");
246
- }
247
- function buildRegionCss(r) {
248
- var sel = (r.selector || "").trim();
249
- if (!sel) return "";
250
- var fam = '"' + r.en + '","' + r.zh + '",sans-serif';
251
- var style = r.italic ? "italic" : "normal";
252
- var scope = withText(sel);
253
- var lines = [
254
- scope + " {",
255
- " font-family: " + fam + " !important;",
256
- " font-weight: " + r.weight + " !important;",
257
- " font-style: " + style + " !important;"
258
- ];
259
- if (r.useColor && r.color) lines.push(" color: " + r.color + " !important;");
260
- lines.push(" font-size: " + r.size + "px !important;");
261
- lines.push(" line-height: " + r.lh + " !important;");
262
- lines.push("}");
263
- return lines.join("\n");
264
- }
265
- function applyAll(regions) {
266
- var u = state.unified || { uc: { enabled: false }, uf: { enabled: false } };
267
- var css = regions
268
- .filter(function (r) {
269
- return r.enabled;
270
- })
271
- .map(function (r) {
272
- // apply unified overrides without mutating the region objects
273
- var tmp = {
274
- selector: r.selector,
275
- en: u.uf.enabled ? u.uf.en : r.en,
276
- zh: u.uf.enabled ? u.uf.zh : r.zh,
277
- size: u.uf.enabled ? u.uf.size : r.size,
278
- weight: u.uf.enabled ? u.uf.weight : r.weight,
279
- italic: u.uf.enabled ? u.uf.italic : r.italic,
280
- lh: r.lh,
281
- useColor: u.uc.enabled || r.useColor,
282
- color: u.uc.enabled ? u.uc.color : r.color
283
- };
284
- return buildRegionCss(tmp);
285
- })
286
- .join("\n\n");
287
- var tag = document.getElementById("dsh-font-enhancer-style");
288
- if (!tag) {
289
- tag = document.createElement("style");
290
- tag.id = "dsh-font-enhancer-style";
291
- tag.dataset.plugin = "dsh-font-enhancer";
292
- document.head.appendChild(tag);
293
- }
294
- tag.textContent = css;
295
- }
296
-
297
- function optionsHtml(list, selected) {
298
- return list
299
- .map(function (f) {
300
- var sel = f[1] === selected ? " selected" : "";
301
- return '<option value="' + esc(f[1]) + '"' + sel + ">" + esc(f[0]) + "</option>";
302
- })
303
- .join("");
304
- }
305
-
306
- // ---------- panel styles — premium dark design-tool aesthetic ----------
307
- function panelCss() {
308
- return [
309
- "#dsh-fe-root *{box-sizing:border-box;scrollbar-width:thin}",
310
- "",
311
- "/* ── toggle button — gem-like floating orb (max-visible) ── */",
312
- "#dsh-fe-root{position:static !important;z-index:2147483647;pointer-events:none}",
313
- "#dsh-fe-toggle{position:fixed;left:auto;top:auto;right:18px;bottom:18px;z-index:2147483647 !important;width:54px !important;height:54px !important;border-radius:50% !important;border:0 !important;background:radial-gradient(circle at 30% 25%,rgba(255,255,255,.15),rgba(40,44,54,.95) 70%) !important;color:#eef0f4 !important;font-size:22px !important;font-weight:700 !important;cursor:grab !important;user-select:none !important;touch-action:none !important;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15) !important;backdrop-filter:blur(12px) !important;line-height:54px !important;text-align:center !important;display:block !important;visibility:visible !important;opacity:1 !important;transform:none !important;pointer-events:auto !important}",
314
- "#dsh-fe-toggle:hover{box-shadow:0 6px 32px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.2) !important;transform:scale(1.06) !important}",
315
- "#dsh-fe-toggle.dsh-fe-dragging{cursor:grabbing !important;transform:scale(1.12) !important;box-shadow:0 8px 40px rgba(0,0,0,.8) !important}",
316
- "/* ── v1.4.3 orb restyle: warm gradient + 🔤 glyph(字体球独立美化,与 skill-browser 统一样式一致;幂等)── */",
317
- "#dsh-fe-root #dsh-fe-toggle{width:46px !important;height:46px !important;line-height:46px !important;font-size:0 !important;color:transparent !important;display:flex !important;align-items:center !important;justify-content:center !important;background:linear-gradient(135deg,#ff8c42,#ffd27a) !important;box-shadow:0 4px 16px rgba(255,140,66,.35) !important}",
318
- "#dsh-fe-root #dsh-fe-toggle::after{content:'🔤';font-size:20px;color:#fff;display:block;line-height:1}",
319
- "/* ── panel root — deep glass studio (position set by JS positionPanel) ── */",
320
- "#dsh-fe-panel{position:fixed !important;z-index:2147483647 !important;width:348px !important;max-width:calc(100vw - 36px) !important;max-height:86vh;overflow-y:auto !important;overflow-x:hidden !important;color:#e6e6e6 !important;border-radius:20px !important;padding:20px !important;font:13px/1.5 KaiTi,'Microsoft YaHei',sans-serif !important;background:rgba(16,18,22,.92) !important;border:1px solid rgba(255,255,255,.08) !important;box-shadow:0 20px 60px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.06) !important;backdrop-filter:blur(24px) !important;-webkit-backdrop-filter:blur(24px) !important;visibility:visible !important;pointer-events:auto !important}",
321
- "#dsh-fe-panel::-webkit-scrollbar{width:3px}",
322
- "#dsh-fe-panel::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:3px}",
323
- "#dsh-fe-panel::-webkit-scrollbar-track{background:transparent}",
324
- "#dsh-fe-root,#dsh-fe-root *{font-family:KaiTi,'Microsoft YaHei',sans-serif!important;font-size:13px!important;line-height:1.5!important}",
325
- "",
326
- "/* ── panel header ── */",
327
- "#dsh-fe-panel .dsh-fe-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid rgba(255,255,255,.06)}",
328
- "#dsh-fe-panel .dsh-fe-header .dsh-fe-hl{display:flex;flex-direction:column;gap:2px}",
329
- "#dsh-fe-panel .dsh-fe-header h4{margin:0;font-size:16px;font-weight:700;letter-spacing:0;color:#f0f2f5;line-height:1.2}",
330
- "#dsh-fe-panel .dsh-fe-header .dsh-fe-sub{font-size:11px;color:#6b7280;font-weight:400;letter-spacing:.01em}",
331
- "#dsh-fe-close{border:0;background:rgba(255,255,255,.06);color:#8b93a1;border-radius:8px;padding:5px 11px;font-size:14px;cursor:pointer;line-height:1.3;transition:all .15s;flex-shrink:0;margin-top:2px}",
332
- "#dsh-fe-close:hover{background:rgba(255,255,255,.13);color:#e6e6e6}",
333
- "",
334
- "/* ── theme bar ── */",
335
- "#dsh-fe-themeblock{display:flex;gap:5px;margin-bottom:14px;padding:10px 12px;background:linear-gradient(135deg,rgba(255,255,255,.04),rgba(255,255,255,.01));border-radius:12px;align-items:center;border:1px solid rgba(255,255,255,.05)}",
336
- "#dsh-fe-themeblock select{flex:1;min-width:0;padding:5px 9px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg width='8' height='6' viewBox='0 0 8 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L4 4.5L7 1.5' stroke='%238b93a1' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 8px center;padding-right:28px;cursor:pointer;transition:border-color .15s}",
337
- "#dsh-fe-themeblock select:focus{border-color:rgba(106,166,255,.5);outline:0}",
338
- "#dsh-fe-themeblock button{padding:5px 9px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#cfd4dc;cursor:pointer;font:inherit;font-size:12px;white-space:nowrap;transition:all .15s;flex-shrink:0}",
339
- "#dsh-fe-themeblock button:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.2)}",
340
- "",
341
- "/* ── toolbar ── */",
342
- ".dsh-fe-toolbar{display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap}",
343
- "#dsh-fe-toolbar .dsh-fe-toolbar2{margin-top:8px}",
344
- ".dsh-fe-toolbar2{display:flex;gap:5px;margin:6px 0 14px;flex-wrap:wrap}",
345
- "#dsh-fe-toolbar button{flex:1;padding:8px 0;border-radius:10px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.04);color:#cfd4dc;cursor:pointer;font:inherit;font-size:12px;font-weight:500;transition:all .18s;letter-spacing:.01em}",
346
- "#dsh-fe-toolbar button:hover{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.2);transform:translateY(-1px)}",
347
- "#dsh-fe-toolbar button:active{transform:translateY(0)}",
348
- "#dsh-fe-toolbar .dsh-fe-btn-accent{border-color:rgba(106,166,255,.3);background:linear-gradient(135deg,rgba(106,166,255,.12),rgba(106,166,255,.06));color:#8ab8ff;text-shadow:0 0 12px rgba(106,166,255,.2)}",
349
- "#dsh-fe-toolbar .dsh-fe-btn-accent:hover{background:linear-gradient(135deg,rgba(106,166,255,.2),rgba(106,166,255,.1));border-color:rgba(106,166,255,.5);box-shadow:0 0 20px rgba(106,166,255,.08)}",
350
- "",
351
- "/* ── unified controls ── */",
352
- "#dsh-fe-unified{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:10px 12px;margin-bottom:12px}",
353
- "#dsh-fe-unified .dsh-fe-u-row{display:flex;align-items:center;gap:6px;margin-bottom:6px;flex-wrap:wrap}",
354
- "#dsh-fe-unified .dsh-fe-u-row:last-child{margin-bottom:0}",
355
- "#dsh-fe-unified label{font-size:11px;color:#8b93a1;display:flex;align-items:center;gap:4px;cursor:pointer;font-weight:500}",
356
- "#dsh-fe-unified label input[type=checkbox]{accent-color:#6aa6ff;width:14px;height:14px;margin:0}",
357
- "#dsh-fe-unified select,#dsh-fe-unified input[type=color],#dsh-fe-unified input[type=number]{padding:3px 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;font-family:KaiTi,'Microsoft YaHei',sans-serif!important;transition:border-color .15s}",
358
- "#dsh-fe-unified select:focus,#dsh-fe-unified input:focus{border-color:rgba(106,166,255,.45);outline:0}",
359
- "#dsh-fe-unified input[type=color]{width:28px;height:22px;padding:0;cursor:pointer}",
360
- "#dsh-fe-unified input[type=number]{width:48px}",
361
- "#dsh-fe-unified .dsh-fe-u-label{font-size:10px;color:#6b7280;font-weight:600;letter-spacing:.03em;text-transform:uppercase;margin-right:2px;flex-basis:100%}",
362
- "",
363
- "/* ── card ── */",
364
- "#dsh-fe-card{border:1px solid rgba(255,255,255,.05);border-radius:12px;padding:12px 14px;margin-bottom:8px;background:linear-gradient(135deg,rgba(255,255,255,.035),rgba(255,255,255,.015));transition:all .2s}",
365
- "#dsh-fe-card:hover{border-color:rgba(255,255,255,.12);background:linear-gradient(135deg,rgba(255,255,255,.055),rgba(255,255,255,.025));box-shadow:0 2px 12px rgba(0,0,0,.15)}",
366
- "#dsh-fe-card-head{display:flex;align-items:center;gap:8px;justify-content:space-between}",
367
- "#dsh-fe-head-left{display:flex;align-items:center;gap:6px;flex:1;min-width:0}",
368
- "#dsh-fe-head-right{display:flex;align-items:center;gap:4px;flex-shrink:0}",
369
- "#dsh-fe-card-head .dsh-fe-cb{display:flex;align-items:center;cursor:pointer;flex-shrink:0;width:18px;height:18px;justify-content:center}",
370
- "#dsh-fe-card-head .dsh-fe-cb input{accent-color:#6aa6ff;width:15px;height:15px;cursor:pointer;margin:0;transition:transform .15s}",
371
- "#dsh-fe-card-head .dsh-fe-cb input:checked{transform:scale(1.18)}",
372
- "#dsh-fe-card-head input[type=text]{flex:1;min-width:0;padding:4px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.08);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;font-weight:500;transition:all .15s}",
373
- "#dsh-fe-card-head input[type=text]:focus{border-color:rgba(106,166,255,.45);background:rgba(0,0,0,.45);outline:0;box-shadow:0 0 0 3px rgba(106,166,255,.08)}",
374
- "#dsh-fe-card-head button{border:0;background:rgba(255,255,255,.05);color:#8b93a1;border-radius:6px;padding:3px 7px;cursor:pointer;font-size:11px;transition:all .15s;flex-shrink:0;font-weight:500}",
375
- "#dsh-fe-card-head button:hover{background:rgba(255,255,255,.13);color:#e6e6e6}",
376
- "#dsh-fe-card-head .dsh-fe-del{color:#8b6c6c;font-size:13px;padding:4px 7px}",
377
- "#dsh-fe-card-head .dsh-fe-del:hover{color:#d47a7a;background:rgba(200,80,80,.15)}",
378
- "#dsh-fe-enb{display:flex;align-items:center;gap:4px;color:#8b93a1;font-size:11px;white-space:nowrap;cursor:pointer;padding:3px 7px;border-radius:6px;transition:all .15s;font-weight:500}",
379
- "#dsh-fe-enb:hover{background:rgba(255,255,255,.06);color:#b9bfc9}",
380
- "#dsh-fe-enb input{accent-color:#6aa6ff;width:13px;height:13px;margin:0}",
381
- "#dsh-fe-dot{width:18px;height:18px;border-radius:50%;flex:none;border:2px solid rgba(255,255,255,.2);background:#888;transition:all .25s;box-shadow:0 0 8px rgba(0,0,0,.2)}",
382
- "#dsh-fe-card:hover #dsh-fe-dot{border-color:rgba(255,255,255,.35);transform:scale(1.05)}",
383
- "",
384
- "/* ── card body ── */",
385
- "#dsh-fe-card-body{display:flex;gap:12px;border-top:1px solid rgba(255,255,255,.05);margin-top:9px;padding-top:9px;animation:dsh-fe-fadeIn .18s ease}",
386
- "@keyframes dsh-fe-fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}",
387
- "#dsh-fe-body-col{flex:1;min-width:0}",
388
- "#dsh-fe-body-col label{display:block;margin:6px 0 3px;color:#8b93a1;font-size:10px;font-weight:600;letter-spacing:.03em;text-transform:uppercase}",
389
- "#dsh-fe-body-col select,#dsh-fe-body-col input[type=text],#dsh-fe-body-col input[type=number]{width:100%;padding:4px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.08);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;transition:all .15s}",
390
- "#dsh-fe-body-col select:focus,#dsh-fe-body-col input:focus{border-color:rgba(106,166,255,.45);outline:0;box-shadow:0 0 0 3px rgba(106,166,255,.08)}",
391
- "#dsh-fe-row{display:flex;gap:6px}",
392
- "#dsh-fe-row>div{flex:1;min-width:0}",
393
- "#dsh-fe-weightrow{margin-top:5px}",
394
- "#dsh-fe-weightrow label{display:flex;justify-content:space-between;align-items:center;margin:0}",
395
- "#dsh-fe-weight-val{color:#8b93a1;font-weight:500}",
396
- "#dsh-fe-panel input[type=range]{width:100%;margin-top:5px;accent-color:#6aa6ff;height:3px;border-radius:2px;cursor:pointer}",
397
- "#dsh-fe-italicrow,#dsh-fe-colorrow{display:flex;align-items:center;gap:6px;margin-top:6px;color:#8b93a1;font-size:12px;cursor:pointer;padding:3px 0;border-radius:6px;transition:all .15s}",
398
- "#dsh-fe-italicrow:hover,#dsh-fe-colorrow:hover{color:#b9bfc9}",
399
- "#dsh-fe-italicrow input,#dsh-fe-colorrow input{accent-color:#6aa6ff;width:14px;height:14px;margin:0}",
400
- "#dsh-fe-colorrow input[type=color]{width:38px;height:28px;padding:0;border:1px solid rgba(255,255,255,.12);border-radius:8px;background:#1b1d22;cursor:pointer;transition:border-color .15s}",
401
- "#dsh-fe-colorrow input[type=color]:hover{border-color:rgba(255,255,255,.25)}",
402
- "",
403
- "/* ── hint ── */",
404
- "#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)}",
405
- "#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}",
406
- "#dsh-fe-customfont .dsh-fe-cf-label{font-size:10px;color:#6b7280;font-weight:600;letter-spacing:.03em;text-transform:uppercase;margin-bottom:8px}",
407
- "#dsh-fe-customfont .dsh-fe-cf-list{display:flex;flex-direction:column;gap:4px}",
408
- "#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)}",
409
- "#dsh-fe-customfont .dsh-fe-cf-name{flex:1;min-width:0;font-size:11px;color:#cfd4dc}",
410
- "#dsh-fe-customfont .dsh-fe-cf-link{font-size:10px;color:#6aa6ff;text-decoration:none;flex-shrink:0}",
411
- "#dsh-fe-customfont .dsh-fe-cf-link:hover{text-decoration:underline}",
412
- "#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}",
413
- "#dsh-fe-customfont .dsh-fe-cf-item button:hover{background:rgba(255,255,255,.12)}",
414
- "#dsh-fe-customfont .dsh-fe-cf-tip{font-size:10px;color:#6b7280;margin-top:6px;line-height:1.5}",
415
- "#dsh-fe-customfont .dsh-fe-cf-file{margin-top:10px;padding-top:10px;border-top:1px dashed rgba(255,255,255,.08)}",
416
- "#dsh-fe-customfont .dsh-fe-cf-file .dsh-fe-cf-label{margin-bottom:6px}",
417
- "#dsh-fe-customfont .dsh-fe-cf-file-row{display:flex;gap:6px;align-items:center}",
418
- "#dsh-fe-customfont .dsh-fe-cf-file-row input[type=file]{flex:1;min-width:0;font-size:11px;color:#cfd4dc}",
419
- "#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}",
420
- "#dsh-fe-customfont .dsh-fe-cf-file-row button:hover{background:rgba(255,255,255,.12)}",
421
- "#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}",
422
- "#dsh-fe-guide h4{margin:0 0 8px;font-size:14px;color:#f0f2f5}",
423
- "#dsh-fe-guide p{margin:0 0 10px}",
424
- "#dsh-fe-guide b{color:#8ab8ff}",
425
- "",
426
- "/* ── toast ── */",
427
- "#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}",
428
- "#dsh-fe-toast.dsh-fe-toast-ok{color:#7bc97b;border-color:rgba(123,201,123,.3)}",
429
- "#dsh-fe-toast.dsh-fe-toast-err{color:#e08080;border-color:rgba(224,128,128,.3)}",
430
- "#dsh-fe-toast.dsh-fe-toast-show{opacity:1}"
431
- ].join("\n");
432
- }
433
-
434
- // ---------- state ----------
435
- var state = loadSettings();
436
- state.customFonts = state.customFonts || [];
437
- var regions = state.regions;
438
- var root = null;
439
- var panel = null;
440
- var cardsEl = null;
441
-
442
- // ── 采集定位区域(录制模式)──────────────────────────────
443
- // 用户点「🔴 采集区域」按钮开/关。开启期间,每次框选/加元素都会把被选中元素的
444
- // 「稳定定位信息」收集并写入 ~/.dsh/data/dsh-font-enhancer/operation.log,
445
- // 便于排查:恢复哪个稳定选择器能在重启后仍准确定位到该元素。
446
- var RECORDING = false;
447
- var recSamples = [];
448
- function recToggle(on) {
449
- RECORDING = !!on;
450
- recSamples = [];
451
- try { localStorage.setItem("dsh-fe-rec", RECORDING ? "1" : "0"); } catch (e) {}
452
- // 同步按钮文案
453
- if (root) {
454
- var b = root.querySelector('[data-act="recordToggle"]');
455
- if (b) { b.textContent = RECORDING ? "⏹ 停止采集" : "🔴 采集区域"; b.classList.toggle("dsh-fe-rec-on", RECORDING); }
456
- }
457
- showToast(RECORDING ? "采集模式已开启:现在框选/加元素会记录元素定位到文件" : "采集模式已关闭", RECORDING ? "ok" : "");
458
- }
459
- // 元素最简稳定选择器(优先语义 data-*、id、class,最后 tag)
460
- function stableSelFor(node) {
461
- if (!node || node.nodeType !== 1) return null;
462
- if (node.id) return "#" + node.id;
463
- var cn = node.getAttribute ? (node.getAttribute("class") || "") : "";
464
- if (cn && cn.trim()) return "." + cn.trim().split(/\s+/)[0].replace(/[:>,\s]/g, "\\$&");
465
- if (node.getAttribute) {
466
- // 语义 data-* 钩子
467
- var attrs = node.attributes;
468
- for (var i = 0; i < attrs.length; i++) {
469
- var n = attrs[i].nodeName;
470
- if (n.indexOf("data-") === 0) return "[" + n + (attrs[i].nodeValue ? '="' + attrs[i].nodeValue + '"' : "") + "]";
471
- }
472
- }
473
- return node.tagName ? node.tagName.toLowerCase() : null;
474
- }
475
- // 元素的「祖先链稳定路径」(向上最多 6 层,每层用最简稳定选择器)
476
- function stablePathFor(el) {
477
- var parts = [], p = el;
478
- while (p && p !== document.body && parts.length < 6) {
479
- var s = stableSelFor(p);
480
- if (s) parts.unshift(s);
481
- p = p.parentElement;
482
- }
483
- return parts.join(" > ") || (el.tagName ? el.tagName.toLowerCase() : "?");
484
- }
485
- // 把本次采集到的元素定位信息写入日志文件
486
- function flushRecord(kind, name, selector) {
487
- if (!RECORDING || !recSamples.length) return;
488
- var payload = {
489
- kind: kind || "REC_PICK",
490
- t: new Date().toISOString(),
491
- regionName: name || "",
492
- regionSelector: selector || "",
493
- samples: recSamples.slice()
494
- };
495
- try { fetch("/dsh-font-enhancer/log", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).catch(function(){}); } catch (e) {}
496
- recSamples = [];
497
- }
498
-
499
- function findRegion(id) {
500
- for (var i = 0; i < regions.length; i++) if (regions[i].id === id) return regions[i];
501
- return null;
502
- }
503
- function getThemes() {
504
- return Array.isArray(state.themes) ? state.themes : [];
505
- }
506
- function persist() {
507
- saveSettings({ regions: regions, themes: getThemes(), unified: state.unified, customFonts: state.customFonts || [], bx: state.bx, by: state.by });
508
- }
509
- // Return merged font lists: built-ins + user custom fonts (deduped).
510
- function mergedEnFonts() {
511
- var list = EN_FONTS.slice();
512
- (state.customFonts || []).forEach(function (f) {
513
- if (f && f.en && !list.some(function (x) { return x[1] === f.en; })) list.push([f.en, f.en]);
514
- });
515
- return list;
516
- }
517
- function mergedZhFonts() {
518
- var list = ZH_FONTS.slice();
519
- (state.customFonts || []).forEach(function (f) {
520
- if (f && f.zh && !list.some(function (x) { return x[1] === f.zh; })) list.push([f.zh, f.zh]);
521
- });
522
- return list;
523
- }
524
-
525
- // ── Point-and-pick custom region: hover to preview, click to select, name ─
526
- // User picks the exact UI text/elements they want as one region, then names
527
- // it. Much finer-grained than auto-discovery and never floods the panel.
528
- var __pick = null;
529
- function pickRegion(targetRid) {
530
- var target = (typeof targetRid === "string" && targetRid) ? findRegion(targetRid) : null;
531
- if (__pick && __pick.active) { pickRegionExit(true); return; }
532
- var toggle = document.getElementById("dsh-fe-toggle");
533
- var pickbar = document.createElement("div");
534
- pickbar.id = "dsh-fe-pickbar";
535
- pickbar.style.cssText = "position:fixed;z-index:2147483647;left:50%;top:64px;display:flex;gap:8px;align-items:center;padding:10px 14px;background:rgba(18,20,26,.97);border:1px solid rgba(255,255,255,.16);border-radius:14px;box-shadow:0 12px 44px rgba(0,0,0,.7);color:#e6e6e6;font:13px/1.5 KaiTi,'Microsoft YaHei',sans-serif;pointer-events:auto;cursor:move";
536
- var modeTip = target ? ('追加到区域「' + target.name + '」') : '拖动 ⠿ 移动工具条;Ctrl+点击多选';
537
- var pickNamePh = target ? (target.name + '' ) : "给这个区域命名…";
538
- pickbar.innerHTML =
539
- '<span id="dsh-fe-pick-handle" title="拖动移动工具条" style="cursor:move;padding:0 8px 0 2px;color:#8b93a1;font-size:14px;user-select:none;-webkit-user-select:none">⠿</span>' +
540
- '<span id="dsh-fe-pick-count">已选 <b>0</b> 个元素</span>' +
541
- '<input id="dsh-fe-pick-name" placeholder="' + pickNamePh + '" style="width:180px;padding:6px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.2);background:rgba(0,0,0,.45);color:#fff;font:inherit;font-size:13px">' +
542
- '<button id="dsh-fe-pick-done" type="button" style="padding:6px 16px;border-radius:8px;border:1px solid rgba(106,166,255,.45);background:rgba(106,166,255,.25);color:#a8ccff;cursor:pointer;font:inherit;font-weight:600">✔ 完成</button>' +
543
- '<button id="dsh-fe-pick-cancel" type="button" style="padding:6px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);color:#9aa;cursor:pointer;font:inherit">✕ 取消</button>' +
544
- '<span style="font-size:11px;color:#8b93a1">' + modeTip + '</span>';
545
- // center it initially (we dropped the translateX trick so dragging works)
546
- pickbar.style.left = Math.max(8, Math.round((window.innerWidth - 420) / 2)) + "px";
547
- var mount = document.documentElement || document.body;
548
- mount.appendChild(pickbar);
549
- // drag handle → move the whole pickbar without triggering element pick
550
- var handle = document.getElementById("dsh-fe-pick-handle");
551
- (function () {
552
- var drag = false, sx = 0, sy = 0, ox = 0, oy = 0;
553
- if (!handle) return;
554
- handle.addEventListener("pointerdown", function (e) {
555
- drag = true; sx = e.clientX; sy = e.clientY;
556
- ox = pickbar.offsetLeft; oy = pickbar.offsetTop;
557
- try { handle.setPointerCapture(e.pointerId); } catch (err) {}
558
- e.preventDefault(); e.stopPropagation();
559
- });
560
- handle.addEventListener("pointermove", function (e) {
561
- if (!drag) return;
562
- var nx = Math.max(4, Math.min(window.innerWidth - pickbar.offsetWidth - 4, ox + (e.clientX - sx)));
563
- var ny = Math.max(4, Math.min(window.innerHeight - pickbar.offsetHeight - 4, oy + (e.clientY - sy)));
564
- pickbar.style.left = nx + "px"; pickbar.style.top = ny + "px";
565
- });
566
- function end() { drag = false; }
567
- handle.addEventListener("pointerup", end);
568
- handle.addEventListener("pointercancel", end);
569
- })();
570
-
571
- var picked = [];
572
- var hoverEl = null;
573
- function hilite(el) {
574
- if (hoverEl && hoverEl !== el) {
575
- hoverEl.style.outline = "";
576
- hoverEl.style.outlineOffset = "";
577
- }
578
- if (el) { el.style.outline = "2px dashed #6aa6ff"; el.style.outlineOffset = "-1px"; }
579
- hoverEl = el;
580
- }
581
- function onMove(e) {
582
- var el = e.target;
583
- if (!el || el === document.documentElement || el === document.body) return;
584
- if (el.closest && el.closest("#dsh-fe-root, #dsh-fe-pickbar")) { hilite(null); return; }
585
- if (el.nodeType === 1) hilite(el);
586
- }
587
- function onPick(e) {
588
- var el = e.target;
589
- if (!el || el.nodeType !== 1) return;
590
- if (el.closest && el.closest("#dsh-fe-root, #dsh-fe-pickbar")) return;
591
- e.preventDefault(); e.stopPropagation();
592
- var multi = e.ctrlKey || e.metaKey || e.shiftKey;
593
- var idx = picked.indexOf(el);
594
- if (idx >= 0) {
595
- // only un-pick on a plain click; Ctrl/Shift always keeps it selected
596
- if (!multi) { picked.splice(idx, 1); el.style.outline = ""; }
597
- } else {
598
- picked.push(el);
599
- // 采集模式:记录被框选元素的稳定定位路径
600
- if (RECORDING) recSamples.push({ path: stablePathFor(el), tag: el.tagName, uid: "…下次分配" });
601
- el.style.outline = multi ? "2px solid #ff8c5a" : "2px dotted #ffd27a";
602
- el.style.outlineOffset = "-1px";
603
- }
604
- var c = document.getElementById("dsh-fe-pick-count");
605
- if (c) c.innerHTML = "已选 <b>" + picked.length + "</b> 个元素";
606
- }
607
- function finish() {
608
- var nameEl = document.getElementById("dsh-fe-pick-name");
609
- var name = (nameEl && nameEl.value.trim()) || ("框选区域 " + (regions.length + 1));
610
- if (!picked.length) { showToast("未选择任何元素", "err"); return; }
611
- var marks = [];
612
- picked.forEach(function (el) {
613
- var m = uid();
614
- el.setAttribute("data-fe-reg", m);
615
- marks.push('[data-fe-reg="' + m + '"]');
616
- el.style.outline = ""; el.style.outlineOffset = "";
617
- });
618
- var target = targetRid ? findRegion(targetRid) : null;
619
- if (target) {
620
- // Append the newly picked elements into an existing region instead of
621
- // creating a new one ("关联框选" from that region's card).
622
- var existing = (target.selector || "").split(",").map(function (s) { return s.trim(); }).filter(Boolean);
623
- marks.forEach(function (m) { if (existing.indexOf(m) < 0) existing.push(m); });
624
- target.selector = existing.join(", ");
625
- target.enabled = true;
626
- renderCards();
627
- applyAll(regions);
628
- persist();
629
- pickRegionExit(true);
630
- flushRecord("REC_ADD", target.name, target.selector);
631
- showToast("已向区域「" + target.name + "」追加 " + picked.length + " 个元素", "ok");
632
- return;
633
- }
634
- var nr = defaultRegion();
635
- nr.id = uid();
636
- nr.name = name;
637
- nr.selector = marks.join(", ");
638
- nr.enabled = true;
639
- nr.useColor = false;
640
- nr.color = "#e6e6e6";
641
- nr.group = "框选";
642
- // A user box-picked region REPLACES any preset/default region with the
643
- // same name (so your custom "左侧导航栏" supersedes the built-in one).
644
- regions = regions.filter(function (x) { return !(x.group && x.group !== "框选" && x.name === name); });
645
- regions.push(nr);
646
- renderCards();
647
- applyAll(regions);
648
- persist();
649
- pickRegionExit(true);
650
- flushRecord("REC_NEW", name, nr.selector);
651
- // Bring the newly-added card into view and pulse it so the user can
652
- // immediately find it and test font/color on it.
653
- var newCard = cardsEl && cardsEl.querySelector('[data-card="' + nr.id + '"]');
654
- if (newCard) {
655
- try { newCard.scrollIntoView({ block: "nearest", behavior: "smooth" }); } catch (e) {}
656
- newCard.style.transition = "box-shadow .4s";
657
- newCard.style.boxShadow = "0 0 0 2px rgba(255,160,70,.7), 0 4px 18px rgba(0,0,0,.4)";
658
- setTimeout(function () { if (newCard) newCard.style.boxShadow = ""; }, 1600);
659
- }
660
- showToast("已添加区域「" + name + "」(" + picked.length + " 个元素),可在卡片里改字体/颜色", "ok");
661
- }
662
- function pickRegionExit(silent) {
663
- if (!__pick) return;
664
- __pick.active = false;
665
- document.removeEventListener("pointerover", __pick.onMove, true);
666
- document.removeEventListener("click", __pick.onPick, true);
667
- (picked || []).forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
668
- var b = document.getElementById("dsh-fe-pickbar"); if (b) b.parentNode && b.parentNode.removeChild(b);
669
- if (hoverEl) { try { hoverEl.style.outline = ""; } catch (e) {} }
670
- var go = document.getElementById("dsh-fe-toggle");
671
- if (go) { go.style.display = "block"; }
672
- }
673
- document.getElementById("dsh-fe-pick-done").addEventListener("click", finish);
674
- document.getElementById("dsh-fe-pick-cancel").addEventListener("click", function () { pickRegionExit(true); });
675
- document.addEventListener("pointerover", onMove, true);
676
- document.addEventListener("click", onPick, true);
677
- __pick = { active: true, onMove: onMove, onPick: onPick, exit: pickRegionExit };
678
- if (toggle) toggle.style.display = "none";
679
- }
680
-
681
- // ── Show the elements a region currently applies to (like the pick outline) ─
682
- // Lets the user see exactly which page elements a saved region covers, and
683
- // doubles as a live check that the region's selector still matches anything.
684
- function showScope(rid) {
685
- if (!document.body || !rid) return;
686
- var r = findRegion(rid);
687
- if (!r) return;
688
- var sel = (r.selector || "").trim();
689
- if (!sel) { showToast("该区域没有选择器", "err"); return; }
690
- var nodes;
691
- try { nodes = document.querySelectorAll(sel); } catch (e) { showToast("选择器无效", "err"); return; }
692
- var arr = Array.prototype.slice.call(nodes).filter(function (n) {
693
- return n && n.nodeType === 1;
694
- });
695
- if (!arr.length) { showToast("该区域当前未匹配到任何元素(可能已失效)", "err"); return; }
696
- arr.forEach(function (el) {
697
- el.style.outline = "2px solid #3ddc84";
698
- el.style.outlineOffset = "-1px";
699
- el.style.transition = "outline-color .3s";
700
- });
701
- showToast("该区域覆盖 " + arr.length + " 个元素(绿色描边,2 秒后消失)", "ok");
702
- // remember to clear after a beat; if another showScope fires we just reset
703
- if (showScope._nodes) showScope._nodes.forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
704
- showScope._nodes = arr;
705
- clearTimeout(showScope._t);
706
- showScope._t = setTimeout(function () {
707
- (showScope._nodes || []).forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
708
- showScope._nodes = null;
709
- }, 2400);
710
- }
711
-
712
- // ── Region config management ──────────────────────────────────────────
713
- // Replace: drop every non-box-picked region (defaults go away), keep picked.
714
- function replaceWithPicked() {
715
- var picked = regions.filter(function (x) { return x.group === "框选"; });
716
- if (!picked.length) { showToast("当前没有框选区域,先新增框选", "err"); return; }
717
- regions = picked;
718
- renderCards(); applyAll(regions); persist();
719
- showToast("已用「" + picked.length + " 个框选区域」取代默认区域", "ok");
720
- }
721
- // Restore: remove box-picked regions and reload the built-in defaults.
722
- function restoreDefault() {
723
- regions = DEFAULT_REGIONS();
724
- renderCards(); applyAll(regions); persist();
725
- showToast("已恢复默认区域(移除框选区域)", "ok");
726
- }
727
- // Save the whole current region set as a named preset ("配置方案").
728
- // Save the whole current region set as a named preset ("保存配置方案").
729
- // Reuses the theme-recipe shape so the saved plan can be applied back via
730
- // the theme "应用" button too.
731
- function saveConfig() {
732
- var themes = getThemes();
733
- var base = "配置方案";
734
- var name = base + "1";
735
- var n = 1;
736
- while (themes.some(function (t) { return t.name === name; })) { n++; name = base + n; }
737
- var recipe = {};
738
- regions.forEach(function (r) {
739
- recipe[r.name] = {
740
- en: r.en, zh: r.zh, size: r.size, lh: r.lh,
741
- weight: r.weight, italic: r.italic === true,
742
- color: r.color, useColor: r.useColor === true
743
- };
744
- });
745
- themes.push({ name: name, recipe: recipe, ts: Date.now() });
746
- state.themes = themes;
747
- renderThemeSel();
748
- persist();
749
- var sel = root.querySelector("#dsh-fe-theme-sel");
750
- if (sel) sel.value = name;
751
- showToast("已保存方案「" + name + "」(含全部区域字体/颜色)", "ok");
752
- }
753
-
754
- function showToast(msg, type) {
755
- var t = document.getElementById("dsh-fe-toast");
756
- if (!t) return;
757
- t.textContent = msg;
758
- t.className = "dsh-fe-toast" + (type === "ok" ? " dsh-fe-toast-ok" : type === "err" ? " dsh-fe-toast-err" : "") + " dsh-fe-toast-show";
759
- clearTimeout(t._hide);
760
- t._hide = setTimeout(function () { t.className = "dsh-fe-toast"; }, 2500);
761
- }
762
- function reloadPlugin() {
763
- try {
764
- var r = document.getElementById("dsh-fe-root");
765
- if (r) r.remove();
766
- var pc = document.getElementById("dsh-fe-panel-css");
767
- if (pc) pc.remove();
768
- var st = document.getElementById("dsh-font-enhancer-style");
769
- if (st) st.remove();
770
- buildUI();
771
- showToast("插件已刷新", "ok");
772
- } catch (e) {}
773
- }
774
- // A color drawn from the FULL hue wheel with random saturation/lightness,
775
- // so every random pick can be green/blue/purple/yellow/red — not just a
776
- // handful of near-red hues. `#ignore` kept for call-site compatibility.
777
- function randVivid(i) {
778
- var h = Math.floor(Math.random() * 360);
779
- var s = 70 + Math.floor(Math.random() * 25); // 70–95%
780
- var l = 42 + Math.floor(Math.random() * 28); // 42–70%
781
- return "hsl(" + h + ", " + s + "%, " + l + "%)";
782
- }
783
- function randomizeFonts(rid) {
784
- var r = findRegion(rid);
785
- if (!r) return;
786
- r.en = randFont(EN_FONTS);
787
- r.zh = randFont(ZH_FONTS);
788
- renderCards();
789
- applyAll(regions);
790
- persist();
791
- }
792
- function randomizeColor(rid) {
793
- var r = findRegion(rid);
794
- if (!r) return;
795
- r.useColor = true;
796
- r.color = randVivid(0);
797
- renderCards();
798
- applyAll(regions);
799
- persist();
800
- }
801
- function randomizeAllFonts() {
802
- // 统一字体开着时,随机统一字体值(视觉立即可见),否则随机各区域
803
- if (state.unified.uf.enabled) {
804
- state.unified.uf.en = randFont(EN_FONTS);
805
- state.unified.uf.zh = randFont(ZH_FONTS);
806
- var se = root.querySelector("#dsh-fe-u-en");
807
- var sz = root.querySelector("#dsh-fe-u-zh");
808
- if (se) se.value = state.unified.uf.en;
809
- if (sz) sz.value = state.unified.uf.zh;
810
- applyAll(regions);
811
- persist();
812
- return;
813
- }
814
- regions.forEach(function (r) {
815
- if (!r.enabled) return;
816
- r.en = randFont(EN_FONTS);
817
- r.zh = randFont(ZH_FONTS);
818
- });
819
- renderCards();
820
- applyAll(regions);
821
- persist();
822
- }
823
- function randomizeAllColors() {
824
- // 统一颜色开着时,随机统一颜色值(视觉立即可见),否则随机各区域
825
- if (state.unified.uc.enabled) {
826
- state.unified.uc.color = randVivid(0);
827
- var pc = root.querySelector("#dsh-fe-u-color-val");
828
- if (pc) pc.value = state.unified.uc.color;
829
- applyAll(regions);
830
- persist();
831
- return;
832
- }
833
- var i = 0;
834
- regions.forEach(function (r) {
835
- if (!r.enabled) return;
836
- r.useColor = true;
837
- r.color = randVivid(i++);
838
- });
839
- renderCards();
840
- applyAll(regions);
841
- persist();
842
- }
843
- function renderThemeSel() {
844
- var sel = root ? root.querySelector("#dsh-fe-theme-sel") : null;
845
- var inp = root ? root.querySelector("#dsh-fe-theme-name") : null;
846
- if (!sel) return;
847
- var opts = '<option value="">— 选择主题 —</option>';
848
- getThemes().forEach(function (t) {
849
- opts += '<option value="' + t.name.replace(/"/g, "&quot;") + '">' + esc(t.name) + "</option>";
850
- });
851
- sel.innerHTML = opts;
852
- // auto-fill the name input with the default name
853
- if (inp) inp.value = "主题 " + (getThemes().length + 1);
854
- }
855
- function saveTheme() {
856
- var inp = root.querySelector("#dsh-fe-theme-name");
857
- var name = inp ? String(inp.value).trim() : "";
858
- if (!name) { showToast("请先在上方输入框填写主题名称", "err"); return; }
859
- var recipe = {};
860
- regions.forEach(function (r) {
861
- recipe[r.name] = {
862
- en: r.en, zh: r.zh, size: r.size, lh: r.lh,
863
- weight: r.weight, italic: r.italic === true,
864
- color: r.color, useColor: r.useColor === true
865
- };
866
- });
867
- // 记住全局统一的勾选状态和值,应用主题时能完整复原
868
- var u = state.unified || {};
869
- recipe.__unified = {
870
- uc: u.uc ? { enabled: u.uc.enabled === true, color: u.uc.color } : { enabled: false, color: "#FFFFFF" },
871
- 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 }
872
- };
873
- var th = getThemes();
874
- // 只新增或覆盖同名主题,绝不删除其他主题
875
- var found = false;
876
- for (var i = 0; i < th.length; i++) {
877
- if (th[i].name === name) { th[i].recipe = recipe; th[i].ts = Date.now(); found = true; break; }
878
- }
879
- if (!found) th.push({ name: name, recipe: recipe, ts: Date.now() });
880
- state.themes = th;
881
- renderThemeSel();
882
- persist();
883
- var sel = root.querySelector("#dsh-fe-theme-sel");
884
- if (sel) sel.value = name;
885
- showToast("已保存:" + name, "ok");
886
- }
887
- function themeRename() {
888
- var sel = root.querySelector("#dsh-fe-theme-sel");
889
- var oldName = sel && sel.value;
890
- if (!oldName) { showToast("请先在下拉框选择要重命名的主题", "err"); return; }
891
- var inp = root.querySelector("#dsh-fe-theme-name");
892
- var newName = inp ? String(inp.value).trim() : "";
893
- if (!newName) { showToast("请先在上方输入框填写新名称", "err"); return; }
894
- if (oldName === newName) { showToast("新名称与旧名称相同", "err"); return; }
895
- var th = getThemes();
896
- var oldRecipe = null;
897
- var found = -1;
898
- for (var i = 0; i < th.length; i++) {
899
- if (th[i].name === oldName) { oldRecipe = th[i].recipe; found = i; break; }
900
- }
901
- if (!oldRecipe) { showToast("未找到原主题数据", "err"); return; }
902
- th.splice(found, 1);
903
- var found2 = false;
904
- for (var j = 0; j < th.length; j++) {
905
- if (th[j].name === newName) { th[j].recipe = oldRecipe; th[j].ts = Date.now(); found2 = true; break; }
906
- }
907
- if (!found2) th.push({ name: newName, recipe: oldRecipe, ts: Date.now() });
908
- state.themes = th;
909
- renderThemeSel();
910
- persist();
911
- if (sel) sel.value = newName;
912
- showToast("已重命名:" + oldName + " → " + newName, "ok");
913
- }
914
- function applyTheme() {
915
- var sel = root.querySelector("#dsh-fe-theme-sel");
916
- var name = sel && sel.value;
917
- if (!name) { showToast("请先在下拉框选择要应用的主题", "err"); return; }
918
- var t = null;
919
- getThemes().forEach(function (x) { if (x.name === name) t = x; });
920
- if (!t) { showToast("未找到主题数据,请重新保存", "err"); return; }
921
- var matched = 0;
922
- regions.forEach(function (r) {
923
- var rc = t.recipe[r.name];
924
- if (rc) {
925
- matched++;
926
- r.en = rc.en; r.zh = rc.zh; r.size = rc.size; r.lh = rc.lh;
927
- r.weight = rc.weight; r.italic = rc.italic === true;
928
- r.color = rc.color; r.useColor = rc.useColor === true;
929
- }
930
- });
931
- // 应用主题时恢复保存的统一状态(勾选 + 值),实现完整复原
932
- if (t.recipe && t.recipe.__unified) {
933
- var sun = t.recipe.__unified;
934
- state.unified = state.unified || {};
935
- if (sun.uc) { state.unified.uc = { enabled: sun.uc.enabled === true, color: sun.uc.color || "#FFFFFF" }; }
936
- if (sun.uf) {
937
- state.unified.uf = {
938
- enabled: sun.uf.enabled === true, en: sun.uf.en || "Times New Roman",
939
- zh: sun.uf.zh || "KaiTi", size: sun.uf.size != null ? sun.uf.size : 15,
940
- weight: sun.uf.weight != null ? sun.uf.weight : 400, italic: sun.uf.italic === true
941
- };
942
- }
943
- // 同步界面上的统一控件
944
- var ucEl2 = root.querySelector("#dsh-fe-u-color"); var ucVal2 = root.querySelector("#dsh-fe-u-color-val");
945
- var ufEl2 = root.querySelector("#dsh-fe-u-font");
946
- var ufEn2 = root.querySelector("#dsh-fe-u-en"); var ufZh2 = root.querySelector("#dsh-fe-u-zh");
947
- var ufSz2 = root.querySelector("#dsh-fe-u-size"); var ufWt2 = root.querySelector("#dsh-fe-u-weight"); var ufIt2 = root.querySelector("#dsh-fe-u-italic");
948
- if (ucEl2) ucEl2.checked = state.unified.uc.enabled; if (ucVal2) ucVal2.value = state.unified.uc.color;
949
- if (ufEl2) ufEl2.checked = state.unified.uf.enabled; if (ufEn2) ufEn2.value = state.unified.uf.en;
950
- if (ufZh2) ufZh2.value = state.unified.uf.zh; if (ufSz2) ufSz2.value = state.unified.uf.size;
951
- if (ufWt2) ufWt2.value = state.unified.uf.weight; if (ufIt2) ufIt2.checked = state.unified.uf.italic;
952
- } else {
953
- // 旧主题没有 __unified:回退为关闭统一,避免误覆盖
954
- 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; }
955
- 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; }
956
- }
957
- renderCards();
958
- applyAll(regions);
959
- persist();
960
- if (matched === 0) { showToast("主题 " + name + " 已应用(无匹配区域)", "ok"); return; }
961
- showToast("已应用主题:" + name + "(" + matched + "个区域)", "ok");
962
- }
963
- function deleteTheme() {
964
- var sel = root.querySelector("#dsh-fe-theme-sel");
965
- var name = sel && sel.value;
966
- if (!name) return;
967
- state.themes = getThemes().filter(function (x) { return x.name !== name; });
968
- renderThemeSel();
969
- persist();
970
- showToast("已删除:" + name, "ok");
971
- }
972
-
973
- // ---------- render region cards ----------
974
- function cardHtml(r) {
975
- var body = "";
976
- if (r.enabled) {
977
- body =
978
- '<div class="dsh-fe-card-body">' +
979
- '<div class="dsh-fe-body-col">' +
980
- "<label>英文字体</label><select data-rid='" + r.id + "' data-fld='en'>" + optionsHtml(mergedEnFonts(), r.en) + "</select>" +
981
- "<label>中文字体</label><select data-rid='" + r.id + "' data-fld='zh'>" + optionsHtml(mergedZhFonts(), r.zh) + "</select>" +
982
- "</div>" +
983
- '<div class="dsh-fe-body-col">' +
984
- '<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>' +
985
- '<div><label>行距</label><input type="number" min="1.3" max="2.2" step="0.1" data-rid="' + r.id + '" data-fld="lh" value="' + r.lh + '"></div></div>' +
986
- '<div class="dsh-fe-weightrow"><label>字重 <span data-rid="' + r.id + '" data-fld="weightVal">' + r.weight + "</span></label>" +
987
- '<input type="range" min="100" max="900" step="100" data-rid="' + r.id + '" data-fld="weight" value="' + r.weight + '"></div>' +
988
- '<label class="dsh-fe-italicrow"><input type="checkbox" data-rid="' + r.id + '" data-fld="italic"' + (r.italic ? " checked" : "") + "> 斜体</label>" +
989
- '<label class="dsh-fe-colorrow"><input type="checkbox" data-rid="' + r.id + '" data-fld="useColor"' + (r.useColor ? " checked" : "") + "> 文字颜色</label>" +
990
- '<input type="color" data-rid="' + r.id + '" data-fld="color" value="' + esc(r.color || "#e6e6e6") + '">' +
991
- "</div>" +
992
- "</div>";
993
- }
994
- return (
995
- '<div class="dsh-fe-card" data-card="' + r.id + '">' +
996
- '<div class="dsh-fe-card-head">' +
997
- '<div class="dsh-fe-head-left">' +
998
- '<label class="dsh-fe-cb"><input type="checkbox" data-rid="' + r.id + '" data-fld="enabled"' + (r.enabled ? " checked" : "") + "></label>" +
999
- '<span class="dsh-fe-dot" data-dot="' + r.id + '" style="background:' + esc(r.color || "#888") + '"></span>' +
1000
- '<input type="text" data-rid="' + r.id + '" data-fld="name" value="' + esc(r.name) + '" title="区域名称">' +
1001
- "</div>" +
1002
- '<div class="dsh-fe-head-right">' +
1003
- '<button type="button" data-rid="' + r.id + '" data-act="pickAdd" title="框选更多元素追加到这个区域">➕加元素</button>' +
1004
- '<button type="button" data-rid="' + r.id + '" data-act="showScope" title="高亮显示该区域覆盖的元素范围">👁范围</button>' +
1005
- '<button type="button" data-rid="' + r.id + '" data-act="randomFont" title="随机字体">🎲字体</button>' +
1006
- '<button type="button" data-rid="' + r.id + '" data-act="randomColor" title="随机颜色">🎨颜色</button>' +
1007
- '<button type="button" data-rid="' + r.id + '" data-act="reset" title="重置本区域">⟳</button>' +
1008
- '<button type="button" data-rid="' + r.id + '" data-act="delete" title="删除区域">✕</button>' +
1009
- "</div>" +
1010
- "</div>" +
1011
- body +
1012
- "</div>"
1013
- );
1014
- }
1015
- function renderCards() {
1016
- if (!cardsEl) return;
1017
- cardsEl.innerHTML = regions
1018
- .map(cardHtml)
1019
- .join("");
1020
- }
1021
-
1022
- // ---------- build UI ----------
1023
- function buildUI() {
1024
- if (document.getElementById("dsh-fe-root")) return;
1025
- if (!document.body) return;
1026
- // DIAG: prove this client really runs in the browser by pinging host's
1027
- // client-alive route (host logs "CLIENT-ALIVE ping received from browser").
1028
- try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {}
1029
-
1030
- var pc = document.createElement("style");
1031
- pc.id = "dsh-fe-panel-css";
1032
- pc.dataset.plugin = "dsh-font-enhancer";
1033
- pc.textContent = panelCss();
1034
- document.head.appendChild(pc);
1035
-
1036
- root = document.createElement("div");
1037
- root.id = "dsh-fe-root";
1038
- root.innerHTML =
1039
- '<button id="dsh-fe-toggle" type="button" title="字体区域设置 Font settings">Aa</button>' +
1040
- '<div id="dsh-fe-panel" style="display:none">' +
1041
- '<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>' +
1042
- '<div style="font-size:11px;color:#8b93a1;padding:4px 4px 8px;line-height:1.5">⬇️ 下滑至底部有 <b style="color:#8ab8ff">快速上手说明</b>;换字体/颜色、框选、一键装可爱字体都在这里。</div>' +
1043
- '<div class="dsh-fe-themeblock">' +
1044
- '<select id="dsh-fe-theme-sel"></select>' +
1045
- '<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">' +
1046
- '<button type="button" data-act="themeSave" title="把当前所有区域的字体/颜色保存为一个方案(填入上方名称)">💾 保存方案</button>' +
1047
- '<button type="button" data-act="themeRename" title="重命名所选主题为输入框的名字">✏改</button>' +
1048
- '<button type="button" data-act="themeApply" title="应用选中主题">应用</button>' +
1049
- '<button type="button" data-act="themeDel" title="删除选中主题">🗑</button>' +
1050
- '<button type="button" data-act="themeClearAll" title="删除所有已保存的主题" style="color:#e08080">🗑 清空全部</button>' +
1051
- "</div>" +
1052
- '<div class="dsh-fe-unified" id="dsh-fe-unified">' +
1053
- '<div class="dsh-fe-u-row">' +
1054
- '<span class="dsh-fe-u-label">全局统一</span>' +
1055
- "</div>" +
1056
- '<div class="dsh-fe-u-row">' +
1057
- '<label><input type="checkbox" id="dsh-fe-u-color" data-u="uc" data-u-fld="enabled">统一颜色</label>' +
1058
- '<input type="color" id="dsh-fe-u-color-val" data-u="uc" data-u-fld="color" value="#FFFFFF">' +
1059
- "</div>" +
1060
- '<div class="dsh-fe-u-row">' +
1061
- '<label><input type="checkbox" id="dsh-fe-u-font" data-u="uf" data-u-fld="enabled">统一字体</label>' +
1062
- '<select id="dsh-fe-u-en" data-u="uf" data-u-fld="en" style="min-width:80px">' + optionsHtml(mergedEnFonts(), "Times New Roman") + "</select>" +
1063
- '<select id="dsh-fe-u-zh" data-u="uf" data-u-fld="zh" style="min-width:70px">' + optionsHtml(mergedZhFonts(), "KaiTi") + "</select>" +
1064
- '<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="字号">' +
1065
- '<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)">' +
1066
- '<label title="统一斜体"><input type="checkbox" id="dsh-fe-u-italic" data-u="uf" data-u-fld="italic">斜体</label>' +
1067
- "</div>" +
1068
- "</div>" +
1069
- '<div class="dsh-fe-toolbar">' +
1070
- '<button type="button" data-act="pickMode" title="在界面框选文字元素,合并成一个区域并命名">🖱 框选区域</button>' +
1071
- '<button type="button" data-act="randomAllFonts" title="给所有启用区域随机分配英/中文字体">🎲 随机字体</button>' +
1072
- '<button type="button" class="dsh-fe-btn-accent" data-act="randomAllColors" title="给所有启用区域随机分配颜色">🎨 随机颜色</button>' +
1073
- "</div>" +
1074
- '<div class="dsh-fe-toolbar dsh-fe-toolbar2">' +
1075
- '<button type="button" data-act="replacePicked" title="删除全部默认区域,只保留你框选的区域">⟳ 框选取代默认</button>' +
1076
- '<button type="button" data-act="restoreDefault" title="删除新增的框选区域,恢复内置默认区域">↺ 恢复默认区域</button>' +
1077
- '<button type="button" data-act="reloadUI" title="刷新界面(新安装的字体立即生效,无需关闭面板)">🔄 刷新</button>' +
1078
- "</div>" +
1079
- '<div id="dsh-fe-cards"></div>' +
1080
- '<div class="dsh-fe-customfont">' +
1081
- '<div class="dsh-fe-cf-label">🎨 推荐可爱/奇特字体(一键下载安装 → 添加启用)</div>' +
1082
- '<div class="dsh-fe-cf-list">' +
1083
- '<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>' +
1084
- '<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>' +
1085
- '<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>' +
1086
- '<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>' +
1087
- '<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>' +
1088
- '<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>' +
1089
- '<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>' +
1090
- '<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>' +
1091
- "</div>" +
1092
- '<div class="dsh-fe-cf-file">' +
1093
- '<div class="dsh-fe-cf-label">📁 添加自提供安装包(清单里没有的字体)</div>' +
1094
- '<div class="dsh-fe-cf-file-row">' +
1095
- '<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">' +
1096
- '<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>' +
1097
- "</div>" +
1098
- '<div class="dsh-fe-cf-tip">选择电脑上已有的字体安装包(.ttf/.otf),点「安装并添加」,自动装到系统并加入字体下拉。</div>' +
1099
- "</div>" +
1100
- '<div class="dsh-fe-cf-tip">💡 点「⬇ 一键安装」自动下载并装到系统,安装成功后<strong>自动添加到字体下拉</strong>,无需手动操作。安装路径会显示在提示里。中文字体较大(几MB),请耐心等待下载完成。</div>' +
1101
- "</div>" +
1102
- '<div id="dsh-fe-guide" class="dsh-fe-guide">' +
1103
- "<h4>📖 快速上手(30 秒学会)</h4>" +
1104
- "<p><b>① 改某个区域的字体/颜色</b><br>勾选区域卡片前的 ☑ → 展开设置 → 左边选字体,右边调字号/字重/斜体/颜色,马上生效。</p>" +
1105
- "<p><b>② 一键统一所有区域</b><br>「全局统一」区勾选 <b>统一颜色</b> 或 <b>统一字体</b>,一个设置全界面生效。</p>" +
1106
- "<p><b>③ 框选自己想要调整的区域</b><br>点 <b>🖱 框选区域</b> → 在界面上点选文字元素(可多选)→ 起个名字 → 确定,就能单独调它。</p>" +
1107
- "<p><b>④ 保存方案,随时切换</b><br>调好后填个名字 → 点 <b>💾 保存方案</b>。以后下拉选它 → 点 <b>应用</b> 一键还原。</p>" +
1108
- "<p><b>⑤ 用可爱字体(一键安装)</b><br>在下方字体列表点 <b>⬇ 一键安装</b> → 等下载完 → 点 <b>🔄 刷新</b>,新字体就在下拉里了。</p>" +
1109
- "<p><b>⑥ 添加自己下载的字体</b><br>点「📁 添加自提供安装包」→ 选电脑上的 .ttf/.otf 文件 → 点 <b>安装并添加</b>。</p>" +
1110
- "<p><b>⑦ 卡住了?</b><br>点 <b>🔄 刷新</b>,或点右下角 Aa 悬浮球收起来再展开。</p>" +
1111
- "<p style='color:#e0a060'><b>⚠️ 网页端提示</b>:如果用的是手机/浏览器打开网页端,装完新字体后需要<b>刷新网页</b>(F5)才能显示新字体;桌面端窗口无需刷新。</p>" +
1112
- "</div>" +
1113
- '<div id="dsh-fe-toast"></div>' +
1114
- "</div>";
1115
- // Mount to the outermost <html> element, NOT document.body — DSH gives
1116
- // the body/app containers `transform`/`filter`/`overflow` which make
1117
- // position:fixed children (the orb + panel) position against a transformed
1118
- // ancestor instead of the viewport, hiding them off screen. Appending to
1119
- // <html> sidesteps that so the floating orb is always visible.
1120
- // v1.2.0: the parent #dsh-fe-root is position:static + pointer-events:none,
1121
- // so position:fixed children stay viewport-relative in BOTH the browser
1122
- // and the Electron desktop window.
1123
- var mount = document.documentElement || document.body;
1124
- // guard against re-append (Electron window can re-run buildUI on focus)
1125
- if (!root.parentNode) mount.appendChild(root);
1126
- // v1.2.0 — Electron desktop window hardening: DSH Desktop renders the
1127
- // same 43120 page in its Electron BrowserWindow, but the native window
1128
- // chrome + zoom can offset position:fixed elements. Detect Electron and,
1129
- // if the orb would land off-viewport, force it to an on-screen corner.
1130
- try {
1131
- var isElectron = /electron/i.test(navigator.userAgent) || (typeof window.process !== "undefined" && window.process.type === "renderer");
1132
- if (isElectron) {
1133
- var btnEl = root.querySelector("#dsh-fe-toggle");
1134
- var panelEl = root.querySelector("#dsh-fe-panel");
1135
- if (btnEl && panelEl) {
1136
- // reset to safe defaults so the orb is never pushed off-screen
1137
- btnEl.style.right = "18px";
1138
- btnEl.style.bottom = "18px";
1139
- btnEl.style.left = "auto";
1140
- btnEl.style.top = "auto";
1141
- // re-pin the panel near the orb once rendered
1142
- var pin = function () {
1143
- var br = btnEl.getBoundingClientRect();
1144
- if (!br || (br.left === 0 && br.top === 0 && br.width === 0)) return;
1145
- var pw = panelEl.offsetWidth || 348;
1146
- var ph = panelEl.offsetHeight || 360;
1147
- var left = Math.max(8, Math.min(window.innerWidth - pw - 8, br.left + br.width / 2 - pw / 2));
1148
- var top = br.top - ph - 10;
1149
- if (top < 8) top = br.bottom + 10;
1150
- panelEl.style.left = left + "px";
1151
- panelEl.style.top = top + "px";
1152
- panelEl.style.right = "auto";
1153
- panelEl.style.bottom = "auto";
1154
- };
1155
- setTimeout(pin, 50);
1156
- window.addEventListener("resize", pin);
1157
- }
1158
- }
1159
- } catch (e) {}
1160
- try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {} // DIAG-2 root appended
1161
-
1162
- panel = root.querySelector("#dsh-fe-panel");
1163
- cardsEl = root.querySelector("#dsh-fe-cards");
1164
- renderCards();
1165
- renderThemeSel();
1166
- applyAll(regions);
1167
-
1168
- // sync unified controls with saved state
1169
- var u = state.unified;
1170
- var ucEl = root.querySelector("#dsh-fe-u-color");
1171
- var ucVal = root.querySelector("#dsh-fe-u-color-val");
1172
- if (ucEl) ucEl.checked = u.uc.enabled;
1173
- if (ucVal) ucVal.value = u.uc.color;
1174
- var ufEl = root.querySelector("#dsh-fe-u-font");
1175
- var ufEn = root.querySelector("#dsh-fe-u-en");
1176
- var ufZh = root.querySelector("#dsh-fe-u-zh");
1177
- var ufSz = root.querySelector("#dsh-fe-u-size");
1178
- var ufWt = root.querySelector("#dsh-fe-u-weight");
1179
- var ufIt = root.querySelector("#dsh-fe-u-italic");
1180
- if (ufEl) ufEl.checked = u.uf.enabled;
1181
- if (ufEn) ufEn.value = u.uf.en;
1182
- if (ufZh) ufZh.value = u.uf.zh;
1183
- if (ufSz) ufSz.value = u.uf.size;
1184
- if (ufWt) ufWt.value = u.uf.weight;
1185
- if (ufIt) ufIt.checked = u.uf.italic;
1186
-
1187
- // Direct event listener for unified color picker (bypass delegation)
1188
- var ucPicker = root.querySelector("#dsh-fe-u-color-val");
1189
- if (ucPicker) {
1190
- ucPicker.addEventListener("input", function () {
1191
- state.unified.uc.color = ucPicker.value;
1192
- applyAll(regions);
1193
- persist();
1194
- });
1195
- }
1196
-
1197
- // initial button position (saved, else default bottom-right).
1198
- // Clamp inside the viewport so a stale/off-screen saved position can
1199
- // never hide the orb (it "disappears" when bx/by put it outside the window).
1200
- var btn = root.querySelector("#dsh-fe-toggle");
1201
- var bw = btn.offsetWidth || 54, bh = btn.offsetHeight || 54;
1202
- if (typeof state.bx === "number" && typeof state.by === "number" &&
1203
- state.bx >= 0 && state.by >= 0 &&
1204
- state.bx <= Math.max(0, window.innerWidth - bw) &&
1205
- state.by <= Math.max(0, window.innerHeight - bh)) {
1206
- btn.style.left = state.bx + "px";
1207
- btn.style.top = state.by + "px";
1208
- btn.style.right = "auto";
1209
- btn.style.bottom = "auto";
1210
- } else {
1211
- // default / invalid position → bottom-right (CSS default covers it)
1212
- btn.style.left = "auto"; btn.style.top = "auto";
1213
- btn.style.right = "18px"; btn.style.bottom = "18px";
1214
- }
1215
-
1216
- wireEvents();
1217
- }
1218
-
1219
- // ---------- event wiring ----------
1220
- function wireEvents() {
1221
- var toggle = root.querySelector("#dsh-fe-toggle");
1222
-
1223
- function show() {
1224
- panel.style.display = "block";
1225
- positionPanel();
1226
- }
1227
- function hide() {
1228
- panel.style.display = "none";
1229
- reloadPlugin();
1230
- }
1231
- function isOpen() {
1232
- return panel.style.display === "block";
1233
- }
1234
- function positionPanel() {
1235
- var r = toggle.getBoundingClientRect();
1236
- var pw = panel.offsetWidth || 320;
1237
- var ph = panel.offsetHeight || 360;
1238
- var left = r.left + r.width / 2 - pw / 2;
1239
- left = Math.max(8, Math.min(window.innerWidth - pw - 8, left));
1240
- var top = r.top - ph - 10;
1241
- if (top < 8) top = r.bottom + 10;
1242
- panel.style.left = left + "px";
1243
- panel.style.top = top + "px";
1244
- panel.style.right = "auto";
1245
- panel.style.bottom = "auto";
1246
- }
1247
-
1248
- // ---- drag the button (pet-like) ----
1249
- var drag = { active: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
1250
- toggle.addEventListener("pointerdown", function (e) {
1251
- drag.active = true;
1252
- drag.moved = false;
1253
- drag.sx = e.clientX;
1254
- drag.sy = e.clientY;
1255
- var r = toggle.getBoundingClientRect();
1256
- drag.ox = r.left;
1257
- drag.oy = r.top;
1258
- try {
1259
- toggle.setPointerCapture(e.pointerId);
1260
- } catch (err) {}
1261
- toggle.classList.add("dsh-fe-dragging");
1262
- e.preventDefault();
1263
- });
1264
- toggle.addEventListener("pointermove", function (e) {
1265
- if (!drag.active) return;
1266
- var dx = e.clientX - drag.sx;
1267
- var dy = e.clientY - drag.sy;
1268
- if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 5) return;
1269
- drag.moved = true;
1270
- var nx = Math.max(0, Math.min(window.innerWidth - toggle.offsetWidth, drag.ox + dx));
1271
- var ny = Math.max(0, Math.min(window.innerHeight - toggle.offsetHeight, drag.oy + dy));
1272
- toggle.style.left = nx + "px";
1273
- toggle.style.top = ny + "px";
1274
- toggle.style.right = "auto";
1275
- toggle.style.bottom = "auto";
1276
- state.bx = Math.round(nx);
1277
- state.by = Math.round(ny);
1278
- persist();
1279
- // 面板展开时跟随悬浮球移动
1280
- if (isOpen()) positionPanel();
1281
- });
1282
- function endDrag(e) {
1283
- if (!drag.active) return;
1284
- drag.active = false;
1285
- toggle.classList.remove("dsh-fe-dragging");
1286
- try {
1287
- toggle.releasePointerCapture(e.pointerId);
1288
- } catch (err) {}
1289
- if (!drag.moved) {
1290
- if (isOpen()) hide();
1291
- else show();
1292
- }
1293
- }
1294
- toggle.addEventListener("pointerup", endDrag);
1295
- toggle.addEventListener("pointercancel", endDrag);
1296
-
1297
- root.querySelector("#dsh-fe-close").addEventListener("click", hide);
1298
-
1299
- // ---- delegated field edits (no re-render, keep focus) ----
1300
- function onField(e) {
1301
- var el = e.target;
1302
- var rid = el.getAttribute("data-rid");
1303
- var fld = el.getAttribute("data-fld");
1304
- if (!rid || !fld) return;
1305
- var r = findRegion(rid);
1306
- if (!r) return;
1307
- if (fld === "enabled") { r.enabled = el.checked; renderCards(); }
1308
- else if (fld === "italic") r.italic = el.checked;
1309
- else if (fld === "useColor") r.useColor = el.checked;
1310
- else if (fld === "name") r.name = el.value;
1311
- else if (fld === "selector") r.selector = el.value;
1312
- else if (fld === "en" || fld === "zh") r[fld] = el.value;
1313
- else if (fld === "size") r.size = clamp(el.value, 12, 22);
1314
- else if (fld === "lh") r.lh = clamp(el.value, 1.3, 2.2);
1315
- else if (fld === "weight") {
1316
- r.weight = clamp(el.value, 100, 900);
1317
- var v = root.querySelector('[data-rid="' + rid + '"][data-fld="weightVal"]');
1318
- if (v) v.textContent = r.weight;
1319
- } else if (fld === "color") {
1320
- r.color = el.value;
1321
- r.useColor = true; // picking a color should always apply it
1322
- var dot = root.querySelector('[data-dot="' + rid + '"]');
1323
- if (dot) dot.style.background = r.color;
1324
- var cb = root.querySelector('[data-rid="' + rid + '"][data-fld="useColor"]');
1325
- if (cb) cb.checked = true;
1326
- }
1327
- else return;
1328
- applyAll(regions);
1329
- persist();
1330
- }
1331
- root.addEventListener("input", onField);
1332
- root.addEventListener("change", onField);
1333
-
1334
- // ---- unified controls ----
1335
- function onUnified(e) {
1336
- var el = e.target;
1337
- var u = el.getAttribute("data-u");
1338
- var fld = el.getAttribute("data-u-fld");
1339
- if (!u || !fld) return;
1340
- var sect = u === "uc" ? "uc" : "uf";
1341
- if (fld === "enabled") state.unified[sect].enabled = el.checked;
1342
- else if (fld === "color") state.unified[sect].color = el.value;
1343
- else if (fld === "en" || fld === "zh") state.unified[sect][fld] = el.value;
1344
- else if (fld === "size") state.unified[sect].size = clamp(el.value, 12, 22);
1345
- else if (fld === "weight") state.unified[sect].weight = clamp(el.value, 100, 900);
1346
- else if (fld === "italic") state.unified[sect].italic = el.checked;
1347
- else return;
1348
- applyAll(regions);
1349
- persist();
1350
- }
1351
- root.addEventListener("input", onUnified);
1352
- root.addEventListener("change", onUnified);
1353
-
1354
- // ---- theme select: auto-fill name input ----
1355
- var themeSel = root.querySelector("#dsh-fe-theme-sel");
1356
- if (themeSel) {
1357
- themeSel.addEventListener("change", function () {
1358
- var inp = root.querySelector("#dsh-fe-theme-name");
1359
- if (inp && themeSel.value) inp.value = themeSel.value;
1360
- });
1361
- }
1362
-
1363
- // ---- delegated actions ----
1364
- root.addEventListener("click", function (e) {
1365
- var btn = e.target.closest ? e.target.closest("[data-act]") : null;
1366
- if (!btn) return;
1367
- var act = btn.getAttribute("data-act");
1368
- var rid = btn.getAttribute("data-rid");
1369
- var el = btn;
1370
-
1371
- if (act === "resetAll") {
1372
- regions = DEFAULT_REGIONS();
1373
- renderCards();
1374
- applyAll(regions);
1375
- persist();
1376
- return;
1377
- }
1378
- if (act === "pickMode") { pickRegion(); return; }
1379
- if (act === "recordToggle") { recToggle(!RECORDING); return; }
1380
- if (act === "replacePicked") { replaceWithPicked(); return; }
1381
- if (act === "restoreDefault") { restoreDefault(); return; }
1382
- if (act === "saveConfig") { saveConfig(); return; }
1383
- if (act === "randomAllFonts") { randomizeAllFonts(); return; }
1384
- if (act === "randomAllColors") { randomizeAllColors(); return; }
1385
- if (act === "themeSave") { saveTheme(); return; }
1386
- if (act === "themeRename") { themeRename(); return; }
1387
- if (act === "themeApply") { applyTheme(); return; }
1388
- if (act === "themeDel") { deleteTheme(); return; }
1389
- if (act === "themeClearAll") { state.themes = []; renderThemeSel(); persist(); showToast("已清空全部主题", "ok"); return; }
1390
- if (act === "reloadUI") {
1391
- // 强制 Chromium 加载新安装的字体,并刷新下拉列表(不关闭面板)
1392
- var names = [];
1393
- (state.customFonts || []).forEach(function (f) {
1394
- if (f.en) names.push(f.en);
1395
- if (f.zh) names.push(f.zh);
1396
- });
1397
- if (document.fonts && document.fonts.load) {
1398
- names.forEach(function (n) {
1399
- try { document.fonts.load('16px "' + n + '"').catch(function(){}); } catch (e) {}
1400
- });
1401
- }
1402
- renderCards();
1403
- renderThemeSel();
1404
- applyAll(regions);
1405
- showToast("🔄 已刷新,新字体应已生效", "ok");
1406
- return;
1407
- }
1408
- if (act === "installFont") {
1409
- var fid = el.getAttribute("data-font-id") || "";
1410
- var fzh = el.getAttribute("data-font-zh") || "";
1411
- var fen = el.getAttribute("data-font-en") || "";
1412
- if (!fid) { showToast("字体信息缺失", "err"); return; }
1413
- var btn = el; btn.textContent = "⏳ 下载中 0%"; btn.disabled = true;
1414
- var done = false;
1415
- // 轮询下载进度,更新按钮百分比
1416
- var poll = setInterval(function () {
1417
- if (done) return;
1418
- try {
1419
- fetch("/dsh-font-enhancer/font-progress?fontId=" + encodeURIComponent(fid), { cache: "no-store" })
1420
- .then(function (r) { return r.json(); })
1421
- .then(function (pg) {
1422
- if (done || !pg) return;
1423
- if (pg.state === "downloading" && pg.pct >= 0) { btn.textContent = "⏳ 下载中 " + pg.pct + "%"; }
1424
- else if (pg.state === "done") { clearInterval(poll); }
1425
- else if (pg.state === "error") { clearInterval(poll); }
1426
- })
1427
- .catch(function () {});
1428
- } catch (e) {}
1429
- }, 400);
1430
- try {
1431
- fetch("/dsh-font-enhancer/install-font", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fontId: fid }) })
1432
- .then(function (r) { return r.json(); })
1433
- .then(function (res) {
1434
- done = true; clearInterval(poll);
1435
- if (res && res.ok) {
1436
- if (fzh && !state.customFonts.some(function (x) { return x.zh === fzh; })) state.customFonts.push({ zh: fzh });
1437
- if (fen && !state.customFonts.some(function (x) { return x.en === fen; })) state.customFonts.push({ en: fen });
1438
- persist(); renderCards(); renderThemeSel();
1439
- var dest = res.dest ? ",路径:" + res.dest : "";
1440
- btn.textContent = "✅ 已安装"; btn.disabled = true;
1441
- showToast("✅ 字体已" + (res.already ? "存在" : "安装") + ":" + (fzh || fen) + ",已自动添加" + dest, "ok");
1442
- } else {
1443
- btn.textContent = "⬇ 重试安装"; btn.disabled = false;
1444
- showToast("安装失败:" + (res && res.error || "未知错误"), "err");
1445
- }
1446
- })
1447
- .catch(function () { done = true; clearInterval(poll); btn.textContent = "⬇ 重试安装"; btn.disabled = false; showToast("安装失败,请检查网络", "err"); });
1448
- } catch (e) { done = true; clearInterval(poll); btn.textContent = "⬇ 重试安装"; btn.disabled = false; showToast("安装失败:" + e, "err"); }
1449
- return;
1450
- }
1451
- if (act === "addCustomFont") {
1452
- var en = el.getAttribute("data-font-en") || "";
1453
- var zh = el.getAttribute("data-font-zh") || "";
1454
- if (!en && !zh) { showToast("字体信息缺失", "err"); return; }
1455
- if (en) { if (!state.customFonts.some(function (f) { return f.en === en; })) state.customFonts.push({ en: en }); }
1456
- if (zh) { if (!state.customFonts.some(function (f) { return f.zh === zh; })) state.customFonts.push({ zh: zh }); }
1457
- persist(); renderCards(); renderThemeSel(); showToast("已添加字体" + (en || zh), "ok");
1458
- return;
1459
- }
1460
- if (act === "installCustomFile") {
1461
- var fileInput = root.querySelector("#dsh-fe-font-file");
1462
- var file = fileInput && fileInput.files && fileInput.files[0];
1463
- if (!file) { showToast("请先选择字体文件(.ttf/.otf)", "err"); return; }
1464
- var okExt = /\.(ttf|otf)$/i.test(file.name);
1465
- if (!okExt) { showToast("请选择 .ttf 或 .otf 字体文件", "err"); return; }
1466
- var btn2 = el; btn2.textContent = "⏳ 上传中..."; btn2.disabled = true;
1467
- var reader = new FileReader();
1468
- reader.onload = function (e) {
1469
- var base64 = String(e.target.result).split(",")[1] || "";
1470
- try {
1471
- fetch("/dsh-font-enhancer/upload-font", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fileName: file.name, data: base64 }) })
1472
- .then(function (r) { return r.json(); })
1473
- .then(function (res) {
1474
- btn2.textContent = "安装并添加"; btn2.disabled = false;
1475
- if (res && res.ok) {
1476
- // 自动添加字体(用文件名去掉扩展名作为字体名)
1477
- var fam = (res.fontName || file.name.replace(/\.(ttf|otf)$/i, "")).trim();
1478
- if (fam && !state.customFonts.some(function (x) { return x.en === fam; }) && !state.customFonts.some(function (x) { return x.zh === fam; })) {
1479
- state.customFonts.push({ en: fam, zh: fam });
1480
- }
1481
- persist(); renderCards(); renderThemeSel();
1482
- var dest = res.dest ? ",路径:" + res.dest : "";
1483
- showToast("✅ 字体已安装并添加:" + fam + dest, "ok");
1484
- if (fileInput) fileInput.value = "";
1485
- } else {
1486
- showToast("安装失败:" + (res && res.error || "未知错误"), "err");
1487
- }
1488
- })
1489
- .catch(function () { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("上传失败,请重试", "err"); });
1490
- } catch (e2) { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("安装失败:" + e2, "err"); }
1491
- };
1492
- reader.onerror = function () { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("读取文件失败", "err"); };
1493
- reader.readAsDataURL(file);
1494
- return;
1495
- }
1496
-
1497
- var r = rid ? findRegion(rid) : null;
1498
- if (!r) return;
1499
- if (act === "pickAdd") { pickRegion(rid); return; }
1500
- if (act === "showScope") { showScope(rid); return; }
1501
- if (act === "delete") {
1502
- regions = regions.filter(function (x) {
1503
- return x.id !== rid;
1504
- });
1505
- renderCards();
1506
- applyAll(regions);
1507
- persist();
1508
- } else if (act === "randomFont") {
1509
- randomizeFonts(rid);
1510
- } else if (act === "randomColor") {
1511
- randomizeColor(rid);
1512
- } else if (act === "reset") {
1513
- // reset styling + color — keep the calibrated name/selector/group set
1514
- r.en = "Times New Roman";
1515
- r.zh = "KaiTi";
1516
- r.size = 15;
1517
- r.lh = 1.6;
1518
- r.weight = 400;
1519
- r.italic = false;
1520
- r.useColor = false;
1521
- r.color = "#e6e6e6";
1522
- renderCards();
1523
- applyAll(regions);
1524
- persist();
1525
- }
1526
- });
1527
-
1528
- document.addEventListener("keydown", function (e) {
1529
- if (e.key === "Escape" && isOpen()) hide();
1530
- });
1531
- window.addEventListener("resize", function () {
1532
- if (isOpen()) positionPanel();
1533
- });
1534
- try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {} // DIAG-3 buildUI complete
1535
- }
1536
-
1537
- // ---------- entry ----------
1538
- // 设置页「悬浮球导航」分区(settings.section,与技能浏览器同款官方机制)。
1539
- // 让字体球(Aa)的显隐由 font-enhancer 自己在 DSH 设置页控制,不依赖其他插件。
1540
- var FE_BALL_VIS_KEY = "dsh-font-ball-visible";
1541
- function feReadStore(key, def) {
1542
- try { var v = localStorage.getItem(key); return v == null ? def : v === "1"; } catch (e) { return def; }
1543
- }
1544
- function feWriteStore(key, val) {
1545
- try { localStorage.setItem(key, val ? "1" : "0"); } catch (e) {}
1546
- }
1547
- function feApplyBallVisibility() {
1548
- var hidden = !feReadStore(FE_BALL_VIS_KEY, true);
1549
- var t = document.getElementById("dsh-fe-toggle");
1550
- if (t) t.style.setProperty("display", hidden ? "none" : "", "important");
1551
- }
1552
- // 监听字体球(防热重载后重建时样式/显隐丢失;低频轮询,CSS 已兜底)
1553
- function feWatchBall() {
1554
- var tries = 0;
1555
- var t = setInterval(function () {
1556
- tries++;
1557
- try { feApplyBallVisibility(); } catch (e) {}
1558
- if (tries > 120) clearInterval(t);
1559
- }, 1000);
1560
- }
1561
-
1562
- var feReact = null;
1563
- try {
1564
- var feReactRaw = require("react");
1565
- feReact = (feReactRaw && feReactRaw.default) || feReactRaw;
1566
- } catch (e) { feReact = null; }
1567
- if (!feReact) { try { feReact = window.React || null; } catch (e) {} }
1568
-
1569
- // v1.4.3(2026-09-06 用户要求):分区智能让位 + UI 对齐 skill-browser 版式。
1570
- // 探测机制:skill-browser 的 🧩 悬浮球 DOM(页面加载即建,不依赖打开设置页)。
1571
- // 让位机制:延迟注册——skill-browser 在场则不注册分区,独立运行才注册兜底分区。
1572
- // 独立存在时(未装 skill-browser):完整版「悬浮球导航」分区——字体球开关
1573
- // + 🎯 一键恢复默认位置 + 推荐插件位(照片显示器 / 技能浏览器)。
1574
-
1575
- // 恢复字体球默认位置(右下角 right:18px bottom:18px),清 bx/by 持久化。
1576
- // v1.4.4 推荐位安装徽标(与 skill-browser 同款探测)
1577
- function feMarkRecBadge(recId, installed) {
1578
- var row = document.querySelector('[data-rec-id="' + recId + '"]');
1579
- if (!row || row.querySelector(".fe-rec-badge")) return;
1580
- var badge = document.createElement("span");
1581
- badge.className = "fe-rec-badge";
1582
- badge.textContent = installed ? "已安装" : "未安装";
1583
- badge.style.cssText = "flex-shrink:0;margin-left:8px;font-size:11px;padding:2px 8px;border-radius:6px;" +
1584
- (installed ? "background:rgba(120,220,150,.15);border:1px solid rgba(120,220,150,.4);color:#9fe0b5"
1585
- : "background:rgba(255,120,120,.15);border:1px solid rgba(255,120,120,.35);color:#ffb3b3") +
1586
- ";white-space:nowrap";
1587
- var star = row.querySelector("a");
1588
- if (star) row.insertBefore(badge, star); else row.appendChild(badge);
1589
- }
1590
- function feApplyRecBadges() {
1591
- feMarkRecBadge("font", true); // 本插件在跑,恒已装
1592
- try {
1593
- var sbOk = !!document.getElementById("dsh-skill-browser-ball");
1594
- feMarkRecBadge("skillbrowser", sbOk);
1595
- } catch (e) { feMarkRecBadge("skillbrowser", false); }
1596
- try {
1597
- fetch("/api/image-gallery/list").then(function (r) { feMarkRecBadge("lightbox", r.ok); }).catch(function () { feMarkRecBadge("lightbox", false); });
1598
- } catch (e) { feMarkRecBadge("lightbox", false); }
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-font-enhancer",
3
+ factory: function (require) {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ // ---- Word-style font lists (label, css-family) ----
8
+ var EN_FONTS = [
9
+ ["Times New Roman", "Times New Roman"],
10
+ ["Arial", "Arial"],
11
+ ["Calibri", "Calibri"],
12
+ ["Cambria", "Cambria"],
13
+ ["Georgia", "Georgia"],
14
+ ["Verdana", "Verdana"],
15
+ ["Tahoma", "Tahoma"],
16
+ ["Segoe UI", "Segoe UI"],
17
+ ["Garamond", "Garamond"],
18
+ ["Book Antiqua", "Book Antiqua"],
19
+ ["Palatino Linotype", "Palatino Linotype"],
20
+ ["Trebuchet MS", "Trebuchet MS"],
21
+ ["Lucida Sans Unicode", "Lucida Sans Unicode"],
22
+ ["Comic Sans MS", "Comic Sans MS"],
23
+ ["Consolas (mono)", "Consolas"],
24
+ ["Courier New (mono)", "Courier New"],
25
+ ["Impact", "Impact"],
26
+ ["Bahnschrift", "Bahnschrift"]
27
+ ];
28
+ // Simplified-Chinese fonts shipped with Word / Windows.
29
+ // 楷体 (KaiTi) is first so it is both the default and the first option.
30
+ var ZH_FONTS = [
31
+ ["楷体 (KaiTi)", "KaiTi"],
32
+ ["微软雅黑 (Microsoft YaHei)", "Microsoft YaHei"],
33
+ ["宋体 (SimSun)", "SimSun"],
34
+ ["黑体 (SimHei)", "SimHei"],
35
+ ["新宋体 (NSimSun)", "NSimSun"],
36
+ ["仿宋 (FangSong)", "FangSong"],
37
+ ["微软雅黑 Light", "Microsoft YaHei Light"],
38
+ ["等线 (DengXian)", "DengXian"],
39
+ ["等线 Light", "DengXian Light"],
40
+ ["华文细黑 (STXihei)", "STXihei"],
41
+ ["华文宋体 (STSong)", "STSong"],
42
+ ["华文中宋 (STZhongsong)", "STZhongsong"],
43
+ ["华文楷体 (STKaiti)", "STKaiti"],
44
+ ["华文行楷 (STXingkai)", "STXingkai"],
45
+ ["隶书 (LiSu)", "LiSu"],
46
+ ["幼圆 (YouYuan)", "YouYuan"],
47
+ ["方正舒体 (FZShuTi)", "FZShuTi"],
48
+ ["方正姚体 (FZYaoti)", "FZYaoti"]
49
+ ];
50
+
51
+ var STORE_KEY = "dsh-font-enhancer";
52
+ // Bump this every time the built-in default region palette changes. A stored
53
+ // settings payload whose `ver` is older than this is treated as stale: the
54
+ // old region colors (e.g. the first cyan/blue scheme) are dropped and the
55
+ // latest DEFAULT_REGIONS() palette is loaded instead. This makes palette
56
+ // refreshes survive a restart without needing the user to clear localStorage.
57
+ var SCHEMA_VERSION = 22;
58
+ // 21: DEFAULT_REGIONS 清空 —— 用户从零开始逐项框选/添加、逐项测试颜色稳定生效
59
+
60
+ // ---------- helpers ----------
61
+ function esc(s) {
62
+ return String(s)
63
+ .replace(/&/g, "&amp;")
64
+ .replace(/</g, "&lt;")
65
+ .replace(/>/g, "&gt;")
66
+ .replace(/"/g, "&quot;");
1599
67
  }
1600
- function feResetBallToDefault() {
1601
- try {
1602
- var data = loadSettings();
1603
- data.bx = null; data.by = null;
1604
- saveSettings(data);
1605
- state.bx = null; state.by = null;
1606
- } catch (e) {}
1607
- var t = document.getElementById("dsh-fe-toggle");
1608
- if (t) {
1609
- t.style.left = "auto"; t.style.top = "auto";
1610
- t.style.right = "18px"; t.style.bottom = "18px";
1611
- }
1612
- }
1613
-
1614
- // skill-browser 是否已安装运行(🧩 球元素存在即算,与球显隐无关)
1615
- function feSkillBrowserPresent() {
1616
- try { return !!document.getElementById("dsh-skill-browser-ball"); } catch (e) { return false; }
1617
- }
1618
-
1619
- function FontSettingsSection(props) {
1620
- if (!feReact) return null;
1621
- var rows = [];
1622
- function mkToggle(id, label, onChange) {
1623
- return feReact.createElement("label", { key: id, style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", cursor: "pointer" } },
1624
- feReact.createElement("input", { type: "checkbox", id: id, onChange: onChange }),
1625
- feReact.createElement("span", null, label));
1626
- }
1627
- rows.push(feReact.createElement("div", { key: "head", style: { fontWeight: "600", marginBottom: "4px" } }, "悬浮球导航"));
1628
- rows.push(mkToggle("fe-set-ball", "字体插件悬浮球(🔤)", function (e) {
1629
- feWriteStore(FE_BALL_VIS_KEY, e.target.checked);
1630
- feApplyBallVisibility();
1631
- }));
1632
- rows.push(feReact.createElement("button", { key: "fe-reset-pos",
1633
- onClick: function () { try { feResetBallToDefault(); } catch (e) {} },
1634
- style: { alignSelf: "flex-start", fontSize: "13px", cursor: "pointer",
1635
- background: "rgba(255,210,122,.12)", border: "1px solid rgba(255,210,122,.35)",
1636
- borderRadius: "6px", padding: "6px 12px", color: "#ffd27a" }
1637
- }, "🎯 一键恢复悬浮球默认位置(右下角)"));
1638
- rows.push(feReact.createElement("div", { key: "hint", style: { fontSize: "12px", color: "#8b93a8" } },
1639
- "悬浮球可拖动;本分区就在 DSH 设置中,随时可改悬浮球显隐"));
1640
- // ── 安装与更新须知(v1.4.4)──
1641
- rows.push(feReact.createElement("div", { key: "fe-notice-block", style: { marginTop: "12px", padding: "10px 12px", background: "rgba(255,255,255,.03)", border: "1px solid rgba(255,255,255,.06)", borderRadius: "10px", fontSize: "12px", color: "#9aa1b5", lineHeight: "1.7" } },
1642
- feReact.createElement("div", { style: { fontWeight: "600", color: "#cfd4dc", marginBottom: "4px" } }, "📌 安装与更新须知"),
1643
- feReact.createElement("div", null, "1. 当前由「字体插件」提供本分区(未检测到技能浏览器)。推荐列表里的技能浏览器等插件装上后,本分区会自动让位,改由对方提供更全的悬浮球导航。"),
1644
- feReact.createElement("div", null, "2. 推荐插件列表实时显示「已安装 / 未安装」状态:绿色「已安装」= 可直接使用;红色「未安装」= 点击 ⭐ Star 前往仓库,或到插件市场搜索安装。"),
1645
- feReact.createElement("div", { style: { color: "#e0b0b0" } }, "3. 【重要】安装或更新插件时,若报错含 minimumReleaseAge 或 ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION:这不是插件坏了,是 DSH 对刚发布 npm 包的 24 小时安全观察期(防止刚发布的投毒包)。"),
1646
- feReact.createElement("div", { style: { color: "#e0b0b0" } }, "4. 遇到 24h 报错的处理:等一天再安装/更新即可自动通过,插件本体没有任何问题。插件作者连发多版时窗口期会顺延一天。"),
1647
- feReact.createElement("div", null, "5. 悬浮球可拖动换位置;拖到屏幕外找不到时,点上方 🎯 按钮一键复位。")
1648
- ));
1649
- // ── 推荐插件(v1.4.4 统一三条清单:字体/技能/照片,探测式安装徽标)──
1650
- function recRow(key, recId, icon, text, repo) {
1651
- return feReact.createElement("div", { key: key, "data-rec-id": recId, style: { display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "13px", padding: "3px 0" } },
1652
- feReact.createElement("span", null, icon + " " + text),
1653
- feReact.createElement("a", { href: "https://github.com/" + repo, target: "_blank", rel: "noopener", style: { textDecoration: "none", fontSize: "12px", background: "rgba(255,255,255,.06)", border: "1px solid rgba(255,255,255,.12)", borderRadius: "6px", padding: "2px 8px", color: "#ffd27a", cursor: "pointer", whiteSpace: "nowrap" } }, "⭐ Star"));
1654
- }
1655
- rows.push(recRow("rec-font", "font", "🔤", "字体插件 — 区域自定义字体/颜色/字号", "loyalchiiina/dsh-font-enhancer"));
1656
- rows.push(recRow("rec-skillbrowser", "skillbrowser", "🧩", "技能浏览器 — 悬浮球面板浏览共享技能库 + 失效台账", "loyalchiiina/dsh-skill-browser"));
1657
- rows.push(recRow("rec-lightbox", "lightbox", "🖼️", "照片显示器 — 聊天图片灯箱(放大/下载/浏览)", "loyalchiiina/dsh-chat-image-lightbox"));return feReact.createElement("div", { "data-fe-slot": "1", style: { display: "flex", flexDirection: "column", gap: "8px", padding: "8px 0" } }, rows);
1658
- }
1659
-
1660
- function registerSettings(ctx) {
1661
- if (!ctx || !ctx.slots || !feReact) return false;
1662
- try {
1663
- ctx.slots.inject("settings.section", function () {
1664
- return ctx.slots.register({
1665
- name: "settings.section",
1666
- id: "dsh-font-enhancer",
1667
- order: 23,
1668
- label: function () { return "悬浮球导航"; },
1669
- inject: function () { return {}; }
1670
- }, FontSettingsSection);
1671
- });
1672
- return true;
1673
- } catch (e) { return false; }
1674
- }
1675
-
1676
- var inject = ["slots", "locale"];
1677
- var __feSectionRegistered = false;
1678
- // v1.4.3 延迟注册让位:skill-browser 在场则永不注册(设置页不会出现空标题分区);
1679
- // 60s 内确认独立存在才注册完整版分区。两插件装卸都以重启为界,轮询窗口足够。
1680
- function feDelayedRegister(ctx) {
1681
- var tries = 0;
1682
- var t = setInterval(function () {
1683
- tries++;
1684
- try {
1685
- if (__feSectionRegistered) { clearInterval(t); return; }
1686
- if (feSkillBrowserPresent()) {
1687
- clearInterval(t);
1688
- try { console.log("[dsh-font-enhancer] settings section skipped (skill-browser present)"); } catch (e) {}
1689
- return;
1690
- }
1691
- if (tries >= 30) { // 60s 仍无 skill-browser → 独立运行,注册兜底分区
1692
- clearInterval(t);
1693
- if (registerSettings(ctx)) { __feSectionRegistered = true; setTimeout(feApplyRecBadges, 1500); setInterval(feApplyRecBadges, 5000); }
1694
- }
1695
- } catch (e) {}
1696
- }, 2000);
1697
- }
1698
- function apply(ctx) {
1699
- try { feDelayedRegister(ctx); } catch (e) {}
1700
- if (document.readyState === "loading") {
1701
- document.addEventListener("DOMContentLoaded", buildUI);
1702
- } else {
1703
- buildUI();
1704
- }
1705
- feWatchBall();
1706
- feApplyBallVisibility();
1707
- // v1.4.3 推荐位安装状态徽标:对每个推荐插件探测是否已安装,
1708
- // 未安装 → 行内注入「未安装」徽标(已安装则不动,保持干净)。
1709
- setTimeout(function () {
1710
- var markNotInstalled = function (recId) {
1711
- var row = document.querySelector('[data-rec-id="' + recId + '"]');
1712
- if (!row || row.querySelector(".fe-rec-badge")) return;
1713
- var badge = document.createElement("span");
1714
- badge.className = "fe-rec-badge";
1715
- badge.textContent = "未安装";
1716
- badge.style.cssText = "flex-shrink:0;margin-left:8px;font-size:11px;padding:2px 8px;border-radius:6px;background:rgba(255,120,120,.15);border:1px solid rgba(255,120,120,.35);color:#ffb3b3;white-space:nowrap";
1717
- var star = row.querySelector("a");
1718
- if (star) row.insertBefore(badge, star); else row.appendChild(badge);
1719
- };
1720
- // skill-browser:client 运行 = 球元素在 DOM(字体分区出现时通常未装 → 徽标显示)
1721
- setTimeout(function () {
1722
- try { if (!document.getElementById("dsh-skill-browser-ball")) markNotInstalled("skillbrowser"); } catch (e) {}
1723
- }, 1500);
1724
- // lightbox:宿主路由 /api/image-gallery/list 通 = 已安装
1725
- try {
1726
- fetch("/api/image-gallery/list").then(function (r) { if (!r.ok) markNotInstalled("lightbox"); }).catch(function () { markNotInstalled("lightbox"); });
1727
- } catch (e) { markNotInstalled("lightbox"); }
1728
- }, 800);
1729
- }
1730
-
1731
- exports.apply = apply;
1732
- exports.inject = inject;
1733
- return module.exports;
1734
- }
1735
- });
68
+ function clamp(v, lo, hi) {
69
+ v = Number(v);
70
+ if (!isFinite(v)) v = lo;
71
+ return Math.max(lo, Math.min(hi, v));
72
+ }
73
+ function uid() {
74
+ return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
75
+ }
76
+ function pickWeight() {
77
+ return [400, 500, 600, 700, 800][Math.floor(Math.random() * 5)];
78
+ }
79
+ function randFont(list) {
80
+ return list[Math.floor(Math.random() * list.length)][1];
81
+ }
82
+
83
+ // ---------- region model ----------
84
+ function defaultRegion() {
85
+ return {
86
+ id: uid(),
87
+ name: "新区域",
88
+ selector: "body",
89
+ enabled: true,
90
+ en: "Times New Roman",
91
+ zh: "KaiTi",
92
+ size: 15,
93
+ lh: 1.6,
94
+ weight: 400,
95
+ italic: false,
96
+ color: "#FFFFFF",
97
+ useColor: false,
98
+ group: "自定义",
99
+ _open: false
100
+ };
101
+ }
102
+ function normRegion(o) {
103
+ var d = defaultRegion();
104
+ if (!o || typeof o !== "object") return d;
105
+ if (typeof o.id === "string") d.id = o.id;
106
+ d.name = typeof o.name === "string" ? o.name : d.name;
107
+ d.selector = typeof o.selector === "string" ? o.selector : d.selector;
108
+ d.enabled = o.enabled !== false;
109
+ if (typeof o.en === "string") d.en = o.en;
110
+ if (typeof o.zh === "string") d.zh = o.zh;
111
+ d.size = clamp(o.size, 12, 22);
112
+ d.lh = clamp(o.lh, 1.3, 2.2);
113
+ d.weight = clamp(o.weight, 100, 900);
114
+ d.italic = o.italic === true;
115
+ if (typeof o.color === "string") d.color = o.color;
116
+ d.useColor = o.useColor === true;
117
+ if (typeof o.group === "string") d.group = o.group;
118
+ return d;
119
+ }
120
+ function DEFAULT_REGIONS() {
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 定位。
125
+ function mk(o) { var d = defaultRegion(); return Object.assign(d, o); }
126
+ return [
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: "对话区" }),
133
+ // 输入区(品红)
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: "左侧栏" })
145
+ ];
146
+ }
147
+ function normThemes(o) {
148
+ if (!o || !Array.isArray(o.themes)) return [];
149
+ return o.themes.filter(function (t) {
150
+ return t && typeof t.name === "string" && t.recipe && typeof t.recipe === "object";
151
+ });
152
+ }
153
+ function normUnified(o) {
154
+ var u = { uc: { enabled: false, color: "#FFFFFF" }, uf: { enabled: false, en: "Times New Roman", zh: "KaiTi", size: 15, weight: 400, italic: false } };
155
+ if (!o || !o.unified || typeof o.unified !== "object") return u;
156
+ var s = o.unified;
157
+ if (s.uc && typeof s.uc === "object") {
158
+ u.uc.enabled = s.uc.enabled === true;
159
+ if (typeof s.uc.color === "string") u.uc.color = s.uc.color;
160
+ }
161
+ if (s.uf && typeof s.uf === "object") {
162
+ u.uf.enabled = s.uf.enabled === true;
163
+ if (typeof s.uf.en === "string") u.uf.en = s.uf.en;
164
+ if (typeof s.uf.zh === "string") u.uf.zh = s.uf.zh;
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;
168
+ }
169
+ return u;
170
+ }
171
+ function loadSettings() {
172
+ try {
173
+ var raw = localStorage.getItem(STORE_KEY);
174
+ if (!raw) return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), customFonts: [], bx: null, by: null };
175
+ var o = JSON.parse(raw);
176
+ var themes = normThemes(o);
177
+ var unified = normUnified(o);
178
+ var customFonts = Array.isArray(o.customFonts) ? o.customFonts : [];
179
+ if (o && (typeof o.ver !== "number" || o.ver < SCHEMA_VERSION)) {
180
+ // Version bump: write back fresh region palette but KEEP themes &
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 };
183
+ if (typeof o.bx === "number") fresh.bx = o.bx;
184
+ if (typeof o.by === "number") fresh.by = o.by;
185
+ localStorage.setItem(STORE_KEY, JSON.stringify(fresh));
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 };
187
+ }
188
+ if (o && Array.isArray(o.regions)) {
189
+ var loaded = o.regions.map(normRegion).filter(function (x) { return x.group !== "已识别"; });
190
+ var hasPicked = loaded.some(function (x) { return x.group === "框选"; });
191
+ var regions = hasPicked
192
+ // Once the user has box-picked regions, those SUPERSEDE the built-in
193
+ // preset regions — the panel shows only what the user picked/owns.
194
+ ? loaded.filter(function (x) { return x.group === "框选"; })
195
+ : loaded;
196
+ return {
197
+ regions: regions,
198
+ themes: themes,
199
+ unified: unified,
200
+ customFonts: customFonts,
201
+ bx: typeof o.bx === "number" ? o.bx : null,
202
+ by: typeof o.by === "number" ? o.by : null
203
+ };
204
+ }
205
+ var g = defaultRegion();
206
+ g.selector = "body";
207
+ g.name = "全局界面(整站兜底)";
208
+ g.enabled = true;
209
+ g._open = true;
210
+ if (typeof o.en === "string") g.en = o.en;
211
+ if (typeof o.zh === "string") g.zh = o.zh;
212
+ g.size = clamp(o.size, 12, 22);
213
+ g.lh = clamp(o.lh, 1.3, 2.2);
214
+ g.weight = clamp(o.weight, 100, 900);
215
+ g.italic = o.italic === true;
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 };
217
+ } catch (e) {
218
+ return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), customFonts: [], bx: null, by: null };
219
+ }
220
+ }
221
+ function saveSettings(data) {
222
+ try {
223
+ data.ver = SCHEMA_VERSION;
224
+ localStorage.setItem(STORE_KEY, JSON.stringify(data));
225
+ } catch (e) {}
226
+ }
227
+
228
+ // ---------- CSS generation ----------
229
+ // No blanket descendant diffusion. Every region is scoped to its own
230
+ // text-bearing nodes only (the element itself + its direct inline/block
231
+ // text wrappers). This is what stops one region's rule from painting the
232
+ // children that belong to a SIBLING region (the previous withDesc() made
233
+ // every region apply "sel *" over all descendants, so body/sidebarCol
234
+ // recoloured every nested label and the regions bled into each other).
235
+ function withText(sel) {
236
+ return sel
237
+ .split(",")
238
+ .map(function (s) {
239
+ s = s.trim();
240
+ // body * 会匹配所有元素(包括输入框),用 !important 覆盖字号可能
241
+ // 破坏交互区域布局。body 只染 body 本身和 SVG text 元素就够了。
242
+ if (s === "body") return "body, body text";
243
+ return s + ", " + s + " *, " + s + " text";
244
+ })
245
+ .join(", ");
246
+ }
247
+ function buildRegionCss(r) {
248
+ var sel = (r.selector || "").trim();
249
+ if (!sel) return "";
250
+ var fam = '"' + r.en + '","' + r.zh + '",sans-serif';
251
+ var style = r.italic ? "italic" : "normal";
252
+ var scope = withText(sel);
253
+ var lines = [
254
+ scope + " {",
255
+ " font-family: " + fam + " !important;",
256
+ " font-weight: " + r.weight + " !important;",
257
+ " font-style: " + style + " !important;"
258
+ ];
259
+ if (r.useColor && r.color) lines.push(" color: " + r.color + " !important;");
260
+ lines.push(" font-size: " + r.size + "px !important;");
261
+ lines.push(" line-height: " + r.lh + " !important;");
262
+ lines.push("}");
263
+ return lines.join("\n");
264
+ }
265
+ function applyAll(regions) {
266
+ var u = state.unified || { uc: { enabled: false }, uf: { enabled: false } };
267
+ var css = regions
268
+ .filter(function (r) {
269
+ return r.enabled;
270
+ })
271
+ .map(function (r) {
272
+ // apply unified overrides without mutating the region objects
273
+ var tmp = {
274
+ selector: r.selector,
275
+ en: u.uf.enabled ? u.uf.en : r.en,
276
+ zh: u.uf.enabled ? u.uf.zh : r.zh,
277
+ size: u.uf.enabled ? u.uf.size : r.size,
278
+ weight: u.uf.enabled ? u.uf.weight : r.weight,
279
+ italic: u.uf.enabled ? u.uf.italic : r.italic,
280
+ lh: r.lh,
281
+ useColor: u.uc.enabled || r.useColor,
282
+ color: u.uc.enabled ? u.uc.color : r.color
283
+ };
284
+ return buildRegionCss(tmp);
285
+ })
286
+ .join("\n\n");
287
+ var tag = document.getElementById("dsh-font-enhancer-style");
288
+ if (!tag) {
289
+ tag = document.createElement("style");
290
+ tag.id = "dsh-font-enhancer-style";
291
+ tag.dataset.plugin = "dsh-font-enhancer";
292
+ document.head.appendChild(tag);
293
+ }
294
+ tag.textContent = css;
295
+ }
296
+
297
+ function optionsHtml(list, selected) {
298
+ return list
299
+ .map(function (f) {
300
+ var sel = f[1] === selected ? " selected" : "";
301
+ return '<option value="' + esc(f[1]) + '"' + sel + ">" + esc(f[0]) + "</option>";
302
+ })
303
+ .join("");
304
+ }
305
+
306
+ // ---------- panel styles — premium dark design-tool aesthetic ----------
307
+ function panelCss() {
308
+ return [
309
+ "#dsh-fe-root *{box-sizing:border-box;scrollbar-width:thin}",
310
+ "",
311
+ "/* ── toggle button — gem-like floating orb (max-visible) ── */",
312
+ "#dsh-fe-root{position:static !important;z-index:2147483647;pointer-events:none}",
313
+ "#dsh-fe-toggle{position:fixed;left:auto;top:auto;right:18px;bottom:18px;z-index:2147483647 !important;width:54px !important;height:54px !important;border-radius:50% !important;border:0 !important;background:radial-gradient(circle at 30% 25%,rgba(255,255,255,.15),rgba(40,44,54,.95) 70%) !important;color:#eef0f4 !important;font-size:22px !important;font-weight:700 !important;cursor:grab !important;user-select:none !important;touch-action:none !important;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15) !important;line-height:normal !important;text-align:center !important;display:block !important;visibility:visible !important;opacity:1 !important;transform:none !important;pointer-events:auto !important}",
314
+ "#dsh-fe-toggle:hover{box-shadow:0 6px 32px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.2) !important;transform:scale(1.06) !important}",
315
+ "#dsh-fe-toggle.dsh-fe-dragging{cursor:grabbing !important;transform:scale(1.12) !important;box-shadow:0 8px 40px rgba(0,0,0,.8) !important}",
316
+ /* ── v1.4.10 二版暖色波澜球(白核+橙粉光带+conic环+呼吸;参照 orb 效果)── */
317
+ '#dsh-fe-root #dsh-fe-toggle{width:46px !important;height:46px !important;border-radius:50% !important;background:radial-gradient(circle at 50% 60%, rgba(255,255,255,.98) 0%, rgba(255,210,150,.92) 9%, rgba(255,130,60,.75) 18%, rgba(255,70,130,.55) 32%, rgba(200,40,120,.4) 46%, rgba(30,10,20,.9) 66%, rgba(14,4,10,.99) 100%) !important;box-shadow:0 0 26px rgba(255,140,70,.55),0 0 4px rgba(255,90,160,.3),0 4px 18px rgba(0,0,0,.55),inset 0 1px 3px rgba(255,255,255,.4),inset 0 -8px 16px rgba(255,90,160,.14),inset 0 0 14px rgba(255,120,80,.2) !important;font-size:15px !important;font-weight:800 !important;color:#ffffff !important;display:flex !important;align-items:center !important;justify-content:center !important;animation:feOrbBreathe 3.2s ease-in-out infinite !important}',
318
+ '#dsh-fe-root #dsh-fe-toggle::before{content:"";position:absolute;inset:1px;border-radius:50%;z-index:1;background:radial-gradient(ellipse 85% 26% at 50% 60%, rgba(255,255,255,1) 0%, rgba(255,230,190,.88) 18%, rgba(255,170,110,.55) 38%, rgba(255,90,140,.4) 55%, transparent 75%),radial-gradient(ellipse 70% 18% at 55% 42%, rgba(255,150,80,.55) 0%, transparent 70%),radial-gradient(ellipse 70% 18% at 45% 74%, rgba(190,60,225,.4) 0%, transparent 70%);filter:blur(1px);animation:bandFlow 4.5s ease-in-out infinite !important}',
319
+ '#dsh-fe-root #dsh-fe-toggle::after{content:"";position:absolute;inset:-1px;border-radius:50%;z-index:1;background:conic-gradient(from 200deg, rgba(255,255,255,.55), rgba(255,170,110,.4), rgba(255,90,160,.4), rgba(255,255,255,.15), rgba(255,140,66,.4), rgba(255,255,255,.55));-webkit-mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);animation:rimSpin 7s linear infinite;filter:blur(.5px)}',
320
+ '@keyframes bandFlow{0%,100%{transform:translateY(-3px) scaleY(.94);opacity:.8}50%{transform:translateY(3px) scaleY(1.12);opacity:1}}',
321
+ '@keyframes rimSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}',
322
+ '@keyframes feOrbBreathe{0%,100%{transform:scale(.96);filter:brightness(.92)}50%{transform:scale(1.05);filter:brightness(1.12)}}',
323
+ '@keyframes feGlow{0%,100%{text-shadow:0 0 4px rgba(255,255,255,.95),0 0 10px rgba(255,200,120,.9),0 0 20px rgba(255,120,80,.7)}50%{text-shadow:0 0 6px rgba(255,255,255,1),0 0 16px rgba(255,220,150,1),0 0 32px rgba(255,140,90,.9)}}',
324
+ '#dsh-fe-root #dsh-fe-toggle:hover{transform:scale(1.08) !important}',
325
+ "/* ── panel root — deep glass studio (position set by JS positionPanel) ── */",
326
+ "#dsh-fe-panel{position:fixed !important;z-index:2147483647 !important;width:348px !important;max-width:calc(100vw - 36px) !important;max-height:86vh;overflow-y:auto !important;overflow-x:hidden !important;color:#e6e6e6 !important;border-radius:20px !important;padding:20px !important;font:13px/1.5 KaiTi,'Microsoft YaHei',sans-serif !important;background:rgba(16,18,22,.92) !important;border:1px solid rgba(255,255,255,.08) !important;box-shadow:0 20px 60px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.06) !important;backdrop-filter:blur(24px) !important;-webkit-backdrop-filter:blur(24px) !important;visibility:visible !important;pointer-events:auto !important}",
327
+ "#dsh-fe-panel::-webkit-scrollbar{width:3px}",
328
+ "#dsh-fe-panel::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:3px}",
329
+ "#dsh-fe-panel::-webkit-scrollbar-track{background:transparent}",
330
+ "#dsh-fe-root,#dsh-fe-root *{font-family:KaiTi,'Microsoft YaHei',sans-serif!important;font-size:13px!important;line-height:1.5!important}",
331
+ "",
332
+ "/* ── panel header ── */",
333
+ "#dsh-fe-panel .dsh-fe-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid rgba(255,255,255,.06)}",
334
+ "#dsh-fe-panel .dsh-fe-header .dsh-fe-hl{display:flex;flex-direction:column;gap:2px}",
335
+ "#dsh-fe-panel .dsh-fe-header h4{margin:0;font-size:16px;font-weight:700;letter-spacing:0;color:#f0f2f5;line-height:1.2}",
336
+ "#dsh-fe-panel .dsh-fe-header .dsh-fe-sub{font-size:11px;color:#6b7280;font-weight:400;letter-spacing:.01em}",
337
+ "#dsh-fe-close{border:0;background:rgba(255,255,255,.06);color:#8b93a1;border-radius:8px;padding:5px 11px;font-size:14px;cursor:pointer;line-height:1.3;transition:all .15s;flex-shrink:0;margin-top:2px}",
338
+ "#dsh-fe-close:hover{background:rgba(255,255,255,.13);color:#e6e6e6}",
339
+ "",
340
+ "/* ── theme bar ── */",
341
+ "#dsh-fe-themeblock{display:flex;gap:5px;margin-bottom:14px;padding:10px 12px;background:linear-gradient(135deg,rgba(255,255,255,.04),rgba(255,255,255,.01));border-radius:12px;align-items:center;border:1px solid rgba(255,255,255,.05)}",
342
+ "#dsh-fe-themeblock select{flex:1;min-width:0;padding:5px 9px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg width='8' height='6' viewBox='0 0 8 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L4 4.5L7 1.5' stroke='%238b93a1' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 8px center;padding-right:28px;cursor:pointer;transition:border-color .15s}",
343
+ "#dsh-fe-themeblock select:focus{border-color:rgba(106,166,255,.5);outline:0}",
344
+ "#dsh-fe-themeblock button{padding:5px 9px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#cfd4dc;cursor:pointer;font:inherit;font-size:12px;white-space:nowrap;transition:all .15s;flex-shrink:0}",
345
+ "#dsh-fe-themeblock button:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.2)}",
346
+ "",
347
+ "/* ── toolbar ── */",
348
+ ".dsh-fe-toolbar{display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap}",
349
+ "#dsh-fe-toolbar .dsh-fe-toolbar2{margin-top:8px}",
350
+ ".dsh-fe-toolbar2{display:flex;gap:5px;margin:6px 0 14px;flex-wrap:wrap}",
351
+ "#dsh-fe-toolbar button{flex:1;padding:8px 0;border-radius:10px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.04);color:#cfd4dc;cursor:pointer;font:inherit;font-size:12px;font-weight:500;transition:all .18s;letter-spacing:.01em}",
352
+ "#dsh-fe-toolbar button:hover{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.2);transform:translateY(-1px)}",
353
+ "#dsh-fe-toolbar button:active{transform:translateY(0)}",
354
+ "#dsh-fe-toolbar .dsh-fe-btn-accent{border-color:rgba(106,166,255,.3);background:linear-gradient(135deg,rgba(106,166,255,.12),rgba(106,166,255,.06));color:#8ab8ff;text-shadow:0 0 12px rgba(106,166,255,.2)}",
355
+ "#dsh-fe-toolbar .dsh-fe-btn-accent:hover{background:linear-gradient(135deg,rgba(106,166,255,.2),rgba(106,166,255,.1));border-color:rgba(106,166,255,.5);box-shadow:0 0 20px rgba(106,166,255,.08)}",
356
+ "",
357
+ "/* ── unified controls ── */",
358
+ "#dsh-fe-unified{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:10px 12px;margin-bottom:12px}",
359
+ "#dsh-fe-unified .dsh-fe-u-row{display:flex;align-items:center;gap:6px;margin-bottom:6px;flex-wrap:wrap}",
360
+ "#dsh-fe-unified .dsh-fe-u-row:last-child{margin-bottom:0}",
361
+ "#dsh-fe-unified label{font-size:11px;color:#8b93a1;display:flex;align-items:center;gap:4px;cursor:pointer;font-weight:500}",
362
+ "#dsh-fe-unified label input[type=checkbox]{accent-color:#6aa6ff;width:14px;height:14px;margin:0}",
363
+ "#dsh-fe-unified select,#dsh-fe-unified input[type=color],#dsh-fe-unified input[type=number]{padding:3px 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;font-family:KaiTi,'Microsoft YaHei',sans-serif!important;transition:border-color .15s}",
364
+ "#dsh-fe-unified select:focus,#dsh-fe-unified input:focus{border-color:rgba(106,166,255,.45);outline:0}",
365
+ "#dsh-fe-unified input[type=color]{width:28px;height:22px;padding:0;cursor:pointer}",
366
+ "#dsh-fe-unified input[type=number]{width:48px}",
367
+ "#dsh-fe-unified .dsh-fe-u-label{font-size:10px;color:#6b7280;font-weight:600;letter-spacing:.03em;text-transform:uppercase;margin-right:2px;flex-basis:100%}",
368
+ "",
369
+ "/* ── card ── */",
370
+ "#dsh-fe-card{border:1px solid rgba(255,255,255,.05);border-radius:12px;padding:12px 14px;margin-bottom:8px;background:linear-gradient(135deg,rgba(255,255,255,.035),rgba(255,255,255,.015));transition:all .2s}",
371
+ "#dsh-fe-card:hover{border-color:rgba(255,255,255,.12);background:linear-gradient(135deg,rgba(255,255,255,.055),rgba(255,255,255,.025));box-shadow:0 2px 12px rgba(0,0,0,.15)}",
372
+ "#dsh-fe-card-head{display:flex;align-items:center;gap:8px;justify-content:space-between}",
373
+ "#dsh-fe-head-left{display:flex;align-items:center;gap:6px;flex:1;min-width:0}",
374
+ "#dsh-fe-head-right{display:flex;align-items:center;gap:4px;flex-shrink:0}",
375
+ "#dsh-fe-card-head .dsh-fe-cb{display:flex;align-items:center;cursor:pointer;flex-shrink:0;width:18px;height:18px;justify-content:center}",
376
+ "#dsh-fe-card-head .dsh-fe-cb input{accent-color:#6aa6ff;width:15px;height:15px;cursor:pointer;margin:0;transition:transform .15s}",
377
+ "#dsh-fe-card-head .dsh-fe-cb input:checked{transform:scale(1.18)}",
378
+ "#dsh-fe-card-head input[type=text]{flex:1;min-width:0;padding:4px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.08);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;font-weight:500;transition:all .15s}",
379
+ "#dsh-fe-card-head input[type=text]:focus{border-color:rgba(106,166,255,.45);background:rgba(0,0,0,.45);outline:0;box-shadow:0 0 0 3px rgba(106,166,255,.08)}",
380
+ "#dsh-fe-card-head button{border:0;background:rgba(255,255,255,.05);color:#8b93a1;border-radius:6px;padding:3px 7px;cursor:pointer;font-size:11px;transition:all .15s;flex-shrink:0;font-weight:500}",
381
+ "#dsh-fe-card-head button:hover{background:rgba(255,255,255,.13);color:#e6e6e6}",
382
+ "#dsh-fe-card-head .dsh-fe-del{color:#8b6c6c;font-size:13px;padding:4px 7px}",
383
+ "#dsh-fe-card-head .dsh-fe-del:hover{color:#d47a7a;background:rgba(200,80,80,.15)}",
384
+ "#dsh-fe-enb{display:flex;align-items:center;gap:4px;color:#8b93a1;font-size:11px;white-space:nowrap;cursor:pointer;padding:3px 7px;border-radius:6px;transition:all .15s;font-weight:500}",
385
+ "#dsh-fe-enb:hover{background:rgba(255,255,255,.06);color:#b9bfc9}",
386
+ "#dsh-fe-enb input{accent-color:#6aa6ff;width:13px;height:13px;margin:0}",
387
+ "#dsh-fe-dot{width:18px;height:18px;border-radius:50%;flex:none;border:2px solid rgba(255,255,255,.2);background:#888;transition:all .25s;box-shadow:0 0 8px rgba(0,0,0,.2)}",
388
+ "#dsh-fe-card:hover #dsh-fe-dot{border-color:rgba(255,255,255,.35);transform:scale(1.05)}",
389
+ "",
390
+ "/* ── card body ── */",
391
+ "#dsh-fe-card-body{display:flex;gap:12px;border-top:1px solid rgba(255,255,255,.05);margin-top:9px;padding-top:9px;animation:dsh-fe-fadeIn .18s ease}",
392
+ "@keyframes dsh-fe-fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}",
393
+ "#dsh-fe-body-col{flex:1;min-width:0}",
394
+ "#dsh-fe-body-col label{display:block;margin:6px 0 3px;color:#8b93a1;font-size:10px;font-weight:600;letter-spacing:.03em;text-transform:uppercase}",
395
+ "#dsh-fe-body-col select,#dsh-fe-body-col input[type=text],#dsh-fe-body-col input[type=number]{width:100%;padding:4px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.08);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;transition:all .15s}",
396
+ "#dsh-fe-body-col select:focus,#dsh-fe-body-col input:focus{border-color:rgba(106,166,255,.45);outline:0;box-shadow:0 0 0 3px rgba(106,166,255,.08)}",
397
+ "#dsh-fe-row{display:flex;gap:6px}",
398
+ "#dsh-fe-row>div{flex:1;min-width:0}",
399
+ "#dsh-fe-weightrow{margin-top:5px}",
400
+ "#dsh-fe-weightrow label{display:flex;justify-content:space-between;align-items:center;margin:0}",
401
+ "#dsh-fe-weight-val{color:#8b93a1;font-weight:500}",
402
+ "#dsh-fe-panel input[type=range]{width:100%;margin-top:5px;accent-color:#6aa6ff;height:3px;border-radius:2px;cursor:pointer}",
403
+ "#dsh-fe-italicrow,#dsh-fe-colorrow{display:flex;align-items:center;gap:6px;margin-top:6px;color:#8b93a1;font-size:12px;cursor:pointer;padding:3px 0;border-radius:6px;transition:all .15s}",
404
+ "#dsh-fe-italicrow:hover,#dsh-fe-colorrow:hover{color:#b9bfc9}",
405
+ "#dsh-fe-italicrow input,#dsh-fe-colorrow input{accent-color:#6aa6ff;width:14px;height:14px;margin:0}",
406
+ "#dsh-fe-colorrow input[type=color]{width:38px;height:28px;padding:0;border:1px solid rgba(255,255,255,.12);border-radius:8px;background:#1b1d22;cursor:pointer;transition:border-color .15s}",
407
+ "#dsh-fe-colorrow input[type=color]:hover{border-color:rgba(255,255,255,.25)}",
408
+ "",
409
+ "/* ── hint ── */",
410
+ "#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)}",
411
+ "#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}",
412
+ "#dsh-fe-customfont .dsh-fe-cf-label{font-size:10px;color:#6b7280;font-weight:600;letter-spacing:.03em;text-transform:uppercase;margin-bottom:8px}",
413
+ "#dsh-fe-customfont .dsh-fe-cf-list{display:flex;flex-direction:column;gap:4px}",
414
+ "#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)}",
415
+ "#dsh-fe-customfont .dsh-fe-cf-name{flex:1;min-width:0;font-size:11px;color:#cfd4dc}",
416
+ "#dsh-fe-customfont .dsh-fe-cf-link{font-size:10px;color:#6aa6ff;text-decoration:none;flex-shrink:0}",
417
+ "#dsh-fe-customfont .dsh-fe-cf-link:hover{text-decoration:underline}",
418
+ "#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}",
419
+ "#dsh-fe-customfont .dsh-fe-cf-item button:hover{background:rgba(255,255,255,.12)}",
420
+ "#dsh-fe-customfont .dsh-fe-cf-tip{font-size:10px;color:#6b7280;margin-top:6px;line-height:1.5}",
421
+ "#dsh-fe-customfont .dsh-fe-cf-file{margin-top:10px;padding-top:10px;border-top:1px dashed rgba(255,255,255,.08)}",
422
+ "#dsh-fe-customfont .dsh-fe-cf-file .dsh-fe-cf-label{margin-bottom:6px}",
423
+ "#dsh-fe-customfont .dsh-fe-cf-file-row{display:flex;gap:6px;align-items:center}",
424
+ "#dsh-fe-customfont .dsh-fe-cf-file-row input[type=file]{flex:1;min-width:0;font-size:11px;color:#cfd4dc}",
425
+ "#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}",
426
+ "#dsh-fe-customfont .dsh-fe-cf-file-row button:hover{background:rgba(255,255,255,.12)}",
427
+ "#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}",
428
+ "#dsh-fe-guide h4{margin:0 0 8px;font-size:14px;color:#f0f2f5}",
429
+ "#dsh-fe-guide p{margin:0 0 10px}",
430
+ "#dsh-fe-guide b{color:#8ab8ff}",
431
+ "",
432
+ "/* ── toast ── */",
433
+ "#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}",
434
+ "#dsh-fe-toast.dsh-fe-toast-ok{color:#7bc97b;border-color:rgba(123,201,123,.3)}",
435
+ "#dsh-fe-toast.dsh-fe-toast-err{color:#e08080;border-color:rgba(224,128,128,.3)}",
436
+ "#dsh-fe-toast.dsh-fe-toast-show{opacity:1}"
437
+ ].join("\n");
438
+ }
439
+
440
+ // ---------- state ----------
441
+ var state = loadSettings();
442
+ state.customFonts = state.customFonts || [];
443
+ var regions = state.regions;
444
+ var root = null;
445
+ var panel = null;
446
+ var cardsEl = null;
447
+
448
+ // ── 采集定位区域(录制模式)──────────────────────────────
449
+ // 用户点「🔴 采集区域」按钮开/关。开启期间,每次框选/加元素都会把被选中元素的
450
+ // 「稳定定位信息」收集并写入 ~/.dsh/data/dsh-font-enhancer/operation.log,
451
+ // 便于排查:恢复哪个稳定选择器能在重启后仍准确定位到该元素。
452
+ var RECORDING = false;
453
+ var recSamples = [];
454
+ function recToggle(on) {
455
+ RECORDING = !!on;
456
+ recSamples = [];
457
+ try { localStorage.setItem("dsh-fe-rec", RECORDING ? "1" : "0"); } catch (e) {}
458
+ // 同步按钮文案
459
+ if (root) {
460
+ var b = root.querySelector('[data-act="recordToggle"]');
461
+ if (b) { b.textContent = RECORDING ? "⏹ 停止采集" : "🔴 采集区域"; b.classList.toggle("dsh-fe-rec-on", RECORDING); }
462
+ }
463
+ showToast(RECORDING ? "采集模式已开启:现在框选/加元素会记录元素定位到文件" : "采集模式已关闭", RECORDING ? "ok" : "");
464
+ }
465
+ // 元素最简稳定选择器(优先语义 data-*、id、class,最后 tag)
466
+ function stableSelFor(node) {
467
+ if (!node || node.nodeType !== 1) return null;
468
+ if (node.id) return "#" + node.id;
469
+ var cn = node.getAttribute ? (node.getAttribute("class") || "") : "";
470
+ if (cn && cn.trim()) return "." + cn.trim().split(/\s+/)[0].replace(/[:>,\s]/g, "\\$&");
471
+ if (node.getAttribute) {
472
+ // 语义 data-* 钩子
473
+ var attrs = node.attributes;
474
+ for (var i = 0; i < attrs.length; i++) {
475
+ var n = attrs[i].nodeName;
476
+ if (n.indexOf("data-") === 0) return "[" + n + (attrs[i].nodeValue ? '="' + attrs[i].nodeValue + '"' : "") + "]";
477
+ }
478
+ }
479
+ return node.tagName ? node.tagName.toLowerCase() : null;
480
+ }
481
+ // 元素的「祖先链稳定路径」(向上最多 6 层,每层用最简稳定选择器)
482
+ function stablePathFor(el) {
483
+ var parts = [], p = el;
484
+ while (p && p !== document.body && parts.length < 6) {
485
+ var s = stableSelFor(p);
486
+ if (s) parts.unshift(s);
487
+ p = p.parentElement;
488
+ }
489
+ return parts.join(" > ") || (el.tagName ? el.tagName.toLowerCase() : "?");
490
+ }
491
+ // 把本次采集到的元素定位信息写入日志文件
492
+ function flushRecord(kind, name, selector) {
493
+ if (!RECORDING || !recSamples.length) return;
494
+ var payload = {
495
+ kind: kind || "REC_PICK",
496
+ t: new Date().toISOString(),
497
+ regionName: name || "",
498
+ regionSelector: selector || "",
499
+ samples: recSamples.slice()
500
+ };
501
+ try { fetch("/dsh-font-enhancer/log", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).catch(function(){}); } catch (e) {}
502
+ recSamples = [];
503
+ }
504
+
505
+ function findRegion(id) {
506
+ for (var i = 0; i < regions.length; i++) if (regions[i].id === id) return regions[i];
507
+ return null;
508
+ }
509
+ function getThemes() {
510
+ return Array.isArray(state.themes) ? state.themes : [];
511
+ }
512
+ function persist() {
513
+ saveSettings({ regions: regions, themes: getThemes(), unified: state.unified, customFonts: state.customFonts || [], bx: state.bx, by: state.by });
514
+ }
515
+ // Return merged font lists: built-ins + user custom fonts (deduped).
516
+ function mergedEnFonts() {
517
+ var list = EN_FONTS.slice();
518
+ (state.customFonts || []).forEach(function (f) {
519
+ if (f && f.en && !list.some(function (x) { return x[1] === f.en; })) list.push([f.en, f.en]);
520
+ });
521
+ return list;
522
+ }
523
+ function mergedZhFonts() {
524
+ var list = ZH_FONTS.slice();
525
+ (state.customFonts || []).forEach(function (f) {
526
+ if (f && f.zh && !list.some(function (x) { return x[1] === f.zh; })) list.push([f.zh, f.zh]);
527
+ });
528
+ return list;
529
+ }
530
+
531
+ // ── Point-and-pick custom region: hover to preview, click to select, name ─
532
+ // User picks the exact UI text/elements they want as one region, then names
533
+ // it. Much finer-grained than auto-discovery and never floods the panel.
534
+ var __pick = null;
535
+ function pickRegion(targetRid) {
536
+ var target = (typeof targetRid === "string" && targetRid) ? findRegion(targetRid) : null;
537
+ if (__pick && __pick.active) { pickRegionExit(true); return; }
538
+ var toggle = document.getElementById("dsh-fe-toggle");
539
+ var pickbar = document.createElement("div");
540
+ pickbar.id = "dsh-fe-pickbar";
541
+ pickbar.style.cssText = "position:fixed;z-index:2147483647;left:50%;top:64px;display:flex;gap:8px;align-items:center;padding:10px 14px;background:rgba(18,20,26,.97);border:1px solid rgba(255,255,255,.16);border-radius:14px;box-shadow:0 12px 44px rgba(0,0,0,.7);color:#e6e6e6;font:13px/1.5 KaiTi,'Microsoft YaHei',sans-serif;pointer-events:auto;cursor:move";
542
+ var modeTip = target ? ('追加到区域「' + target.name + '」') : '拖动 ⠿ 移动工具条;Ctrl+点击多选';
543
+ var pickNamePh = target ? (target.name + '' ) : "给这个区域命名…";
544
+ pickbar.innerHTML =
545
+ '<span id="dsh-fe-pick-handle" title="拖动移动工具条" style="cursor:move;padding:0 8px 0 2px;color:#8b93a1;font-size:14px;user-select:none;-webkit-user-select:none">⠿</span>' +
546
+ '<span id="dsh-fe-pick-count">已选 <b>0</b> 个元素</span>' +
547
+ '<input id="dsh-fe-pick-name" placeholder="' + pickNamePh + '" style="width:180px;padding:6px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.2);background:rgba(0,0,0,.45);color:#fff;font:inherit;font-size:13px">' +
548
+ '<button id="dsh-fe-pick-done" type="button" style="padding:6px 16px;border-radius:8px;border:1px solid rgba(106,166,255,.45);background:rgba(106,166,255,.25);color:#a8ccff;cursor:pointer;font:inherit;font-weight:600">✔ 完成</button>' +
549
+ '<button id="dsh-fe-pick-cancel" type="button" style="padding:6px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);color:#9aa;cursor:pointer;font:inherit">✕ 取消</button>' +
550
+ '<span style="font-size:11px;color:#8b93a1">' + modeTip + '</span>';
551
+ // center it initially (we dropped the translateX trick so dragging works)
552
+ pickbar.style.left = Math.max(8, Math.round((window.innerWidth - 420) / 2)) + "px";
553
+ var mount = document.documentElement || document.body;
554
+ mount.appendChild(pickbar);
555
+ // drag handle → move the whole pickbar without triggering element pick
556
+ var handle = document.getElementById("dsh-fe-pick-handle");
557
+ (function () {
558
+ var drag = false, sx = 0, sy = 0, ox = 0, oy = 0;
559
+ if (!handle) return;
560
+ handle.addEventListener("pointerdown", function (e) {
561
+ drag = true; sx = e.clientX; sy = e.clientY;
562
+ ox = pickbar.offsetLeft; oy = pickbar.offsetTop;
563
+ try { handle.setPointerCapture(e.pointerId); } catch (err) {}
564
+ e.preventDefault(); e.stopPropagation();
565
+ });
566
+ handle.addEventListener("pointermove", function (e) {
567
+ if (!drag) return;
568
+ var nx = Math.max(4, Math.min(window.innerWidth - pickbar.offsetWidth - 4, ox + (e.clientX - sx)));
569
+ var ny = Math.max(4, Math.min(window.innerHeight - pickbar.offsetHeight - 4, oy + (e.clientY - sy)));
570
+ pickbar.style.left = nx + "px"; pickbar.style.top = ny + "px";
571
+ });
572
+ function end() { drag = false; }
573
+ handle.addEventListener("pointerup", end);
574
+ handle.addEventListener("pointercancel", end);
575
+ })();
576
+
577
+ var picked = [];
578
+ var hoverEl = null;
579
+ function hilite(el) {
580
+ if (hoverEl && hoverEl !== el) {
581
+ hoverEl.style.outline = "";
582
+ hoverEl.style.outlineOffset = "";
583
+ }
584
+ if (el) { el.style.outline = "2px dashed #6aa6ff"; el.style.outlineOffset = "-1px"; }
585
+ hoverEl = el;
586
+ }
587
+ function onMove(e) {
588
+ var el = e.target;
589
+ if (!el || el === document.documentElement || el === document.body) return;
590
+ if (el.closest && el.closest("#dsh-fe-root, #dsh-fe-pickbar")) { hilite(null); return; }
591
+ if (el.nodeType === 1) hilite(el);
592
+ }
593
+ function onPick(e) {
594
+ var el = e.target;
595
+ if (!el || el.nodeType !== 1) return;
596
+ if (el.closest && el.closest("#dsh-fe-root, #dsh-fe-pickbar")) return;
597
+ e.preventDefault(); e.stopPropagation();
598
+ var multi = e.ctrlKey || e.metaKey || e.shiftKey;
599
+ var idx = picked.indexOf(el);
600
+ if (idx >= 0) {
601
+ // only un-pick on a plain click; Ctrl/Shift always keeps it selected
602
+ if (!multi) { picked.splice(idx, 1); el.style.outline = ""; }
603
+ } else {
604
+ picked.push(el);
605
+ // 采集模式:记录被框选元素的稳定定位路径
606
+ if (RECORDING) recSamples.push({ path: stablePathFor(el), tag: el.tagName, uid: "…下次分配" });
607
+ el.style.outline = multi ? "2px solid #ff8c5a" : "2px dotted #ffd27a";
608
+ el.style.outlineOffset = "-1px";
609
+ }
610
+ var c = document.getElementById("dsh-fe-pick-count");
611
+ if (c) c.innerHTML = "已选 <b>" + picked.length + "</b> 个元素";
612
+ }
613
+ function finish() {
614
+ var nameEl = document.getElementById("dsh-fe-pick-name");
615
+ var name = (nameEl && nameEl.value.trim()) || ("框选区域 " + (regions.length + 1));
616
+ if (!picked.length) { showToast("未选择任何元素", "err"); return; }
617
+ var marks = [];
618
+ picked.forEach(function (el) {
619
+ var m = uid();
620
+ el.setAttribute("data-fe-reg", m);
621
+ marks.push('[data-fe-reg="' + m + '"]');
622
+ el.style.outline = ""; el.style.outlineOffset = "";
623
+ });
624
+ var target = targetRid ? findRegion(targetRid) : null;
625
+ if (target) {
626
+ // Append the newly picked elements into an existing region instead of
627
+ // creating a new one ("关联框选" from that region's card).
628
+ var existing = (target.selector || "").split(",").map(function (s) { return s.trim(); }).filter(Boolean);
629
+ marks.forEach(function (m) { if (existing.indexOf(m) < 0) existing.push(m); });
630
+ target.selector = existing.join(", ");
631
+ target.enabled = true;
632
+ renderCards();
633
+ applyAll(regions);
634
+ persist();
635
+ pickRegionExit(true);
636
+ flushRecord("REC_ADD", target.name, target.selector);
637
+ showToast("已向区域「" + target.name + "」追加 " + picked.length + " 个元素", "ok");
638
+ return;
639
+ }
640
+ var nr = defaultRegion();
641
+ nr.id = uid();
642
+ nr.name = name;
643
+ nr.selector = marks.join(", ");
644
+ nr.enabled = true;
645
+ nr.useColor = false;
646
+ nr.color = "#e6e6e6";
647
+ nr.group = "框选";
648
+ // A user box-picked region REPLACES any preset/default region with the
649
+ // same name (so your custom "左侧导航栏" supersedes the built-in one).
650
+ regions = regions.filter(function (x) { return !(x.group && x.group !== "框选" && x.name === name); });
651
+ regions.push(nr);
652
+ renderCards();
653
+ applyAll(regions);
654
+ persist();
655
+ pickRegionExit(true);
656
+ flushRecord("REC_NEW", name, nr.selector);
657
+ // Bring the newly-added card into view and pulse it so the user can
658
+ // immediately find it and test font/color on it.
659
+ var newCard = cardsEl && cardsEl.querySelector('[data-card="' + nr.id + '"]');
660
+ if (newCard) {
661
+ try { newCard.scrollIntoView({ block: "nearest", behavior: "smooth" }); } catch (e) {}
662
+ newCard.style.transition = "box-shadow .4s";
663
+ newCard.style.boxShadow = "0 0 0 2px rgba(255,160,70,.7), 0 4px 18px rgba(0,0,0,.4)";
664
+ setTimeout(function () { if (newCard) newCard.style.boxShadow = ""; }, 1600);
665
+ }
666
+ showToast("已添加区域「" + name + "」(" + picked.length + " 个元素),可在卡片里改字体/颜色", "ok");
667
+ }
668
+ function pickRegionExit(silent) {
669
+ if (!__pick) return;
670
+ __pick.active = false;
671
+ document.removeEventListener("pointerover", __pick.onMove, true);
672
+ document.removeEventListener("click", __pick.onPick, true);
673
+ (picked || []).forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
674
+ var b = document.getElementById("dsh-fe-pickbar"); if (b) b.parentNode && b.parentNode.removeChild(b);
675
+ if (hoverEl) { try { hoverEl.style.outline = ""; } catch (e) {} }
676
+ var go = document.getElementById("dsh-fe-toggle");
677
+ if (go) { go.style.display = "block"; }
678
+ }
679
+ document.getElementById("dsh-fe-pick-done").addEventListener("click", finish);
680
+ document.getElementById("dsh-fe-pick-cancel").addEventListener("click", function () { pickRegionExit(true); });
681
+ document.addEventListener("pointerover", onMove, true);
682
+ document.addEventListener("click", onPick, true);
683
+ __pick = { active: true, onMove: onMove, onPick: onPick, exit: pickRegionExit };
684
+ if (toggle) toggle.style.display = "none";
685
+ }
686
+
687
+ // ── Show the elements a region currently applies to (like the pick outline) ─
688
+ // Lets the user see exactly which page elements a saved region covers, and
689
+ // doubles as a live check that the region's selector still matches anything.
690
+ function showScope(rid) {
691
+ if (!document.body || !rid) return;
692
+ var r = findRegion(rid);
693
+ if (!r) return;
694
+ var sel = (r.selector || "").trim();
695
+ if (!sel) { showToast("该区域没有选择器", "err"); return; }
696
+ var nodes;
697
+ try { nodes = document.querySelectorAll(sel); } catch (e) { showToast("选择器无效", "err"); return; }
698
+ var arr = Array.prototype.slice.call(nodes).filter(function (n) {
699
+ return n && n.nodeType === 1;
700
+ });
701
+ if (!arr.length) { showToast("该区域当前未匹配到任何元素(可能已失效)", "err"); return; }
702
+ arr.forEach(function (el) {
703
+ el.style.outline = "2px solid #3ddc84";
704
+ el.style.outlineOffset = "-1px";
705
+ el.style.transition = "outline-color .3s";
706
+ });
707
+ showToast("该区域覆盖 " + arr.length + " 个元素(绿色描边,2 秒后消失)", "ok");
708
+ // remember to clear after a beat; if another showScope fires we just reset
709
+ if (showScope._nodes) showScope._nodes.forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
710
+ showScope._nodes = arr;
711
+ clearTimeout(showScope._t);
712
+ showScope._t = setTimeout(function () {
713
+ (showScope._nodes || []).forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
714
+ showScope._nodes = null;
715
+ }, 2400);
716
+ }
717
+
718
+ // ── Region config management ──────────────────────────────────────────
719
+ // Replace: drop every non-box-picked region (defaults go away), keep picked.
720
+ function replaceWithPicked() {
721
+ var picked = regions.filter(function (x) { return x.group === "框选"; });
722
+ if (!picked.length) { showToast("当前没有框选区域,先新增框选", "err"); return; }
723
+ regions = picked;
724
+ renderCards(); applyAll(regions); persist();
725
+ showToast("已用「" + picked.length + " 个框选区域」取代默认区域", "ok");
726
+ }
727
+ // Restore: remove box-picked regions and reload the built-in defaults.
728
+ function restoreDefault() {
729
+ regions = DEFAULT_REGIONS();
730
+ renderCards(); applyAll(regions); persist();
731
+ showToast("已恢复默认区域(移除框选区域)", "ok");
732
+ }
733
+ // Save the whole current region set as a named preset ("配置方案").
734
+ // Save the whole current region set as a named preset ("保存配置方案").
735
+ // Reuses the theme-recipe shape so the saved plan can be applied back via
736
+ // the theme "应用" button too.
737
+ function saveConfig() {
738
+ var themes = getThemes();
739
+ var base = "配置方案";
740
+ var name = base + "1";
741
+ var n = 1;
742
+ while (themes.some(function (t) { return t.name === name; })) { n++; name = base + n; }
743
+ var recipe = {};
744
+ regions.forEach(function (r) {
745
+ recipe[r.name] = {
746
+ en: r.en, zh: r.zh, size: r.size, lh: r.lh,
747
+ weight: r.weight, italic: r.italic === true,
748
+ color: r.color, useColor: r.useColor === true
749
+ };
750
+ });
751
+ themes.push({ name: name, recipe: recipe, ts: Date.now() });
752
+ state.themes = themes;
753
+ renderThemeSel();
754
+ persist();
755
+ var sel = root.querySelector("#dsh-fe-theme-sel");
756
+ if (sel) sel.value = name;
757
+ showToast("已保存方案「" + name + "」(含全部区域字体/颜色)", "ok");
758
+ }
759
+
760
+ function showToast(msg, type) {
761
+ var t = document.getElementById("dsh-fe-toast");
762
+ if (!t) return;
763
+ t.textContent = msg;
764
+ t.className = "dsh-fe-toast" + (type === "ok" ? " dsh-fe-toast-ok" : type === "err" ? " dsh-fe-toast-err" : "") + " dsh-fe-toast-show";
765
+ clearTimeout(t._hide);
766
+ t._hide = setTimeout(function () { t.className = "dsh-fe-toast"; }, 2500);
767
+ }
768
+ function reloadPlugin() {
769
+ try {
770
+ var r = document.getElementById("dsh-fe-root");
771
+ if (r) r.remove();
772
+ var pc = document.getElementById("dsh-fe-panel-css");
773
+ if (pc) pc.remove();
774
+ var st = document.getElementById("dsh-font-enhancer-style");
775
+ if (st) st.remove();
776
+ buildUI();
777
+ showToast("插件已刷新", "ok");
778
+ } catch (e) {}
779
+ }
780
+ // A color drawn from the FULL hue wheel with random saturation/lightness,
781
+ // so every random pick can be green/blue/purple/yellow/red — not just a
782
+ // handful of near-red hues. `#ignore` kept for call-site compatibility.
783
+ function randVivid(i) {
784
+ var h = Math.floor(Math.random() * 360);
785
+ var s = 70 + Math.floor(Math.random() * 25); // 70–95%
786
+ var l = 42 + Math.floor(Math.random() * 28); // 42–70%
787
+ return "hsl(" + h + ", " + s + "%, " + l + "%)";
788
+ }
789
+ function randomizeFonts(rid) {
790
+ var r = findRegion(rid);
791
+ if (!r) return;
792
+ r.en = randFont(EN_FONTS);
793
+ r.zh = randFont(ZH_FONTS);
794
+ renderCards();
795
+ applyAll(regions);
796
+ persist();
797
+ }
798
+ function randomizeColor(rid) {
799
+ var r = findRegion(rid);
800
+ if (!r) return;
801
+ r.useColor = true;
802
+ r.color = randVivid(0);
803
+ renderCards();
804
+ applyAll(regions);
805
+ persist();
806
+ }
807
+ function randomizeAllFonts() {
808
+ // 统一字体开着时,随机统一字体值(视觉立即可见),否则随机各区域
809
+ if (state.unified.uf.enabled) {
810
+ state.unified.uf.en = randFont(EN_FONTS);
811
+ state.unified.uf.zh = randFont(ZH_FONTS);
812
+ var se = root.querySelector("#dsh-fe-u-en");
813
+ var sz = root.querySelector("#dsh-fe-u-zh");
814
+ if (se) se.value = state.unified.uf.en;
815
+ if (sz) sz.value = state.unified.uf.zh;
816
+ applyAll(regions);
817
+ persist();
818
+ return;
819
+ }
820
+ regions.forEach(function (r) {
821
+ if (!r.enabled) return;
822
+ r.en = randFont(EN_FONTS);
823
+ r.zh = randFont(ZH_FONTS);
824
+ });
825
+ renderCards();
826
+ applyAll(regions);
827
+ persist();
828
+ }
829
+ function randomizeAllColors() {
830
+ // 统一颜色开着时,随机统一颜色值(视觉立即可见),否则随机各区域
831
+ if (state.unified.uc.enabled) {
832
+ state.unified.uc.color = randVivid(0);
833
+ var pc = root.querySelector("#dsh-fe-u-color-val");
834
+ if (pc) pc.value = state.unified.uc.color;
835
+ applyAll(regions);
836
+ persist();
837
+ return;
838
+ }
839
+ var i = 0;
840
+ regions.forEach(function (r) {
841
+ if (!r.enabled) return;
842
+ r.useColor = true;
843
+ r.color = randVivid(i++);
844
+ });
845
+ renderCards();
846
+ applyAll(regions);
847
+ persist();
848
+ }
849
+ function renderThemeSel() {
850
+ var sel = root ? root.querySelector("#dsh-fe-theme-sel") : null;
851
+ var inp = root ? root.querySelector("#dsh-fe-theme-name") : null;
852
+ if (!sel) return;
853
+ var opts = '<option value="">— 选择主题 —</option>';
854
+ getThemes().forEach(function (t) {
855
+ opts += '<option value="' + t.name.replace(/"/g, "&quot;") + '">' + esc(t.name) + "</option>";
856
+ });
857
+ sel.innerHTML = opts;
858
+ // auto-fill the name input with the default name
859
+ if (inp) inp.value = "主题 " + (getThemes().length + 1);
860
+ }
861
+ function saveTheme() {
862
+ var inp = root.querySelector("#dsh-fe-theme-name");
863
+ var name = inp ? String(inp.value).trim() : "";
864
+ if (!name) { showToast("请先在上方输入框填写主题名称", "err"); return; }
865
+ var recipe = {};
866
+ regions.forEach(function (r) {
867
+ recipe[r.name] = {
868
+ en: r.en, zh: r.zh, size: r.size, lh: r.lh,
869
+ weight: r.weight, italic: r.italic === true,
870
+ color: r.color, useColor: r.useColor === true
871
+ };
872
+ });
873
+ // 记住全局统一的勾选状态和值,应用主题时能完整复原
874
+ var u = state.unified || {};
875
+ recipe.__unified = {
876
+ uc: u.uc ? { enabled: u.uc.enabled === true, color: u.uc.color } : { enabled: false, color: "#FFFFFF" },
877
+ 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 }
878
+ };
879
+ var th = getThemes();
880
+ // 只新增或覆盖同名主题,绝不删除其他主题
881
+ var found = false;
882
+ for (var i = 0; i < th.length; i++) {
883
+ if (th[i].name === name) { th[i].recipe = recipe; th[i].ts = Date.now(); found = true; break; }
884
+ }
885
+ if (!found) th.push({ name: name, recipe: recipe, ts: Date.now() });
886
+ state.themes = th;
887
+ renderThemeSel();
888
+ persist();
889
+ var sel = root.querySelector("#dsh-fe-theme-sel");
890
+ if (sel) sel.value = name;
891
+ showToast("已保存:" + name, "ok");
892
+ }
893
+ function themeRename() {
894
+ var sel = root.querySelector("#dsh-fe-theme-sel");
895
+ var oldName = sel && sel.value;
896
+ if (!oldName) { showToast("请先在下拉框选择要重命名的主题", "err"); return; }
897
+ var inp = root.querySelector("#dsh-fe-theme-name");
898
+ var newName = inp ? String(inp.value).trim() : "";
899
+ if (!newName) { showToast("请先在上方输入框填写新名称", "err"); return; }
900
+ if (oldName === newName) { showToast("新名称与旧名称相同", "err"); return; }
901
+ var th = getThemes();
902
+ var oldRecipe = null;
903
+ var found = -1;
904
+ for (var i = 0; i < th.length; i++) {
905
+ if (th[i].name === oldName) { oldRecipe = th[i].recipe; found = i; break; }
906
+ }
907
+ if (!oldRecipe) { showToast("未找到原主题数据", "err"); return; }
908
+ th.splice(found, 1);
909
+ var found2 = false;
910
+ for (var j = 0; j < th.length; j++) {
911
+ if (th[j].name === newName) { th[j].recipe = oldRecipe; th[j].ts = Date.now(); found2 = true; break; }
912
+ }
913
+ if (!found2) th.push({ name: newName, recipe: oldRecipe, ts: Date.now() });
914
+ state.themes = th;
915
+ renderThemeSel();
916
+ persist();
917
+ if (sel) sel.value = newName;
918
+ showToast("已重命名:" + oldName + " → " + newName, "ok");
919
+ }
920
+ function applyTheme() {
921
+ var sel = root.querySelector("#dsh-fe-theme-sel");
922
+ var name = sel && sel.value;
923
+ if (!name) { showToast("请先在下拉框选择要应用的主题", "err"); return; }
924
+ var t = null;
925
+ getThemes().forEach(function (x) { if (x.name === name) t = x; });
926
+ if (!t) { showToast("未找到主题数据,请重新保存", "err"); return; }
927
+ var matched = 0;
928
+ regions.forEach(function (r) {
929
+ var rc = t.recipe[r.name];
930
+ if (rc) {
931
+ matched++;
932
+ r.en = rc.en; r.zh = rc.zh; r.size = rc.size; r.lh = rc.lh;
933
+ r.weight = rc.weight; r.italic = rc.italic === true;
934
+ r.color = rc.color; r.useColor = rc.useColor === true;
935
+ }
936
+ });
937
+ // 应用主题时恢复保存的统一状态(勾选 + 值),实现完整复原
938
+ if (t.recipe && t.recipe.__unified) {
939
+ var sun = t.recipe.__unified;
940
+ state.unified = state.unified || {};
941
+ if (sun.uc) { state.unified.uc = { enabled: sun.uc.enabled === true, color: sun.uc.color || "#FFFFFF" }; }
942
+ if (sun.uf) {
943
+ state.unified.uf = {
944
+ enabled: sun.uf.enabled === true, en: sun.uf.en || "Times New Roman",
945
+ zh: sun.uf.zh || "KaiTi", size: sun.uf.size != null ? sun.uf.size : 15,
946
+ weight: sun.uf.weight != null ? sun.uf.weight : 400, italic: sun.uf.italic === true
947
+ };
948
+ }
949
+ // 同步界面上的统一控件
950
+ var ucEl2 = root.querySelector("#dsh-fe-u-color"); var ucVal2 = root.querySelector("#dsh-fe-u-color-val");
951
+ var ufEl2 = root.querySelector("#dsh-fe-u-font");
952
+ var ufEn2 = root.querySelector("#dsh-fe-u-en"); var ufZh2 = root.querySelector("#dsh-fe-u-zh");
953
+ var ufSz2 = root.querySelector("#dsh-fe-u-size"); var ufWt2 = root.querySelector("#dsh-fe-u-weight"); var ufIt2 = root.querySelector("#dsh-fe-u-italic");
954
+ if (ucEl2) ucEl2.checked = state.unified.uc.enabled; if (ucVal2) ucVal2.value = state.unified.uc.color;
955
+ if (ufEl2) ufEl2.checked = state.unified.uf.enabled; if (ufEn2) ufEn2.value = state.unified.uf.en;
956
+ if (ufZh2) ufZh2.value = state.unified.uf.zh; if (ufSz2) ufSz2.value = state.unified.uf.size;
957
+ if (ufWt2) ufWt2.value = state.unified.uf.weight; if (ufIt2) ufIt2.checked = state.unified.uf.italic;
958
+ } else {
959
+ // 旧主题没有 __unified:回退为关闭统一,避免误覆盖
960
+ 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; }
961
+ 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; }
962
+ }
963
+ renderCards();
964
+ applyAll(regions);
965
+ persist();
966
+ if (matched === 0) { showToast("主题 " + name + " 已应用(无匹配区域)", "ok"); return; }
967
+ showToast("已应用主题:" + name + "(" + matched + "个区域)", "ok");
968
+ }
969
+ function deleteTheme() {
970
+ var sel = root.querySelector("#dsh-fe-theme-sel");
971
+ var name = sel && sel.value;
972
+ if (!name) return;
973
+ state.themes = getThemes().filter(function (x) { return x.name !== name; });
974
+ renderThemeSel();
975
+ persist();
976
+ showToast("已删除:" + name, "ok");
977
+ }
978
+
979
+ // ---------- render region cards ----------
980
+ function cardHtml(r) {
981
+ var body = "";
982
+ if (r.enabled) {
983
+ body =
984
+ '<div class="dsh-fe-card-body">' +
985
+ '<div class="dsh-fe-body-col">' +
986
+ "<label>英文字体</label><select data-rid='" + r.id + "' data-fld='en'>" + optionsHtml(mergedEnFonts(), r.en) + "</select>" +
987
+ "<label>中文字体</label><select data-rid='" + r.id + "' data-fld='zh'>" + optionsHtml(mergedZhFonts(), r.zh) + "</select>" +
988
+ "</div>" +
989
+ '<div class="dsh-fe-body-col">' +
990
+ '<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>' +
991
+ '<div><label>行距</label><input type="number" min="1.3" max="2.2" step="0.1" data-rid="' + r.id + '" data-fld="lh" value="' + r.lh + '"></div></div>' +
992
+ '<div class="dsh-fe-weightrow"><label>字重 <span data-rid="' + r.id + '" data-fld="weightVal">' + r.weight + "</span></label>" +
993
+ '<input type="range" min="100" max="900" step="100" data-rid="' + r.id + '" data-fld="weight" value="' + r.weight + '"></div>' +
994
+ '<label class="dsh-fe-italicrow"><input type="checkbox" data-rid="' + r.id + '" data-fld="italic"' + (r.italic ? " checked" : "") + "> 斜体</label>" +
995
+ '<label class="dsh-fe-colorrow"><input type="checkbox" data-rid="' + r.id + '" data-fld="useColor"' + (r.useColor ? " checked" : "") + "> 文字颜色</label>" +
996
+ '<input type="color" data-rid="' + r.id + '" data-fld="color" value="' + esc(r.color || "#e6e6e6") + '">' +
997
+ "</div>" +
998
+ "</div>";
999
+ }
1000
+ return (
1001
+ '<div class="dsh-fe-card" data-card="' + r.id + '">' +
1002
+ '<div class="dsh-fe-card-head">' +
1003
+ '<div class="dsh-fe-head-left">' +
1004
+ '<label class="dsh-fe-cb"><input type="checkbox" data-rid="' + r.id + '" data-fld="enabled"' + (r.enabled ? " checked" : "") + "></label>" +
1005
+ '<span class="dsh-fe-dot" data-dot="' + r.id + '" style="background:' + esc(r.color || "#888") + '"></span>' +
1006
+ '<input type="text" data-rid="' + r.id + '" data-fld="name" value="' + esc(r.name) + '" title="区域名称">' +
1007
+ "</div>" +
1008
+ '<div class="dsh-fe-head-right">' +
1009
+ '<button type="button" data-rid="' + r.id + '" data-act="pickAdd" title="框选更多元素追加到这个区域">➕加元素</button>' +
1010
+ '<button type="button" data-rid="' + r.id + '" data-act="showScope" title="高亮显示该区域覆盖的元素范围">👁范围</button>' +
1011
+ '<button type="button" data-rid="' + r.id + '" data-act="randomFont" title="随机字体">🎲字体</button>' +
1012
+ '<button type="button" data-rid="' + r.id + '" data-act="randomColor" title="随机颜色">🎨颜色</button>' +
1013
+ '<button type="button" data-rid="' + r.id + '" data-act="reset" title="重置本区域">⟳</button>' +
1014
+ '<button type="button" data-rid="' + r.id + '" data-act="delete" title="删除区域">✕</button>' +
1015
+ "</div>" +
1016
+ "</div>" +
1017
+ body +
1018
+ "</div>"
1019
+ );
1020
+ }
1021
+ function renderCards() {
1022
+ if (!cardsEl) return;
1023
+ cardsEl.innerHTML = regions
1024
+ .map(cardHtml)
1025
+ .join("");
1026
+ }
1027
+
1028
+ // ---------- build UI ----------
1029
+ function buildUI() {
1030
+ if (document.getElementById("dsh-fe-root")) return;
1031
+ if (!document.body) return;
1032
+ // DIAG: prove this client really runs in the browser by pinging host's
1033
+ // client-alive route (host logs "CLIENT-ALIVE ping received from browser").
1034
+ try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {}
1035
+
1036
+ var pc = document.createElement("style");
1037
+ pc.id = "dsh-fe-panel-css";
1038
+ pc.dataset.plugin = "dsh-font-enhancer";
1039
+ pc.textContent = panelCss();
1040
+ document.head.appendChild(pc);
1041
+
1042
+ root = document.createElement("div");
1043
+ root.id = "dsh-fe-root";
1044
+ root.innerHTML =
1045
+ '<button id="dsh-fe-toggle" type="button" title="字体插件(点击打开字体设置面板)">字</button>' +
1046
+ '<div id="dsh-fe-panel" style="display:none">' +
1047
+ '<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>' +
1048
+ '<div style="font-size:11px;color:#8b93a1;padding:4px 4px 8px;line-height:1.5">⬇️ 下滑至底部有 <b style="color:#8ab8ff">快速上手说明</b>;换字体/颜色、框选、一键装可爱字体都在这里。</div>' +
1049
+ '<div class="dsh-fe-themeblock">' +
1050
+ '<select id="dsh-fe-theme-sel"></select>' +
1051
+ '<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">' +
1052
+ '<button type="button" data-act="themeSave" title="把当前所有区域的字体/颜色保存为一个方案(填入上方名称)">💾 保存方案</button>' +
1053
+ '<button type="button" data-act="themeRename" title="重命名所选主题为输入框的名字">✏改</button>' +
1054
+ '<button type="button" data-act="themeApply" title="应用选中主题">应用</button>' +
1055
+ '<button type="button" data-act="themeDel" title="删除选中主题">🗑</button>' +
1056
+ '<button type="button" data-act="themeClearAll" title="删除所有已保存的主题" style="color:#e08080">🗑 清空全部</button>' +
1057
+ "</div>" +
1058
+ '<div class="dsh-fe-unified" id="dsh-fe-unified">' +
1059
+ '<div class="dsh-fe-u-row">' +
1060
+ '<span class="dsh-fe-u-label">全局统一</span>' +
1061
+ "</div>" +
1062
+ '<div class="dsh-fe-u-row">' +
1063
+ '<label><input type="checkbox" id="dsh-fe-u-color" data-u="uc" data-u-fld="enabled">统一颜色</label>' +
1064
+ '<input type="color" id="dsh-fe-u-color-val" data-u="uc" data-u-fld="color" value="#FFFFFF">' +
1065
+ "</div>" +
1066
+ '<div class="dsh-fe-u-row">' +
1067
+ '<label><input type="checkbox" id="dsh-fe-u-font" data-u="uf" data-u-fld="enabled">统一字体</label>' +
1068
+ '<select id="dsh-fe-u-en" data-u="uf" data-u-fld="en" style="min-width:80px">' + optionsHtml(mergedEnFonts(), "Times New Roman") + "</select>" +
1069
+ '<select id="dsh-fe-u-zh" data-u="uf" data-u-fld="zh" style="min-width:70px">' + optionsHtml(mergedZhFonts(), "KaiTi") + "</select>" +
1070
+ '<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="字号">' +
1071
+ '<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)">' +
1072
+ '<label title="统一斜体"><input type="checkbox" id="dsh-fe-u-italic" data-u="uf" data-u-fld="italic">斜体</label>' +
1073
+ "</div>" +
1074
+ "</div>" +
1075
+ '<div class="dsh-fe-toolbar">' +
1076
+ '<button type="button" data-act="pickMode" title="在界面框选文字元素,合并成一个区域并命名">🖱 框选区域</button>' +
1077
+ '<button type="button" data-act="randomAllFonts" title="给所有启用区域随机分配英/中文字体">🎲 随机字体</button>' +
1078
+ '<button type="button" class="dsh-fe-btn-accent" data-act="randomAllColors" title="给所有启用区域随机分配颜色">🎨 随机颜色</button>' +
1079
+ "</div>" +
1080
+ '<div class="dsh-fe-toolbar dsh-fe-toolbar2">' +
1081
+ '<button type="button" data-act="replacePicked" title="删除全部默认区域,只保留你框选的区域">⟳ 框选取代默认</button>' +
1082
+ '<button type="button" data-act="restoreDefault" title="删除新增的框选区域,恢复内置默认区域">↺ 恢复默认区域</button>' +
1083
+ '<button type="button" data-act="reloadUI" title="刷新界面(新安装的字体立即生效,无需关闭面板)">🔄 刷新</button>' +
1084
+ "</div>" +
1085
+ '<div id="dsh-fe-cards"></div>' +
1086
+ '<div class="dsh-fe-customfont">' +
1087
+ '<div class="dsh-fe-cf-label">🎨 推荐可爱/奇特字体(一键下载安装 → 添加启用)</div>' +
1088
+ '<div class="dsh-fe-cf-list">' +
1089
+ '<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>' +
1090
+ '<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>' +
1091
+ '<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>' +
1092
+ '<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>' +
1093
+ '<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>' +
1094
+ '<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>' +
1095
+ '<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>' +
1096
+ '<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>' +
1097
+ "</div>" +
1098
+ '<div class="dsh-fe-cf-file">' +
1099
+ '<div class="dsh-fe-cf-label">📁 添加自提供安装包(清单里没有的字体)</div>' +
1100
+ '<div class="dsh-fe-cf-file-row">' +
1101
+ '<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">' +
1102
+ '<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>' +
1103
+ "</div>" +
1104
+ '<div class="dsh-fe-cf-tip">选择电脑上已有的字体安装包(.ttf/.otf),点「安装并添加」,自动装到系统并加入字体下拉。</div>' +
1105
+ "</div>" +
1106
+ '<div class="dsh-fe-cf-tip">💡 点「⬇ 一键安装」自动下载并装到系统,安装成功后<strong>自动添加到字体下拉</strong>,无需手动操作。安装路径会显示在提示里。中文字体较大(几MB),请耐心等待下载完成。</div>' +
1107
+ "</div>" +
1108
+ '<div id="dsh-fe-guide" class="dsh-fe-guide">' +
1109
+ "<h4>📖 快速上手(30 秒学会)</h4>" +
1110
+ "<p><b>① 改某个区域的字体/颜色</b><br>勾选区域卡片前的 ☑ → 展开设置 → 左边选字体,右边调字号/字重/斜体/颜色,马上生效。</p>" +
1111
+ "<p><b>② 一键统一所有区域</b><br>「全局统一」区勾选 <b>统一颜色</b> 或 <b>统一字体</b>,一个设置全界面生效。</p>" +
1112
+ "<p><b>③ 框选自己想要调整的区域</b><br>点 <b>🖱 框选区域</b> → 在界面上点选文字元素(可多选)→ 起个名字 → 确定,就能单独调它。</p>" +
1113
+ "<p><b>④ 保存方案,随时切换</b><br>调好后填个名字 → 点 <b>💾 保存方案</b>。以后下拉选它 → 点 <b>应用</b> 一键还原。</p>" +
1114
+ "<p><b>⑤ 用可爱字体(一键安装)</b><br>在下方字体列表点 <b>⬇ 一键安装</b> → 等下载完 → 点 <b>🔄 刷新</b>,新字体就在下拉里了。</p>" +
1115
+ "<p><b>⑥ 添加自己下载的字体</b><br>点「📁 添加自提供安装包」→ 选电脑上的 .ttf/.otf 文件 → 点 <b>安装并添加</b>。</p>" +
1116
+ "<p><b>⑦ 卡住了?</b><br>点 <b>🔄 刷新</b>,或点右下角 Aa 悬浮球收起来再展开。</p>" +
1117
+ "<p style='color:#e0a060'><b>⚠️ 网页端提示</b>:如果用的是手机/浏览器打开网页端,装完新字体后需要<b>刷新网页</b>(F5)才能显示新字体;桌面端窗口无需刷新。</p>" +
1118
+ "</div>" +
1119
+ '<div id="dsh-fe-toast"></div>' +
1120
+ "</div>";
1121
+ // Mount to the outermost <html> element, NOT document.body — DSH gives
1122
+ // the body/app containers `transform`/`filter`/`overflow` which make
1123
+ // position:fixed children (the orb + panel) position against a transformed
1124
+ // ancestor instead of the viewport, hiding them off screen. Appending to
1125
+ // <html> sidesteps that so the floating orb is always visible.
1126
+ // v1.2.0: the parent #dsh-fe-root is position:static + pointer-events:none,
1127
+ // so position:fixed children stay viewport-relative in BOTH the browser
1128
+ // and the Electron desktop window.
1129
+ var mount = document.documentElement || document.body;
1130
+ // guard against re-append (Electron window can re-run buildUI on focus)
1131
+ if (!root.parentNode) mount.appendChild(root);
1132
+ // v1.2.0 — Electron desktop window hardening: DSH Desktop renders the
1133
+ // same 43120 page in its Electron BrowserWindow, but the native window
1134
+ // chrome + zoom can offset position:fixed elements. Detect Electron and,
1135
+ // if the orb would land off-viewport, force it to an on-screen corner.
1136
+ try {
1137
+ var isElectron = /electron/i.test(navigator.userAgent) || (typeof window.process !== "undefined" && window.process.type === "renderer");
1138
+ if (isElectron) {
1139
+ var btnEl = root.querySelector("#dsh-fe-toggle");
1140
+ var panelEl = root.querySelector("#dsh-fe-panel");
1141
+ if (btnEl && panelEl) {
1142
+ // reset to safe defaults so the orb is never pushed off-screen
1143
+ btnEl.style.right = "18px";
1144
+ btnEl.style.bottom = "18px";
1145
+ btnEl.style.left = "auto";
1146
+ btnEl.style.top = "auto";
1147
+ // re-pin the panel near the orb once rendered
1148
+ var pin = function () {
1149
+ var br = btnEl.getBoundingClientRect();
1150
+ if (!br || (br.left === 0 && br.top === 0 && br.width === 0)) return;
1151
+ var pw = panelEl.offsetWidth || 348;
1152
+ var ph = panelEl.offsetHeight || 360;
1153
+ var left = Math.max(8, Math.min(window.innerWidth - pw - 8, br.left + br.width / 2 - pw / 2));
1154
+ var top = br.top - ph - 10;
1155
+ if (top < 8) top = br.bottom + 10;
1156
+ panelEl.style.left = left + "px";
1157
+ panelEl.style.top = top + "px";
1158
+ panelEl.style.right = "auto";
1159
+ panelEl.style.bottom = "auto";
1160
+ };
1161
+ setTimeout(pin, 50);
1162
+ window.addEventListener("resize", pin);
1163
+ }
1164
+ }
1165
+ } catch (e) {}
1166
+ try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {} // DIAG-2 root appended
1167
+
1168
+ panel = root.querySelector("#dsh-fe-panel");
1169
+ cardsEl = root.querySelector("#dsh-fe-cards");
1170
+ renderCards();
1171
+ renderThemeSel();
1172
+ applyAll(regions);
1173
+
1174
+ // sync unified controls with saved state
1175
+ var u = state.unified;
1176
+ var ucEl = root.querySelector("#dsh-fe-u-color");
1177
+ var ucVal = root.querySelector("#dsh-fe-u-color-val");
1178
+ if (ucEl) ucEl.checked = u.uc.enabled;
1179
+ if (ucVal) ucVal.value = u.uc.color;
1180
+ var ufEl = root.querySelector("#dsh-fe-u-font");
1181
+ var ufEn = root.querySelector("#dsh-fe-u-en");
1182
+ var ufZh = root.querySelector("#dsh-fe-u-zh");
1183
+ var ufSz = root.querySelector("#dsh-fe-u-size");
1184
+ var ufWt = root.querySelector("#dsh-fe-u-weight");
1185
+ var ufIt = root.querySelector("#dsh-fe-u-italic");
1186
+ if (ufEl) ufEl.checked = u.uf.enabled;
1187
+ if (ufEn) ufEn.value = u.uf.en;
1188
+ if (ufZh) ufZh.value = u.uf.zh;
1189
+ if (ufSz) ufSz.value = u.uf.size;
1190
+ if (ufWt) ufWt.value = u.uf.weight;
1191
+ if (ufIt) ufIt.checked = u.uf.italic;
1192
+
1193
+ // Direct event listener for unified color picker (bypass delegation)
1194
+ var ucPicker = root.querySelector("#dsh-fe-u-color-val");
1195
+ if (ucPicker) {
1196
+ ucPicker.addEventListener("input", function () {
1197
+ state.unified.uc.color = ucPicker.value;
1198
+ applyAll(regions);
1199
+ persist();
1200
+ });
1201
+ }
1202
+
1203
+ // initial button position (saved, else default bottom-right).
1204
+ // Clamp inside the viewport so a stale/off-screen saved position can
1205
+ // never hide the orb (it "disappears" when bx/by put it outside the window).
1206
+ var btn = root.querySelector("#dsh-fe-toggle");
1207
+ var bw = btn.offsetWidth || 54, bh = btn.offsetHeight || 54;
1208
+ if (typeof state.bx === "number" && typeof state.by === "number" &&
1209
+ state.bx >= 0 && state.by >= 0 &&
1210
+ state.bx <= Math.max(0, window.innerWidth - bw) &&
1211
+ state.by <= Math.max(0, window.innerHeight - bh)) {
1212
+ btn.style.left = state.bx + "px";
1213
+ btn.style.top = state.by + "px";
1214
+ btn.style.right = "auto";
1215
+ btn.style.bottom = "auto";
1216
+ } else {
1217
+ // default / invalid position → bottom-right (CSS default covers it)
1218
+ btn.style.left = "auto"; btn.style.top = "auto";
1219
+ btn.style.right = "18px"; btn.style.bottom = "18px";
1220
+ }
1221
+
1222
+ wireEvents();
1223
+ }
1224
+
1225
+ // ---------- event wiring ----------
1226
+ function wireEvents() {
1227
+ var toggle = root.querySelector("#dsh-fe-toggle");
1228
+
1229
+ function show() {
1230
+ panel.style.display = "block";
1231
+ positionPanel();
1232
+ }
1233
+ function hide() {
1234
+ panel.style.display = "none";
1235
+ reloadPlugin();
1236
+ }
1237
+ function isOpen() {
1238
+ return panel.style.display === "block";
1239
+ }
1240
+ function positionPanel() {
1241
+ var r = toggle.getBoundingClientRect();
1242
+ var pw = panel.offsetWidth || 320;
1243
+ var ph = panel.offsetHeight || 360;
1244
+ var left = r.left + r.width / 2 - pw / 2;
1245
+ left = Math.max(8, Math.min(window.innerWidth - pw - 8, left));
1246
+ var top = r.top - ph - 10;
1247
+ if (top < 8) top = r.bottom + 10;
1248
+ panel.style.left = left + "px";
1249
+ panel.style.top = top + "px";
1250
+ panel.style.right = "auto";
1251
+ panel.style.bottom = "auto";
1252
+ }
1253
+
1254
+ // ---- drag the button (pet-like) ----
1255
+ var drag = { active: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
1256
+ toggle.addEventListener("pointerdown", function (e) {
1257
+ drag.active = true;
1258
+ drag.moved = false;
1259
+ drag.sx = e.clientX;
1260
+ drag.sy = e.clientY;
1261
+ var r = toggle.getBoundingClientRect();
1262
+ drag.ox = r.left;
1263
+ drag.oy = r.top;
1264
+ try {
1265
+ toggle.setPointerCapture(e.pointerId);
1266
+ } catch (err) {}
1267
+ toggle.classList.add("dsh-fe-dragging");
1268
+ e.preventDefault();
1269
+ });
1270
+ toggle.addEventListener("pointermove", function (e) {
1271
+ if (!drag.active) return;
1272
+ var dx = e.clientX - drag.sx;
1273
+ var dy = e.clientY - drag.sy;
1274
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 5) return;
1275
+ drag.moved = true;
1276
+ var nx = Math.max(0, Math.min(window.innerWidth - toggle.offsetWidth, drag.ox + dx));
1277
+ var ny = Math.max(0, Math.min(window.innerHeight - toggle.offsetHeight, drag.oy + dy));
1278
+ toggle.style.left = nx + "px";
1279
+ toggle.style.top = ny + "px";
1280
+ toggle.style.right = "auto";
1281
+ toggle.style.bottom = "auto";
1282
+ state.bx = Math.round(nx);
1283
+ state.by = Math.round(ny);
1284
+ persist();
1285
+ // 面板展开时跟随悬浮球移动
1286
+ if (isOpen()) positionPanel();
1287
+ });
1288
+ function endDrag(e) {
1289
+ if (!drag.active) return;
1290
+ drag.active = false;
1291
+ toggle.classList.remove("dsh-fe-dragging");
1292
+ try {
1293
+ toggle.releasePointerCapture(e.pointerId);
1294
+ } catch (err) {}
1295
+ if (!drag.moved) {
1296
+ if (isOpen()) hide();
1297
+ else show();
1298
+ }
1299
+ }
1300
+ toggle.addEventListener("pointerup", endDrag);
1301
+ toggle.addEventListener("pointercancel", endDrag);
1302
+
1303
+ root.querySelector("#dsh-fe-close").addEventListener("click", hide);
1304
+
1305
+ // ---- delegated field edits (no re-render, keep focus) ----
1306
+ function onField(e) {
1307
+ var el = e.target;
1308
+ var rid = el.getAttribute("data-rid");
1309
+ var fld = el.getAttribute("data-fld");
1310
+ if (!rid || !fld) return;
1311
+ var r = findRegion(rid);
1312
+ if (!r) return;
1313
+ if (fld === "enabled") { r.enabled = el.checked; renderCards(); }
1314
+ else if (fld === "italic") r.italic = el.checked;
1315
+ else if (fld === "useColor") r.useColor = el.checked;
1316
+ else if (fld === "name") r.name = el.value;
1317
+ else if (fld === "selector") r.selector = el.value;
1318
+ else if (fld === "en" || fld === "zh") r[fld] = el.value;
1319
+ else if (fld === "size") r.size = clamp(el.value, 12, 22);
1320
+ else if (fld === "lh") r.lh = clamp(el.value, 1.3, 2.2);
1321
+ else if (fld === "weight") {
1322
+ r.weight = clamp(el.value, 100, 900);
1323
+ var v = root.querySelector('[data-rid="' + rid + '"][data-fld="weightVal"]');
1324
+ if (v) v.textContent = r.weight;
1325
+ } else if (fld === "color") {
1326
+ r.color = el.value;
1327
+ r.useColor = true; // picking a color should always apply it
1328
+ var dot = root.querySelector('[data-dot="' + rid + '"]');
1329
+ if (dot) dot.style.background = r.color;
1330
+ var cb = root.querySelector('[data-rid="' + rid + '"][data-fld="useColor"]');
1331
+ if (cb) cb.checked = true;
1332
+ }
1333
+ else return;
1334
+ applyAll(regions);
1335
+ persist();
1336
+ }
1337
+ root.addEventListener("input", onField);
1338
+ root.addEventListener("change", onField);
1339
+
1340
+ // ---- unified controls ----
1341
+ function onUnified(e) {
1342
+ var el = e.target;
1343
+ var u = el.getAttribute("data-u");
1344
+ var fld = el.getAttribute("data-u-fld");
1345
+ if (!u || !fld) return;
1346
+ var sect = u === "uc" ? "uc" : "uf";
1347
+ if (fld === "enabled") state.unified[sect].enabled = el.checked;
1348
+ else if (fld === "color") state.unified[sect].color = el.value;
1349
+ else if (fld === "en" || fld === "zh") state.unified[sect][fld] = el.value;
1350
+ else if (fld === "size") state.unified[sect].size = clamp(el.value, 12, 22);
1351
+ else if (fld === "weight") state.unified[sect].weight = clamp(el.value, 100, 900);
1352
+ else if (fld === "italic") state.unified[sect].italic = el.checked;
1353
+ else return;
1354
+ applyAll(regions);
1355
+ persist();
1356
+ }
1357
+ root.addEventListener("input", onUnified);
1358
+ root.addEventListener("change", onUnified);
1359
+
1360
+ // ---- theme select: auto-fill name input ----
1361
+ var themeSel = root.querySelector("#dsh-fe-theme-sel");
1362
+ if (themeSel) {
1363
+ themeSel.addEventListener("change", function () {
1364
+ var inp = root.querySelector("#dsh-fe-theme-name");
1365
+ if (inp && themeSel.value) inp.value = themeSel.value;
1366
+ });
1367
+ }
1368
+
1369
+ // ---- delegated actions ----
1370
+ root.addEventListener("click", function (e) {
1371
+ var btn = e.target.closest ? e.target.closest("[data-act]") : null;
1372
+ if (!btn) return;
1373
+ var act = btn.getAttribute("data-act");
1374
+ var rid = btn.getAttribute("data-rid");
1375
+ var el = btn;
1376
+
1377
+ if (act === "resetAll") {
1378
+ regions = DEFAULT_REGIONS();
1379
+ renderCards();
1380
+ applyAll(regions);
1381
+ persist();
1382
+ return;
1383
+ }
1384
+ if (act === "pickMode") { pickRegion(); return; }
1385
+ if (act === "recordToggle") { recToggle(!RECORDING); return; }
1386
+ if (act === "replacePicked") { replaceWithPicked(); return; }
1387
+ if (act === "restoreDefault") { restoreDefault(); return; }
1388
+ if (act === "saveConfig") { saveConfig(); return; }
1389
+ if (act === "randomAllFonts") { randomizeAllFonts(); return; }
1390
+ if (act === "randomAllColors") { randomizeAllColors(); return; }
1391
+ if (act === "themeSave") { saveTheme(); return; }
1392
+ if (act === "themeRename") { themeRename(); return; }
1393
+ if (act === "themeApply") { applyTheme(); return; }
1394
+ if (act === "themeDel") { deleteTheme(); return; }
1395
+ if (act === "themeClearAll") { state.themes = []; renderThemeSel(); persist(); showToast("已清空全部主题", "ok"); return; }
1396
+ if (act === "reloadUI") {
1397
+ // 强制 Chromium 加载新安装的字体,并刷新下拉列表(不关闭面板)
1398
+ var names = [];
1399
+ (state.customFonts || []).forEach(function (f) {
1400
+ if (f.en) names.push(f.en);
1401
+ if (f.zh) names.push(f.zh);
1402
+ });
1403
+ if (document.fonts && document.fonts.load) {
1404
+ names.forEach(function (n) {
1405
+ try { document.fonts.load('16px "' + n + '"').catch(function(){}); } catch (e) {}
1406
+ });
1407
+ }
1408
+ renderCards();
1409
+ renderThemeSel();
1410
+ applyAll(regions);
1411
+ showToast("🔄 已刷新,新字体应已生效", "ok");
1412
+ return;
1413
+ }
1414
+ if (act === "installFont") {
1415
+ var fid = el.getAttribute("data-font-id") || "";
1416
+ var fzh = el.getAttribute("data-font-zh") || "";
1417
+ var fen = el.getAttribute("data-font-en") || "";
1418
+ if (!fid) { showToast("字体信息缺失", "err"); return; }
1419
+ var btn = el; btn.textContent = "⏳ 下载中 0%"; btn.disabled = true;
1420
+ var done = false;
1421
+ // 轮询下载进度,更新按钮百分比
1422
+ var poll = setInterval(function () {
1423
+ if (done) return;
1424
+ try {
1425
+ fetch("/dsh-font-enhancer/font-progress?fontId=" + encodeURIComponent(fid), { cache: "no-store" })
1426
+ .then(function (r) { return r.json(); })
1427
+ .then(function (pg) {
1428
+ if (done || !pg) return;
1429
+ if (pg.state === "downloading" && pg.pct >= 0) { btn.textContent = "⏳ 下载中 " + pg.pct + "%"; }
1430
+ else if (pg.state === "done") { clearInterval(poll); }
1431
+ else if (pg.state === "error") { clearInterval(poll); }
1432
+ })
1433
+ .catch(function () {});
1434
+ } catch (e) {}
1435
+ }, 400);
1436
+ try {
1437
+ fetch("/dsh-font-enhancer/install-font", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fontId: fid }) })
1438
+ .then(function (r) { return r.json(); })
1439
+ .then(function (res) {
1440
+ done = true; clearInterval(poll);
1441
+ if (res && res.ok) {
1442
+ if (fzh && !state.customFonts.some(function (x) { return x.zh === fzh; })) state.customFonts.push({ zh: fzh });
1443
+ if (fen && !state.customFonts.some(function (x) { return x.en === fen; })) state.customFonts.push({ en: fen });
1444
+ persist(); renderCards(); renderThemeSel();
1445
+ var dest = res.dest ? ",路径:" + res.dest : "";
1446
+ btn.textContent = "✅ 已安装"; btn.disabled = true;
1447
+ showToast("✅ 字体已" + (res.already ? "存在" : "安装") + ":" + (fzh || fen) + ",已自动添加" + dest, "ok");
1448
+ } else {
1449
+ btn.textContent = "⬇ 重试安装"; btn.disabled = false;
1450
+ showToast("安装失败:" + (res && res.error || "未知错误"), "err");
1451
+ }
1452
+ })
1453
+ .catch(function () { done = true; clearInterval(poll); btn.textContent = "⬇ 重试安装"; btn.disabled = false; showToast("安装失败,请检查网络", "err"); });
1454
+ } catch (e) { done = true; clearInterval(poll); btn.textContent = "⬇ 重试安装"; btn.disabled = false; showToast("安装失败:" + e, "err"); }
1455
+ return;
1456
+ }
1457
+ if (act === "addCustomFont") {
1458
+ var en = el.getAttribute("data-font-en") || "";
1459
+ var zh = el.getAttribute("data-font-zh") || "";
1460
+ if (!en && !zh) { showToast("字体信息缺失", "err"); return; }
1461
+ if (en) { if (!state.customFonts.some(function (f) { return f.en === en; })) state.customFonts.push({ en: en }); }
1462
+ if (zh) { if (!state.customFonts.some(function (f) { return f.zh === zh; })) state.customFonts.push({ zh: zh }); }
1463
+ persist(); renderCards(); renderThemeSel(); showToast("已添加字体" + (en || zh), "ok");
1464
+ return;
1465
+ }
1466
+ if (act === "installCustomFile") {
1467
+ var fileInput = root.querySelector("#dsh-fe-font-file");
1468
+ var file = fileInput && fileInput.files && fileInput.files[0];
1469
+ if (!file) { showToast("请先选择字体文件(.ttf/.otf)", "err"); return; }
1470
+ var okExt = /\.(ttf|otf)$/i.test(file.name);
1471
+ if (!okExt) { showToast("请选择 .ttf 或 .otf 字体文件", "err"); return; }
1472
+ var btn2 = el; btn2.textContent = "⏳ 上传中..."; btn2.disabled = true;
1473
+ var reader = new FileReader();
1474
+ reader.onload = function (e) {
1475
+ var base64 = String(e.target.result).split(",")[1] || "";
1476
+ try {
1477
+ fetch("/dsh-font-enhancer/upload-font", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fileName: file.name, data: base64 }) })
1478
+ .then(function (r) { return r.json(); })
1479
+ .then(function (res) {
1480
+ btn2.textContent = "安装并添加"; btn2.disabled = false;
1481
+ if (res && res.ok) {
1482
+ // 自动添加字体(用文件名去掉扩展名作为字体名)
1483
+ var fam = (res.fontName || file.name.replace(/\.(ttf|otf)$/i, "")).trim();
1484
+ if (fam && !state.customFonts.some(function (x) { return x.en === fam; }) && !state.customFonts.some(function (x) { return x.zh === fam; })) {
1485
+ state.customFonts.push({ en: fam, zh: fam });
1486
+ }
1487
+ persist(); renderCards(); renderThemeSel();
1488
+ var dest = res.dest ? ",路径:" + res.dest : "";
1489
+ showToast("✅ 字体已安装并添加:" + fam + dest, "ok");
1490
+ if (fileInput) fileInput.value = "";
1491
+ } else {
1492
+ showToast("安装失败:" + (res && res.error || "未知错误"), "err");
1493
+ }
1494
+ })
1495
+ .catch(function () { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("上传失败,请重试", "err"); });
1496
+ } catch (e2) { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("安装失败:" + e2, "err"); }
1497
+ };
1498
+ reader.onerror = function () { btn2.textContent = "安装并添加"; btn2.disabled = false; showToast("读取文件失败", "err"); };
1499
+ reader.readAsDataURL(file);
1500
+ return;
1501
+ }
1502
+
1503
+ var r = rid ? findRegion(rid) : null;
1504
+ if (!r) return;
1505
+ if (act === "pickAdd") { pickRegion(rid); return; }
1506
+ if (act === "showScope") { showScope(rid); return; }
1507
+ if (act === "delete") {
1508
+ regions = regions.filter(function (x) {
1509
+ return x.id !== rid;
1510
+ });
1511
+ renderCards();
1512
+ applyAll(regions);
1513
+ persist();
1514
+ } else if (act === "randomFont") {
1515
+ randomizeFonts(rid);
1516
+ } else if (act === "randomColor") {
1517
+ randomizeColor(rid);
1518
+ } else if (act === "reset") {
1519
+ // reset styling + color — keep the calibrated name/selector/group set
1520
+ r.en = "Times New Roman";
1521
+ r.zh = "KaiTi";
1522
+ r.size = 15;
1523
+ r.lh = 1.6;
1524
+ r.weight = 400;
1525
+ r.italic = false;
1526
+ r.useColor = false;
1527
+ r.color = "#e6e6e6";
1528
+ renderCards();
1529
+ applyAll(regions);
1530
+ persist();
1531
+ }
1532
+ });
1533
+
1534
+ document.addEventListener("keydown", function (e) {
1535
+ if (e.key === "Escape" && isOpen()) hide();
1536
+ });
1537
+ window.addEventListener("resize", function () {
1538
+ if (isOpen()) positionPanel();
1539
+ });
1540
+ try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {} // DIAG-3 buildUI complete
1541
+ }
1542
+
1543
+ // ---------- entry ----------
1544
+ // 设置页「悬浮球导航」分区(settings.section,与技能浏览器同款官方机制)。
1545
+ // 让字体球(Aa)的显隐由 font-enhancer 自己在 DSH 设置页控制,不依赖其他插件。
1546
+ var FE_BALL_VIS_KEY = "dsh-font-ball-visible";
1547
+ function feReadStore(key, def) {
1548
+ try { var v = localStorage.getItem(key); return v == null ? def : v === "1"; } catch (e) { return def; }
1549
+ }
1550
+ function feWriteStore(key, val) {
1551
+ try { localStorage.setItem(key, val ? "1" : "0"); } catch (e) {}
1552
+ }
1553
+ function feApplyBallVisibility() {
1554
+ var hidden = !feReadStore(FE_BALL_VIS_KEY, true);
1555
+ var t = document.getElementById("dsh-fe-toggle");
1556
+ if (t) t.style.setProperty("display", hidden ? "none" : "", "important");
1557
+ }
1558
+ // 监听字体球(防热重载后重建时样式/显隐丢失;低频轮询,CSS 已兜底)
1559
+ function feWatchBall() {
1560
+ var tries = 0;
1561
+ var t = setInterval(function () {
1562
+ tries++;
1563
+ try { feApplyBallVisibility(); } catch (e) {}
1564
+ if (tries > 120) clearInterval(t);
1565
+ }, 1000);
1566
+ }
1567
+
1568
+ var feReact = null;
1569
+ try {
1570
+ var feReactRaw = require("react");
1571
+ feReact = (feReactRaw && feReactRaw.default) || feReactRaw;
1572
+ } catch (e) { feReact = null; }
1573
+ if (!feReact) { try { feReact = window.React || null; } catch (e) {} }
1574
+
1575
+ // v1.4.3(2026-09-06 用户要求):分区智能让位 + UI 对齐 skill-browser 版式。
1576
+ // 探测机制:skill-browser 的 🧩 悬浮球 DOM(页面加载即建,不依赖打开设置页)。
1577
+ // 让位机制:延迟注册——skill-browser 在场则不注册分区,独立运行才注册兜底分区。
1578
+ // 独立存在时(未装 skill-browser):完整版「悬浮球导航」分区——字体球开关
1579
+ // + 🎯 一键恢复默认位置 + 推荐插件位(照片显示器 / 技能浏览器)。
1580
+
1581
+ // 恢复字体球默认位置(右下角 right:18px bottom:18px),清 bx/by 持久化。
1582
+ // v1.4.4 推荐位安装徽标(与 skill-browser 同款探测)
1583
+ function feMarkRecBadge(recId, installed) {
1584
+ var row = document.querySelector('[data-rec-id="' + recId + '"]');
1585
+ if (!row || row.querySelector(".fe-rec-badge")) return;
1586
+ var badge = document.createElement("span");
1587
+ badge.className = "fe-rec-badge";
1588
+ badge.textContent = installed ? "已安装" : "未安装";
1589
+ badge.style.cssText = "flex-shrink:0;margin-left:8px;font-size:11px;padding:2px 8px;border-radius:6px;" +
1590
+ (installed ? "background:rgba(120,220,150,.15);border:1px solid rgba(120,220,150,.4);color:#9fe0b5"
1591
+ : "background:rgba(255,120,120,.15);border:1px solid rgba(255,120,120,.35);color:#ffb3b3") +
1592
+ ";white-space:nowrap";
1593
+ var star = row.querySelector("a");
1594
+ if (star) row.insertBefore(badge, star); else row.appendChild(badge);
1595
+ }
1596
+ function feApplyRecBadges() {
1597
+ feMarkRecBadge("font", true); // 本插件在跑,恒已装
1598
+ try {
1599
+ var sbOk = !!document.getElementById("dsh-skill-browser-ball");
1600
+ feMarkRecBadge("skillbrowser", sbOk);
1601
+ } catch (e) { feMarkRecBadge("skillbrowser", false); }
1602
+ try {
1603
+ fetch("/api/image-gallery/list").then(function (r) { feMarkRecBadge("lightbox", r.ok); }).catch(function () { feMarkRecBadge("lightbox", false); });
1604
+ } catch (e) { feMarkRecBadge("lightbox", false); }
1605
+ }
1606
+ function feResetBallToDefault() {
1607
+ try {
1608
+ var data = loadSettings();
1609
+ data.bx = null; data.by = null;
1610
+ saveSettings(data);
1611
+ state.bx = null; state.by = null;
1612
+ } catch (e) {}
1613
+ var t = document.getElementById("dsh-fe-toggle");
1614
+ if (t) {
1615
+ t.style.left = "auto"; t.style.top = "auto";
1616
+ t.style.right = "18px"; t.style.bottom = "18px";
1617
+ }
1618
+ }
1619
+
1620
+ // skill-browser 是否已安装运行(🧩 球元素存在即算,与球显隐无关)
1621
+ function feSkillBrowserPresent() {
1622
+ try { return !!document.getElementById("dsh-skill-browser-ball"); } catch (e) { return false; }
1623
+ }
1624
+
1625
+ function FontSettingsSection(props) {
1626
+ if (!feReact) return null;
1627
+ var rows = [];
1628
+ function mkToggle(id, label, onChange) {
1629
+ return feReact.createElement("label", { key: id, style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", cursor: "pointer" } },
1630
+ feReact.createElement("input", { type: "checkbox", id: id, onChange: onChange }),
1631
+ feReact.createElement("span", null, label));
1632
+ }
1633
+ rows.push(feReact.createElement("div", { key: "head", style: { fontWeight: "600", marginBottom: "4px" } }, "悬浮球导航"));
1634
+ rows.push(mkToggle("fe-set-ball", "字体插件悬浮球(🔤)", function (e) {
1635
+ feWriteStore(FE_BALL_VIS_KEY, e.target.checked);
1636
+ feApplyBallVisibility();
1637
+ }));
1638
+ rows.push(feReact.createElement("button", { key: "fe-reset-pos",
1639
+ onClick: function () { try { feResetBallToDefault(); } catch (e) {} },
1640
+ style: { alignSelf: "flex-start", fontSize: "13px", cursor: "pointer",
1641
+ background: "rgba(255,210,122,.12)", border: "1px solid rgba(255,210,122,.35)",
1642
+ borderRadius: "6px", padding: "6px 12px", color: "#ffd27a" }
1643
+ }, "🎯 一键恢复悬浮球默认位置(右下角)"));
1644
+ rows.push(feReact.createElement("div", { key: "hint", style: { fontSize: "12px", color: "#8b93a8" } },
1645
+ "悬浮球可拖动;本分区就在 DSH 设置中,随时可改悬浮球显隐"));
1646
+ // ── 安装与更新须知(v1.4.4)──
1647
+ rows.push(feReact.createElement("div", { key: "fe-notice-block", style: { marginTop: "12px", padding: "10px 12px", background: "rgba(255,255,255,.03)", border: "1px solid rgba(255,255,255,.06)", borderRadius: "10px", fontSize: "12px", color: "#9aa1b5", lineHeight: "1.7" } },
1648
+ feReact.createElement("div", { style: { fontWeight: "600", color: "#cfd4dc", marginBottom: "4px" } }, "📌 安装与更新须知"),
1649
+ feReact.createElement("div", null, "1. 当前由「字体插件」提供本分区(未检测到技能浏览器)。推荐列表里的技能浏览器等插件装上后,本分区会自动让位,改由对方提供更全的悬浮球导航。"),
1650
+ feReact.createElement("div", null, "2. 推荐插件列表实时显示「已安装 / 未安装」状态:绿色「已安装」= 可直接使用;红色「未安装」= 点击 ⭐ Star 前往仓库,或到插件市场搜索安装。"),
1651
+ feReact.createElement("div", { style: { color: "#e0b0b0" } }, "3. 【重要】安装或更新插件时,若报错含 minimumReleaseAge 或 ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION:这不是插件坏了,是 DSH 对刚发布 npm 包的 24 小时安全观察期(防止刚发布的投毒包)。"),
1652
+ feReact.createElement("div", { style: { color: "#e0b0b0" } }, "4. 遇到 24h 报错的处理:等一天再安装/更新即可自动通过,插件本体没有任何问题。插件作者连发多版时窗口期会顺延一天。"),
1653
+ feReact.createElement("div", null, "5. 悬浮球可拖动换位置;拖到屏幕外找不到时,点上方 🎯 按钮一键复位。")
1654
+ ));
1655
+ // ── 推荐插件(v1.4.4 统一三条清单:字体/技能/照片,探测式安装徽标)──
1656
+ function recRow(key, recId, icon, text, repo) {
1657
+ return feReact.createElement("div", { key: key, "data-rec-id": recId, style: { display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "13px", padding: "3px 0" } },
1658
+ feReact.createElement("span", null, icon + " " + text),
1659
+ feReact.createElement("a", { href: "https://github.com/" + repo, target: "_blank", rel: "noopener", style: { textDecoration: "none", fontSize: "12px", background: "rgba(255,255,255,.06)", border: "1px solid rgba(255,255,255,.12)", borderRadius: "6px", padding: "2px 8px", color: "#ffd27a", cursor: "pointer", whiteSpace: "nowrap" } }, "⭐ Star"));
1660
+ }
1661
+ rows.push(recRow("rec-font", "font", "🔤", "字体插件 — 区域自定义字体/颜色/字号", "loyalchiiina/dsh-font-enhancer"));
1662
+ rows.push(recRow("rec-skillbrowser", "skillbrowser", "🧩", "技能浏览器 — 悬浮球面板浏览共享技能库 + 失效台账", "loyalchiiina/dsh-skill-browser"));
1663
+ rows.push(recRow("rec-lightbox", "lightbox", "🖼️", "照片显示器 — 聊天图片灯箱(放大/下载/浏览)", "loyalchiiina/dsh-chat-image-lightbox"));return feReact.createElement("div", { "data-fe-slot": "1", style: { display: "flex", flexDirection: "column", gap: "8px", padding: "8px 0" } }, rows);
1664
+ }
1665
+
1666
+ function registerSettings(ctx) {
1667
+ if (!ctx || !ctx.slots || !feReact) return false;
1668
+ try {
1669
+ ctx.slots.inject("settings.section", function () {
1670
+ return ctx.slots.register({
1671
+ name: "settings.section",
1672
+ id: "dsh-font-enhancer",
1673
+ order: 23,
1674
+ label: function () { return "悬浮球导航"; },
1675
+ inject: function () { return {}; }
1676
+ }, FontSettingsSection);
1677
+ });
1678
+ return true;
1679
+ } catch (e) { return false; }
1680
+ }
1681
+
1682
+ var inject = ["slots", "locale"];
1683
+ var __feSectionRegistered = false;
1684
+ // v1.4.3 延迟注册让位:skill-browser 在场则永不注册(设置页不会出现空标题分区);
1685
+ // 60s 内确认独立存在才注册完整版分区。两插件装卸都以重启为界,轮询窗口足够。
1686
+ function feDelayedRegister(ctx) {
1687
+ var tries = 0;
1688
+ var t = setInterval(function () {
1689
+ tries++;
1690
+ try {
1691
+ if (__feSectionRegistered) { clearInterval(t); return; }
1692
+ if (feSkillBrowserPresent()) {
1693
+ clearInterval(t);
1694
+ try { console.log("[dsh-font-enhancer] settings section skipped (skill-browser present)"); } catch (e) {}
1695
+ return;
1696
+ }
1697
+ if (tries >= 30) { // 60s 仍无 skill-browser → 独立运行,注册兜底分区
1698
+ clearInterval(t);
1699
+ if (registerSettings(ctx)) { __feSectionRegistered = true; setTimeout(feApplyRecBadges, 1500); setInterval(feApplyRecBadges, 5000); }
1700
+ }
1701
+ } catch (e) {}
1702
+ }, 2000);
1703
+ }
1704
+ function apply(ctx) {
1705
+ try { feDelayedRegister(ctx); } catch (e) {}
1706
+ if (document.readyState === "loading") {
1707
+ document.addEventListener("DOMContentLoaded", buildUI);
1708
+ } else {
1709
+ buildUI();
1710
+ }
1711
+ feWatchBall();
1712
+ feApplyBallVisibility();
1713
+ // v1.4.3 推荐位安装状态徽标:对每个推荐插件探测是否已安装,
1714
+ // 未安装 → 行内注入「未安装」徽标(已安装则不动,保持干净)。
1715
+ setTimeout(function () {
1716
+ var markNotInstalled = function (recId) {
1717
+ var row = document.querySelector('[data-rec-id="' + recId + '"]');
1718
+ if (!row || row.querySelector(".fe-rec-badge")) return;
1719
+ var badge = document.createElement("span");
1720
+ badge.className = "fe-rec-badge";
1721
+ badge.textContent = "未安装";
1722
+ badge.style.cssText = "flex-shrink:0;margin-left:8px;font-size:11px;padding:2px 8px;border-radius:6px;background:rgba(255,120,120,.15);border:1px solid rgba(255,120,120,.35);color:#ffb3b3;white-space:nowrap";
1723
+ var star = row.querySelector("a");
1724
+ if (star) row.insertBefore(badge, star); else row.appendChild(badge);
1725
+ };
1726
+ // skill-browser:client 运行 = 球元素在 DOM(字体分区出现时通常未装 → 徽标显示)
1727
+ setTimeout(function () {
1728
+ try { if (!document.getElementById("dsh-skill-browser-ball")) markNotInstalled("skillbrowser"); } catch (e) {}
1729
+ }, 1500);
1730
+ // lightbox:宿主路由 /api/image-gallery/list 通 = 已安装
1731
+ try {
1732
+ fetch("/api/image-gallery/list").then(function (r) { if (!r.ok) markNotInstalled("lightbox"); }).catch(function () { markNotInstalled("lightbox"); });
1733
+ } catch (e) { markNotInstalled("lightbox"); }
1734
+ }, 800);
1735
+ }
1736
+
1737
+ exports.apply = apply;
1738
+ exports.inject = inject;
1739
+ return module.exports;
1740
+ }
1741
+ });