dsh-happy-reader 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -18
- package/icon.svg +1 -0
- package/lib/client.js +1263 -368
- package/lib/index.js +3 -11
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +8 -4
package/lib/client.js
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
|
-
// dsh-happy-reader(Happy Reader)——
|
|
1
|
+
// dsh-happy-reader(Happy Reader)—— 浏览器半边(官方客户端插件包格式:
|
|
2
|
+
// __ModuleLoader__.load({ id: 包名, factory }),factory 导出 { inject: ['slots'], apply })。
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// ③ 字号 10–40px(10–22 与官方设置直连,超出部分用缩放层补齐;代码块内容随之同步缩放)
|
|
7
|
-
// ④ 内容宽度(官方范围内与官方手柄同键同步;超出官方钳制上限时扩展,拖手柄即交还)
|
|
8
|
-
// ⑤ 字体选择(正文字体·中文/西文分设 + 代码字体,只列出本机已装字体)
|
|
9
|
-
// ⑥ 全屏(网页全屏 API;Esc 或再点开关退出,状态与真实全屏双向同步)
|
|
10
|
-
// ⑦ 行距 / 字距(面板最下方;1.0× / 0px 即官方原样)
|
|
11
|
-
// ⑧ 面板可拖动 / 可拉拽高度(双击标题栏复位为自动布局;位置与高度都会记住)
|
|
12
|
-
// ⑨ 复制为 Markdown(选中消息内容复制时,公式还原为 LaTeX 源码、代码转围栏等)
|
|
4
|
+
// 功能:隐藏输入区/上边栏、字号 10–40(10–22 直连官方,超出缩放补齐)、内容宽度、
|
|
5
|
+
// 字体(中英分设 + 官方正文/代码/终端角色镜像)、全屏、行距/字距、
|
|
6
|
+
// 面板拖动/拉高、复制为 Markdown。
|
|
13
7
|
//
|
|
14
|
-
//
|
|
15
|
-
// ·
|
|
16
|
-
// · <style
|
|
17
|
-
// ·
|
|
18
|
-
// ·
|
|
19
|
-
//
|
|
20
|
-
// ·
|
|
21
|
-
// 标签→Markdown 映射由 vendored turndown + 官方 gfm 插件承担(均 MIT,逐字内联于文件尾)
|
|
8
|
+
// 关键约束:
|
|
9
|
+
// · 隐藏输入区用 height:0(官方滚动锚定依赖几何),不用 display:none
|
|
10
|
+
// · <style>.disabled 必须挂载后再设(未连接时设置会被 CSSOM 静默丢弃)
|
|
11
|
+
// · 文本 UI 只走官方 shell.overlay / settings.general.item 槽位、locale 与主题 token
|
|
12
|
+
// · 官方字号/字体走 theme.setFontSize / setFontFamily 角色接口;中英分字用
|
|
13
|
+
// @font-face(local()+unicode-range) 补官方单一字族表达不了的部分
|
|
14
|
+
// · 选区复制:KaTeX annotation 还原公式,turndown + gfm 生成 Markdown(文件尾 vendored)
|
|
22
15
|
//
|
|
23
|
-
// 本文件采用官方客户端插件包格式:外壳加载时全局 __ModuleLoader__ 已就绪,
|
|
24
|
-
// 登记 { id: 包名, factory };factory 在插件挂载时执行,导出的 apply 即插件本体。
|
|
25
16
|
window.__ModuleLoader__.load({
|
|
26
17
|
id: 'dsh-happy-reader',
|
|
27
18
|
factory: (require) => {
|
|
@@ -29,7 +20,9 @@ window.__ModuleLoader__.load({
|
|
|
29
20
|
var exports = module.exports
|
|
30
21
|
|
|
31
22
|
// ── 常量 ────────────────────────────────────────────────────────────
|
|
32
|
-
const VERSION = '0.
|
|
23
|
+
const VERSION = '0.3.1'
|
|
24
|
+
const INSTANCE_TOKEN = {}
|
|
25
|
+
const GROUPS_VERSION = 3
|
|
33
26
|
const STATE_KEY = 'dsh.reader-boost.v1'
|
|
34
27
|
const FAB_ID = 'dsrb-fab'
|
|
35
28
|
const PANEL_ID = 'dsrb-panel'
|
|
@@ -41,7 +34,7 @@ window.__ModuleLoader__.load({
|
|
|
41
34
|
const FS_STYLE_ID = 'dsrb-fullscreen'
|
|
42
35
|
const FAB_SIZE = 40
|
|
43
36
|
const PANEL_WIDTH = 280
|
|
44
|
-
const PANEL_MAX_H =
|
|
37
|
+
const PANEL_MAX_H = 560 // 面板高度锁:新增选项不再增高面板,超出部分由内部滚动查看
|
|
45
38
|
const PANEL_MIN_H = 180 // 面板拉拽高度的下限
|
|
46
39
|
const BORDER_Y = 2 // 面板上下边框各 1px(外框高 = 内容高 + 2,定位预算统一用它)
|
|
47
40
|
|
|
@@ -54,6 +47,12 @@ window.__ModuleLoader__.load({
|
|
|
54
47
|
const FONT_MIN = 10
|
|
55
48
|
const FONT_MAX = 40
|
|
56
49
|
const FONT_OFFICIAL_MAX = 22
|
|
50
|
+
// 官方代码字号范围(0.2.2 起 theme.setFontSize('code') 只接受 10–16 的整数)
|
|
51
|
+
const CODE_FONT_MIN = 10
|
|
52
|
+
const CODE_FONT_MAX = 16
|
|
53
|
+
// 官方终端字号范围(theme.setFontSize('terminal') 10–20)
|
|
54
|
+
const TERMINAL_FONT_MIN = 10
|
|
55
|
+
const TERMINAL_FONT_MAX = 20
|
|
57
56
|
|
|
58
57
|
// 行距倍率与字距范围(0.1 步进;边界值 = 完全还原官方排版)
|
|
59
58
|
const LINEH_MIN = 1
|
|
@@ -66,105 +65,145 @@ window.__ModuleLoader__.load({
|
|
|
66
65
|
|
|
67
66
|
// ── 样式 ────────────────────────────────────────────────────────────
|
|
68
67
|
const CSS = `
|
|
69
|
-
.dsrb-
|
|
70
|
-
|
|
68
|
+
.dsrb-slot-host{pointer-events:none}
|
|
69
|
+
/* 表单控件默认不继承字体:统一继承宿主/面板字体 */
|
|
70
|
+
.dsrb-panel button,.dsrb-panel input,.dsrb-panel select,.dsrb-settings select{font-family:inherit}
|
|
71
|
+
/* 旧外壳无官方 overlay 层时的回退宿主:自成视口层,保证不被页面内容盖住 */
|
|
72
|
+
.dsrb-slot-host.dsrb-standalone{position:fixed;inset:0;z-index:9999;pointer-events:none}
|
|
73
|
+
.dsrb-fab{position:fixed;box-sizing:border-box;width:40px;height:40px;border-radius:12px;
|
|
74
|
+
border:1px solid var(--dsw-alias-border-l3,rgba(128,128,128,.35));
|
|
75
|
+
background:var(--dsw-alias-button-floating-fill,rgba(28,28,30,.85));
|
|
76
|
+
color:var(--dsw-alias-label-primary,#f5f5f5);
|
|
71
77
|
font:600 15px/1 system-ui,-apple-system,"Segoe UI",sans-serif;display:flex;align-items:center;
|
|
72
|
-
justify-content:center;cursor:grab;user-select:none;-webkit-user-select:none;
|
|
73
|
-
box-shadow:0 4px 14px rgba(0,0,0,.28);backdrop-filter:blur(6px)}
|
|
78
|
+
justify-content:center;cursor:grab;user-select:none;-webkit-user-select:none;pointer-events:auto;
|
|
79
|
+
box-shadow:var(--dsw-elevation-panel,0 4px 14px rgba(0,0,0,.28));backdrop-filter:blur(6px)}
|
|
80
|
+
.dsrb-fab:hover{background:var(--dsw-alias-button-floating-hover,rgba(40,40,42,.92))}
|
|
74
81
|
.dsrb-fab:active{cursor:grabbing}
|
|
75
|
-
.dsrb-panel{position:fixed;
|
|
76
|
-
max-height:calc(100vh - 18px);display:flex;flex-direction:column;overflow:hidden;
|
|
77
|
-
border:1px solid rgba(128,128,128,.3)
|
|
82
|
+
.dsrb-panel{position:fixed;width:${PANEL_WIDTH}px;padding:0;border-radius:var(--dsw-radius-lg,16px);
|
|
83
|
+
max-height:calc(100vh - 18px);display:flex;flex-direction:column;overflow:hidden;pointer-events:auto;
|
|
84
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));
|
|
85
|
+
background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2,rgba(28,28,30,.96)));
|
|
86
|
+
color:var(--dsw-alias-label-primary,#ececec);
|
|
78
87
|
font:13px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
79
|
-
box-shadow:0 8px 28px rgba(0,0,0,.38);backdrop-filter:blur(8px)}
|
|
88
|
+
box-shadow:var(--dsw-elevation-prominent,var(--dsw-elevation-panel,0 8px 28px rgba(0,0,0,.38)));backdrop-filter:blur(8px)}
|
|
80
89
|
/* 横向内边距放在滚动区:面板零内边距,滚动条才能贴住面板右缘 */
|
|
81
90
|
.dsrb-body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:12px 14px}
|
|
82
91
|
.dsrb-body::-webkit-scrollbar{width:8px}
|
|
83
|
-
.dsrb-body::-webkit-scrollbar-thumb{background:
|
|
84
|
-
.dsrb-body:hover::-webkit-scrollbar-thumb{background:rgba(128,128,128,.45)}
|
|
85
|
-
.dsrb-body:hover::-webkit-scrollbar-thumb:hover{background:rgba(128,128,128,.65)}
|
|
92
|
+
.dsrb-body::-webkit-scrollbar-thumb{background:transparent;border-radius:4px;transition:background .15s}
|
|
93
|
+
.dsrb-body:hover::-webkit-scrollbar-thumb{background:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.45))}
|
|
94
|
+
.dsrb-body:hover::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-scrollbar-hover-l2,rgba(128,128,128,.65))}
|
|
86
95
|
.dsrb-body::-webkit-scrollbar-track{background:transparent}
|
|
96
|
+
@media (hover:none){.dsrb-body::-webkit-scrollbar-thumb{background:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.45))}}
|
|
87
97
|
.dsrb-head{display:flex;align-items:center;justify-content:space-between;margin:0 0 4px;cursor:grab}
|
|
88
98
|
.dsrb-head:active{cursor:grabbing}
|
|
89
|
-
.dsrb-resize{flex:none;height:10px;cursor:ns-resize;border-radius:0 0
|
|
90
|
-
.dsrb-resize-top{flex:none;height:10px;cursor:ns-resize;border-radius:
|
|
91
|
-
.dsrb-resize:hover,.dsrb-resize-top:hover{background:rgba(255,255,255,.07)}
|
|
92
|
-
.dsrb-head h4{margin:0;font-size:
|
|
93
|
-
.dsrb-headHint{font-size:11.5px;color
|
|
94
|
-
.dsrb-x{width:22px;height:22px;padding:0;border-radius:6px;
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
.dsrb-resize{flex:none;height:10px;cursor:ns-resize;border-radius:0 0 var(--dsw-radius-lg,16px) var(--dsw-radius-lg,16px)}
|
|
100
|
+
.dsrb-resize-top{flex:none;height:10px;cursor:ns-resize;border-radius:var(--dsw-radius-lg,16px) var(--dsw-radius-lg,16px) 0 0}
|
|
101
|
+
.dsrb-resize:hover,.dsrb-resize-top:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.07))}
|
|
102
|
+
.dsrb-head h4{margin:0;font-size:14px;font-weight:600}
|
|
103
|
+
.dsrb-headHint{font-size:11.5px;color:var(--dsw-alias-label-tertiary,#8d8d8d);margin-left:auto;margin-right:8px;white-space:nowrap}
|
|
104
|
+
.dsrb-x{width:22px;height:22px;padding:0;border-radius:6px;
|
|
105
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));
|
|
106
|
+
background:transparent;color:var(--dsw-alias-label-secondary,#ddd);font-size:13px;line-height:1;cursor:pointer}
|
|
107
|
+
.dsrb-x:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.16))}
|
|
97
108
|
.dsrb-row{display:flex;align-items:flex-start;gap:10px;padding:8px 0;
|
|
98
|
-
border-top:1px solid rgba(128,128,128,.14)}
|
|
99
|
-
.dsrb-head+.dsrb-row{border-top:0}
|
|
109
|
+
border-top:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.14))}
|
|
100
110
|
.dsrb-row-click{cursor:pointer}
|
|
101
|
-
.dsrb-row-click:hover{background:rgba(255,255,255,.045)}
|
|
111
|
+
.dsrb-row-click:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.045))}
|
|
102
112
|
.dsrb-rowMain{flex:1;min-width:0}
|
|
103
113
|
.dsrb-rowTitle{font-weight:600}
|
|
104
|
-
.dsrb-rowTitle b{color
|
|
105
|
-
.dsrb-rowSub{color
|
|
106
|
-
|
|
114
|
+
.dsrb-rowTitle b{color:var(--dsw-alias-label-primary,#fff);font-weight:600}
|
|
115
|
+
.dsrb-rowSub{color:var(--dsw-alias-label-secondary,#9a9a9a);font-size:12px;margin-top:2px}
|
|
116
|
+
/* 开关:对齐官方 role=switch 视觉(36×20、2px 内边距、16px 滑块、16px 行程) */
|
|
117
|
+
.dsrb-switch{position:relative;flex:none;width:36px;height:20px;padding:2px;box-sizing:border-box;
|
|
118
|
+
margin-top:1px;cursor:pointer;border-radius:999px;background:var(--dsw-alias-border-l3,rgba(120,120,120,.35));
|
|
119
|
+
transition:background .12s}
|
|
107
120
|
.dsrb-switch input{position:absolute;inset:0;opacity:0;margin:0;pointer-events:none}
|
|
108
|
-
.dsrb-switch span{position:absolute;inset:0;border-radius:
|
|
109
|
-
transition:background .15s;pointer-events:none}
|
|
121
|
+
.dsrb-switch span{position:absolute;inset:0;border-radius:999px;pointer-events:none}
|
|
110
122
|
.dsrb-switch span:after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;
|
|
111
|
-
border-radius:50%;background
|
|
112
|
-
.dsrb-switch input:checked+span{background
|
|
113
|
-
.dsrb-switch
|
|
123
|
+
border-radius:50%;background:var(--dsw-alias-switch-thumb,#fff);transition:transform .12s}
|
|
124
|
+
.dsrb-switch input:checked+span:after{transform:translateX(16px);background:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
125
|
+
.dsrb-switch:has(input:checked){background:var(--dsw-alias-brand-primary,#2f7af0)}
|
|
126
|
+
.dsrb-switch:has(input:focus-visible){outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#2f7af0));outline-offset:2px}
|
|
114
127
|
.dsrb-stepper{flex:none;display:flex;gap:6px;align-items:center}
|
|
115
|
-
.dsrb-stepper button{width:
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
.dsrb-stepper button{width:30px;height:30px;border-radius:8px;
|
|
129
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:transparent;
|
|
130
|
+
color:var(--dsw-alias-label-primary,#eee);font-size:14px;line-height:1;cursor:pointer}
|
|
131
|
+
.dsrb-stepper button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.14))}
|
|
118
132
|
.dsrb-stepper button:disabled{opacity:.45;cursor:default}
|
|
119
133
|
.dsrb-widthRow{display:flex;align-items:center;gap:8px;margin-top:7px}
|
|
120
|
-
.dsrb-widthRow input[type=range]{flex:1;min-width:0}
|
|
134
|
+
.dsrb-widthRow input[type=range]{flex:1;min-width:0;accent-color:var(--dsw-alias-brand-primary,#2f7af0)}
|
|
121
135
|
.dsrb-widthRow button{flex:none;height:24px;padding:0 10px;border-radius:8px;
|
|
122
|
-
border:1px solid rgba(128,128,128,.3);background:
|
|
123
|
-
font-size:12px;cursor:pointer}
|
|
124
|
-
.dsrb-widthRow button:hover{background:rgba(255,255,255,.14)}
|
|
136
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:transparent;
|
|
137
|
+
color:var(--dsw-alias-label-primary,#eee);font-size:12px;cursor:pointer}
|
|
138
|
+
.dsrb-widthRow button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.14))}
|
|
125
139
|
.dsrb-widthRow button:disabled{opacity:.45;cursor:default}
|
|
126
|
-
.dsrb-select{width:100%;margin-top:7px;height:
|
|
127
|
-
border:1px solid rgba(128,128,128,.3)
|
|
140
|
+
.dsrb-select{box-sizing:border-box;width:100%;margin-top:7px;height:30px;padding:0 8px;border-radius:8px;
|
|
141
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));
|
|
142
|
+
background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#eee);
|
|
128
143
|
font-size:12px;cursor:pointer}
|
|
129
|
-
.dsrb-select:focus{outline:
|
|
130
|
-
.dsrb-select option{background
|
|
144
|
+
.dsrb-select:focus{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#2f7af0));outline-offset:-2px}
|
|
145
|
+
.dsrb-select option{background:var(--dsw-alias-bg-layer-3,#1c1c1e);color:var(--dsw-alias-label-primary,#eee)}
|
|
131
146
|
.dsrb-pair{display:flex;gap:8px;margin-top:7px}
|
|
132
147
|
.dsrb-pairCol{flex:1;min-width:0}
|
|
133
|
-
.dsrb-mini{font-size:11px;color
|
|
148
|
+
.dsrb-mini{font-size:11px;color:var(--dsw-alias-label-secondary,#9a9a9a);margin-bottom:3px}
|
|
134
149
|
.dsrb-pairCol .dsrb-select{margin-top:0}
|
|
135
|
-
/*
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
:
|
|
150
|
+
/* 字体设置折叠组(正文字体 / 代码字体 / 终端字体):标题行整条可点,chevron 指示展开态 */
|
|
151
|
+
.dsrb-group{display:flex;flex-direction:column;margin:6px 0;border:1px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l2,rgba(128,128,128,.22)));
|
|
152
|
+
border-radius:var(--dsw-radius-md,12px);overflow:hidden;
|
|
153
|
+
background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#232324));
|
|
154
|
+
background:color-mix(in srgb,var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#232324)) 78%,transparent)}
|
|
155
|
+
.dsrb-groupHead{display:flex;align-items:center;gap:8px;box-sizing:border-box;width:100%;padding:10px 12px;border:0;background:transparent;
|
|
156
|
+
color:inherit;font:inherit;text-align:left;cursor:pointer}
|
|
157
|
+
.dsrb-groupHead:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.045))}
|
|
158
|
+
.dsrb-groupHead:focus-visible,.dsrb-fab:focus-visible,.dsrb-x:focus-visible,.dsrb-stepper button:focus-visible,.dsrb-widthRow button:focus-visible{
|
|
159
|
+
outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#2f7af0));outline-offset:-2px}
|
|
160
|
+
.dsrb-groupBody .dsrb-row{padding:8px 12px}
|
|
161
|
+
.dsrb-groupTitle{flex:1;min-width:0;font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
162
|
+
.dsrb-groupSub{flex:none;max-width:55%;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-secondary,#9a9a9a);font-size:12px;white-space:nowrap}
|
|
163
|
+
.dsrb-chevron{flex:none;width:20px;height:20px;position:relative;color:var(--dsw-alias-label-tertiary,#8d8d8d)}
|
|
164
|
+
.dsrb-chevron:before{content:'';position:absolute;left:5px;top:5px;width:6px;height:6px;
|
|
165
|
+
border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
|
|
166
|
+
transform:rotate(45deg);transition:transform var(--ds-transition-duration-fast,.1s) var(--ds-ease-in-out,ease)}
|
|
167
|
+
.dsrb-groupHead[aria-expanded="true"] .dsrb-chevron:before{transform:rotate(-135deg);top:8px}
|
|
168
|
+
.dsrb-groupBody{display:flex;flex-direction:column}
|
|
169
|
+
.dsrb-input{width:100%;margin-top:7px;height:30px;padding:0 8px;box-sizing:border-box;border-radius:8px;
|
|
170
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));
|
|
171
|
+
background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#eee);
|
|
172
|
+
font-size:12px}
|
|
173
|
+
.dsrb-input:focus{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#2f7af0));outline-offset:-2px}
|
|
174
|
+
/* 官方「通用设置」内的字体行(settings.general.item):沿用官方行距/字号/分隔线规格 */
|
|
175
|
+
.dsrb-settings{width:100%}
|
|
176
|
+
.dsrb-set-row{padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2);display:flex;flex-direction:column;gap:8px}
|
|
177
|
+
.dsrb-set-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
|
|
178
|
+
.dsrb-set-desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
|
|
179
|
+
.dsrb-set-select{box-sizing:border-box;width:100%;height:36px;padding:0 10px;border-radius:var(--dsw-radius-md,12px);
|
|
180
|
+
border:1px solid transparent;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-module-platform,rgba(128,128,128,.12)));
|
|
181
|
+
color:var(--dsw-alias-label-primary);font-size:14px;line-height:1.4;font-family:inherit;cursor:pointer}
|
|
182
|
+
.dsrb-set-select:focus{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#2f7af0));outline-offset:-2px}
|
|
183
|
+
.dsrb-set-select option{background:var(--dsw-alias-bg-layer-3,#1c1c1e);color:var(--dsw-alias-label-primary,#eee)}
|
|
139
184
|
`
|
|
140
185
|
// 隐藏输入区(关键:height:0 而非 display:none —— 官方滚动锚定依赖该元素的几何信息)
|
|
141
186
|
const HIDE_CSS = '[data-composer-seat]{height:0!important;min-height:0!important;overflow:hidden!important}'
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// 各带关键控件(插件页标题含"添加插件"按钮、嵌入页头含唯一返回按钮),不得误伤
|
|
145
|
-
// (v0.2.6 修:旧写法"匹配全部再排除对话框"曾把插件页标题整条隐藏,安装入口消失)。
|
|
146
|
-
// 对话框排除条款保留:弹出层内永不隐藏(防锁死返回,v0.10.4)。
|
|
187
|
+
// 隐藏上边栏:正向判据(官方 conversation.header 槽位宿主带 data-conversation-header-leading,
|
|
188
|
+
// 全应用仅此一处);排除对话框内,避免误伤插件页/嵌入页的头部。
|
|
147
189
|
const HIDE_TOP_CSS = 'header[data-window-drag]:has(> [data-conversation-header-leading]):not([role="dialog"] *){display:none!important}'
|
|
148
190
|
// 字号缩放层(!important 样式表仅在需要缩放时启用;官方内联值原样保留在其下)
|
|
149
191
|
const FONT_CSS = `body{--dsh-content-font-size:calc(var(--dsrb-font-base,14px)*var(--dsrb-font-scale,1))!important}`
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// 全屏隐藏菜单;规则由 html[data-dsrb-fullscreen] 门控,退出全屏即完全还原。
|
|
154
|
-
// 注:.BynINW_* 为应用构建期类名(随应用更新可能变;失效即表现为全屏残留,
|
|
155
|
-
// 按新构建重新对齐即可——无更稳定锚点可用,属原则②下已接受的取舍)。
|
|
192
|
+
// 全屏补偿:官方 40px 窗口预留条在网页全屏下仍占位,需逐项清零;不能归零
|
|
193
|
+
// --dsh-windows-titlebar-height(悬浮按钮/菜单的定位基准)。.mP3x-q_* 是构建期类名,
|
|
194
|
+
// 无稳定锚点,升级后按新构建重新对齐;规则由 html[data-dsrb-fullscreen] 门控。
|
|
156
195
|
const FS_CSS = [
|
|
157
196
|
// 镜像官方 html[data-windows-titlebar][data-fullscreen]:overlay 顶距 20px、chrome 顶距 0
|
|
158
197
|
'html[data-windows-titlebar][data-dsrb-fullscreen]{--dsh-frame-overlay-top:20px;--dsh-frame-chrome-top:0px}',
|
|
159
198
|
// frame 的 40px 预留条清零 → 两侧列内容顶到屏幕最上缘
|
|
160
|
-
'html[data-dsrb-fullscreen] .
|
|
161
|
-
'html[data-dsrb-fullscreen] .
|
|
199
|
+
'html[data-dsrb-fullscreen] .mP3x-q_frame{padding-top:0!important}',
|
|
200
|
+
'html[data-dsrb-fullscreen] .mP3x-q_frame::before{height:0!important}',
|
|
162
201
|
// 收起态:内容列左上圆角归零(收起时圆角在 (0,0) 露出 frame 底色成为缺口)
|
|
163
|
-
'html[data-dsrb-fullscreen]:has([data-sidebar-collapsed=true]) .
|
|
202
|
+
'html[data-dsrb-fullscreen]:has([data-sidebar-collapsed=true]) .mP3x-q_centerCol{border-radius:0!important}',
|
|
164
203
|
// 展开态:侧栏列自留 40px 顶部,悬浮按钮下方内容位置与窗口态保持一致
|
|
165
|
-
'html[data-dsrb-fullscreen] .
|
|
204
|
+
'html[data-dsrb-fullscreen] .mP3x-q_sidebarCol{padding-top:var(--dsh-windows-titlebar-height)}',
|
|
166
205
|
// 侧栏拖拽手柄随之顶到 0(其 top 同样以 var 为基准)
|
|
167
|
-
'html[data-dsrb-fullscreen] .
|
|
206
|
+
'html[data-dsrb-fullscreen] .mP3x-q_handle{top:0!important}',
|
|
168
207
|
// 全屏隐藏 preload"应用/编辑"菜单(退出全屏即恢复;无键盘通道,不影响 Esc 退出)
|
|
169
208
|
'html[data-dsrb-fullscreen] [data-windows-menu]{display:none!important}'
|
|
170
209
|
].join('')
|
|
@@ -177,7 +216,8 @@ window.__ModuleLoader__.load({
|
|
|
177
216
|
function readState() {
|
|
178
217
|
try {
|
|
179
218
|
const raw = localStorage.getItem(STATE_KEY)
|
|
180
|
-
|
|
219
|
+
const value = raw ? JSON.parse(raw) : null
|
|
220
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
181
221
|
} catch {
|
|
182
222
|
return {}
|
|
183
223
|
}
|
|
@@ -238,15 +278,16 @@ window.__ModuleLoader__.load({
|
|
|
238
278
|
}
|
|
239
279
|
return out
|
|
240
280
|
}
|
|
281
|
+
// 代码字体候选(仅列本机已装;选中后经官方 setFontFamily('code') 写入,不再自建 @font-face)
|
|
241
282
|
const CODE_FONTS = [
|
|
242
|
-
{ id: 'consolas', label: 'Consolas',
|
|
243
|
-
{ id: 'cascadia', label: 'Cascadia Code',
|
|
244
|
-
{ id: 'cascadiamono', label: 'Cascadia Mono',
|
|
245
|
-
{ id: 'courier', label: 'Courier New',
|
|
246
|
-
{ id: 'nsimsun', label: '新宋体',
|
|
247
|
-
{ id: 'firacode', label: 'Fira Code',
|
|
248
|
-
{ id: 'jetbrains', label: 'JetBrains Mono',
|
|
249
|
-
{ id: 'sourcecode', label: 'Source Code Pro',
|
|
283
|
+
{ id: 'consolas', label: 'Consolas', name: 'Consolas' },
|
|
284
|
+
{ id: 'cascadia', label: 'Cascadia Code', name: 'Cascadia Code' },
|
|
285
|
+
{ id: 'cascadiamono', label: 'Cascadia Mono', name: 'Cascadia Mono' },
|
|
286
|
+
{ id: 'courier', label: 'Courier New', name: 'Courier New' },
|
|
287
|
+
{ id: 'nsimsun', label: '新宋体', name: 'NSimSun' },
|
|
288
|
+
{ id: 'firacode', label: 'Fira Code', name: 'Fira Code' },
|
|
289
|
+
{ id: 'jetbrains', label: 'JetBrains Mono', name: 'JetBrains Mono' },
|
|
290
|
+
{ id: 'sourcecode', label: 'Source Code Pro', name: 'Source Code Pro' },
|
|
250
291
|
]
|
|
251
292
|
let fontDetectCtx = null
|
|
252
293
|
// 字体可用性检测:canvas 字宽比对(与三套通用字族任一不同 = 本机已装)。
|
|
@@ -271,6 +312,13 @@ window.__ModuleLoader__.load({
|
|
|
271
312
|
return true
|
|
272
313
|
}
|
|
273
314
|
}
|
|
315
|
+
/** 字体候选与官方值首个名字是否同一字体(英文名 / 本地化名都认)。 */
|
|
316
|
+
function fontMatches(f, first) {
|
|
317
|
+
const want = String(first || '').toLowerCase()
|
|
318
|
+
if (!want) return false
|
|
319
|
+
if (f.name.toLowerCase() === want) return true
|
|
320
|
+
return (f.names || []).some((n) => String(n).toLowerCase() === want)
|
|
321
|
+
}
|
|
274
322
|
function fontChoice(list, id) {
|
|
275
323
|
if (!id) return null
|
|
276
324
|
for (const f of list) if (f.id === id) return f
|
|
@@ -279,6 +327,7 @@ window.__ModuleLoader__.load({
|
|
|
279
327
|
// 官方把整套排印 token(--dsw-font-* / --ds-font-*)定义在 :root 与 body 上。
|
|
280
328
|
// 成功解析一次后缓存;官方主题尚未就绪时返回 null(调用方靠定时器稍后重试)。
|
|
281
329
|
let themeFontDecls = null
|
|
330
|
+
let lastThemePreference
|
|
282
331
|
function readThemeFontDecls() {
|
|
283
332
|
if (themeFontDecls) return themeFontDecls
|
|
284
333
|
const decls = new Map()
|
|
@@ -301,51 +350,35 @@ window.__ModuleLoader__.load({
|
|
|
301
350
|
if (!decls) return null
|
|
302
351
|
const cjk = fontChoice(TEXT_FONTS_CJK, state.textFontCjk)
|
|
303
352
|
const lat = fontChoice(TEXT_FONTS_LATIN, state.textFontLatin)
|
|
304
|
-
const code = fontChoice(CODE_FONTS, state.codeFont)
|
|
305
353
|
const lineH = typeof state.lineHeight === 'number' ? state.lineHeight : null
|
|
306
354
|
const letterSp = typeof state.letterSpacing === 'number' ? state.letterSpacing : null
|
|
307
|
-
if (!cjk && !lat &&
|
|
355
|
+
if (!cjk && !lat && lineH === null && letterSp === null) return null
|
|
308
356
|
const offText = decls.get('--dsw-font-family')
|
|
309
|
-
const offCode = decls.get('--ds-font-family-code')
|
|
310
357
|
// 虚拟字族按 西文→中文 排序:引号/破折号等共享区间由西文面优先接住
|
|
311
358
|
const faces = []
|
|
312
359
|
const famNames = []
|
|
313
360
|
if (lat) { faces.push(faceCss('dsrb-text-latin', lat, LATIN_RANGE)); famNames.push('"dsrb-text-latin"') }
|
|
314
361
|
if (cjk) { faces.push(faceCss('dsrb-text-cjk', cjk, CJK_RANGE)); famNames.push('"dsrb-text-cjk"') }
|
|
315
362
|
const fam = famNames.length ? 'var(--dsrb-family)' : offText
|
|
316
|
-
const cod = code ? 'var(--dsrb-code)' : offCode
|
|
317
363
|
const parts = []
|
|
318
364
|
// 虚拟字族在前(按区间分流);官方原栈殿后兜底:区间外字符与缺字逐字回退
|
|
319
365
|
if (famNames.length) parts.push('--dsrb-family:' + famNames.join(', ') + ', ' + offText)
|
|
320
|
-
if (code) parts.push('--dsrb-code:' + code.css + ', ' + offCode)
|
|
321
366
|
if (famNames.length) parts.push('--dsw-font-family:' + fam)
|
|
322
|
-
if (code) parts.push('--ds-font-family-code:' + cod)
|
|
323
367
|
if (famNames.length) parts.push('font-family:var(--dsrb-family)') // 无自身字族的后代(如用户气泡正文)由继承拿到
|
|
324
368
|
for (const [name, value] of decls) {
|
|
325
369
|
if (name === '--dsw-font-family' || name === '--ds-font-family-code') continue
|
|
326
370
|
let next = value
|
|
327
371
|
if (famNames.length && next.indexOf('var(--dsw-font-family)') !== -1) next = next.split('var(--dsw-font-family)').join(fam)
|
|
328
|
-
if (code && next.indexOf('var(--ds-font-family-code)') !== -1) next = next.split('var(--ds-font-family-code)').join(cod)
|
|
329
|
-
// 代码块类 token 并入字号增量(v0.2.7;官方原版固定 11px 不随字号,基础层已有同式
|
|
330
|
-
// 兜底覆盖,这里只在因字族/行距本就需重发时带上,维持"非必要不重发")。先换形
|
|
331
|
-
//(11px→calc(11px + delta))再走下方行距倍率,与正文 calc 形同一条放大路径。
|
|
332
|
-
if (
|
|
333
|
-
(name === '--dsw-font-markdown-code-block' || name === '--dsw-font-markdown-code-block-small') &&
|
|
334
|
-
(next !== value || lineH !== null)
|
|
335
|
-
) {
|
|
336
|
-
next = next
|
|
337
|
-
.replace(/^(\d+(?:\.\d+)?)px/, 'calc($1px + var(--dsh-content-font-delta))')
|
|
338
|
-
.replace(/(\/\s*)(\d+(?:\.\d+)?)px/, (m, lead, n) => lead + 'calc(' + n + 'px + var(--dsh-content-font-delta))')
|
|
339
|
-
}
|
|
340
372
|
// 行距:官方行高 token 有两种真实形态(简写里的 "/ 行高" 段,均出自 asar 实测)——
|
|
341
|
-
// 正文/标题:"/ calc(Npx + var(--dsh-content-font-delta))"
|
|
342
|
-
//
|
|
343
|
-
//
|
|
373
|
+
// 正文/标题:"/ calc(Npx + var(--dsh-content-font-delta))"
|
|
374
|
+
// 代码类(0.2.2 起):"/ calc(Npx + var(--dsh-code-font-delta))"
|
|
375
|
+
// 其余旧式:"/ Npx"(纯 px)
|
|
376
|
+
// 增量函数的变量名按 --dsh-*-font-delta* 泛化匹配;calc 形先行(其产物不含 "/数字px" 段)。
|
|
344
377
|
if (lineH !== null) {
|
|
345
378
|
next = next
|
|
346
379
|
.replace(
|
|
347
|
-
/(\/\s*)calc\((\d+(?:\.\d+)?)px \+ var\(--dsh-
|
|
348
|
-
(m, lead, n) => lead + 'calc((' + n + 'px + var(--dsh-content-font-delta)) * ' + lineH + ')',
|
|
380
|
+
/(\/\s*)calc\((\d+(?:\.\d+)?)px \+ var\(--dsh-[a-z0-9-]*font-delta[a-z0-9-]*\)\)/g,
|
|
381
|
+
(m, lead, n) => lead + 'calc((' + n + 'px + var(--dsh-' + (m.includes('code-font-delta') ? 'code' : 'content') + '-font-delta' + (m.includes('font-delta-secondary') ? '-secondary' : '') + ')) * ' + lineH + ')',
|
|
349
382
|
)
|
|
350
383
|
.replace(/(\/\s*)(\d+(?:\.\d+)?)px/g, (m, lead, n) => lead + Math.round(Number(n) * lineH * 2) / 2 + 'px')
|
|
351
384
|
}
|
|
@@ -379,53 +412,71 @@ window.__ModuleLoader__.load({
|
|
|
379
412
|
if (!container) return null
|
|
380
413
|
return { container, target: container.parentElement ?? container, column: container.offsetWidth }
|
|
381
414
|
}
|
|
382
|
-
/**
|
|
383
|
-
function
|
|
415
|
+
/** 记录“当前宽度是插件扩展”这一事实;切换时落盘,供下次启动区分扩展与官方镜像。 */
|
|
416
|
+
function markWidthExt(on) {
|
|
417
|
+
if (state.widthExt === on) return
|
|
418
|
+
state.widthExt = on
|
|
419
|
+
writeState({ widthExt: on })
|
|
420
|
+
}
|
|
421
|
+
/** 官方手柄拖动优先:把一个宽度状态应用到页面(state.widthPx 为准)。
|
|
422
|
+
* clearAuto:仅在用户主动选"自动"时清除官方键;启动/外部变化不动官方设置。 */
|
|
423
|
+
function applyWidthState(clearAuto) {
|
|
384
424
|
const t = widthTargets()
|
|
385
425
|
if (!t) return
|
|
386
426
|
const { container, target, column } = t
|
|
427
|
+
if (!(column > 0)) return // 会话尚未排版:无可靠列宽,先不动官方值
|
|
387
428
|
const omax = officialMaxFor(column)
|
|
388
429
|
const cap = extCapFor(column)
|
|
389
430
|
const px = state.widthPx
|
|
390
431
|
if (px == null) {
|
|
391
|
-
|
|
392
|
-
|
|
432
|
+
if (clearAuto) {
|
|
433
|
+
localStorage.removeItem(WIDTH_PREF_KEY)
|
|
434
|
+
target.style.removeProperty('--dsh-chat-user-width')
|
|
435
|
+
}
|
|
393
436
|
container.style.removeProperty('--dsh-chat-content-width')
|
|
437
|
+
markWidthExt(false)
|
|
394
438
|
} else if (px <= omax) {
|
|
395
439
|
container.style.removeProperty('--dsh-chat-content-width')
|
|
396
440
|
localStorage.setItem(WIDTH_PREF_KEY, String(px))
|
|
397
441
|
target.style.setProperty('--dsh-chat-user-width', px + 'px')
|
|
442
|
+
markWidthExt(false)
|
|
398
443
|
} else {
|
|
399
444
|
const w = Math.min(px, cap)
|
|
400
445
|
localStorage.setItem(WIDTH_PREF_KEY, String(omax))
|
|
401
446
|
target.style.setProperty('--dsh-chat-user-width', omax + 'px')
|
|
402
447
|
container.style.setProperty('--dsh-chat-content-width', w + 'px')
|
|
448
|
+
markWidthExt(true)
|
|
403
449
|
}
|
|
404
450
|
}
|
|
405
451
|
function currentWidthInfo() {
|
|
406
452
|
const t = widthTargets()
|
|
407
|
-
if (!t) return null
|
|
453
|
+
if (!t || !(t.column > 0)) return null
|
|
408
454
|
const omax = officialMaxFor(t.column)
|
|
409
455
|
const cap = extCapFor(t.column)
|
|
410
456
|
const px = state.widthPx
|
|
411
457
|
const extActive = px != null && px > omax
|
|
412
458
|
let shown
|
|
459
|
+
let officialPx = null
|
|
413
460
|
if (px != null) shown = extActive ? Math.min(px, cap) : px
|
|
414
461
|
else {
|
|
415
462
|
const raw = getComputedStyle(t.container).getPropertyValue('--dsh-chat-user-width').trim()
|
|
416
|
-
|
|
463
|
+
officialPx = raw ? Number.parseFloat(raw) : null
|
|
464
|
+
shown = Number.isFinite(officialPx) ? officialPx : resolveAutoWidth(t.column)
|
|
417
465
|
}
|
|
418
|
-
return { px: shown, auto: px == null, ext: extActive, column: t.column, min: CONTENT_MIN, max: cap }
|
|
466
|
+
return { px: shown, auto: px == null && !Number.isFinite(officialPx), ext: extActive, column: t.column, min: CONTENT_MIN, max: cap }
|
|
419
467
|
}
|
|
420
468
|
/** 漂移修复:官方重发布 / 窗口变化后把我们的状态归位(官方手柄拖动中不插手)。 */
|
|
421
469
|
function normalizeWidthDrift() {
|
|
422
470
|
if (officialDragging) return
|
|
423
471
|
const t = widthTargets()
|
|
424
|
-
if (!t) return
|
|
472
|
+
if (!t || !(t.column > 0)) return
|
|
425
473
|
const omax = officialMaxFor(t.column)
|
|
426
474
|
const cap = extCapFor(t.column)
|
|
427
475
|
const px = state.widthPx
|
|
428
|
-
if (px == null)
|
|
476
|
+
if (px == null) {
|
|
477
|
+
markWidthExt(false)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
429
480
|
if (px > omax) {
|
|
430
481
|
const w = Math.min(px, cap) + 'px'
|
|
431
482
|
if (t.container.style.getPropertyValue('--dsh-chat-content-width') !== w) {
|
|
@@ -438,6 +489,7 @@ window.__ModuleLoader__.load({
|
|
|
438
489
|
if (t.target.style.getPropertyValue('--dsh-chat-user-width') !== omax + 'px') {
|
|
439
490
|
t.target.style.setProperty('--dsh-chat-user-width', omax + 'px')
|
|
440
491
|
}
|
|
492
|
+
markWidthExt(true)
|
|
441
493
|
} else {
|
|
442
494
|
if (t.container.style.getPropertyValue('--dsh-chat-content-width')) {
|
|
443
495
|
t.container.style.removeProperty('--dsh-chat-content-width')
|
|
@@ -448,13 +500,15 @@ window.__ModuleLoader__.load({
|
|
|
448
500
|
if (t.target.style.getPropertyValue('--dsh-chat-user-width') !== px + 'px') {
|
|
449
501
|
t.target.style.setProperty('--dsh-chat-user-width', px + 'px')
|
|
450
502
|
}
|
|
503
|
+
markWidthExt(false)
|
|
451
504
|
}
|
|
452
505
|
}
|
|
453
506
|
|
|
454
507
|
// ── 字号(官方直连 + 超范围缩放层) ─────────────────────────────────
|
|
455
|
-
const fontState = { official: null }
|
|
508
|
+
const fontState = { official: null, code: null, codeFamily: '', textFamily: '', terminal: null, terminalFamily: '' }
|
|
456
509
|
let fontSvc = null
|
|
457
|
-
let
|
|
510
|
+
let roleSizeSupported = false // setFontSize(role, px) 角色形
|
|
511
|
+
let familySupported = false // setFontFamily(role, value)
|
|
458
512
|
function fallbackBasePx() {
|
|
459
513
|
// 官方把该变量写在 body 内联样式上(见上方常量注释);主题服务未就绪时的兜底
|
|
460
514
|
const inline = Number.parseFloat(document.body.style.getPropertyValue(CONTENT_FONT_SIZE_VARIABLE))
|
|
@@ -472,19 +526,37 @@ window.__ModuleLoader__.load({
|
|
|
472
526
|
root.style.setProperty('--dsrb-font-scale', String(scale))
|
|
473
527
|
tag.disabled = Math.abs(scale - 1) < 0.001 // 无缩放 → 样式表彻底停用,回到官方原样
|
|
474
528
|
}
|
|
475
|
-
/**
|
|
476
|
-
function
|
|
529
|
+
/** 调官方角色字号(0.2.2+ 为 role,px;旧版单参仅 text 可用)。 */
|
|
530
|
+
function officialSetFontSize(role, px) {
|
|
477
531
|
if (!fontSvc || typeof fontSvc.setFontSize !== 'function') return
|
|
478
|
-
if (fontState.official === px) return
|
|
479
|
-
fontSelfWrite = true
|
|
480
532
|
try {
|
|
481
|
-
fontSvc.setFontSize(px)
|
|
533
|
+
if (fontSvc.setFontSize.length >= 2) fontSvc.setFontSize(role, px)
|
|
534
|
+
else if (role === 'text') fontSvc.setFontSize(px)
|
|
482
535
|
} catch {
|
|
483
|
-
//
|
|
536
|
+
// 官方校验拒绝:调用处已夹在范围内,静默忽略
|
|
484
537
|
}
|
|
485
|
-
|
|
538
|
+
}
|
|
539
|
+
/** 调官方角色字体族:value 为逗号分隔的列表,空串恢复默认栈。 */
|
|
540
|
+
function officialSetFontFamily(role, value) {
|
|
541
|
+
if (!fontSvc || typeof fontSvc.setFontFamily !== 'function') return
|
|
542
|
+
try {
|
|
543
|
+
fontSvc.setFontFamily(role, value)
|
|
544
|
+
} catch { /* 官方校验拒绝:静默忽略 */ }
|
|
545
|
+
}
|
|
546
|
+
/** 写官方正文字号(仅用户主动调节时调用)。 */
|
|
547
|
+
function writeOfficialFont(px) {
|
|
548
|
+
if (fontState.official === px) return
|
|
549
|
+
officialSetFontSize('text', px)
|
|
486
550
|
}
|
|
487
551
|
// ── 工具 ────────────────────────────────────────────────────────────
|
|
552
|
+
/** 清空容器:先显式 remove 元素,再兜底 textContent(仿真/真实 DOM 都彻底)。 */
|
|
553
|
+
function clearChildren(node) {
|
|
554
|
+
if (!node) return
|
|
555
|
+
for (const child of [...(node.children || [])]) {
|
|
556
|
+
try { if (typeof child.remove === 'function') child.remove() } catch { /* 忽略 */ }
|
|
557
|
+
}
|
|
558
|
+
node.textContent = ''
|
|
559
|
+
}
|
|
488
560
|
function el(tag, className, text) {
|
|
489
561
|
const node = document.createElement(tag)
|
|
490
562
|
if (className) node.className = className
|
|
@@ -499,16 +571,274 @@ window.__ModuleLoader__.load({
|
|
|
499
571
|
if (e.target === toggle) return // 键盘空格:走原生(change 监听里统一 apply)
|
|
500
572
|
e.preventDefault()
|
|
501
573
|
toggle.checked = !toggle.checked
|
|
574
|
+
toggle.setAttribute('aria-checked', toggle.checked ? 'true' : 'false')
|
|
502
575
|
apply()
|
|
503
576
|
})
|
|
504
577
|
}
|
|
505
578
|
|
|
506
|
-
// ──
|
|
579
|
+
// ── 插件本体(官方客户端插件形态:React 槽位 + locale + 主题服务) ──────
|
|
580
|
+
// 官方指南:UI 通过 slot 贡献 React 组件,不向 document.body 追加应用;
|
|
581
|
+
// 文案走 Client locale 服务;样式只用 --dsw-alias-* / --dsw-elevation-* token。
|
|
582
|
+
let React = null
|
|
583
|
+
try { React = require('react') } catch { React = null }
|
|
584
|
+
|
|
585
|
+
const LOCALE_NS = 'dshHappyReader'
|
|
586
|
+
let activeLocaleNs = LOCALE_NS // 热重载时旧命名空间可能尚未注销:备用命名空间避免复用将失效的字典
|
|
587
|
+
const TEXTS = {
|
|
588
|
+
zh: {
|
|
589
|
+
panelTitle: 'Happy Reader', hintClose: '点面板外关闭', close: '关闭',
|
|
590
|
+
dragHint: '面板任意空白处可拖动 · 双击标题栏复位',
|
|
591
|
+
fabTitle: 'Happy Reader(拖动可移动)',
|
|
592
|
+
hideComposerTitle: '隐藏输入框', hideComposerSub: '收起底部输入区',
|
|
593
|
+
hideTopTitle: '隐藏上边栏', hideTopSub: '收起会话标题栏',
|
|
594
|
+
typeRowTitle: '行距 / 字距', typeRowSub: '默认 = 官方原样',
|
|
595
|
+
lineHeight: '行距 ', letterSpacing: '字距 ', defaultText: '默认',
|
|
596
|
+
fontSize: '字号 ', fontSizeSync: '与官方设置同步',
|
|
597
|
+
codeFontSizeSub: '与官方设置同步(10–16)',
|
|
598
|
+
textFontTitle: '正文字体',
|
|
599
|
+
cjkLabel: '中文', latinLabel: '西文',
|
|
600
|
+
codeFontTitle: '代码字体', codeFontSub: '与官方设置同步 · 仅显示本机已装字体',
|
|
601
|
+
widthTitle: '内容宽度 ', widthSub: '与官方手柄同步', widthAuto: '自动',
|
|
602
|
+
widthUnavailable: '—(打开会话后可用)',
|
|
603
|
+
fullscreenTitle: '全屏', fullscreenSub: '铺满屏幕 · Esc 退出',
|
|
604
|
+
fontDefaultOption: '默认(官方)', minus: '减小', plus: '增大',
|
|
605
|
+
splitTitle: '中英分设', splitSub: '中文 / 西文分别选择 · 本机字体',
|
|
606
|
+
textBaseTitle: '官方正文字体', textBaseSub: '基础字族;中英分设优先',
|
|
607
|
+
terminalFontTitle: '终端字体', terminalFontSub: '与官方设置同步 · 侧栏终端',
|
|
608
|
+
terminalFontSizeSub: '与官方设置同步(10–20)',
|
|
609
|
+
customFontOption: '自定义…', currentFontPrefix: '当前:', fontRowLabel: '字体', sizeRowLabel: '字号 ',
|
|
610
|
+
customFontPlaceholder: '字体名,可用逗号分隔多个',
|
|
611
|
+
groupSummarySplit: '中英分设', groupSummaryOfficial: '官方字体',
|
|
612
|
+
settingsCjkTitle: '正文字体 · 中文', settingsCjkDesc: '只作用于汉字与中文标点;仅显示本机已装字体',
|
|
613
|
+
settingsLatinTitle: '正文字体 · 西文', settingsLatinDesc: '只作用于字母、数字与西文标点;仅显示本机已装字体',
|
|
614
|
+
},
|
|
615
|
+
en: {
|
|
616
|
+
panelTitle: 'Happy Reader', hintClose: 'Click outside to close', close: 'Close',
|
|
617
|
+
dragHint: 'Drag any empty area to move · double-click the title to reset',
|
|
618
|
+
fabTitle: 'Happy Reader (drag to move)',
|
|
619
|
+
hideComposerTitle: 'Hide composer', hideComposerSub: 'Collapse the input area',
|
|
620
|
+
hideTopTitle: 'Hide title bar', hideTopSub: 'Collapse the session title bar',
|
|
621
|
+
typeRowTitle: 'Line & letter spacing', typeRowSub: 'Default = official',
|
|
622
|
+
lineHeight: 'Line ', letterSpacing: 'Letter ', defaultText: 'Default',
|
|
623
|
+
fontSize: 'Font size ', fontSizeSync: 'Synced with official settings',
|
|
624
|
+
codeFontSizeSub: 'Synced with official settings (10–16)',
|
|
625
|
+
textFontTitle: 'Body font',
|
|
626
|
+
cjkLabel: 'Chinese', latinLabel: 'Latin',
|
|
627
|
+
codeFontTitle: 'Code font', codeFontSub: 'Synced with official settings · installed fonts only',
|
|
628
|
+
widthTitle: 'Content width ', widthSub: 'Synced with the official handle', widthAuto: 'Auto',
|
|
629
|
+
widthUnavailable: '— (open a session first)',
|
|
630
|
+
fullscreenTitle: 'Fullscreen', fullscreenSub: 'Fill the screen · Esc to exit',
|
|
631
|
+
fontDefaultOption: 'Default (official)', minus: 'Decrease', plus: 'Increase',
|
|
632
|
+
splitTitle: 'Chinese / Latin split', splitSub: 'Pick each side separately · installed fonts',
|
|
633
|
+
textBaseTitle: 'Official body font', textBaseSub: 'Base family; the split takes precedence',
|
|
634
|
+
terminalFontTitle: 'Terminal font', terminalFontSub: 'Synced with official settings · sidebar terminal',
|
|
635
|
+
terminalFontSizeSub: 'Synced with official settings (10–20)',
|
|
636
|
+
customFontOption: 'Custom…', currentFontPrefix: 'Current: ', fontRowLabel: 'Font', sizeRowLabel: 'Size ',
|
|
637
|
+
customFontPlaceholder: 'Font names, comma separated',
|
|
638
|
+
groupSummarySplit: 'split', groupSummaryOfficial: 'official font',
|
|
639
|
+
settingsCjkTitle: 'Body font · Chinese', settingsCjkDesc: 'Applies to Han characters and Chinese punctuation only; installed fonts only',
|
|
640
|
+
settingsLatinTitle: 'Body font · Latin', settingsLatinDesc: 'Applies to letters, digits and Latin punctuation only; installed fonts only',
|
|
641
|
+
},
|
|
642
|
+
}
|
|
643
|
+
function fallbackT(key) {
|
|
644
|
+
return TEXTS.zh[key] ?? key
|
|
645
|
+
}
|
|
646
|
+
let t = fallbackT
|
|
647
|
+
|
|
648
|
+
// 挂载句柄(槽位宿主 ref ↔ 实体生命周期)
|
|
649
|
+
let ui = null
|
|
650
|
+
let hostNode = null
|
|
651
|
+
let hostDispose = null
|
|
652
|
+
let overlayAlive = false
|
|
653
|
+
let settingsHost = null
|
|
654
|
+
let settingsDispose = null
|
|
655
|
+
let settingsRefresh = null
|
|
656
|
+
let settingsAlive = false
|
|
657
|
+
let remounting = false // 语言切换等就地重挂:不算插件停用,不退出全屏
|
|
658
|
+
|
|
659
|
+
/** 官方主题快照 → 字号/字体族桥接(0.2.2 角色形与旧版单值形都读)。 */
|
|
660
|
+
function syncTheme(snap) {
|
|
661
|
+
const sizes = snap && snap.fontSizes
|
|
662
|
+
const families = snap && snap.fontFamilies
|
|
663
|
+
const legacy = snap && typeof snap.fontSize === 'number' ? snap.fontSize : null
|
|
664
|
+
const text = sizes && typeof sizes.text === 'number' ? sizes.text : legacy
|
|
665
|
+
if (typeof text === 'number' && text > 0) fontState.official = text
|
|
666
|
+
if (sizes && typeof sizes.code === 'number') {
|
|
667
|
+
fontState.code = sizes.code
|
|
668
|
+
} else {
|
|
669
|
+
const inlineCode = Number.parseFloat(document.body.style.getPropertyValue('--dsh-code-font-size'))
|
|
670
|
+
if (Number.isFinite(inlineCode) && inlineCode > 0) fontState.code = Math.round(inlineCode)
|
|
671
|
+
}
|
|
672
|
+
if (sizes && typeof sizes.terminal === 'number') {
|
|
673
|
+
fontState.terminal = sizes.terminal
|
|
674
|
+
} else {
|
|
675
|
+
const inlineTerm = Number.parseFloat(document.body.style.getPropertyValue('--dsh-terminal-font-size'))
|
|
676
|
+
if (Number.isFinite(inlineTerm) && inlineTerm > 0) fontState.terminal = Math.round(inlineTerm)
|
|
677
|
+
}
|
|
678
|
+
const prevTextFamily = fontState.textFamily
|
|
679
|
+
if (families && typeof families.text === 'string') fontState.textFamily = families.text
|
|
680
|
+
if (families && typeof families.code === 'string') fontState.codeFamily = families.code
|
|
681
|
+
if (families && typeof families.terminal === 'string') fontState.terminalFamily = families.terminal
|
|
682
|
+
const prevPref = lastThemePreference
|
|
683
|
+
if (snap && typeof snap.preference === 'string') lastThemePreference = snap.preference
|
|
684
|
+
const themeSwitched = prevPref !== undefined && lastThemePreference !== prevPref
|
|
685
|
+
// 官方字族或主题切换后,旧 token 缓存作废
|
|
686
|
+
if (prevTextFamily !== fontState.textFamily || themeSwitched) themeFontDecls = null
|
|
687
|
+
// 官方是 10–22px 的权威来源:启动/外部变化时只采纳官方值,绝不反写;
|
|
688
|
+
// 超出 22 的绝对目标保留,由缩放层相对官方基准适配。
|
|
689
|
+
const prevFontPx = state.fontPx
|
|
690
|
+
if (typeof fontState.official === 'number') {
|
|
691
|
+
if (typeof state.fontPx !== 'number') state.fontPx = fontState.official
|
|
692
|
+
else if (state.fontPx <= FONT_OFFICIAL_MAX) state.fontPx = fontState.official
|
|
693
|
+
}
|
|
694
|
+
if (state.fontPx !== prevFontPx) writeState({ fontPx: state.fontPx })
|
|
695
|
+
if (ui) ui.renderFontRow()
|
|
696
|
+
refreshThemeUi(prevTextFamily, themeSwitched)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** 主题/字号变化后刷新已挂载的 UI(未挂载时零开销)。 */
|
|
700
|
+
function refreshThemeUi(prevTextFamily, themeSwitched) {
|
|
701
|
+
if (!ui) return
|
|
702
|
+
ui.renderCodeSizeRow()
|
|
703
|
+
ui.renderCodeFontRow()
|
|
704
|
+
ui.renderTerminalSizeRow()
|
|
705
|
+
ui.renderTerminalFontRow()
|
|
706
|
+
ui.renderTextBaseRow()
|
|
707
|
+
ui.applyFont()
|
|
708
|
+
if (prevTextFamily !== fontState.textFamily || themeSwitched) ui.refreshFontPick()
|
|
709
|
+
ui.renderGroupSummaries()
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** 槽位组件:零尺寸宿主 div;实体在 ref 挂载时创建(不触碰 document.body)。 */
|
|
713
|
+
function HappyReaderOverlay() {
|
|
714
|
+
return React.createElement('div', { className: 'dsrb-slot-host', ref: overlayHostRef })
|
|
715
|
+
}
|
|
716
|
+
function overlayHostRef(node) {
|
|
717
|
+
if (node) {
|
|
718
|
+
hostNode = node
|
|
719
|
+
if (!hostDispose) hostDispose = mount(node)
|
|
720
|
+
} else if (hostDispose) {
|
|
721
|
+
try { hostDispose() } catch { /* 拆除失败不阻塞 */ }
|
|
722
|
+
hostDispose = null
|
|
723
|
+
hostNode = null
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function remount() {
|
|
727
|
+
remounting = true
|
|
728
|
+
try {
|
|
729
|
+
if (overlayAlive && hostNode) {
|
|
730
|
+
if (hostDispose) { try { hostDispose() } catch { /* 忽略 */ } }
|
|
731
|
+
hostDispose = mount(hostNode)
|
|
732
|
+
}
|
|
733
|
+
if (settingsAlive && settingsHost) {
|
|
734
|
+
if (settingsDispose) { try { settingsDispose() } catch { /* 忽略 */ } }
|
|
735
|
+
settingsDispose = mountSettingsRows(settingsHost)
|
|
736
|
+
}
|
|
737
|
+
} finally {
|
|
738
|
+
remounting = false
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** 本机已装字体(三套候选统一检测一次,面板与官方设置行共用)。 */
|
|
743
|
+
let installedCache = null
|
|
744
|
+
function installedFonts(role) {
|
|
745
|
+
if (!installedCache) {
|
|
746
|
+
installedCache = {
|
|
747
|
+
cjk: TEXT_FONTS_CJK.filter((f) => fontAvailable(f.name)),
|
|
748
|
+
latin: TEXT_FONTS_LATIN.filter((f) => fontAvailable(f.name)),
|
|
749
|
+
code: CODE_FONTS.filter((f) => fontAvailable(f.name)),
|
|
750
|
+
}
|
|
751
|
+
fontDetectCtx = null // 检测完成即释放 canvas 上下文
|
|
752
|
+
}
|
|
753
|
+
if (role === 'text') {
|
|
754
|
+
if (!installedCache.text) {
|
|
755
|
+
installedCache.text = installedCache.cjk.concat(
|
|
756
|
+
installedCache.latin.filter((f) => !installedCache.cjk.some((c) => c.name === f.name)),
|
|
757
|
+
)
|
|
758
|
+
}
|
|
759
|
+
return installedCache.text
|
|
760
|
+
}
|
|
761
|
+
return installedCache[role] || []
|
|
762
|
+
}
|
|
763
|
+
/** 下拉:默认(官方)+ 本机已装候选。 */
|
|
764
|
+
function fillFontOptions(select, list) {
|
|
765
|
+
clearChildren(select)
|
|
766
|
+
const def = el('option', null, t('fontDefaultOption'))
|
|
767
|
+
def.value = ''
|
|
768
|
+
select.append(def)
|
|
769
|
+
for (const f of list) {
|
|
770
|
+
const opt = el('option', null, f.label)
|
|
771
|
+
opt.value = f.id
|
|
772
|
+
select.append(opt)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** 正文字体:两处 UI(浮动面板 / 官方通用设置)共用的唯一写入口。 */
|
|
777
|
+
function commitTextFont(role, id) {
|
|
778
|
+
const next = id && fontChoice(installedFonts(role), id) ? id : null
|
|
779
|
+
if (role === 'cjk') state.textFontCjk = next
|
|
780
|
+
else state.textFontLatin = next
|
|
781
|
+
writeState({ textFontCjk: state.textFontCjk, textFontLatin: state.textFontLatin })
|
|
782
|
+
if (ui && typeof ui.applyTextFonts === 'function') ui.applyTextFonts()
|
|
783
|
+
if (settingsRefresh) settingsRefresh()
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** 官方「通用设置」里的两行正文字体(settings.general.item 槽位组件)。 */
|
|
787
|
+
function HappyReaderSettingsFonts() {
|
|
788
|
+
return React.createElement('div', { className: 'dsrb-settings', ref: settingsHostRef })
|
|
789
|
+
}
|
|
790
|
+
function settingsHostRef(node) {
|
|
791
|
+
if (node) {
|
|
792
|
+
settingsHost = node
|
|
793
|
+
if (!settingsDispose) settingsDispose = mountSettingsRows(node)
|
|
794
|
+
} else {
|
|
795
|
+
if (settingsDispose) { try { settingsDispose() } catch { /* 拆除失败不阻塞 */ } }
|
|
796
|
+
settingsDispose = null
|
|
797
|
+
settingsHost = null
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/** 按官方 FontFamilyRow 的行距/字号/分隔线规格构建两行下拉;返回清理函数。 */
|
|
801
|
+
function mountSettingsRows(host) {
|
|
802
|
+
try {
|
|
803
|
+
settingsAlive = true
|
|
804
|
+
clearChildren(host)
|
|
805
|
+
const makeRow = (role, titleKey, descKey) => {
|
|
806
|
+
const row = el('div', 'dsrb-set-row')
|
|
807
|
+
const text = el('div', 'dsrb-set-text')
|
|
808
|
+
text.append(el('div', 'dsrb-set-title', t(titleKey)), el('div', 'dsrb-set-desc', t(descKey)))
|
|
809
|
+
const select = el('select', 'dsrb-set-select')
|
|
810
|
+
select.setAttribute('aria-label', t(titleKey))
|
|
811
|
+
fillFontOptions(select, installedFonts(role))
|
|
812
|
+
select.value = (role === 'cjk' ? state.textFontCjk : state.textFontLatin) ?? ''
|
|
813
|
+
select.addEventListener('change', () => commitTextFont(role, select.value))
|
|
814
|
+
row.append(text, select)
|
|
815
|
+
return { row, select }
|
|
816
|
+
}
|
|
817
|
+
const cjk = makeRow('cjk', 'settingsCjkTitle', 'settingsCjkDesc')
|
|
818
|
+
const latin = makeRow('latin', 'settingsLatinTitle', 'settingsLatinDesc')
|
|
819
|
+
host.append(cjk.row, latin.row)
|
|
820
|
+
settingsRefresh = () => {
|
|
821
|
+
cjk.select.value = state.textFontCjk ?? ''
|
|
822
|
+
latin.select.value = state.textFontLatin ?? ''
|
|
823
|
+
}
|
|
824
|
+
return () => {
|
|
825
|
+
settingsAlive = false
|
|
826
|
+
settingsRefresh = null
|
|
827
|
+
clearChildren(host)
|
|
828
|
+
}
|
|
829
|
+
} catch (err) {
|
|
830
|
+
console.warn('[dsh-happy-reader] 设置行初始化失败:', err)
|
|
831
|
+
settingsAlive = false
|
|
832
|
+
settingsRefresh = null
|
|
833
|
+
try { clearChildren(host) } catch { /* 忽略 */ }
|
|
834
|
+
return () => {}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
507
838
|
function apply(ctx) {
|
|
508
839
|
try {
|
|
509
840
|
// 热重载:DSH 监视插件文件变化,会再次执行本客户端。先拆旧实例、再按 id
|
|
510
|
-
// 清扫遗留节点、最后重建 ——
|
|
511
|
-
// 随节点删除失联、残留定时器空转,均无害)。
|
|
841
|
+
// 清扫遗留节点、最后重建 —— 页面上运行的必然是最新代码。
|
|
512
842
|
const prevDispose = window.__dsrbDispose
|
|
513
843
|
if (typeof prevDispose === 'function') {
|
|
514
844
|
try { prevDispose() } catch { /* 旧实例拆除失败也不阻塞重建 */ }
|
|
@@ -517,10 +847,186 @@ window.__ModuleLoader__.load({
|
|
|
517
847
|
const staleNode = document.getElementById(staleId)
|
|
518
848
|
if (staleNode) staleNode.remove()
|
|
519
849
|
}
|
|
520
|
-
|
|
850
|
+
// 旧外壳回退宿主没有固定 id:按专属类名清扫(槽位宿主由 React 自己卸载)
|
|
851
|
+
for (const staleHost of document.querySelectorAll('.dsrb-slot-host.dsrb-standalone')) staleHost.remove()
|
|
852
|
+
// 热重载时旧模块的设置页行可能还在宿主里:按行清扫,避免叠加成四行
|
|
853
|
+
for (const staleRow of document.querySelectorAll('.dsrb-set-row')) staleRow.remove()
|
|
521
854
|
|
|
522
855
|
state = readState()
|
|
856
|
+
// ── 启动时应用已保存的功能状态(主题同步先行使用,故放在最前) ──
|
|
857
|
+
state.open = !!state.open
|
|
858
|
+
state.hideComposer = !!state.hideComposer
|
|
859
|
+
state.hideTop = !!state.hideTop
|
|
860
|
+
state.x = typeof state.x === 'number' ? state.x : null
|
|
861
|
+
state.y = typeof state.y === 'number' ? state.y : null
|
|
862
|
+
if (typeof state.fontPx === 'number') {
|
|
863
|
+
state.fontPx = Math.min(FONT_MAX, Math.max(FONT_MIN, Math.round(state.fontPx)))
|
|
864
|
+
} else {
|
|
865
|
+
state.fontPx = null
|
|
866
|
+
}
|
|
867
|
+
if (typeof state.widthPx !== 'number' || !(state.widthPx > 0)) state.widthPx = null
|
|
868
|
+
state.widthExt = typeof state.widthExt === 'boolean' ? state.widthExt : null // null=旧版状态,启动时推断
|
|
869
|
+
if (typeof state.lineHeight === 'number') {
|
|
870
|
+
const lh = Math.min(LINEH_MAX, Math.max(LINEH_MIN, Math.round(state.lineHeight * 10) / 10))
|
|
871
|
+
state.lineHeight = lh > LINEH_MIN ? lh : null
|
|
872
|
+
} else state.lineHeight = null
|
|
873
|
+
if (typeof state.letterSpacing === 'number') {
|
|
874
|
+
const ls = Math.min(LETTERSP_MAX, Math.max(LETTERSP_MIN, Math.round(state.letterSpacing * 10) / 10))
|
|
875
|
+
state.letterSpacing = ls !== 0 ? ls : null
|
|
876
|
+
} else state.letterSpacing = null
|
|
877
|
+
state.panelX = typeof state.panelX === 'number' ? state.panelX : null
|
|
878
|
+
state.panelY = typeof state.panelY === 'number' ? state.panelY : null
|
|
879
|
+
state.panelH = typeof state.panelH === 'number' && state.panelH > 0 ? state.panelH : null
|
|
880
|
+
// 字体折叠组展开态:默认三行全部收起(打开面板先看到三行),点哪行展开哪行;记忆用户选择
|
|
881
|
+
if (state.groupsVersion !== GROUPS_VERSION) {
|
|
882
|
+
state.groups = { text: false, code: false, terminal: false }
|
|
883
|
+
state.groupsVersion = GROUPS_VERSION
|
|
884
|
+
writeState({ groups: state.groups, groupsVersion: GROUPS_VERSION })
|
|
885
|
+
} else {
|
|
886
|
+
if (!state.groups || typeof state.groups !== 'object') state.groups = {}
|
|
887
|
+
state.groups.text = typeof state.groups.text === 'boolean' ? state.groups.text : false
|
|
888
|
+
state.groups.code = typeof state.groups.code === 'boolean' ? state.groups.code : false
|
|
889
|
+
state.groups.terminal = typeof state.groups.terminal === 'boolean' ? state.groups.terminal : false
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ── 本地化:注册字典(owning effect)+ 绑定 t;语言切换时重挂实体刷新文案 ──
|
|
893
|
+
ctx.inject(['locale'], (lctx) => {
|
|
894
|
+
const loc = lctx.locale
|
|
895
|
+
const dicts = { zh: TEXTS.zh, en: TEXTS.en }
|
|
896
|
+
let registered = false
|
|
897
|
+
let regErr = null
|
|
898
|
+
try {
|
|
899
|
+
lctx.effect(() => loc.register(LOCALE_NS, dicts), 'dsh-happy-reader: 语言字典')
|
|
900
|
+
registered = true
|
|
901
|
+
} catch (err) {
|
|
902
|
+
// 热重载时旧命名空间可能尚在:换一个本实例独占的命名空间重新注册,
|
|
903
|
+
// 避免绑定到随后会被旧 effect 注销的旧字典
|
|
904
|
+
regErr = err
|
|
905
|
+
const alt = LOCALE_NS + '-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
|
|
906
|
+
try {
|
|
907
|
+
lctx.effect(() => loc.register(alt, dicts), 'dsh-happy-reader: 语言字典(备用)')
|
|
908
|
+
activeLocaleNs = alt
|
|
909
|
+
registered = true
|
|
910
|
+
regErr = null
|
|
911
|
+
} catch { /* 保持 regErr,下面探测旧字典 */ }
|
|
912
|
+
}
|
|
913
|
+
let bound = null
|
|
914
|
+
try {
|
|
915
|
+
if (typeof loc.bind === 'function') bound = loc.bind(activeLocaleNs)
|
|
916
|
+
} catch { /* 视为不可用 */ }
|
|
917
|
+
let usable = registered && typeof bound === 'function'
|
|
918
|
+
if (!usable && typeof bound === 'function' && activeLocaleNs !== LOCALE_NS) {
|
|
919
|
+
try {
|
|
920
|
+
bound = loc.bind(LOCALE_NS)
|
|
921
|
+
usable = typeof bound === 'function' && bound('panelTitle') !== 'panelTitle'
|
|
922
|
+
} catch { /* 视为不可用 */ }
|
|
923
|
+
}
|
|
924
|
+
if (usable) {
|
|
925
|
+
t = bound
|
|
926
|
+
if (!registered) console.warn('[dsh-happy-reader] 语言字典沿用旧命名空间(可能在旧实例注销后失效):', regErr || '')
|
|
927
|
+
} else {
|
|
928
|
+
console.warn('[dsh-happy-reader] 语言服务不可用(沿用内置中文):', regErr || 'bind failed')
|
|
929
|
+
}
|
|
930
|
+
remount() // 服务晚到时刷新已挂载文案(启动时尚未挂载则无事发生)
|
|
931
|
+
lctx.on('locale/change', () => remount())
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
// ── 主题服务:字号/字体族桥接(官方接口优先) ──────────────────
|
|
935
|
+
ctx.inject(['theme'], (tctx) => {
|
|
936
|
+
fontSvc = tctx.theme
|
|
937
|
+
// 能力按接口形状判定:即使首读快照异常,控件仍可用(写入失败由官方静默拒绝)
|
|
938
|
+
roleSizeSupported = !!(fontSvc && typeof fontSvc.setFontSize === 'function' && fontSvc.setFontSize.length >= 2)
|
|
939
|
+
familySupported = !!(fontSvc && typeof fontSvc.setFontFamily === 'function')
|
|
940
|
+
tctx.on('theme/change', (snap) => syncTheme(snap))
|
|
941
|
+
try {
|
|
942
|
+
syncTheme(tctx.theme.getTheme())
|
|
943
|
+
} catch {
|
|
944
|
+
if (ui) ui.applyFont() // 服务首读异常时至少按兜底基准绘制一次
|
|
945
|
+
}
|
|
946
|
+
})
|
|
523
947
|
|
|
948
|
+
// ── 官方会话列表事件:会话切换后重新定位/应用宽度(不轮询 DOM) ──
|
|
949
|
+
ctx.inject(['sessions'], (sctx) => {
|
|
950
|
+
const list = sctx.sessions && sctx.sessions.list
|
|
951
|
+
if (!list || typeof list.subscribe !== 'function') return
|
|
952
|
+
try {
|
|
953
|
+
sctx.effect(() => list.subscribe(() => {
|
|
954
|
+
if (ui && ui.scheduleWidthRefresh) ui.scheduleWidthRefresh()
|
|
955
|
+
}), 'dsh-happy-reader: 会话切换复核宽度')
|
|
956
|
+
} catch (err) {
|
|
957
|
+
console.warn('[dsh-happy-reader] 会话订阅失败(宽度仅在面板/窗口事件时复核):', err)
|
|
958
|
+
}
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
// ── 渲染面:官方 shell.overlay 槽位(框架级覆盖层,pointer-events:none) ──
|
|
962
|
+
let registered = false
|
|
963
|
+
let standalone = null // 旧外壳回退时直挂 body 的宿主,卸载时一并移除
|
|
964
|
+
if (React && ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
965
|
+
try {
|
|
966
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
967
|
+
name: 'shell.overlay',
|
|
968
|
+
id: 'dsh-happy-reader',
|
|
969
|
+
order: -1, // 排在官方 overlay 条目之前:右键菜单/提示条等始终盖在插件之上
|
|
970
|
+
locale: activeLocaleNs,
|
|
971
|
+
}, HappyReaderOverlay))
|
|
972
|
+
registered = true
|
|
973
|
+
} catch (err) {
|
|
974
|
+
console.warn('[dsh-happy-reader] 槽位注册失败,回退直挂:', err)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (registered) {
|
|
978
|
+
// 官方「通用设置 → 字号大小」组下方增加两行正文字体(中文 / 西文)。
|
|
979
|
+
// settings.general.item 是官方列表槽位;旧外壳无此槽位时注册被忽略。
|
|
980
|
+
try {
|
|
981
|
+
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
|
|
982
|
+
name: 'settings.general.item',
|
|
983
|
+
id: 'dsh-happy-reader.fonts',
|
|
984
|
+
order: 11.5,
|
|
985
|
+
locale: activeLocaleNs,
|
|
986
|
+
}, HappyReaderSettingsFonts))
|
|
987
|
+
} catch (err) {
|
|
988
|
+
console.warn('[dsh-happy-reader] 设置行注册失败(旧版外壳忽略):', err)
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (!registered) {
|
|
992
|
+
// 旧版/无槽位宿主:宿主 div 退回直挂 body(UI 仍包在宿主内,不逐个裸挂)
|
|
993
|
+
standalone = el('div', 'dsrb-slot-host dsrb-standalone')
|
|
994
|
+
document.body.appendChild(standalone)
|
|
995
|
+
hostNode = standalone // 回退路径同样支持语言切换时就地重挂
|
|
996
|
+
hostDispose = mount(standalone)
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
ctx.effect(() => () => {
|
|
1000
|
+
if (hostDispose) {
|
|
1001
|
+
try { hostDispose() } catch { /* 忽略 */ }
|
|
1002
|
+
hostDispose = null
|
|
1003
|
+
}
|
|
1004
|
+
// 插件被禁用/卸载时,设置页里的插件行也必须立即消失(不等 React 迟到的卸载)
|
|
1005
|
+
if (settingsDispose) {
|
|
1006
|
+
try { settingsDispose() } catch { /* 忽略 */ }
|
|
1007
|
+
settingsDispose = null
|
|
1008
|
+
}
|
|
1009
|
+
settingsHost = null
|
|
1010
|
+
settingsRefresh = null
|
|
1011
|
+
settingsAlive = false
|
|
1012
|
+
if (standalone) {
|
|
1013
|
+
standalone.remove()
|
|
1014
|
+
standalone = null
|
|
1015
|
+
}
|
|
1016
|
+
// 不清空 window.__dsrbDispose:若新实例已接管,这里清空会破坏其热重载拆除句柄;
|
|
1017
|
+
// 旧句柄本身是幂等的,下次热重载再调用也安全。
|
|
1018
|
+
}, 'dsh-happy-reader: 槽位实体')
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
// 任何意外都只记日志,不影响应用本体
|
|
1021
|
+
console.error('[dsh-happy-reader] 挂载失败:', err)
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** 实体:构建面板/按钮/监听/定时器并把它们放进槽位宿主;返回清理函数。 */
|
|
1026
|
+
function mount(host) {
|
|
1027
|
+
let disposed = false
|
|
1028
|
+
overlayAlive = true
|
|
1029
|
+
try {
|
|
524
1030
|
// 1) 样式表:基础外观 + 各功能覆盖层
|
|
525
1031
|
const style = el('style')
|
|
526
1032
|
style.id = STYLE_ID
|
|
@@ -539,7 +1045,7 @@ window.__ModuleLoader__.load({
|
|
|
539
1045
|
const fsTag = el('style') // 全屏补偿层(常驻;仅存在全屏标记时才有匹配规则生效)
|
|
540
1046
|
fsTag.id = FS_STYLE_ID
|
|
541
1047
|
fsTag.textContent = FS_CSS
|
|
542
|
-
|
|
1048
|
+
host.append(style, hideTag, hideTopTag, fontTag, fontPickTag, fsTag)
|
|
543
1049
|
// disabled 必须在挂入文档后再设:未连接的 <style> 没有关联样式表,挂载前设置
|
|
544
1050
|
// 会被静默丢弃(CSSOM 行为),否则隐藏层将无视已保存状态(字体层由 rebuildFontPick 按需启用)。
|
|
545
1051
|
hideTag.disabled = !state.hideComposer
|
|
@@ -550,7 +1056,8 @@ window.__ModuleLoader__.load({
|
|
|
550
1056
|
const fab = el('button', 'dsrb-fab', 'Aa')
|
|
551
1057
|
fab.id = FAB_ID
|
|
552
1058
|
fab.type = 'button'
|
|
553
|
-
fab.title = '
|
|
1059
|
+
fab.title = t('fabTitle')
|
|
1060
|
+
fab.setAttribute('aria-label', t('fabTitle'))
|
|
554
1061
|
const [fx, fy] = clampPos(
|
|
555
1062
|
state.x ?? window.innerWidth - FAB_SIZE - 16,
|
|
556
1063
|
state.y ?? window.innerHeight - FAB_SIZE - 16,
|
|
@@ -559,148 +1066,305 @@ window.__ModuleLoader__.load({
|
|
|
559
1066
|
)
|
|
560
1067
|
fab.style.left = fx + 'px'
|
|
561
1068
|
fab.style.top = fy + 'px'
|
|
562
|
-
|
|
1069
|
+
host.appendChild(fab)
|
|
563
1070
|
|
|
564
1071
|
// 3) 面板
|
|
565
1072
|
const panel = el('div', 'dsrb-panel')
|
|
566
1073
|
panel.id = PANEL_ID
|
|
1074
|
+
panel.setAttribute('role', 'region')
|
|
1075
|
+
panel.setAttribute('aria-label', t('panelTitle'))
|
|
567
1076
|
|
|
568
1077
|
const headRow = el('div', 'dsrb-head')
|
|
569
|
-
const title = el('h4')
|
|
570
|
-
|
|
571
|
-
const headHint = el('span', 'dsrb-headHint', '点面板外关闭')
|
|
1078
|
+
const title = el('h4', null, t('panelTitle'))
|
|
1079
|
+
const headHint = el('span', 'dsrb-headHint', t('hintClose'))
|
|
572
1080
|
const closeBtn = el('button', 'dsrb-x', '×')
|
|
573
1081
|
closeBtn.type = 'button'
|
|
574
|
-
closeBtn.title = '
|
|
1082
|
+
closeBtn.title = t('close')
|
|
1083
|
+
closeBtn.setAttribute('aria-label', t('close'))
|
|
575
1084
|
headRow.append(title, headHint, closeBtn)
|
|
576
1085
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
rowTop.append(topBox, topMain)
|
|
1086
|
+
/** 开关行:官方 36×20 视觉 + role=switch;整条可点由 bindRowToggle 绑定。 */
|
|
1087
|
+
function makeSwitchRow(titleText, subText) {
|
|
1088
|
+
const row = el('div', 'dsrb-row dsrb-row-click')
|
|
1089
|
+
const box = el('span', 'dsrb-switch') // span 而非 label:避免原生转发激活面
|
|
1090
|
+
const input = el('input')
|
|
1091
|
+
input.type = 'checkbox'
|
|
1092
|
+
input.setAttribute('role', 'switch')
|
|
1093
|
+
input.setAttribute('aria-checked', 'false')
|
|
1094
|
+
input.setAttribute('aria-label', titleText)
|
|
1095
|
+
box.append(input, el('span'))
|
|
1096
|
+
const main = el('div', 'dsrb-rowMain')
|
|
1097
|
+
main.append(el('div', 'dsrb-rowTitle', titleText), el('div', 'dsrb-rowSub', subText))
|
|
1098
|
+
row.append(box, main)
|
|
1099
|
+
return { row, input }
|
|
1100
|
+
}
|
|
1101
|
+
const { row: rowHide, input: hideToggle } = makeSwitchRow(t('hideComposerTitle'), t('hideComposerSub'))
|
|
1102
|
+
|
|
1103
|
+
const { row: rowTop, input: topToggle } = makeSwitchRow(t('hideTopTitle'), t('hideTopSub'))
|
|
596
1104
|
|
|
597
1105
|
// 行:行距 / 字距(面板最下方;默认 = 官方原样)
|
|
598
1106
|
const rowType = el('div', 'dsrb-row')
|
|
599
1107
|
const typeMain = el('div', 'dsrb-rowMain')
|
|
600
|
-
typeMain.append(el('div', 'dsrb-rowTitle', '
|
|
1108
|
+
typeMain.append(el('div', 'dsrb-rowTitle', t('typeRowTitle')), el('div', 'dsrb-rowSub', t('typeRowSub')))
|
|
601
1109
|
const typePair = el('div', 'dsrb-pair')
|
|
602
1110
|
const lhCol = el('div', 'dsrb-pairCol')
|
|
603
1111
|
const lhMini = el('div', 'dsrb-mini')
|
|
604
|
-
const lhVal = el('b', null, '
|
|
605
|
-
lhMini.append('
|
|
1112
|
+
const lhVal = el('b', null, t('defaultText'))
|
|
1113
|
+
lhMini.append(t('lineHeight'), lhVal)
|
|
606
1114
|
const lhStepper = el('div', 'dsrb-stepper')
|
|
607
1115
|
const lhMinus = el('button', null, '−')
|
|
608
1116
|
const lhPlus = el('button', null, '+')
|
|
609
1117
|
lhMinus.type = lhPlus.type = 'button'
|
|
1118
|
+
lhMinus.setAttribute('aria-label', t('minus'))
|
|
1119
|
+
lhPlus.setAttribute('aria-label', t('plus'))
|
|
610
1120
|
lhStepper.append(lhMinus, lhPlus)
|
|
611
1121
|
lhCol.append(lhMini, lhStepper)
|
|
612
1122
|
const lsCol = el('div', 'dsrb-pairCol')
|
|
613
1123
|
const lsMini = el('div', 'dsrb-mini')
|
|
614
|
-
const lsVal = el('b', null, '
|
|
615
|
-
lsMini.append('
|
|
1124
|
+
const lsVal = el('b', null, t('defaultText'))
|
|
1125
|
+
lsMini.append(t('letterSpacing'), lsVal)
|
|
616
1126
|
const lsStepper = el('div', 'dsrb-stepper')
|
|
617
1127
|
const lsMinus = el('button', null, '−')
|
|
618
1128
|
const lsPlus = el('button', null, '+')
|
|
619
1129
|
lsMinus.type = lsPlus.type = 'button'
|
|
1130
|
+
lsMinus.setAttribute('aria-label', t('minus'))
|
|
1131
|
+
lsPlus.setAttribute('aria-label', t('plus'))
|
|
620
1132
|
lsStepper.append(lsMinus, lsPlus)
|
|
621
1133
|
lsCol.append(lsMini, lsStepper)
|
|
622
1134
|
typePair.append(lhCol, lsCol)
|
|
623
1135
|
typeMain.append(typePair)
|
|
624
1136
|
rowType.append(typeMain)
|
|
625
1137
|
|
|
626
|
-
//
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1138
|
+
// ── 字体设置折叠组:正文字体 / 代码字体 / 终端字体 ──────────────
|
|
1139
|
+
const groups = {}
|
|
1140
|
+
function makeGroup(key, titleKey) {
|
|
1141
|
+
const group = el('div', 'dsrb-group')
|
|
1142
|
+
const head = el('button', 'dsrb-groupHead')
|
|
1143
|
+
head.type = 'button'
|
|
1144
|
+
head.setAttribute('aria-expanded', 'false')
|
|
1145
|
+
const title = el('span', 'dsrb-groupTitle', t(titleKey))
|
|
1146
|
+
const sub = el('span', 'dsrb-groupSub')
|
|
1147
|
+
const chev = el('span', 'dsrb-chevron')
|
|
1148
|
+
chev.setAttribute('aria-hidden', 'true')
|
|
1149
|
+
head.append(title, sub, chev)
|
|
1150
|
+
const body = el('div', 'dsrb-groupBody')
|
|
1151
|
+
body.id = 'dsrb-group-body-' + key
|
|
1152
|
+
head.setAttribute('aria-controls', body.id)
|
|
1153
|
+
group.append(head, body)
|
|
1154
|
+
head.addEventListener('click', () => toggleGroup(key))
|
|
1155
|
+
groups[key] = { group, head, body, sub }
|
|
1156
|
+
return groups[key]
|
|
1157
|
+
}
|
|
1158
|
+
function applyGroupState() {
|
|
1159
|
+
for (const key of Object.keys(groups)) {
|
|
1160
|
+
const open = !!state.groups[key]
|
|
1161
|
+
groups[key].body.style.display = open ? '' : 'none'
|
|
1162
|
+
groups[key].head.setAttribute('aria-expanded', open ? 'true' : 'false')
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
function toggleGroup(key) {
|
|
1166
|
+
const open = !state.groups[key]
|
|
1167
|
+
const keepScroll = panelBody.scrollTop
|
|
1168
|
+
state.groups[key] = open
|
|
1169
|
+
applyGroupState()
|
|
1170
|
+
writeState({ groups: state.groups })
|
|
1171
|
+
if (state.open) placePanel(keepScroll)
|
|
1172
|
+
}
|
|
1173
|
+
makeGroup('text', 'textFontTitle')
|
|
1174
|
+
makeGroup('code', 'codeFontTitle')
|
|
1175
|
+
makeGroup('terminal', 'terminalFontTitle')
|
|
1176
|
+
|
|
1177
|
+
// 角色字体行:本机已装字体下拉 + “自定义…”文本框(官方逗号列表同款能力)。
|
|
1178
|
+
// 官方值不在候选表时显示“当前:…”,绝不静默误报为默认。
|
|
1179
|
+
const FAMILY_KEY = { text: 'textFamily', code: 'codeFamily', terminal: 'terminalFamily' }
|
|
1180
|
+
function makeRoleFontPicker(role, titleKey, subKey, extraClass, ariaKey) {
|
|
1181
|
+
const row = el('div', 'dsrb-row')
|
|
1182
|
+
const main = el('div', 'dsrb-rowMain')
|
|
1183
|
+
main.append(el('div', 'dsrb-rowTitle', t(titleKey)), el('div', 'dsrb-rowSub', t(subKey)))
|
|
1184
|
+
const select = el('select', 'dsrb-select ' + extraClass)
|
|
1185
|
+
const ariaName = t(ariaKey || titleKey)
|
|
1186
|
+
select.setAttribute('aria-label', ariaName)
|
|
1187
|
+
const input = el('input', 'dsrb-input ' + extraClass + '-custom')
|
|
1188
|
+
input.type = 'text'
|
|
1189
|
+
input.setAttribute('aria-label', ariaName)
|
|
1190
|
+
input.placeholder = t('customFontPlaceholder')
|
|
1191
|
+
input.spellcheck = false
|
|
1192
|
+
input.autocomplete = 'off'
|
|
1193
|
+
input.maxLength = 200
|
|
1194
|
+
input.style.display = 'none'
|
|
1195
|
+
main.append(select, input)
|
|
1196
|
+
row.append(main)
|
|
1197
|
+
const picker = { row, select, input, role, list: [] }
|
|
1198
|
+
const customOpt = el('option', null, t('customFontOption'))
|
|
1199
|
+
customOpt.value = '__custom__'
|
|
1200
|
+
let currentOpt = null
|
|
1201
|
+
const clearCurrent = () => {
|
|
1202
|
+
if (!currentOpt) return
|
|
1203
|
+
if (typeof currentOpt.remove === 'function') currentOpt.remove()
|
|
1204
|
+
currentOpt = null
|
|
1205
|
+
}
|
|
1206
|
+
picker.fill = (list) => {
|
|
1207
|
+
picker.list = list
|
|
1208
|
+
fillFontOptions(select, list)
|
|
1209
|
+
select.append(customOpt)
|
|
1210
|
+
picker.render()
|
|
1211
|
+
}
|
|
1212
|
+
picker.setEnabled = (on) => {
|
|
1213
|
+
select.disabled = !on
|
|
1214
|
+
input.disabled = !on
|
|
1215
|
+
if (!on) {
|
|
1216
|
+
input.style.display = 'none'
|
|
1217
|
+
select.value = ''
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
picker.render = () => {
|
|
1221
|
+
if (input.style.display !== 'none' && document.activeElement === input) return
|
|
1222
|
+
const value = String(fontState[FAMILY_KEY[role]] || '')
|
|
1223
|
+
if (!value) {
|
|
1224
|
+
clearCurrent()
|
|
1225
|
+
select.value = ''
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
const parts = value.split(',').map((x) => x.trim()).filter(Boolean)
|
|
1229
|
+
const first = parts[0] ? parts[0].replace(/^"|"$/g, '') : ''
|
|
1230
|
+
const hit = parts.length === 1 ? picker.list.find((f) => fontMatches(f, first)) : null
|
|
1231
|
+
if (hit) {
|
|
1232
|
+
clearCurrent()
|
|
1233
|
+
select.value = hit.id
|
|
1234
|
+
return
|
|
1235
|
+
}
|
|
1236
|
+
if (!currentOpt) {
|
|
1237
|
+
currentOpt = el('option', null, '')
|
|
1238
|
+
currentOpt.value = '__current__'
|
|
1239
|
+
select.append(currentOpt)
|
|
1240
|
+
}
|
|
1241
|
+
currentOpt.textContent = t('currentFontPrefix') + value
|
|
1242
|
+
select.value = '__current__'
|
|
1243
|
+
}
|
|
1244
|
+
select.addEventListener('change', () => {
|
|
1245
|
+
const v = select.value
|
|
1246
|
+
if (v === '__custom__') {
|
|
1247
|
+
input.value = String(fontState[FAMILY_KEY[role]] || '')
|
|
1248
|
+
input.style.display = ''
|
|
1249
|
+
if (typeof input.focus === 'function') input.focus()
|
|
1250
|
+
return
|
|
1251
|
+
}
|
|
1252
|
+
if (v === '__current__') {
|
|
1253
|
+
picker.render()
|
|
1254
|
+
return
|
|
1255
|
+
}
|
|
1256
|
+
const entry = fontChoice(picker.list, v)
|
|
1257
|
+
officialSetFontFamily(role, entry ? entry.name : '')
|
|
1258
|
+
picker.render()
|
|
1259
|
+
})
|
|
1260
|
+
const commit = () => {
|
|
1261
|
+
if (input.style.display === 'none') return
|
|
1262
|
+
input.style.display = 'none'
|
|
1263
|
+
officialSetFontFamily(role, input.value.trim())
|
|
1264
|
+
picker.render()
|
|
1265
|
+
}
|
|
1266
|
+
input.addEventListener('blur', commit)
|
|
1267
|
+
input.addEventListener('keydown', (e) => {
|
|
1268
|
+
if (e.key === 'Enter') {
|
|
1269
|
+
e.preventDefault()
|
|
1270
|
+
if (typeof input.blur === 'function') input.blur()
|
|
1271
|
+
else commit()
|
|
1272
|
+
} else if (e.key === 'Escape') {
|
|
1273
|
+
e.preventDefault()
|
|
1274
|
+
input.value = ''
|
|
1275
|
+
input.style.display = 'none'
|
|
1276
|
+
picker.render()
|
|
1277
|
+
}
|
|
1278
|
+
})
|
|
1279
|
+
return picker
|
|
1280
|
+
}
|
|
1281
|
+
const textBasePicker = makeRoleFontPicker('text', 'textBaseTitle', 'textBaseSub', 'dsrb-textbase', 'textBaseTitle')
|
|
1282
|
+
const codeFontPicker = makeRoleFontPicker('code', 'fontRowLabel', 'codeFontSub', 'dsrb-codefont', 'codeFontTitle')
|
|
1283
|
+
const terminalFontPicker = makeRoleFontPicker('terminal', 'fontRowLabel', 'terminalFontSub', 'dsrb-termfont', 'terminalFontTitle')
|
|
1284
|
+
|
|
1285
|
+
/** 字号行:标题 + 当前值 + −/+ 步进器。 */
|
|
1286
|
+
function makeStepperRow(titleText, subText) {
|
|
1287
|
+
const row = el('div', 'dsrb-row')
|
|
1288
|
+
const main = el('div', 'dsrb-rowMain')
|
|
1289
|
+
const title = el('div', 'dsrb-rowTitle')
|
|
1290
|
+
const val = el('b', null, '…')
|
|
1291
|
+
title.append(titleText, val)
|
|
1292
|
+
main.append(title, el('div', 'dsrb-rowSub', subText))
|
|
1293
|
+
const stepper = el('div', 'dsrb-stepper')
|
|
1294
|
+
const minus = el('button', null, '−')
|
|
1295
|
+
const plus = el('button', null, '+')
|
|
1296
|
+
minus.type = plus.type = 'button'
|
|
1297
|
+
minus.setAttribute('aria-label', t('minus'))
|
|
1298
|
+
plus.setAttribute('aria-label', t('plus'))
|
|
1299
|
+
stepper.append(minus, plus)
|
|
1300
|
+
row.append(main, stepper)
|
|
1301
|
+
return { row, val, minus, plus }
|
|
1302
|
+
}
|
|
1303
|
+
const { row: rowFont, val: fontVal, minus: fontMinus, plus: fontPlus } = makeStepperRow(t('fontSize'), t('fontSizeSync'))
|
|
1304
|
+
|
|
1305
|
+
const { row: rowCodeSize, val: codeSizeVal, minus: codeMinus, plus: codePlus } = makeStepperRow(t('sizeRowLabel'), t('codeFontSizeSub'))
|
|
1306
|
+
|
|
1307
|
+
// 行:中英分设(中文、西文两个独立下拉压缩在同一行;中文下拉只接管中日韩
|
|
641
1308
|
// 字符与中文标点,西文下拉只接管拉丁字母/数字/西文标点,各自独立、互不串扰)
|
|
642
1309
|
const rowTextFont = el('div', 'dsrb-row')
|
|
643
1310
|
const textFontMain = el('div', 'dsrb-rowMain')
|
|
644
|
-
textFontMain.append(el('div', 'dsrb-rowTitle', '
|
|
1311
|
+
textFontMain.append(el('div', 'dsrb-rowTitle', t('splitTitle')), el('div', 'dsrb-rowSub', t('splitSub')))
|
|
645
1312
|
const textPair = el('div', 'dsrb-pair')
|
|
646
1313
|
const cjkCol = el('div', 'dsrb-pairCol')
|
|
647
|
-
cjkCol.append(el('div', 'dsrb-mini', '
|
|
648
|
-
const textCjkSelect = el('select', 'dsrb-select')
|
|
1314
|
+
cjkCol.append(el('div', 'dsrb-mini', t('cjkLabel')))
|
|
1315
|
+
const textCjkSelect = el('select', 'dsrb-select dsrb-cjk')
|
|
1316
|
+
textCjkSelect.setAttribute('aria-label', t('cjkLabel'))
|
|
649
1317
|
cjkCol.append(textCjkSelect)
|
|
650
1318
|
const latinCol = el('div', 'dsrb-pairCol')
|
|
651
|
-
latinCol.append(el('div', 'dsrb-mini', '
|
|
652
|
-
const textLatinSelect = el('select', 'dsrb-select')
|
|
1319
|
+
latinCol.append(el('div', 'dsrb-mini', t('latinLabel')))
|
|
1320
|
+
const textLatinSelect = el('select', 'dsrb-select dsrb-latin')
|
|
1321
|
+
textLatinSelect.setAttribute('aria-label', t('latinLabel'))
|
|
653
1322
|
latinCol.append(textLatinSelect)
|
|
654
1323
|
textPair.append(cjkCol, latinCol)
|
|
655
1324
|
textFontMain.append(textPair)
|
|
656
1325
|
rowTextFont.append(textFontMain)
|
|
657
1326
|
|
|
658
|
-
|
|
659
|
-
const rowCodeFont = el('div', 'dsrb-row')
|
|
660
|
-
const codeFontMain = el('div', 'dsrb-rowMain')
|
|
661
|
-
codeFontMain.append(el('div', 'dsrb-rowTitle', '代码字体'), el('div', 'dsrb-rowSub', '仅显示本机已装字体'))
|
|
662
|
-
const codeFontSelect = el('select', 'dsrb-select')
|
|
663
|
-
codeFontMain.append(codeFontSelect)
|
|
664
|
-
rowCodeFont.append(codeFontMain)
|
|
1327
|
+
const { row: rowTerminalSize, val: termSizeVal, minus: termMinus, plus: termPlus } = makeStepperRow(t('sizeRowLabel'), t('terminalFontSizeSub'))
|
|
665
1328
|
|
|
666
1329
|
// 行:内容宽度
|
|
667
1330
|
const rowWidth = el('div', 'dsrb-row')
|
|
668
1331
|
const widthMain = el('div', 'dsrb-rowMain')
|
|
669
1332
|
const widthTitle = el('div', 'dsrb-rowTitle')
|
|
670
1333
|
const widthVal = el('b', null, '…')
|
|
671
|
-
widthTitle.append('
|
|
672
|
-
widthMain.append(widthTitle, el('div', 'dsrb-rowSub', '
|
|
1334
|
+
widthTitle.append(t('widthTitle'), widthVal)
|
|
1335
|
+
widthMain.append(widthTitle, el('div', 'dsrb-rowSub', t('widthSub')))
|
|
673
1336
|
const widthRow = el('div', 'dsrb-widthRow')
|
|
674
1337
|
const widthSlider = el('input')
|
|
675
1338
|
widthSlider.type = 'range'
|
|
1339
|
+
widthSlider.setAttribute('aria-label', t('widthTitle').trim())
|
|
676
1340
|
widthSlider.step = '5'
|
|
677
1341
|
widthSlider.min = String(CONTENT_MIN)
|
|
678
1342
|
widthSlider.max = String(CONTENT_MIN)
|
|
679
|
-
const widthReset = el('button', null, '
|
|
1343
|
+
const widthReset = el('button', null, t('widthAuto'))
|
|
680
1344
|
widthReset.type = 'button'
|
|
681
1345
|
widthRow.append(widthSlider, widthReset)
|
|
682
1346
|
widthMain.append(widthRow)
|
|
683
1347
|
rowWidth.append(widthMain)
|
|
684
1348
|
|
|
685
|
-
//
|
|
686
|
-
const rowFullscreen =
|
|
687
|
-
const fsBox = el('span', 'dsrb-switch')
|
|
688
|
-
const fsToggle = el('input')
|
|
689
|
-
fsToggle.type = 'checkbox'
|
|
690
|
-
fsBox.append(fsToggle, el('span'))
|
|
691
|
-
const fsMain = el('div', 'dsrb-rowMain')
|
|
692
|
-
fsMain.append(el('div', 'dsrb-rowTitle', '全屏'), el('div', 'dsrb-rowSub', '铺满屏幕 · Esc 退出'))
|
|
693
|
-
rowFullscreen.append(fsBox, fsMain)
|
|
1349
|
+
// 行:全屏(开关与真实全屏状态双向同步)
|
|
1350
|
+
const { row: rowFullscreen, input: fsToggle } = makeSwitchRow(t('fullscreenTitle'), t('fullscreenSub'))
|
|
694
1351
|
|
|
695
1352
|
// 三段式结构:拉拽条 / 滚动内容区 / 拉拽条 —— 内容在中间滚动,两条拉拽条
|
|
696
1353
|
// 永远贴住面板的视觉上/下缘(滚动容器里的绝对定位元素会随内容滚走,不能用绝对定位)
|
|
697
1354
|
const resizeTop = el('div', 'dsrb-resize-top')
|
|
698
1355
|
const resizeBottom = el('div', 'dsrb-resize')
|
|
699
1356
|
const panelBody = el('div', 'dsrb-body')
|
|
700
|
-
|
|
1357
|
+
// 三组折叠:组头常显,组体按记住的展开态显示
|
|
1358
|
+
groups.text.body.append(rowFont, textBasePicker.row, rowTextFont)
|
|
1359
|
+
groups.code.body.append(codeFontPicker.row, rowCodeSize)
|
|
1360
|
+
groups.terminal.body.append(terminalFontPicker.row, rowTerminalSize)
|
|
1361
|
+
applyGroupState()
|
|
1362
|
+
panelBody.append(headRow, groups.text.group, groups.code.group, groups.terminal.group, rowWidth, rowFullscreen, rowHide, rowTop, rowType)
|
|
701
1363
|
panel.append(resizeTop, panelBody, resizeBottom)
|
|
702
1364
|
panel.style.display = state.open ? '' : 'none'
|
|
703
|
-
|
|
1365
|
+
fab.setAttribute('aria-controls', PANEL_ID)
|
|
1366
|
+
fab.setAttribute('aria-expanded', state.open ? 'true' : 'false')
|
|
1367
|
+
host.appendChild(panel)
|
|
704
1368
|
|
|
705
1369
|
// ── 面板定位(四个方位择一,绝不压住按钮;避开右上角系统按钮条) ──
|
|
706
1370
|
// 右上角三枚系统按钮是 Electron WCO 原生叠层(绘制在网页层之上、无法覆盖):
|
|
@@ -709,7 +1373,8 @@ window.__ModuleLoader__.load({
|
|
|
709
1373
|
const titlebarH =
|
|
710
1374
|
Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--dsh-windows-titlebar-height')) || 40
|
|
711
1375
|
const CAP_W = 138
|
|
712
|
-
function placePanel() {
|
|
1376
|
+
function placePanel(keepScroll) {
|
|
1377
|
+
const scrollTop = typeof keepScroll === 'number' ? keepScroll : panelBody.scrollTop
|
|
713
1378
|
const r = fab.getBoundingClientRect()
|
|
714
1379
|
panel.style.height = ''
|
|
715
1380
|
panel.style.maxHeight = '' // 先按自然高度测量
|
|
@@ -771,14 +1436,22 @@ window.__ModuleLoader__.load({
|
|
|
771
1436
|
const finalH = Math.max(PANEL_MIN_H, Math.min(ph, vh - best.y - M - BORDER_Y))
|
|
772
1437
|
panel.style.maxHeight = finalH + 'px'
|
|
773
1438
|
if (typeof state.panelH === 'number') panel.style.height = finalH + 'px'
|
|
1439
|
+
// 重算高度/位置会让浏览器把超界 scrollTop 归零;按重排前的值恢复
|
|
1440
|
+
const maxScroll = Math.max(0, panelBody.scrollHeight - panelBody.clientHeight)
|
|
1441
|
+
panelBody.scrollTop = Math.min(scrollTop, maxScroll)
|
|
774
1442
|
}
|
|
775
1443
|
function togglePanel(open) {
|
|
776
1444
|
state.open = open
|
|
777
1445
|
panel.style.display = open ? '' : 'none'
|
|
1446
|
+
if (!open && panel.contains(document.activeElement) && typeof fab.focus === 'function') fab.focus()
|
|
1447
|
+
fab.setAttribute('aria-expanded', open ? 'true' : 'false')
|
|
778
1448
|
if (open) {
|
|
779
|
-
|
|
780
|
-
|
|
1449
|
+
renderAll()
|
|
1450
|
+
rebuildFontPick()
|
|
1451
|
+
refreshWidthTargets()
|
|
781
1452
|
placePanel()
|
|
1453
|
+
} else {
|
|
1454
|
+
refreshWidthTargets()
|
|
782
1455
|
}
|
|
783
1456
|
writeState({ open })
|
|
784
1457
|
}
|
|
@@ -791,6 +1464,7 @@ window.__ModuleLoader__.load({
|
|
|
791
1464
|
// 超出 22:官方设置保持原样,仅用缩放层补齐
|
|
792
1465
|
applyFont()
|
|
793
1466
|
renderFontRow()
|
|
1467
|
+
renderGroupSummaries()
|
|
794
1468
|
writeState({ fontPx: next })
|
|
795
1469
|
}
|
|
796
1470
|
function renderFontRow() {
|
|
@@ -798,12 +1472,48 @@ window.__ModuleLoader__.load({
|
|
|
798
1472
|
fontMinus.disabled = typeof state.fontPx !== 'number' || state.fontPx <= FONT_MIN
|
|
799
1473
|
fontPlus.disabled = typeof state.fontPx !== 'number' || state.fontPx >= FONT_MAX
|
|
800
1474
|
}
|
|
1475
|
+
// 官方角色字号行(代码 / 终端):只镜像快照,步进经官方接口写回
|
|
1476
|
+
function renderSizeRow(valEl, minus, plus, n, min, max) {
|
|
1477
|
+
valEl.textContent = typeof n === 'number' ? n + 'px' : '—'
|
|
1478
|
+
minus.disabled = !roleSizeSupported || typeof n !== 'number' || n <= min
|
|
1479
|
+
plus.disabled = !roleSizeSupported || typeof n !== 'number' || n >= max
|
|
1480
|
+
}
|
|
1481
|
+
function renderCodeSizeRow() {
|
|
1482
|
+
renderSizeRow(codeSizeVal, codeMinus, codePlus, fontState.code, CODE_FONT_MIN, CODE_FONT_MAX)
|
|
1483
|
+
}
|
|
1484
|
+
function renderTerminalSizeRow() {
|
|
1485
|
+
renderSizeRow(termSizeVal, termMinus, termPlus, fontState.terminal, TERMINAL_FONT_MIN, TERMINAL_FONT_MAX)
|
|
1486
|
+
}
|
|
1487
|
+
function familyLabel(list, value) {
|
|
1488
|
+
const first = String(value || '').split(',')[0].trim().replace(/^"|"$/g, '')
|
|
1489
|
+
if (!first) return ''
|
|
1490
|
+
const hit = list.find((f) => fontMatches(f, first))
|
|
1491
|
+
return hit ? hit.label : first
|
|
1492
|
+
}
|
|
1493
|
+
function syncFontPicker(picker) {
|
|
1494
|
+
picker.setEnabled(familySupported)
|
|
1495
|
+
if (familySupported) picker.render()
|
|
1496
|
+
}
|
|
1497
|
+
function renderCodeFontRow() { syncFontPicker(codeFontPicker) }
|
|
1498
|
+
function renderTerminalFontRow() { syncFontPicker(terminalFontPicker) }
|
|
1499
|
+
function renderTextBaseRow() { syncFontPicker(textBasePicker) }
|
|
1500
|
+
function renderGroupSummaries() {
|
|
1501
|
+
const textSize = typeof state.fontPx === 'number' ? state.fontPx + 'px' : '—'
|
|
1502
|
+
const split = !!(state.textFontCjk || state.textFontLatin)
|
|
1503
|
+
groups.text.sub.textContent =
|
|
1504
|
+
textSize + ' · ' + (split ? t('groupSummarySplit') : fontState.textFamily ? t('groupSummaryOfficial') : t('defaultText'))
|
|
1505
|
+
const codeSize = typeof fontState.code === 'number' ? fontState.code + 'px' : '—'
|
|
1506
|
+
groups.code.sub.textContent = codeSize + ' · ' + (familyLabel(availCode, fontState.codeFamily) || t('defaultText'))
|
|
1507
|
+
const termSize = typeof fontState.terminal === 'number' ? fontState.terminal + 'px' : '—'
|
|
1508
|
+
groups.terminal.sub.textContent =
|
|
1509
|
+
termSize + ' · ' + (familyLabel(availCode, fontState.terminalFamily) || t('defaultText'))
|
|
1510
|
+
}
|
|
801
1511
|
function renderWidthRow() {
|
|
802
1512
|
const info = currentWidthInfo()
|
|
803
1513
|
if (!info) {
|
|
804
1514
|
widthSlider.disabled = true
|
|
805
1515
|
widthReset.disabled = true
|
|
806
|
-
widthVal.textContent = '
|
|
1516
|
+
widthVal.textContent = t('widthUnavailable')
|
|
807
1517
|
return
|
|
808
1518
|
}
|
|
809
1519
|
widthSlider.min = String(info.min)
|
|
@@ -817,11 +1527,18 @@ window.__ModuleLoader__.load({
|
|
|
817
1527
|
}
|
|
818
1528
|
function renderAll() {
|
|
819
1529
|
hideToggle.checked = !!state.hideComposer
|
|
1530
|
+
hideToggle.setAttribute('aria-checked', hideToggle.checked ? 'true' : 'false')
|
|
820
1531
|
topToggle.checked = !!state.hideTop
|
|
1532
|
+
topToggle.setAttribute('aria-checked', topToggle.checked ? 'true' : 'false')
|
|
821
1533
|
textCjkSelect.value = state.textFontCjk ?? ''
|
|
822
1534
|
textLatinSelect.value = state.textFontLatin ?? ''
|
|
823
|
-
codeFontSelect.value = state.codeFont ?? ''
|
|
824
1535
|
renderFontRow()
|
|
1536
|
+
renderCodeSizeRow()
|
|
1537
|
+
renderTerminalSizeRow()
|
|
1538
|
+
renderCodeFontRow()
|
|
1539
|
+
renderTerminalFontRow()
|
|
1540
|
+
renderTextBaseRow()
|
|
1541
|
+
renderGroupSummaries()
|
|
825
1542
|
renderTypeRow()
|
|
826
1543
|
renderWidthRow()
|
|
827
1544
|
}
|
|
@@ -829,6 +1546,7 @@ window.__ModuleLoader__.load({
|
|
|
829
1546
|
// ── 功能 1:隐藏输入区(开关 + 整条长条可点) ────────────────────
|
|
830
1547
|
function applyHideComposer() {
|
|
831
1548
|
state.hideComposer = hideToggle.checked
|
|
1549
|
+
hideToggle.setAttribute('aria-checked', hideToggle.checked ? 'true' : 'false')
|
|
832
1550
|
hideTag.disabled = !state.hideComposer
|
|
833
1551
|
writeState({ hideComposer: state.hideComposer })
|
|
834
1552
|
}
|
|
@@ -838,6 +1556,7 @@ window.__ModuleLoader__.load({
|
|
|
838
1556
|
// ── 功能 2:隐藏上边栏(开关 + 整条长条可点) ────────────────────
|
|
839
1557
|
function applyHideTop() {
|
|
840
1558
|
state.hideTop = topToggle.checked
|
|
1559
|
+
topToggle.setAttribute('aria-checked', topToggle.checked ? 'true' : 'false')
|
|
841
1560
|
hideTopTag.disabled = !state.hideTop
|
|
842
1561
|
writeState({ hideTop: state.hideTop })
|
|
843
1562
|
}
|
|
@@ -853,6 +1572,7 @@ window.__ModuleLoader__.load({
|
|
|
853
1572
|
// 其它元素全屏),因此由 fullscreenchange 事件无条件校正开关与补偿标记
|
|
854
1573
|
const on = inPageFullscreen()
|
|
855
1574
|
fsToggle.checked = on
|
|
1575
|
+
fsToggle.setAttribute('aria-checked', on ? 'true' : 'false')
|
|
856
1576
|
const root = document.documentElement
|
|
857
1577
|
if (on) root.setAttribute(FS_ATTR, '')
|
|
858
1578
|
else root.removeAttribute(FS_ATTR)
|
|
@@ -886,53 +1606,47 @@ window.__ModuleLoader__.load({
|
|
|
886
1606
|
fontPlus.addEventListener('click', () => {
|
|
887
1607
|
setFontPx((typeof state.fontPx === 'number' ? state.fontPx : fontState.official ?? 16) + 1)
|
|
888
1608
|
})
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
state.textFontCjk = fontChoice(availCjk, id) ? id : null
|
|
894
|
-
textCjkSelect.value = state.textFontCjk ?? ''
|
|
895
|
-
writeState({ textFontCjk: state.textFontCjk })
|
|
896
|
-
fontPickBuilt = false
|
|
897
|
-
rebuildFontPick()
|
|
1609
|
+
// 代码字号:官方角色字号(10–16)的入口;写回后由 theme/change 快照刷新显示
|
|
1610
|
+
codeMinus.addEventListener('click', () => {
|
|
1611
|
+
if (typeof fontState.code !== 'number') return
|
|
1612
|
+
officialSetFontSize('code', Math.max(CODE_FONT_MIN, fontState.code - 1))
|
|
898
1613
|
})
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
textLatinSelect.value = state.textFontLatin ?? ''
|
|
903
|
-
writeState({ textFontLatin: state.textFontLatin })
|
|
904
|
-
fontPickBuilt = false
|
|
905
|
-
rebuildFontPick()
|
|
1614
|
+
codePlus.addEventListener('click', () => {
|
|
1615
|
+
if (typeof fontState.code !== 'number') return
|
|
1616
|
+
officialSetFontSize('code', Math.min(CODE_FONT_MAX, fontState.code + 1))
|
|
906
1617
|
})
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1618
|
+
|
|
1619
|
+
// ── 功能 5:字体选择(中英分设 / 官方基础 / 代码 / 终端) ──────
|
|
1620
|
+
textCjkSelect.addEventListener('change', () => commitTextFont('cjk', textCjkSelect.value))
|
|
1621
|
+
textLatinSelect.addEventListener('change', () => commitTextFont('latin', textLatinSelect.value))
|
|
1622
|
+
// 终端字号(官方角色字号)
|
|
1623
|
+
termMinus.addEventListener('click', () => {
|
|
1624
|
+
if (typeof fontState.terminal !== 'number') return
|
|
1625
|
+
officialSetFontSize('terminal', Math.max(TERMINAL_FONT_MIN, fontState.terminal - 1))
|
|
1626
|
+
})
|
|
1627
|
+
termPlus.addEventListener('click', () => {
|
|
1628
|
+
if (typeof fontState.terminal !== 'number') return
|
|
1629
|
+
officialSetFontSize('terminal', Math.min(TERMINAL_FONT_MAX, fontState.terminal + 1))
|
|
914
1630
|
})
|
|
915
1631
|
|
|
916
1632
|
// ── 功能 7:行距 / 字距(0.1 步进;到达边界即回"默认",覆盖层随之停用) ──
|
|
917
1633
|
function setLineHeight(v) {
|
|
918
1634
|
const next = Math.min(LINEH_MAX, Math.max(LINEH_MIN, Math.round(v * 10) / 10))
|
|
919
1635
|
state.lineHeight = next > LINEH_MIN ? next : null
|
|
920
|
-
|
|
921
|
-
rebuildFontPick()
|
|
1636
|
+
kickFontPick()
|
|
922
1637
|
renderTypeRow()
|
|
923
1638
|
writeState({ lineHeight: state.lineHeight })
|
|
924
1639
|
}
|
|
925
1640
|
function setLetterSpacing(v) {
|
|
926
1641
|
const next = Math.min(LETTERSP_MAX, Math.max(LETTERSP_MIN, Math.round(v * 10) / 10))
|
|
927
1642
|
state.letterSpacing = next !== 0 ? next : null
|
|
928
|
-
|
|
929
|
-
rebuildFontPick()
|
|
1643
|
+
kickFontPick()
|
|
930
1644
|
renderTypeRow()
|
|
931
1645
|
writeState({ letterSpacing: state.letterSpacing })
|
|
932
1646
|
}
|
|
933
1647
|
function renderTypeRow() {
|
|
934
|
-
lhVal.textContent = state.lineHeight ? state.lineHeight.toFixed(1) + '×' : '
|
|
935
|
-
lsVal.textContent = state.letterSpacing ? state.letterSpacing.toFixed(1) + 'px' : '
|
|
1648
|
+
lhVal.textContent = state.lineHeight ? state.lineHeight.toFixed(1) + '×' : t('defaultText')
|
|
1649
|
+
lsVal.textContent = state.letterSpacing ? state.letterSpacing.toFixed(1) + 'px' : t('defaultText')
|
|
936
1650
|
}
|
|
937
1651
|
lhMinus.addEventListener('click', () => setLineHeight((state.lineHeight ?? LINEH_MIN) - 0.1))
|
|
938
1652
|
lhPlus.addEventListener('click', () => setLineHeight((state.lineHeight ?? LINEH_MIN) + 0.1))
|
|
@@ -945,16 +1659,67 @@ window.__ModuleLoader__.load({
|
|
|
945
1659
|
let officialDragMoved = false
|
|
946
1660
|
let officialDragStart = null
|
|
947
1661
|
let officialDragAdopt = false
|
|
1662
|
+
let widthStartupAdopted = false
|
|
1663
|
+
/** 首次拿到有效列宽时,范围宽度以官方键为准:插件停用期间官方改过的宽度
|
|
1664
|
+
* 不会被旧状态覆盖;扩展宽度(> 官方上限)由插件自己持有,不参与采纳。 */
|
|
1665
|
+
function adoptOfficialWidthOnStart() {
|
|
1666
|
+
if (widthStartupAdopted) return
|
|
1667
|
+
const t = widthTargets()
|
|
1668
|
+
if (!t || !(t.column > 0)) return
|
|
1669
|
+
widthStartupAdopted = true
|
|
1670
|
+
if (state.widthPx == null) {
|
|
1671
|
+
markWidthExt(false)
|
|
1672
|
+
return
|
|
1673
|
+
}
|
|
1674
|
+
const omax = officialMaxFor(t.column)
|
|
1675
|
+
const pref = readWidthPreference()
|
|
1676
|
+
if (state.widthExt === true) return // 插件扩展:官方 peg 值不参与
|
|
1677
|
+
if (state.widthExt === false) {
|
|
1678
|
+
// 已知不是扩展:跟随官方。官方键超当前上限按上限;键缺失则收到上限(不再越限)
|
|
1679
|
+
markWidthExt(false)
|
|
1680
|
+
const official = pref == null ? Math.min(state.widthPx, omax) : Math.min(pref, omax)
|
|
1681
|
+
if (official !== state.widthPx) {
|
|
1682
|
+
state.widthPx = Math.min(Math.max(Math.round(official), CONTENT_MIN), extCapFor(t.column))
|
|
1683
|
+
writeState({ widthPx: state.widthPx })
|
|
1684
|
+
}
|
|
1685
|
+
return
|
|
1686
|
+
}
|
|
1687
|
+
// 旧版状态(无 widthExt):用官方键与插件值的关系推断
|
|
1688
|
+
if (pref == null) {
|
|
1689
|
+
if (state.widthPx > omax) markWidthExt(true)
|
|
1690
|
+
else markWidthExt(false)
|
|
1691
|
+
return
|
|
1692
|
+
}
|
|
1693
|
+
if (pref === state.widthPx && state.widthPx > omax) {
|
|
1694
|
+
// 同一官方镜像因窗口变窄而超出上限:按上限收敛,不能误当扩展覆盖
|
|
1695
|
+
markWidthExt(false)
|
|
1696
|
+
state.widthPx = Math.min(Math.max(Math.round(omax), CONTENT_MIN), extCapFor(t.column))
|
|
1697
|
+
writeState({ widthPx: state.widthPx })
|
|
1698
|
+
return
|
|
1699
|
+
}
|
|
1700
|
+
if (pref < state.widthPx) {
|
|
1701
|
+
// 官方键更低:旧版扩展通常被 peg 在旧上限;保留插件宽度,避免丢扩展
|
|
1702
|
+
markWidthExt(state.widthPx > omax)
|
|
1703
|
+
return
|
|
1704
|
+
}
|
|
1705
|
+
// 官方调大或一致:采纳官方值(超上限按上限)
|
|
1706
|
+
const official = Math.min(pref, omax)
|
|
1707
|
+
markWidthExt(false)
|
|
1708
|
+
if (official === state.widthPx) return
|
|
1709
|
+
state.widthPx = Math.min(Math.max(Math.round(official), CONTENT_MIN), extCapFor(t.column))
|
|
1710
|
+
writeState({ widthPx: state.widthPx })
|
|
1711
|
+
}
|
|
948
1712
|
function setWidthPx(v) {
|
|
949
1713
|
if (v === null) {
|
|
950
1714
|
state.widthPx = null
|
|
951
1715
|
} else {
|
|
952
1716
|
const t = widthTargets()
|
|
953
|
-
if (!t) return //
|
|
1717
|
+
if (!t || !(t.column > 0)) return // 无可用会话列宽(滑块与"自动"此时均处于禁用态)
|
|
954
1718
|
state.widthPx = Math.min(Math.max(Math.round(v), CONTENT_MIN), extCapFor(t.column))
|
|
955
1719
|
}
|
|
956
|
-
applyWidthState()
|
|
1720
|
+
applyWidthState(v === null)
|
|
957
1721
|
writeState({ widthPx: state.widthPx })
|
|
1722
|
+
refreshWidthTargets()
|
|
958
1723
|
renderWidthRow()
|
|
959
1724
|
}
|
|
960
1725
|
widthSlider.addEventListener('pointerdown', () => {
|
|
@@ -967,6 +1732,7 @@ window.__ModuleLoader__.load({
|
|
|
967
1732
|
}
|
|
968
1733
|
}
|
|
969
1734
|
window.addEventListener('pointerup', onWindowPointerUp)
|
|
1735
|
+
window.addEventListener('pointercancel', onWindowPointerUp)
|
|
970
1736
|
widthSlider.addEventListener('input', () => {
|
|
971
1737
|
setWidthPx(Number(widthSlider.value))
|
|
972
1738
|
})
|
|
@@ -979,11 +1745,13 @@ window.__ModuleLoader__.load({
|
|
|
979
1745
|
const target = e.target
|
|
980
1746
|
const handle = target && target.closest ? target.closest('[data-width-handle]') : null
|
|
981
1747
|
if (handle) {
|
|
1748
|
+
if (state.open) attachWidthObserver()
|
|
1749
|
+
attachColumnObserver()
|
|
982
1750
|
officialDragging = true
|
|
983
1751
|
officialDragMoved = false
|
|
984
1752
|
officialDragStart = { x: e.clientX, y: e.clientY }
|
|
985
1753
|
const t = widthTargets()
|
|
986
|
-
officialDragAdopt = !!(t && state.widthPx != null && state.widthPx > officialMaxFor(t.column))
|
|
1754
|
+
officialDragAdopt = !!(t && t.column > 0 && state.widthPx != null && state.widthPx > officialMaxFor(t.column))
|
|
987
1755
|
}
|
|
988
1756
|
if (state.open && !panel.contains(target) && !fab.contains(target)) {
|
|
989
1757
|
togglePanel(false)
|
|
@@ -1002,29 +1770,38 @@ window.__ModuleLoader__.load({
|
|
|
1002
1770
|
}
|
|
1003
1771
|
function onDocPointerUp() {
|
|
1004
1772
|
if (!officialDragging) return
|
|
1005
|
-
const
|
|
1773
|
+
const moved = officialDragMoved
|
|
1006
1774
|
officialDragMoved = false
|
|
1007
1775
|
officialDragStart = null
|
|
1008
1776
|
officialDragAdopt = false
|
|
1009
|
-
if (!
|
|
1777
|
+
if (!moved) {
|
|
1010
1778
|
officialDragging = false
|
|
1779
|
+
refreshWidthTargets()
|
|
1011
1780
|
return
|
|
1012
1781
|
}
|
|
1013
|
-
//
|
|
1782
|
+
// 任何一次官方手柄拖动松手后都采纳官方最终值(含范围内拖动);
|
|
1783
|
+
// 等待 0ms 让官方先提交,期间保持"官方拖动中"避免漂移修复抢跑。
|
|
1014
1784
|
setTimeout(() => {
|
|
1785
|
+
if (disposed) return
|
|
1015
1786
|
officialDragging = false
|
|
1016
1787
|
const pref = readWidthPreference()
|
|
1017
1788
|
state.widthPx = pref != null ? pref : null
|
|
1018
1789
|
applyWidthState()
|
|
1019
1790
|
writeState({ widthPx: state.widthPx })
|
|
1020
|
-
|
|
1791
|
+
refreshWidthTargets()
|
|
1021
1792
|
}, 0)
|
|
1022
1793
|
}
|
|
1023
1794
|
function onDocPointerCancel() {
|
|
1795
|
+
const restore = officialDragAdopt && state.widthPx != null
|
|
1024
1796
|
officialDragging = false
|
|
1025
1797
|
officialDragMoved = false
|
|
1026
1798
|
officialDragStart = null
|
|
1027
1799
|
officialDragAdopt = false
|
|
1800
|
+
if (restore) {
|
|
1801
|
+
// 拖动被系统取消(未松手):把交还官方时移除的扩展覆盖装回去
|
|
1802
|
+
applyWidthState()
|
|
1803
|
+
}
|
|
1804
|
+
refreshWidthTargets() // 释放拖动期临时挂上的观察器,并按当前宽度归位
|
|
1028
1805
|
}
|
|
1029
1806
|
document.addEventListener('pointerdown', onDocPointerDown, true)
|
|
1030
1807
|
document.addEventListener('pointermove', onDocPointerMove, true)
|
|
@@ -1034,7 +1811,7 @@ window.__ModuleLoader__.load({
|
|
|
1034
1811
|
// 官方手柄拖动 / 窗口尺寸变化 → 目标元素 style 变化 → 反向同步面板
|
|
1035
1812
|
let watchedTarget = null
|
|
1036
1813
|
const widthObserver = new MutationObserver(() => {
|
|
1037
|
-
if (!widthDragging) renderWidthRow()
|
|
1814
|
+
if (!disposed && state.open && !widthDragging) renderWidthRow()
|
|
1038
1815
|
})
|
|
1039
1816
|
function attachWidthObserver() {
|
|
1040
1817
|
const t = widthTargets()
|
|
@@ -1050,7 +1827,11 @@ window.__ModuleLoader__.load({
|
|
|
1050
1827
|
// 官方键/官方变量的收敛压到同一批回调内,开合过程不再出现滞留窄值的中间态。
|
|
1051
1828
|
let watchedColumn = null
|
|
1052
1829
|
const columnObserver = new ResizeObserver(() => {
|
|
1830
|
+
if (disposed) return
|
|
1831
|
+
adoptOfficialWidthOnStart()
|
|
1053
1832
|
normalizeWidthDrift()
|
|
1833
|
+
if (state.widthPx != null && !widthTargets()) scheduleWidthTargetRetry()
|
|
1834
|
+
else widthTargetRetryCount = 0
|
|
1054
1835
|
if (state.open && !officialDragging && !widthDragging) renderWidthRow()
|
|
1055
1836
|
})
|
|
1056
1837
|
function attachColumnObserver() {
|
|
@@ -1062,84 +1843,58 @@ window.__ModuleLoader__.load({
|
|
|
1062
1843
|
if (container) columnObserver.observe(container)
|
|
1063
1844
|
}
|
|
1064
1845
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
firstSync &&
|
|
1075
|
-
typeof state.fontPx === 'number' &&
|
|
1076
|
-
state.fontPx <= FONT_OFFICIAL_MAX &&
|
|
1077
|
-
state.fontPx !== fontState.official
|
|
1078
|
-
) {
|
|
1079
|
-
// 启动恢复:面板上次设定的字号(范围内)直写回官方设置,
|
|
1080
|
-
// 保证"范围内 ⇔ 官方值 === 面板值"的稳定关系
|
|
1081
|
-
firstSync = false
|
|
1082
|
-
applyFont()
|
|
1083
|
-
writeOfficialFont(state.fontPx) // 官方更新后会再次进入本函数(自写标记路径)
|
|
1084
|
-
return
|
|
1085
|
-
}
|
|
1086
|
-
firstSync = false
|
|
1087
|
-
if (typeof state.fontPx !== 'number') state.fontPx = fontState.official
|
|
1088
|
-
else if (state.fontPx <= FONT_OFFICIAL_MAX) state.fontPx = fontState.official // 范围内以官方为准
|
|
1089
|
-
// 超出 22 的绝对目标保持不动,倍率自动适配
|
|
1090
|
-
writeState({ fontPx: state.fontPx })
|
|
1091
|
-
renderFontRow()
|
|
1092
|
-
}
|
|
1093
|
-
applyFont()
|
|
1846
|
+
/** 只在明确事件(启动/会话切换/面板打开/窗口变化/官方手柄)后复核宽度。 */
|
|
1847
|
+
function refreshWidthTargets() {
|
|
1848
|
+
adoptOfficialWidthOnStart()
|
|
1849
|
+
// 只有面板打开时才需要监听官方手柄写变量(用于滑杆实时回显);
|
|
1850
|
+
// 列观察仅在扩展宽度生效或面板打开时需要;其余时候不挂任何后台观察器。
|
|
1851
|
+
if (state.open) attachWidthObserver()
|
|
1852
|
+
else {
|
|
1853
|
+
widthObserver.disconnect()
|
|
1854
|
+
watchedTarget = null
|
|
1094
1855
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
sync(tctx.theme.getTheme())
|
|
1100
|
-
} catch {
|
|
1101
|
-
applyFont() // 服务首读异常时至少按兜底基准绘制一次
|
|
1856
|
+
if (state.open || state.widthPx != null) attachColumnObserver()
|
|
1857
|
+
else {
|
|
1858
|
+
columnObserver.disconnect()
|
|
1859
|
+
watchedColumn = null
|
|
1102
1860
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
state.
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1861
|
+
normalizeWidthDrift()
|
|
1862
|
+
// 会话容器可能晚于插件挂载:有保存宽度但暂时没有目标时,做有限次重试
|
|
1863
|
+
if (state.widthPx != null && !widthTargets()) scheduleWidthTargetRetry()
|
|
1864
|
+
else widthTargetRetryCount = 0
|
|
1865
|
+
if (state.open && !officialDragging && !widthDragging) renderWidthRow()
|
|
1866
|
+
}
|
|
1867
|
+
let widthRefreshTimer = null
|
|
1868
|
+
let widthTargetRetry = null
|
|
1869
|
+
let widthTargetRetryCount = 0
|
|
1870
|
+
function scheduleWidthTargetRetry() {
|
|
1871
|
+
if (disposed || widthTargetRetry || widthTargetRetryCount >= 3) return
|
|
1872
|
+
widthTargetRetry = setTimeout(() => {
|
|
1873
|
+
widthTargetRetry = null
|
|
1874
|
+
widthTargetRetryCount += 1
|
|
1875
|
+
if (!disposed) refreshWidthTargets()
|
|
1876
|
+
}, 250)
|
|
1877
|
+
}
|
|
1878
|
+
function scheduleWidthRefresh() {
|
|
1879
|
+
// 自动宽度且面板关闭时,会话切换没有宽度工作要做,直接忽略
|
|
1880
|
+
if (!state.open && state.widthPx == null) return
|
|
1881
|
+
if (disposed || widthRefreshTimer) return
|
|
1882
|
+
widthRefreshTimer = setTimeout(() => {
|
|
1883
|
+
widthRefreshTimer = null
|
|
1884
|
+
if (!disposed) refreshWidthTargets()
|
|
1885
|
+
}, 0)
|
|
1110
1886
|
}
|
|
1111
|
-
if (typeof state.widthPx !== 'number' || !(state.widthPx > 0)) state.widthPx = null
|
|
1112
|
-
if (typeof state.lineHeight === 'number') {
|
|
1113
|
-
const lh = Math.min(LINEH_MAX, Math.max(LINEH_MIN, Math.round(state.lineHeight * 10) / 10))
|
|
1114
|
-
state.lineHeight = lh > LINEH_MIN ? lh : null
|
|
1115
|
-
} else state.lineHeight = null
|
|
1116
|
-
if (typeof state.letterSpacing === 'number') {
|
|
1117
|
-
const ls = Math.min(LETTERSP_MAX, Math.max(LETTERSP_MIN, Math.round(state.letterSpacing * 10) / 10))
|
|
1118
|
-
state.letterSpacing = ls !== 0 ? ls : null
|
|
1119
|
-
} else state.letterSpacing = null
|
|
1120
|
-
state.panelX = typeof state.panelX === 'number' ? state.panelX : null
|
|
1121
|
-
state.panelY = typeof state.panelY === 'number' ? state.panelY : null
|
|
1122
|
-
state.panelH = typeof state.panelH === 'number' && state.panelH > 0 ? state.panelH : null
|
|
1123
1887
|
|
|
1124
1888
|
// ── 字体选择:本机可用性过滤 + 选择恢复 + 覆盖层生成 ────────────
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
const availCjk = TEXT_FONTS_CJK.filter((f) => fontAvailable(f.name)) // 只保留本机已装
|
|
1137
|
-
const availLatin = TEXT_FONTS_LATIN.filter((f) => fontAvailable(f.name))
|
|
1138
|
-
const availCode = CODE_FONTS.filter((f) => fontAvailable(f.name))
|
|
1139
|
-
fillFontSelect(textCjkSelect, availCjk)
|
|
1140
|
-
fillFontSelect(textLatinSelect, availLatin)
|
|
1141
|
-
fillFontSelect(codeFontSelect, availCode)
|
|
1142
|
-
// 已保存的选择若已不在本机(被卸载 / 换机器)→ 回默认
|
|
1889
|
+
const availCjk = installedFonts('cjk')
|
|
1890
|
+
const availLatin = installedFonts('latin')
|
|
1891
|
+
const availCode = installedFonts('code')
|
|
1892
|
+
fillFontOptions(textCjkSelect, availCjk)
|
|
1893
|
+
fillFontOptions(textLatinSelect, availLatin)
|
|
1894
|
+
textBasePicker.fill(installedFonts('text'))
|
|
1895
|
+
codeFontPicker.fill(availCode)
|
|
1896
|
+
terminalFontPicker.fill(availCode)
|
|
1897
|
+
// 已保存的正文字体选择若已不在本机(被卸载 / 换机器)→ 回默认
|
|
1143
1898
|
let fontsReset = false
|
|
1144
1899
|
if (state.textFontCjk && !fontChoice(availCjk, state.textFontCjk)) {
|
|
1145
1900
|
state.textFontCjk = null
|
|
@@ -1149,29 +1904,61 @@ window.__ModuleLoader__.load({
|
|
|
1149
1904
|
state.textFontLatin = null
|
|
1150
1905
|
fontsReset = true
|
|
1151
1906
|
}
|
|
1152
|
-
if (state.
|
|
1153
|
-
state.codeFont = null
|
|
1154
|
-
fontsReset = true
|
|
1155
|
-
}
|
|
1156
|
-
if (fontsReset) writeState({ textFontCjk: state.textFontCjk, textFontLatin: state.textFontLatin, codeFont: state.codeFont })
|
|
1907
|
+
if (fontsReset) writeState({ textFontCjk: state.textFontCjk, textFontLatin: state.textFontLatin })
|
|
1157
1908
|
let fontPickBuilt = false
|
|
1158
|
-
|
|
1909
|
+
let fontRetryTimer = null
|
|
1910
|
+
let fontRetryStep = 0
|
|
1911
|
+
const FONT_RETRY_DELAYS = [250, 750, 1500, 3000, 5000]
|
|
1912
|
+
function hasFontFeatures() {
|
|
1913
|
+
return !!(state.textFontCjk || state.textFontLatin || state.lineHeight != null || state.letterSpacing != null)
|
|
1914
|
+
}
|
|
1915
|
+
function cancelFontRetry() {
|
|
1916
|
+
if (fontRetryTimer) {
|
|
1917
|
+
clearTimeout(fontRetryTimer)
|
|
1918
|
+
fontRetryTimer = null
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
/** 官方主题样式可能晚于插件就绪:有限次退避重试,不常驻轮询。 */
|
|
1922
|
+
function scheduleFontRetry() {
|
|
1923
|
+
if (disposed || fontPickBuilt || fontRetryTimer || !hasFontFeatures()) return
|
|
1924
|
+
if (fontRetryStep >= FONT_RETRY_DELAYS.length) return
|
|
1925
|
+
const delay = FONT_RETRY_DELAYS[fontRetryStep++]
|
|
1926
|
+
fontRetryTimer = setTimeout(() => {
|
|
1927
|
+
fontRetryTimer = null
|
|
1928
|
+
if (!disposed) rebuildFontPick()
|
|
1929
|
+
}, delay)
|
|
1930
|
+
}
|
|
1931
|
+
/** 生成 / 刷新字体与排版覆盖层;主题未就绪时保持停用并安排有限重试。 */
|
|
1159
1932
|
function rebuildFontPick() {
|
|
1160
|
-
if (!
|
|
1933
|
+
if (!hasFontFeatures()) {
|
|
1934
|
+
cancelFontRetry()
|
|
1161
1935
|
fontPickTag.disabled = true
|
|
1162
1936
|
fontPickBuilt = false
|
|
1163
1937
|
return
|
|
1164
1938
|
}
|
|
1165
1939
|
if (fontPickBuilt) return
|
|
1166
1940
|
const css = buildFontPickCss()
|
|
1167
|
-
if (!css)
|
|
1941
|
+
if (!css) {
|
|
1942
|
+
scheduleFontRetry()
|
|
1943
|
+
return
|
|
1944
|
+
}
|
|
1945
|
+
cancelFontRetry()
|
|
1946
|
+
fontRetryStep = 0
|
|
1168
1947
|
fontPickTag.textContent = css
|
|
1169
1948
|
fontPickTag.disabled = false
|
|
1170
1949
|
fontPickBuilt = true
|
|
1171
1950
|
}
|
|
1951
|
+
/** 用户主动改字体/行距:重开一轮有限重试。 */
|
|
1952
|
+
function kickFontPick() {
|
|
1953
|
+
fontRetryStep = 0
|
|
1954
|
+
cancelFontRetry()
|
|
1955
|
+
fontPickBuilt = false
|
|
1956
|
+
rebuildFontPick()
|
|
1957
|
+
}
|
|
1172
1958
|
|
|
1173
1959
|
applyFont()
|
|
1174
1960
|
renderAll()
|
|
1961
|
+
adoptOfficialWidthOnStart()
|
|
1175
1962
|
applyWidthState()
|
|
1176
1963
|
rebuildFontPick()
|
|
1177
1964
|
// 面板开着启动(上次退出未关):补挂载定位——不经 togglePanel 时面板没有
|
|
@@ -1214,6 +2001,11 @@ window.__ModuleLoader__.load({
|
|
|
1214
2001
|
fab.addEventListener('pointermove', onPointerMove)
|
|
1215
2002
|
fab.addEventListener('pointerup', onPointerUp)
|
|
1216
2003
|
fab.addEventListener('pointercancel', onPointerCancel)
|
|
2004
|
+
// 键盘激活(Enter/Space)产生的 click 没有指针坐标,单独走开合;
|
|
2005
|
+
// 鼠标 click 的 detail>0,由 pointerup 路径处理,避免双触发。
|
|
2006
|
+
fab.addEventListener('click', (e) => {
|
|
2007
|
+
if (e.detail === 0) togglePanel(!state.open)
|
|
2008
|
+
})
|
|
1217
2009
|
closeBtn.addEventListener('click', () => togglePanel(false))
|
|
1218
2010
|
|
|
1219
2011
|
// ── 功能 8:面板拖动 / 拉拽高度 ──────────────────────────────────
|
|
@@ -1322,7 +2114,7 @@ window.__ModuleLoader__.load({
|
|
|
1322
2114
|
window.addEventListener('pointermove', onPanelPointerMove)
|
|
1323
2115
|
window.addEventListener('pointerup', onPanelPointerUp)
|
|
1324
2116
|
window.addEventListener('pointercancel', onPanelPointerUp)
|
|
1325
|
-
headRow.title = '
|
|
2117
|
+
headRow.title = t('dragHint')
|
|
1326
2118
|
headRow.addEventListener('dblclick', () => {
|
|
1327
2119
|
// 复位:回到自动布局(位置 + 高度一起,回到"高度锁"状态)
|
|
1328
2120
|
state.panelX = null
|
|
@@ -1369,16 +2161,46 @@ window.__ModuleLoader__.load({
|
|
|
1369
2161
|
})
|
|
1370
2162
|
return turndownSvc
|
|
1371
2163
|
}
|
|
2164
|
+
/** 压缩段间空行,但围栏代码块内部原样保留(代码里的连续空行不能被折掉)。 */
|
|
2165
|
+
function tidyMarkdown(md) {
|
|
2166
|
+
const out = []
|
|
2167
|
+
let fence = null
|
|
2168
|
+
let blanks = 0
|
|
2169
|
+
for (const line of md.split('\n')) {
|
|
2170
|
+
const m = /^\s*(?:>\s*)*(`{3,}|~{3,})/.exec(line)
|
|
2171
|
+
if (fence) {
|
|
2172
|
+
out.push(line)
|
|
2173
|
+
if (m && m[1][0] === fence[0] && m[1].length >= fence.length &&
|
|
2174
|
+
/^\s*(?:>\s*)*(?:`{3,}|~{3,})\s*$/.test(line)) fence = null
|
|
2175
|
+
continue
|
|
2176
|
+
}
|
|
2177
|
+
if (!line.trim()) {
|
|
2178
|
+
if (blanks < 1) out.push('')
|
|
2179
|
+
blanks += 1
|
|
2180
|
+
continue
|
|
2181
|
+
}
|
|
2182
|
+
blanks = 0
|
|
2183
|
+
out.push(line)
|
|
2184
|
+
if (m) fence = m[1]
|
|
2185
|
+
}
|
|
2186
|
+
return out.join('\n').trim()
|
|
2187
|
+
}
|
|
1372
2188
|
function onCopy(e) {
|
|
1373
2189
|
try {
|
|
1374
2190
|
const sel = document.getSelection()
|
|
1375
2191
|
if (!sel || sel.isCollapsed || !sel.rangeCount) return
|
|
1376
2192
|
// 选区两端都要在消息行内才接管(跨区选择保持系统默认行为)
|
|
1377
2193
|
const inChat = (n) => {
|
|
1378
|
-
const
|
|
1379
|
-
return !!(
|
|
2194
|
+
const node = n && (n.nodeType === 3 ? n.parentElement : n)
|
|
2195
|
+
return !!(node && node.closest && node.closest('[data-chat-node-key]'))
|
|
1380
2196
|
}
|
|
1381
2197
|
if (!inChat(sel.anchorNode) || !inChat(sel.focusNode)) return
|
|
2198
|
+
// 可编辑区域(输入框/富文本编辑器)保持原生复制
|
|
2199
|
+
const inEditable = (n) => {
|
|
2200
|
+
const node = n && (n.nodeType === 3 ? n.parentElement : n)
|
|
2201
|
+
return !!(node && node.closest && node.closest('input, textarea, [contenteditable="true"], [contenteditable=""], [contenteditable="plaintext-only"]'))
|
|
2202
|
+
}
|
|
2203
|
+
if (inEditable(sel.anchorNode) || inEditable(sel.focusNode)) return
|
|
1382
2204
|
const elOf = (n) => (n && n.nodeType === 3 ? n.parentElement : n)
|
|
1383
2205
|
const ka = elOf(sel.anchorNode).closest('.katex')
|
|
1384
2206
|
const kf = elOf(sel.focusNode).closest('.katex')
|
|
@@ -1393,10 +2215,17 @@ window.__ModuleLoader__.load({
|
|
|
1393
2215
|
}
|
|
1394
2216
|
}
|
|
1395
2217
|
// 与 KaTeX 官方 copy-tex 同策略:端点落在公式内时先把工作范围扩到整个公式,
|
|
1396
|
-
// "只碰一角"也能拿到完整 LaTeX
|
|
2218
|
+
// "只碰一角"也能拿到完整 LaTeX。必须按 Range 的实际起止端判断,
|
|
2219
|
+
// 不能按 anchor/focus —— 从右向左拖选时方向相反,按方向扩会截掉选区左侧。
|
|
1397
2220
|
const work = sel.getRangeAt(0).cloneRange()
|
|
1398
|
-
|
|
1399
|
-
|
|
2221
|
+
const katexAt = (n) => {
|
|
2222
|
+
const node = elOf(n)
|
|
2223
|
+
return node && node.closest ? node.closest('.katex') : null
|
|
2224
|
+
}
|
|
2225
|
+
const ks = katexAt(work.startContainer)
|
|
2226
|
+
const ke = katexAt(work.endContainer)
|
|
2227
|
+
if (ks) work.setStartBefore(ks)
|
|
2228
|
+
if (ke) work.setEndAfter(ke)
|
|
1400
2229
|
const box = document.createElement('div')
|
|
1401
2230
|
// 选区克隆丢掉共同祖先往上的层级(代码块外层的 <pre> 即如此丢的),转换器的
|
|
1402
2231
|
// 空白折叠随即把换行折掉;按现场回收"预格式化"祖先链(判据=实际生效的
|
|
@@ -1409,10 +2238,16 @@ window.__ModuleLoader__.load({
|
|
|
1409
2238
|
anc = anc.parentElement
|
|
1410
2239
|
}
|
|
1411
2240
|
let topShell = -1
|
|
2241
|
+
let inlineShell = -1
|
|
2242
|
+
const INLINE_SHELL = new Set(['CODE', 'STRONG', 'B', 'EM', 'I', 'DEL', 'S', 'STRIKE', 'A'])
|
|
1412
2243
|
for (let i = 0; i < ancShells.length; i++) {
|
|
1413
2244
|
const ws = getComputedStyle(ancShells[i]).whiteSpace
|
|
1414
2245
|
if (ws === 'pre' || ws === 'pre-wrap' || ws === 'pre-line' || ws === 'break-spaces') topShell = i
|
|
2246
|
+
// 行内语义元素同样会在克隆时丢外壳:<code> 补 `…`,粗体/斜体/
|
|
2247
|
+
// 删除线/链接补对应 Markdown;取最外层,嵌套格式才能完整保留。
|
|
2248
|
+
if (INLINE_SHELL.has(ancShells[i].tagName)) inlineShell = i
|
|
1415
2249
|
}
|
|
2250
|
+
if (topShell < 0 && inlineShell >= 0) topShell = inlineShell
|
|
1416
2251
|
let nestAt = box
|
|
1417
2252
|
for (let i = topShell; i >= 0; i--) {
|
|
1418
2253
|
const shell = ancShells[i].cloneNode(false)
|
|
@@ -1422,8 +2257,7 @@ window.__ModuleLoader__.load({
|
|
|
1422
2257
|
nestAt.append(work.cloneContents())
|
|
1423
2258
|
// 公式场景先留存原始 HTML(含 <math>):粘进 Word 等富目标仍可显示公式
|
|
1424
2259
|
const html = box.querySelector('.katex') ? box.innerHTML : null
|
|
1425
|
-
|
|
1426
|
-
const md = getTurndown().turndown(box).replace(/\n{3,}/g, '\n\n').trim()
|
|
2260
|
+
const md = tidyMarkdown(getTurndown().turndown(box))
|
|
1427
2261
|
if (!md) return
|
|
1428
2262
|
e.clipboardData.setData('text/plain', md)
|
|
1429
2263
|
if (html) e.clipboardData.setData('text/html', html)
|
|
@@ -1434,43 +2268,54 @@ window.__ModuleLoader__.load({
|
|
|
1434
2268
|
}
|
|
1435
2269
|
}
|
|
1436
2270
|
document.addEventListener('copy', onCopy, true)
|
|
2271
|
+
/** 文本编辑类控件里的 Esc 先交给控件(取消输入/收起下拉),不关面板。 */
|
|
2272
|
+
function isTextEditingTarget(n) {
|
|
2273
|
+
if (!n || !n.closest) return false
|
|
2274
|
+
if (n.closest('textarea, [contenteditable="true"], [contenteditable=""], [contenteditable="plaintext-only"]')) return true
|
|
2275
|
+
return n.tagName === 'INPUT' && /^(text|search|email|url|tel|password|number)$/i.test(String(n.type || ''))
|
|
2276
|
+
}
|
|
2277
|
+
function onDocKeyDown(e) {
|
|
2278
|
+
if (e.key !== 'Escape' || !state.open || e.defaultPrevented) return
|
|
2279
|
+
if (document.fullscreenElement) return // 全屏下 Esc 先交给浏览器退出全屏
|
|
2280
|
+
// 只保护插件面板内的文本编辑控件;其它区域照常 Esc 收起面板
|
|
2281
|
+
if (e.target && e.target.closest && e.target.closest('.dsrb-panel') && isTextEditingTarget(e.target)) return
|
|
2282
|
+
// 官方对话框 / 菜单里按 Esc:让官方先处理,不抢关面板
|
|
2283
|
+
const inOverlay = (n) => !!(n && n.closest && n.closest('[role="dialog"], [role="menu"], [role="listbox"]'))
|
|
2284
|
+
if (inOverlay(e.target) || inOverlay(document.activeElement)) return
|
|
2285
|
+
togglePanel(false) // 不 preventDefault:不吞官方自己的 Esc 行为
|
|
2286
|
+
}
|
|
2287
|
+
document.addEventListener('keydown', onDocKeyDown)
|
|
1437
2288
|
|
|
1438
2289
|
function onResize() {
|
|
1439
2290
|
const [x, y] = clampPos(fab.offsetLeft, fab.offsetTop, FAB_SIZE, FAB_SIZE)
|
|
1440
2291
|
fab.style.left = x + 'px'
|
|
1441
2292
|
fab.style.top = y + 'px'
|
|
1442
|
-
|
|
1443
|
-
if (state.open)
|
|
1444
|
-
renderWidthRow()
|
|
1445
|
-
placePanel()
|
|
1446
|
-
}
|
|
2293
|
+
refreshWidthTargets()
|
|
2294
|
+
if (state.open) placePanel()
|
|
1447
2295
|
}
|
|
1448
2296
|
window.addEventListener('resize', onResize)
|
|
1449
2297
|
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
attachWidthObserver()
|
|
1453
|
-
attachColumnObserver()
|
|
1454
|
-
const retryTimer = setInterval(() => {
|
|
1455
|
-
attachWidthObserver()
|
|
1456
|
-
attachColumnObserver()
|
|
1457
|
-
normalizeWidthDrift()
|
|
1458
|
-
rebuildFontPick() // 官方主题样式稍后就绪时补建覆盖层(已建则零开销早退)
|
|
1459
|
-
if (state.open && !officialDragging && !widthDragging) renderWidthRow()
|
|
1460
|
-
}, 2000)
|
|
2298
|
+
// 宽度目标只随事件变化:启动、面板打开、窗口尺寸、会话切换、官方手柄操作。
|
|
2299
|
+
refreshWidthTargets()
|
|
1461
2300
|
|
|
1462
2301
|
// 启动标记:记录加载时刻与版本(供诊断确认插件已加载;不影响功能)
|
|
1463
2302
|
writeState({ lastBootAt: Date.now(), lastBootVersion: VERSION })
|
|
1464
|
-
console.log('[dsh-happy-reader] 客户端半边已加载 v' + VERSION)
|
|
1465
2303
|
|
|
1466
|
-
// ──
|
|
1467
|
-
//
|
|
2304
|
+
// ── 卸载清理(React 槽位卸载 / 插件热重载 / ctx.effect 三路都会触发;
|
|
2305
|
+
// 幂等,可重复调用) ──
|
|
1468
2306
|
const dispose = () => {
|
|
1469
|
-
|
|
2307
|
+
if (disposed) return // 幂等:热重载/槽位卸载可能重复调用,不能重复抹掉新实例的全局变量
|
|
2308
|
+
disposed = true
|
|
2309
|
+
overlayAlive = false
|
|
2310
|
+
ui = null
|
|
2311
|
+
if (widthRefreshTimer) { clearTimeout(widthRefreshTimer); widthRefreshTimer = null }
|
|
2312
|
+
if (widthTargetRetry) { clearTimeout(widthTargetRetry); widthTargetRetry = null }
|
|
2313
|
+
cancelFontRetry()
|
|
1470
2314
|
widthObserver.disconnect()
|
|
1471
2315
|
columnObserver.disconnect()
|
|
1472
2316
|
window.removeEventListener('resize', onResize)
|
|
1473
2317
|
window.removeEventListener('pointerup', onWindowPointerUp)
|
|
2318
|
+
window.removeEventListener('pointercancel', onWindowPointerUp)
|
|
1474
2319
|
window.removeEventListener('pointermove', onPanelPointerMove)
|
|
1475
2320
|
window.removeEventListener('pointerup', onPanelPointerUp)
|
|
1476
2321
|
window.removeEventListener('pointercancel', onPanelPointerUp)
|
|
@@ -1479,6 +2324,7 @@ window.__ModuleLoader__.load({
|
|
|
1479
2324
|
document.removeEventListener('pointerup', onDocPointerUp, true)
|
|
1480
2325
|
document.removeEventListener('pointercancel', onDocPointerCancel, true)
|
|
1481
2326
|
document.removeEventListener('copy', onCopy, true)
|
|
2327
|
+
document.removeEventListener('keydown', onDocKeyDown)
|
|
1482
2328
|
fab.remove()
|
|
1483
2329
|
panel.remove()
|
|
1484
2330
|
style.remove()
|
|
@@ -1491,14 +2337,62 @@ window.__ModuleLoader__.load({
|
|
|
1491
2337
|
document.documentElement.removeAttribute(FS_ATTR)
|
|
1492
2338
|
document.documentElement.style.removeProperty('--dsrb-font-base')
|
|
1493
2339
|
document.documentElement.style.removeProperty('--dsrb-font-scale')
|
|
1494
|
-
|
|
1495
|
-
|
|
2340
|
+
turndownSvc = null
|
|
2341
|
+
const target = widthTargets()
|
|
2342
|
+
if (target) target.container.style.removeProperty('--dsh-chat-content-width')
|
|
2343
|
+
// 仅当全局句柄仍属于本实例时才释放,避免抹掉新实例的热重载句柄
|
|
2344
|
+
if (window.__dsrbDisposeOwner === INSTANCE_TOKEN) {
|
|
2345
|
+
window.__dsrbDispose = null
|
|
2346
|
+
window.__dsrbDisposeOwner = null
|
|
2347
|
+
}
|
|
2348
|
+
// 插件被停用/卸载(语言切换重挂除外)时退出本插件进入的全屏,恢复窗口态
|
|
2349
|
+
if (!remounting && document.fullscreenElement === document.documentElement) {
|
|
2350
|
+
try {
|
|
2351
|
+
const p = document.exitFullscreen()
|
|
2352
|
+
if (p && typeof p.catch === 'function') p.catch(() => {})
|
|
2353
|
+
} catch { /* 退出失败不阻塞拆除 */ }
|
|
2354
|
+
}
|
|
1496
2355
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
2356
|
+
// 主题/语言事件通过该句柄刷新本层 UI(apply 级用到)
|
|
2357
|
+
function applyTextFonts() {
|
|
2358
|
+
textCjkSelect.value = state.textFontCjk ?? ''
|
|
2359
|
+
textLatinSelect.value = state.textFontLatin ?? ''
|
|
2360
|
+
kickFontPick()
|
|
2361
|
+
renderGroupSummaries()
|
|
2362
|
+
}
|
|
2363
|
+
function refreshFontPick() { kickFontPick() }
|
|
2364
|
+
ui = {
|
|
2365
|
+
applyFont,
|
|
2366
|
+
renderFontRow,
|
|
2367
|
+
renderCodeSizeRow,
|
|
2368
|
+
renderCodeFontRow,
|
|
2369
|
+
renderTerminalSizeRow,
|
|
2370
|
+
renderTerminalFontRow,
|
|
2371
|
+
renderTextBaseRow,
|
|
2372
|
+
renderGroupSummaries,
|
|
2373
|
+
refreshFontPick,
|
|
2374
|
+
applyTextFonts,
|
|
2375
|
+
scheduleWidthRefresh,
|
|
2376
|
+
}
|
|
2377
|
+
// 热重载句柄必须连同设置页一起拆:设置行不在 mount 的 DOM 树里
|
|
2378
|
+
window.__dsrbDispose = () => {
|
|
2379
|
+
dispose()
|
|
2380
|
+
if (settingsDispose) {
|
|
2381
|
+
try { settingsDispose() } catch { /* 忽略 */ }
|
|
2382
|
+
settingsDispose = null
|
|
2383
|
+
}
|
|
2384
|
+
settingsHost = null
|
|
2385
|
+
settingsRefresh = null
|
|
2386
|
+
settingsAlive = false
|
|
2387
|
+
}
|
|
2388
|
+
window.__dsrbDisposeOwner = INSTANCE_TOKEN
|
|
2389
|
+
return dispose
|
|
1499
2390
|
} catch (err) {
|
|
1500
|
-
//
|
|
1501
|
-
console.error('[dsh-happy-reader]
|
|
2391
|
+
// 任何意外都只记日志并清掉半成品,不影响应用本体
|
|
2392
|
+
console.error('[dsh-happy-reader] 面板实体初始化失败:', err)
|
|
2393
|
+
overlayAlive = false
|
|
2394
|
+
try { host.textContent = '' } catch { /* 忽略 */ }
|
|
2395
|
+
return () => {}
|
|
1502
2396
|
}
|
|
1503
2397
|
}
|
|
1504
2398
|
|
|
@@ -2526,6 +3420,7 @@ return exports;
|
|
|
2526
3420
|
// ══ vendored: turndown-plugin-gfm end ══
|
|
2527
3421
|
|
|
2528
3422
|
exports.apply = apply
|
|
3423
|
+
exports.inject = ['slots']
|
|
2529
3424
|
|
|
2530
3425
|
return module.exports
|
|
2531
3426
|
},
|