dsh-happy-reader 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -15
- package/icon.svg +1 -0
- package/lib/client.js +1257 -353
- 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.0'
|
|
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,101 +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}
|
|
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)}
|
|
135
184
|
`
|
|
136
185
|
// 隐藏输入区(关键:height:0 而非 display:none —— 官方滚动锚定依赖该元素的几何信息)
|
|
137
186
|
const HIDE_CSS = '[data-composer-seat]{height:0!important;min-height:0!important;overflow:hidden!important}'
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// 各带关键控件(插件页标题含"添加插件"按钮、嵌入页头含唯一返回按钮),不得误伤
|
|
141
|
-
// (v0.2.6 修:旧写法"匹配全部再排除对话框"曾把插件页标题整条隐藏,安装入口消失)。
|
|
142
|
-
// 对话框排除条款保留:弹出层内永不隐藏(防锁死返回,v0.10.4)。
|
|
187
|
+
// 隐藏上边栏:正向判据(官方 conversation.header 槽位宿主带 data-conversation-header-leading,
|
|
188
|
+
// 全应用仅此一处);排除对话框内,避免误伤插件页/嵌入页的头部。
|
|
143
189
|
const HIDE_TOP_CSS = 'header[data-window-drag]:has(> [data-conversation-header-leading]):not([role="dialog"] *){display:none!important}'
|
|
144
190
|
// 字号缩放层(!important 样式表仅在需要缩放时启用;官方内联值原样保留在其下)
|
|
145
191
|
const FONT_CSS = `body{--dsh-content-font-size:calc(var(--dsrb-font-base,14px)*var(--dsrb-font-scale,1))!important}`
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// 全屏隐藏菜单;规则由 html[data-dsrb-fullscreen] 门控,退出全屏即完全还原。
|
|
150
|
-
// 注:.BynINW_* 为应用构建期类名(随应用更新可能变;失效即表现为全屏残留,
|
|
151
|
-
// 按新构建重新对齐即可——无更稳定锚点可用,属原则②下已接受的取舍)。
|
|
192
|
+
// 全屏补偿:官方 40px 窗口预留条在网页全屏下仍占位,需逐项清零;不能归零
|
|
193
|
+
// --dsh-windows-titlebar-height(悬浮按钮/菜单的定位基准)。.mP3x-q_* 是构建期类名,
|
|
194
|
+
// 无稳定锚点,升级后按新构建重新对齐;规则由 html[data-dsrb-fullscreen] 门控。
|
|
152
195
|
const FS_CSS = [
|
|
153
196
|
// 镜像官方 html[data-windows-titlebar][data-fullscreen]:overlay 顶距 20px、chrome 顶距 0
|
|
154
197
|
'html[data-windows-titlebar][data-dsrb-fullscreen]{--dsh-frame-overlay-top:20px;--dsh-frame-chrome-top:0px}',
|
|
155
198
|
// frame 的 40px 预留条清零 → 两侧列内容顶到屏幕最上缘
|
|
156
|
-
'html[data-dsrb-fullscreen] .
|
|
157
|
-
'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}',
|
|
158
201
|
// 收起态:内容列左上圆角归零(收起时圆角在 (0,0) 露出 frame 底色成为缺口)
|
|
159
|
-
'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}',
|
|
160
203
|
// 展开态:侧栏列自留 40px 顶部,悬浮按钮下方内容位置与窗口态保持一致
|
|
161
|
-
'html[data-dsrb-fullscreen] .
|
|
204
|
+
'html[data-dsrb-fullscreen] .mP3x-q_sidebarCol{padding-top:var(--dsh-windows-titlebar-height)}',
|
|
162
205
|
// 侧栏拖拽手柄随之顶到 0(其 top 同样以 var 为基准)
|
|
163
|
-
'html[data-dsrb-fullscreen] .
|
|
206
|
+
'html[data-dsrb-fullscreen] .mP3x-q_handle{top:0!important}',
|
|
164
207
|
// 全屏隐藏 preload"应用/编辑"菜单(退出全屏即恢复;无键盘通道,不影响 Esc 退出)
|
|
165
208
|
'html[data-dsrb-fullscreen] [data-windows-menu]{display:none!important}'
|
|
166
209
|
].join('')
|
|
@@ -173,7 +216,8 @@ window.__ModuleLoader__.load({
|
|
|
173
216
|
function readState() {
|
|
174
217
|
try {
|
|
175
218
|
const raw = localStorage.getItem(STATE_KEY)
|
|
176
|
-
|
|
219
|
+
const value = raw ? JSON.parse(raw) : null
|
|
220
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
177
221
|
} catch {
|
|
178
222
|
return {}
|
|
179
223
|
}
|
|
@@ -234,15 +278,16 @@ window.__ModuleLoader__.load({
|
|
|
234
278
|
}
|
|
235
279
|
return out
|
|
236
280
|
}
|
|
281
|
+
// 代码字体候选(仅列本机已装;选中后经官方 setFontFamily('code') 写入,不再自建 @font-face)
|
|
237
282
|
const CODE_FONTS = [
|
|
238
|
-
{ id: 'consolas', label: 'Consolas',
|
|
239
|
-
{ id: 'cascadia', label: 'Cascadia Code',
|
|
240
|
-
{ id: 'cascadiamono', label: 'Cascadia Mono',
|
|
241
|
-
{ id: 'courier', label: 'Courier New',
|
|
242
|
-
{ id: 'nsimsun', label: '新宋体',
|
|
243
|
-
{ id: 'firacode', label: 'Fira Code',
|
|
244
|
-
{ id: 'jetbrains', label: 'JetBrains Mono',
|
|
245
|
-
{ 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' },
|
|
246
291
|
]
|
|
247
292
|
let fontDetectCtx = null
|
|
248
293
|
// 字体可用性检测:canvas 字宽比对(与三套通用字族任一不同 = 本机已装)。
|
|
@@ -267,6 +312,13 @@ window.__ModuleLoader__.load({
|
|
|
267
312
|
return true
|
|
268
313
|
}
|
|
269
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
|
+
}
|
|
270
322
|
function fontChoice(list, id) {
|
|
271
323
|
if (!id) return null
|
|
272
324
|
for (const f of list) if (f.id === id) return f
|
|
@@ -275,6 +327,7 @@ window.__ModuleLoader__.load({
|
|
|
275
327
|
// 官方把整套排印 token(--dsw-font-* / --ds-font-*)定义在 :root 与 body 上。
|
|
276
328
|
// 成功解析一次后缓存;官方主题尚未就绪时返回 null(调用方靠定时器稍后重试)。
|
|
277
329
|
let themeFontDecls = null
|
|
330
|
+
let lastThemePreference
|
|
278
331
|
function readThemeFontDecls() {
|
|
279
332
|
if (themeFontDecls) return themeFontDecls
|
|
280
333
|
const decls = new Map()
|
|
@@ -297,40 +350,35 @@ window.__ModuleLoader__.load({
|
|
|
297
350
|
if (!decls) return null
|
|
298
351
|
const cjk = fontChoice(TEXT_FONTS_CJK, state.textFontCjk)
|
|
299
352
|
const lat = fontChoice(TEXT_FONTS_LATIN, state.textFontLatin)
|
|
300
|
-
const code = fontChoice(CODE_FONTS, state.codeFont)
|
|
301
353
|
const lineH = typeof state.lineHeight === 'number' ? state.lineHeight : null
|
|
302
354
|
const letterSp = typeof state.letterSpacing === 'number' ? state.letterSpacing : null
|
|
303
|
-
if (!cjk && !lat &&
|
|
355
|
+
if (!cjk && !lat && lineH === null && letterSp === null) return null
|
|
304
356
|
const offText = decls.get('--dsw-font-family')
|
|
305
|
-
const offCode = decls.get('--ds-font-family-code')
|
|
306
357
|
// 虚拟字族按 西文→中文 排序:引号/破折号等共享区间由西文面优先接住
|
|
307
358
|
const faces = []
|
|
308
359
|
const famNames = []
|
|
309
360
|
if (lat) { faces.push(faceCss('dsrb-text-latin', lat, LATIN_RANGE)); famNames.push('"dsrb-text-latin"') }
|
|
310
361
|
if (cjk) { faces.push(faceCss('dsrb-text-cjk', cjk, CJK_RANGE)); famNames.push('"dsrb-text-cjk"') }
|
|
311
362
|
const fam = famNames.length ? 'var(--dsrb-family)' : offText
|
|
312
|
-
const cod = code ? 'var(--dsrb-code)' : offCode
|
|
313
363
|
const parts = []
|
|
314
364
|
// 虚拟字族在前(按区间分流);官方原栈殿后兜底:区间外字符与缺字逐字回退
|
|
315
365
|
if (famNames.length) parts.push('--dsrb-family:' + famNames.join(', ') + ', ' + offText)
|
|
316
|
-
if (code) parts.push('--dsrb-code:' + code.css + ', ' + offCode)
|
|
317
366
|
if (famNames.length) parts.push('--dsw-font-family:' + fam)
|
|
318
|
-
if (code) parts.push('--ds-font-family-code:' + cod)
|
|
319
367
|
if (famNames.length) parts.push('font-family:var(--dsrb-family)') // 无自身字族的后代(如用户气泡正文)由继承拿到
|
|
320
368
|
for (const [name, value] of decls) {
|
|
321
369
|
if (name === '--dsw-font-family' || name === '--ds-font-family-code') continue
|
|
322
370
|
let next = value
|
|
323
371
|
if (famNames.length && next.indexOf('var(--dsw-font-family)') !== -1) next = next.split('var(--dsw-font-family)').join(fam)
|
|
324
|
-
if (code && next.indexOf('var(--ds-font-family-code)') !== -1) next = next.split('var(--ds-font-family-code)').join(cod)
|
|
325
372
|
// 行距:官方行高 token 有两种真实形态(简写里的 "/ 行高" 段,均出自 asar 实测)——
|
|
326
|
-
// 正文/标题:"/ calc(Npx + var(--dsh-content-font-delta))"
|
|
327
|
-
//
|
|
328
|
-
//
|
|
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" 段)。
|
|
329
377
|
if (lineH !== null) {
|
|
330
378
|
next = next
|
|
331
379
|
.replace(
|
|
332
|
-
/(\/\s*)calc\((\d+(?:\.\d+)?)px \+ var\(--dsh-
|
|
333
|
-
(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 + ')',
|
|
334
382
|
)
|
|
335
383
|
.replace(/(\/\s*)(\d+(?:\.\d+)?)px/g, (m, lead, n) => lead + Math.round(Number(n) * lineH * 2) / 2 + 'px')
|
|
336
384
|
}
|
|
@@ -364,53 +412,71 @@ window.__ModuleLoader__.load({
|
|
|
364
412
|
if (!container) return null
|
|
365
413
|
return { container, target: container.parentElement ?? container, column: container.offsetWidth }
|
|
366
414
|
}
|
|
367
|
-
/**
|
|
368
|
-
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) {
|
|
369
424
|
const t = widthTargets()
|
|
370
425
|
if (!t) return
|
|
371
426
|
const { container, target, column } = t
|
|
427
|
+
if (!(column > 0)) return // 会话尚未排版:无可靠列宽,先不动官方值
|
|
372
428
|
const omax = officialMaxFor(column)
|
|
373
429
|
const cap = extCapFor(column)
|
|
374
430
|
const px = state.widthPx
|
|
375
431
|
if (px == null) {
|
|
376
|
-
|
|
377
|
-
|
|
432
|
+
if (clearAuto) {
|
|
433
|
+
localStorage.removeItem(WIDTH_PREF_KEY)
|
|
434
|
+
target.style.removeProperty('--dsh-chat-user-width')
|
|
435
|
+
}
|
|
378
436
|
container.style.removeProperty('--dsh-chat-content-width')
|
|
437
|
+
markWidthExt(false)
|
|
379
438
|
} else if (px <= omax) {
|
|
380
439
|
container.style.removeProperty('--dsh-chat-content-width')
|
|
381
440
|
localStorage.setItem(WIDTH_PREF_KEY, String(px))
|
|
382
441
|
target.style.setProperty('--dsh-chat-user-width', px + 'px')
|
|
442
|
+
markWidthExt(false)
|
|
383
443
|
} else {
|
|
384
444
|
const w = Math.min(px, cap)
|
|
385
445
|
localStorage.setItem(WIDTH_PREF_KEY, String(omax))
|
|
386
446
|
target.style.setProperty('--dsh-chat-user-width', omax + 'px')
|
|
387
447
|
container.style.setProperty('--dsh-chat-content-width', w + 'px')
|
|
448
|
+
markWidthExt(true)
|
|
388
449
|
}
|
|
389
450
|
}
|
|
390
451
|
function currentWidthInfo() {
|
|
391
452
|
const t = widthTargets()
|
|
392
|
-
if (!t) return null
|
|
453
|
+
if (!t || !(t.column > 0)) return null
|
|
393
454
|
const omax = officialMaxFor(t.column)
|
|
394
455
|
const cap = extCapFor(t.column)
|
|
395
456
|
const px = state.widthPx
|
|
396
457
|
const extActive = px != null && px > omax
|
|
397
458
|
let shown
|
|
459
|
+
let officialPx = null
|
|
398
460
|
if (px != null) shown = extActive ? Math.min(px, cap) : px
|
|
399
461
|
else {
|
|
400
462
|
const raw = getComputedStyle(t.container).getPropertyValue('--dsh-chat-user-width').trim()
|
|
401
|
-
|
|
463
|
+
officialPx = raw ? Number.parseFloat(raw) : null
|
|
464
|
+
shown = Number.isFinite(officialPx) ? officialPx : resolveAutoWidth(t.column)
|
|
402
465
|
}
|
|
403
|
-
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 }
|
|
404
467
|
}
|
|
405
468
|
/** 漂移修复:官方重发布 / 窗口变化后把我们的状态归位(官方手柄拖动中不插手)。 */
|
|
406
469
|
function normalizeWidthDrift() {
|
|
407
470
|
if (officialDragging) return
|
|
408
471
|
const t = widthTargets()
|
|
409
|
-
if (!t) return
|
|
472
|
+
if (!t || !(t.column > 0)) return
|
|
410
473
|
const omax = officialMaxFor(t.column)
|
|
411
474
|
const cap = extCapFor(t.column)
|
|
412
475
|
const px = state.widthPx
|
|
413
|
-
if (px == null)
|
|
476
|
+
if (px == null) {
|
|
477
|
+
markWidthExt(false)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
414
480
|
if (px > omax) {
|
|
415
481
|
const w = Math.min(px, cap) + 'px'
|
|
416
482
|
if (t.container.style.getPropertyValue('--dsh-chat-content-width') !== w) {
|
|
@@ -423,6 +489,7 @@ window.__ModuleLoader__.load({
|
|
|
423
489
|
if (t.target.style.getPropertyValue('--dsh-chat-user-width') !== omax + 'px') {
|
|
424
490
|
t.target.style.setProperty('--dsh-chat-user-width', omax + 'px')
|
|
425
491
|
}
|
|
492
|
+
markWidthExt(true)
|
|
426
493
|
} else {
|
|
427
494
|
if (t.container.style.getPropertyValue('--dsh-chat-content-width')) {
|
|
428
495
|
t.container.style.removeProperty('--dsh-chat-content-width')
|
|
@@ -433,13 +500,15 @@ window.__ModuleLoader__.load({
|
|
|
433
500
|
if (t.target.style.getPropertyValue('--dsh-chat-user-width') !== px + 'px') {
|
|
434
501
|
t.target.style.setProperty('--dsh-chat-user-width', px + 'px')
|
|
435
502
|
}
|
|
503
|
+
markWidthExt(false)
|
|
436
504
|
}
|
|
437
505
|
}
|
|
438
506
|
|
|
439
507
|
// ── 字号(官方直连 + 超范围缩放层) ─────────────────────────────────
|
|
440
|
-
const fontState = { official: null }
|
|
508
|
+
const fontState = { official: null, code: null, codeFamily: '', textFamily: '', terminal: null, terminalFamily: '' }
|
|
441
509
|
let fontSvc = null
|
|
442
|
-
let
|
|
510
|
+
let roleSizeSupported = false // setFontSize(role, px) 角色形
|
|
511
|
+
let familySupported = false // setFontFamily(role, value)
|
|
443
512
|
function fallbackBasePx() {
|
|
444
513
|
// 官方把该变量写在 body 内联样式上(见上方常量注释);主题服务未就绪时的兜底
|
|
445
514
|
const inline = Number.parseFloat(document.body.style.getPropertyValue(CONTENT_FONT_SIZE_VARIABLE))
|
|
@@ -457,19 +526,37 @@ window.__ModuleLoader__.load({
|
|
|
457
526
|
root.style.setProperty('--dsrb-font-scale', String(scale))
|
|
458
527
|
tag.disabled = Math.abs(scale - 1) < 0.001 // 无缩放 → 样式表彻底停用,回到官方原样
|
|
459
528
|
}
|
|
460
|
-
/**
|
|
461
|
-
function
|
|
529
|
+
/** 调官方角色字号(0.2.2+ 为 role,px;旧版单参仅 text 可用)。 */
|
|
530
|
+
function officialSetFontSize(role, px) {
|
|
462
531
|
if (!fontSvc || typeof fontSvc.setFontSize !== 'function') return
|
|
463
|
-
if (fontState.official === px) return
|
|
464
|
-
fontSelfWrite = true
|
|
465
532
|
try {
|
|
466
|
-
fontSvc.setFontSize(px)
|
|
533
|
+
if (fontSvc.setFontSize.length >= 2) fontSvc.setFontSize(role, px)
|
|
534
|
+
else if (role === 'text') fontSvc.setFontSize(px)
|
|
467
535
|
} catch {
|
|
468
|
-
//
|
|
536
|
+
// 官方校验拒绝:调用处已夹在范围内,静默忽略
|
|
469
537
|
}
|
|
470
|
-
|
|
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)
|
|
471
550
|
}
|
|
472
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
|
+
}
|
|
473
560
|
function el(tag, className, text) {
|
|
474
561
|
const node = document.createElement(tag)
|
|
475
562
|
if (className) node.className = className
|
|
@@ -484,16 +571,274 @@ window.__ModuleLoader__.load({
|
|
|
484
571
|
if (e.target === toggle) return // 键盘空格:走原生(change 监听里统一 apply)
|
|
485
572
|
e.preventDefault()
|
|
486
573
|
toggle.checked = !toggle.checked
|
|
574
|
+
toggle.setAttribute('aria-checked', toggle.checked ? 'true' : 'false')
|
|
487
575
|
apply()
|
|
488
576
|
})
|
|
489
577
|
}
|
|
490
578
|
|
|
491
|
-
// ──
|
|
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
|
+
|
|
492
838
|
function apply(ctx) {
|
|
493
839
|
try {
|
|
494
840
|
// 热重载:DSH 监视插件文件变化,会再次执行本客户端。先拆旧实例、再按 id
|
|
495
|
-
// 清扫遗留节点、最后重建 ——
|
|
496
|
-
// 随节点删除失联、残留定时器空转,均无害)。
|
|
841
|
+
// 清扫遗留节点、最后重建 —— 页面上运行的必然是最新代码。
|
|
497
842
|
const prevDispose = window.__dsrbDispose
|
|
498
843
|
if (typeof prevDispose === 'function') {
|
|
499
844
|
try { prevDispose() } catch { /* 旧实例拆除失败也不阻塞重建 */ }
|
|
@@ -502,10 +847,186 @@ window.__ModuleLoader__.load({
|
|
|
502
847
|
const staleNode = document.getElementById(staleId)
|
|
503
848
|
if (staleNode) staleNode.remove()
|
|
504
849
|
}
|
|
505
|
-
|
|
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()
|
|
506
854
|
|
|
507
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
|
+
})
|
|
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
|
+
}
|
|
508
1024
|
|
|
1025
|
+
/** 实体:构建面板/按钮/监听/定时器并把它们放进槽位宿主;返回清理函数。 */
|
|
1026
|
+
function mount(host) {
|
|
1027
|
+
let disposed = false
|
|
1028
|
+
overlayAlive = true
|
|
1029
|
+
try {
|
|
509
1030
|
// 1) 样式表:基础外观 + 各功能覆盖层
|
|
510
1031
|
const style = el('style')
|
|
511
1032
|
style.id = STYLE_ID
|
|
@@ -524,7 +1045,7 @@ window.__ModuleLoader__.load({
|
|
|
524
1045
|
const fsTag = el('style') // 全屏补偿层(常驻;仅存在全屏标记时才有匹配规则生效)
|
|
525
1046
|
fsTag.id = FS_STYLE_ID
|
|
526
1047
|
fsTag.textContent = FS_CSS
|
|
527
|
-
|
|
1048
|
+
host.append(style, hideTag, hideTopTag, fontTag, fontPickTag, fsTag)
|
|
528
1049
|
// disabled 必须在挂入文档后再设:未连接的 <style> 没有关联样式表,挂载前设置
|
|
529
1050
|
// 会被静默丢弃(CSSOM 行为),否则隐藏层将无视已保存状态(字体层由 rebuildFontPick 按需启用)。
|
|
530
1051
|
hideTag.disabled = !state.hideComposer
|
|
@@ -535,7 +1056,8 @@ window.__ModuleLoader__.load({
|
|
|
535
1056
|
const fab = el('button', 'dsrb-fab', 'Aa')
|
|
536
1057
|
fab.id = FAB_ID
|
|
537
1058
|
fab.type = 'button'
|
|
538
|
-
fab.title = '
|
|
1059
|
+
fab.title = t('fabTitle')
|
|
1060
|
+
fab.setAttribute('aria-label', t('fabTitle'))
|
|
539
1061
|
const [fx, fy] = clampPos(
|
|
540
1062
|
state.x ?? window.innerWidth - FAB_SIZE - 16,
|
|
541
1063
|
state.y ?? window.innerHeight - FAB_SIZE - 16,
|
|
@@ -544,148 +1066,305 @@ window.__ModuleLoader__.load({
|
|
|
544
1066
|
)
|
|
545
1067
|
fab.style.left = fx + 'px'
|
|
546
1068
|
fab.style.top = fy + 'px'
|
|
547
|
-
|
|
1069
|
+
host.appendChild(fab)
|
|
548
1070
|
|
|
549
1071
|
// 3) 面板
|
|
550
1072
|
const panel = el('div', 'dsrb-panel')
|
|
551
1073
|
panel.id = PANEL_ID
|
|
1074
|
+
panel.setAttribute('role', 'region')
|
|
1075
|
+
panel.setAttribute('aria-label', t('panelTitle'))
|
|
552
1076
|
|
|
553
1077
|
const headRow = el('div', 'dsrb-head')
|
|
554
|
-
const title = el('h4')
|
|
555
|
-
|
|
556
|
-
const headHint = el('span', 'dsrb-headHint', '点面板外关闭')
|
|
1078
|
+
const title = el('h4', null, t('panelTitle'))
|
|
1079
|
+
const headHint = el('span', 'dsrb-headHint', t('hintClose'))
|
|
557
1080
|
const closeBtn = el('button', 'dsrb-x', '×')
|
|
558
1081
|
closeBtn.type = 'button'
|
|
559
|
-
closeBtn.title = '
|
|
1082
|
+
closeBtn.title = t('close')
|
|
1083
|
+
closeBtn.setAttribute('aria-label', t('close'))
|
|
560
1084
|
headRow.append(title, headHint, closeBtn)
|
|
561
1085
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
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'))
|
|
581
1104
|
|
|
582
1105
|
// 行:行距 / 字距(面板最下方;默认 = 官方原样)
|
|
583
1106
|
const rowType = el('div', 'dsrb-row')
|
|
584
1107
|
const typeMain = el('div', 'dsrb-rowMain')
|
|
585
|
-
typeMain.append(el('div', 'dsrb-rowTitle', '
|
|
1108
|
+
typeMain.append(el('div', 'dsrb-rowTitle', t('typeRowTitle')), el('div', 'dsrb-rowSub', t('typeRowSub')))
|
|
586
1109
|
const typePair = el('div', 'dsrb-pair')
|
|
587
1110
|
const lhCol = el('div', 'dsrb-pairCol')
|
|
588
1111
|
const lhMini = el('div', 'dsrb-mini')
|
|
589
|
-
const lhVal = el('b', null, '
|
|
590
|
-
lhMini.append('
|
|
1112
|
+
const lhVal = el('b', null, t('defaultText'))
|
|
1113
|
+
lhMini.append(t('lineHeight'), lhVal)
|
|
591
1114
|
const lhStepper = el('div', 'dsrb-stepper')
|
|
592
1115
|
const lhMinus = el('button', null, '−')
|
|
593
1116
|
const lhPlus = el('button', null, '+')
|
|
594
1117
|
lhMinus.type = lhPlus.type = 'button'
|
|
1118
|
+
lhMinus.setAttribute('aria-label', t('minus'))
|
|
1119
|
+
lhPlus.setAttribute('aria-label', t('plus'))
|
|
595
1120
|
lhStepper.append(lhMinus, lhPlus)
|
|
596
1121
|
lhCol.append(lhMini, lhStepper)
|
|
597
1122
|
const lsCol = el('div', 'dsrb-pairCol')
|
|
598
1123
|
const lsMini = el('div', 'dsrb-mini')
|
|
599
|
-
const lsVal = el('b', null, '
|
|
600
|
-
lsMini.append('
|
|
1124
|
+
const lsVal = el('b', null, t('defaultText'))
|
|
1125
|
+
lsMini.append(t('letterSpacing'), lsVal)
|
|
601
1126
|
const lsStepper = el('div', 'dsrb-stepper')
|
|
602
1127
|
const lsMinus = el('button', null, '−')
|
|
603
1128
|
const lsPlus = el('button', null, '+')
|
|
604
1129
|
lsMinus.type = lsPlus.type = 'button'
|
|
1130
|
+
lsMinus.setAttribute('aria-label', t('minus'))
|
|
1131
|
+
lsPlus.setAttribute('aria-label', t('plus'))
|
|
605
1132
|
lsStepper.append(lsMinus, lsPlus)
|
|
606
1133
|
lsCol.append(lsMini, lsStepper)
|
|
607
1134
|
typePair.append(lhCol, lsCol)
|
|
608
1135
|
typeMain.append(typePair)
|
|
609
1136
|
rowType.append(typeMain)
|
|
610
1137
|
|
|
611
|
-
//
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
+
// 行:中英分设(中文、西文两个独立下拉压缩在同一行;中文下拉只接管中日韩
|
|
626
1308
|
// 字符与中文标点,西文下拉只接管拉丁字母/数字/西文标点,各自独立、互不串扰)
|
|
627
1309
|
const rowTextFont = el('div', 'dsrb-row')
|
|
628
1310
|
const textFontMain = el('div', 'dsrb-rowMain')
|
|
629
|
-
textFontMain.append(el('div', 'dsrb-rowTitle', '
|
|
1311
|
+
textFontMain.append(el('div', 'dsrb-rowTitle', t('splitTitle')), el('div', 'dsrb-rowSub', t('splitSub')))
|
|
630
1312
|
const textPair = el('div', 'dsrb-pair')
|
|
631
1313
|
const cjkCol = el('div', 'dsrb-pairCol')
|
|
632
|
-
cjkCol.append(el('div', 'dsrb-mini', '
|
|
633
|
-
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'))
|
|
634
1317
|
cjkCol.append(textCjkSelect)
|
|
635
1318
|
const latinCol = el('div', 'dsrb-pairCol')
|
|
636
|
-
latinCol.append(el('div', 'dsrb-mini', '
|
|
637
|
-
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'))
|
|
638
1322
|
latinCol.append(textLatinSelect)
|
|
639
1323
|
textPair.append(cjkCol, latinCol)
|
|
640
1324
|
textFontMain.append(textPair)
|
|
641
1325
|
rowTextFont.append(textFontMain)
|
|
642
1326
|
|
|
643
|
-
|
|
644
|
-
const rowCodeFont = el('div', 'dsrb-row')
|
|
645
|
-
const codeFontMain = el('div', 'dsrb-rowMain')
|
|
646
|
-
codeFontMain.append(el('div', 'dsrb-rowTitle', '代码字体'), el('div', 'dsrb-rowSub', '仅显示本机已装字体'))
|
|
647
|
-
const codeFontSelect = el('select', 'dsrb-select')
|
|
648
|
-
codeFontMain.append(codeFontSelect)
|
|
649
|
-
rowCodeFont.append(codeFontMain)
|
|
1327
|
+
const { row: rowTerminalSize, val: termSizeVal, minus: termMinus, plus: termPlus } = makeStepperRow(t('sizeRowLabel'), t('terminalFontSizeSub'))
|
|
650
1328
|
|
|
651
1329
|
// 行:内容宽度
|
|
652
1330
|
const rowWidth = el('div', 'dsrb-row')
|
|
653
1331
|
const widthMain = el('div', 'dsrb-rowMain')
|
|
654
1332
|
const widthTitle = el('div', 'dsrb-rowTitle')
|
|
655
1333
|
const widthVal = el('b', null, '…')
|
|
656
|
-
widthTitle.append('
|
|
657
|
-
widthMain.append(widthTitle, el('div', 'dsrb-rowSub', '
|
|
1334
|
+
widthTitle.append(t('widthTitle'), widthVal)
|
|
1335
|
+
widthMain.append(widthTitle, el('div', 'dsrb-rowSub', t('widthSub')))
|
|
658
1336
|
const widthRow = el('div', 'dsrb-widthRow')
|
|
659
1337
|
const widthSlider = el('input')
|
|
660
1338
|
widthSlider.type = 'range'
|
|
1339
|
+
widthSlider.setAttribute('aria-label', t('widthTitle').trim())
|
|
661
1340
|
widthSlider.step = '5'
|
|
662
1341
|
widthSlider.min = String(CONTENT_MIN)
|
|
663
1342
|
widthSlider.max = String(CONTENT_MIN)
|
|
664
|
-
const widthReset = el('button', null, '
|
|
1343
|
+
const widthReset = el('button', null, t('widthAuto'))
|
|
665
1344
|
widthReset.type = 'button'
|
|
666
1345
|
widthRow.append(widthSlider, widthReset)
|
|
667
1346
|
widthMain.append(widthRow)
|
|
668
1347
|
rowWidth.append(widthMain)
|
|
669
1348
|
|
|
670
|
-
//
|
|
671
|
-
const rowFullscreen =
|
|
672
|
-
const fsBox = el('span', 'dsrb-switch')
|
|
673
|
-
const fsToggle = el('input')
|
|
674
|
-
fsToggle.type = 'checkbox'
|
|
675
|
-
fsBox.append(fsToggle, el('span'))
|
|
676
|
-
const fsMain = el('div', 'dsrb-rowMain')
|
|
677
|
-
fsMain.append(el('div', 'dsrb-rowTitle', '全屏'), el('div', 'dsrb-rowSub', '铺满屏幕 · Esc 退出'))
|
|
678
|
-
rowFullscreen.append(fsBox, fsMain)
|
|
1349
|
+
// 行:全屏(开关与真实全屏状态双向同步)
|
|
1350
|
+
const { row: rowFullscreen, input: fsToggle } = makeSwitchRow(t('fullscreenTitle'), t('fullscreenSub'))
|
|
679
1351
|
|
|
680
1352
|
// 三段式结构:拉拽条 / 滚动内容区 / 拉拽条 —— 内容在中间滚动,两条拉拽条
|
|
681
1353
|
// 永远贴住面板的视觉上/下缘(滚动容器里的绝对定位元素会随内容滚走,不能用绝对定位)
|
|
682
1354
|
const resizeTop = el('div', 'dsrb-resize-top')
|
|
683
1355
|
const resizeBottom = el('div', 'dsrb-resize')
|
|
684
1356
|
const panelBody = el('div', 'dsrb-body')
|
|
685
|
-
|
|
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)
|
|
686
1363
|
panel.append(resizeTop, panelBody, resizeBottom)
|
|
687
1364
|
panel.style.display = state.open ? '' : 'none'
|
|
688
|
-
|
|
1365
|
+
fab.setAttribute('aria-controls', PANEL_ID)
|
|
1366
|
+
fab.setAttribute('aria-expanded', state.open ? 'true' : 'false')
|
|
1367
|
+
host.appendChild(panel)
|
|
689
1368
|
|
|
690
1369
|
// ── 面板定位(四个方位择一,绝不压住按钮;避开右上角系统按钮条) ──
|
|
691
1370
|
// 右上角三枚系统按钮是 Electron WCO 原生叠层(绘制在网页层之上、无法覆盖):
|
|
@@ -694,7 +1373,8 @@ window.__ModuleLoader__.load({
|
|
|
694
1373
|
const titlebarH =
|
|
695
1374
|
Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--dsh-windows-titlebar-height')) || 40
|
|
696
1375
|
const CAP_W = 138
|
|
697
|
-
function placePanel() {
|
|
1376
|
+
function placePanel(keepScroll) {
|
|
1377
|
+
const scrollTop = typeof keepScroll === 'number' ? keepScroll : panelBody.scrollTop
|
|
698
1378
|
const r = fab.getBoundingClientRect()
|
|
699
1379
|
panel.style.height = ''
|
|
700
1380
|
panel.style.maxHeight = '' // 先按自然高度测量
|
|
@@ -756,14 +1436,22 @@ window.__ModuleLoader__.load({
|
|
|
756
1436
|
const finalH = Math.max(PANEL_MIN_H, Math.min(ph, vh - best.y - M - BORDER_Y))
|
|
757
1437
|
panel.style.maxHeight = finalH + 'px'
|
|
758
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)
|
|
759
1442
|
}
|
|
760
1443
|
function togglePanel(open) {
|
|
761
1444
|
state.open = open
|
|
762
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')
|
|
763
1448
|
if (open) {
|
|
764
|
-
|
|
765
|
-
|
|
1449
|
+
renderAll()
|
|
1450
|
+
rebuildFontPick()
|
|
1451
|
+
refreshWidthTargets()
|
|
766
1452
|
placePanel()
|
|
1453
|
+
} else {
|
|
1454
|
+
refreshWidthTargets()
|
|
767
1455
|
}
|
|
768
1456
|
writeState({ open })
|
|
769
1457
|
}
|
|
@@ -776,6 +1464,7 @@ window.__ModuleLoader__.load({
|
|
|
776
1464
|
// 超出 22:官方设置保持原样,仅用缩放层补齐
|
|
777
1465
|
applyFont()
|
|
778
1466
|
renderFontRow()
|
|
1467
|
+
renderGroupSummaries()
|
|
779
1468
|
writeState({ fontPx: next })
|
|
780
1469
|
}
|
|
781
1470
|
function renderFontRow() {
|
|
@@ -783,12 +1472,48 @@ window.__ModuleLoader__.load({
|
|
|
783
1472
|
fontMinus.disabled = typeof state.fontPx !== 'number' || state.fontPx <= FONT_MIN
|
|
784
1473
|
fontPlus.disabled = typeof state.fontPx !== 'number' || state.fontPx >= FONT_MAX
|
|
785
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
|
+
}
|
|
786
1511
|
function renderWidthRow() {
|
|
787
1512
|
const info = currentWidthInfo()
|
|
788
1513
|
if (!info) {
|
|
789
1514
|
widthSlider.disabled = true
|
|
790
1515
|
widthReset.disabled = true
|
|
791
|
-
widthVal.textContent = '
|
|
1516
|
+
widthVal.textContent = t('widthUnavailable')
|
|
792
1517
|
return
|
|
793
1518
|
}
|
|
794
1519
|
widthSlider.min = String(info.min)
|
|
@@ -802,11 +1527,18 @@ window.__ModuleLoader__.load({
|
|
|
802
1527
|
}
|
|
803
1528
|
function renderAll() {
|
|
804
1529
|
hideToggle.checked = !!state.hideComposer
|
|
1530
|
+
hideToggle.setAttribute('aria-checked', hideToggle.checked ? 'true' : 'false')
|
|
805
1531
|
topToggle.checked = !!state.hideTop
|
|
1532
|
+
topToggle.setAttribute('aria-checked', topToggle.checked ? 'true' : 'false')
|
|
806
1533
|
textCjkSelect.value = state.textFontCjk ?? ''
|
|
807
1534
|
textLatinSelect.value = state.textFontLatin ?? ''
|
|
808
|
-
codeFontSelect.value = state.codeFont ?? ''
|
|
809
1535
|
renderFontRow()
|
|
1536
|
+
renderCodeSizeRow()
|
|
1537
|
+
renderTerminalSizeRow()
|
|
1538
|
+
renderCodeFontRow()
|
|
1539
|
+
renderTerminalFontRow()
|
|
1540
|
+
renderTextBaseRow()
|
|
1541
|
+
renderGroupSummaries()
|
|
810
1542
|
renderTypeRow()
|
|
811
1543
|
renderWidthRow()
|
|
812
1544
|
}
|
|
@@ -814,6 +1546,7 @@ window.__ModuleLoader__.load({
|
|
|
814
1546
|
// ── 功能 1:隐藏输入区(开关 + 整条长条可点) ────────────────────
|
|
815
1547
|
function applyHideComposer() {
|
|
816
1548
|
state.hideComposer = hideToggle.checked
|
|
1549
|
+
hideToggle.setAttribute('aria-checked', hideToggle.checked ? 'true' : 'false')
|
|
817
1550
|
hideTag.disabled = !state.hideComposer
|
|
818
1551
|
writeState({ hideComposer: state.hideComposer })
|
|
819
1552
|
}
|
|
@@ -823,6 +1556,7 @@ window.__ModuleLoader__.load({
|
|
|
823
1556
|
// ── 功能 2:隐藏上边栏(开关 + 整条长条可点) ────────────────────
|
|
824
1557
|
function applyHideTop() {
|
|
825
1558
|
state.hideTop = topToggle.checked
|
|
1559
|
+
topToggle.setAttribute('aria-checked', topToggle.checked ? 'true' : 'false')
|
|
826
1560
|
hideTopTag.disabled = !state.hideTop
|
|
827
1561
|
writeState({ hideTop: state.hideTop })
|
|
828
1562
|
}
|
|
@@ -838,6 +1572,7 @@ window.__ModuleLoader__.load({
|
|
|
838
1572
|
// 其它元素全屏),因此由 fullscreenchange 事件无条件校正开关与补偿标记
|
|
839
1573
|
const on = inPageFullscreen()
|
|
840
1574
|
fsToggle.checked = on
|
|
1575
|
+
fsToggle.setAttribute('aria-checked', on ? 'true' : 'false')
|
|
841
1576
|
const root = document.documentElement
|
|
842
1577
|
if (on) root.setAttribute(FS_ATTR, '')
|
|
843
1578
|
else root.removeAttribute(FS_ATTR)
|
|
@@ -871,53 +1606,47 @@ window.__ModuleLoader__.load({
|
|
|
871
1606
|
fontPlus.addEventListener('click', () => {
|
|
872
1607
|
setFontPx((typeof state.fontPx === 'number' ? state.fontPx : fontState.official ?? 16) + 1)
|
|
873
1608
|
})
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
state.textFontCjk = fontChoice(availCjk, id) ? id : null
|
|
879
|
-
textCjkSelect.value = state.textFontCjk ?? ''
|
|
880
|
-
writeState({ textFontCjk: state.textFontCjk })
|
|
881
|
-
fontPickBuilt = false
|
|
882
|
-
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))
|
|
883
1613
|
})
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
textLatinSelect.value = state.textFontLatin ?? ''
|
|
888
|
-
writeState({ textFontLatin: state.textFontLatin })
|
|
889
|
-
fontPickBuilt = false
|
|
890
|
-
rebuildFontPick()
|
|
1614
|
+
codePlus.addEventListener('click', () => {
|
|
1615
|
+
if (typeof fontState.code !== 'number') return
|
|
1616
|
+
officialSetFontSize('code', Math.min(CODE_FONT_MAX, fontState.code + 1))
|
|
891
1617
|
})
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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))
|
|
899
1630
|
})
|
|
900
1631
|
|
|
901
1632
|
// ── 功能 7:行距 / 字距(0.1 步进;到达边界即回"默认",覆盖层随之停用) ──
|
|
902
1633
|
function setLineHeight(v) {
|
|
903
1634
|
const next = Math.min(LINEH_MAX, Math.max(LINEH_MIN, Math.round(v * 10) / 10))
|
|
904
1635
|
state.lineHeight = next > LINEH_MIN ? next : null
|
|
905
|
-
|
|
906
|
-
rebuildFontPick()
|
|
1636
|
+
kickFontPick()
|
|
907
1637
|
renderTypeRow()
|
|
908
1638
|
writeState({ lineHeight: state.lineHeight })
|
|
909
1639
|
}
|
|
910
1640
|
function setLetterSpacing(v) {
|
|
911
1641
|
const next = Math.min(LETTERSP_MAX, Math.max(LETTERSP_MIN, Math.round(v * 10) / 10))
|
|
912
1642
|
state.letterSpacing = next !== 0 ? next : null
|
|
913
|
-
|
|
914
|
-
rebuildFontPick()
|
|
1643
|
+
kickFontPick()
|
|
915
1644
|
renderTypeRow()
|
|
916
1645
|
writeState({ letterSpacing: state.letterSpacing })
|
|
917
1646
|
}
|
|
918
1647
|
function renderTypeRow() {
|
|
919
|
-
lhVal.textContent = state.lineHeight ? state.lineHeight.toFixed(1) + '×' : '
|
|
920
|
-
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')
|
|
921
1650
|
}
|
|
922
1651
|
lhMinus.addEventListener('click', () => setLineHeight((state.lineHeight ?? LINEH_MIN) - 0.1))
|
|
923
1652
|
lhPlus.addEventListener('click', () => setLineHeight((state.lineHeight ?? LINEH_MIN) + 0.1))
|
|
@@ -930,16 +1659,67 @@ window.__ModuleLoader__.load({
|
|
|
930
1659
|
let officialDragMoved = false
|
|
931
1660
|
let officialDragStart = null
|
|
932
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
|
+
}
|
|
933
1712
|
function setWidthPx(v) {
|
|
934
1713
|
if (v === null) {
|
|
935
1714
|
state.widthPx = null
|
|
936
1715
|
} else {
|
|
937
1716
|
const t = widthTargets()
|
|
938
|
-
if (!t) return //
|
|
1717
|
+
if (!t || !(t.column > 0)) return // 无可用会话列宽(滑块与"自动"此时均处于禁用态)
|
|
939
1718
|
state.widthPx = Math.min(Math.max(Math.round(v), CONTENT_MIN), extCapFor(t.column))
|
|
940
1719
|
}
|
|
941
|
-
applyWidthState()
|
|
1720
|
+
applyWidthState(v === null)
|
|
942
1721
|
writeState({ widthPx: state.widthPx })
|
|
1722
|
+
refreshWidthTargets()
|
|
943
1723
|
renderWidthRow()
|
|
944
1724
|
}
|
|
945
1725
|
widthSlider.addEventListener('pointerdown', () => {
|
|
@@ -952,6 +1732,7 @@ window.__ModuleLoader__.load({
|
|
|
952
1732
|
}
|
|
953
1733
|
}
|
|
954
1734
|
window.addEventListener('pointerup', onWindowPointerUp)
|
|
1735
|
+
window.addEventListener('pointercancel', onWindowPointerUp)
|
|
955
1736
|
widthSlider.addEventListener('input', () => {
|
|
956
1737
|
setWidthPx(Number(widthSlider.value))
|
|
957
1738
|
})
|
|
@@ -964,11 +1745,13 @@ window.__ModuleLoader__.load({
|
|
|
964
1745
|
const target = e.target
|
|
965
1746
|
const handle = target && target.closest ? target.closest('[data-width-handle]') : null
|
|
966
1747
|
if (handle) {
|
|
1748
|
+
if (state.open) attachWidthObserver()
|
|
1749
|
+
attachColumnObserver()
|
|
967
1750
|
officialDragging = true
|
|
968
1751
|
officialDragMoved = false
|
|
969
1752
|
officialDragStart = { x: e.clientX, y: e.clientY }
|
|
970
1753
|
const t = widthTargets()
|
|
971
|
-
officialDragAdopt = !!(t && state.widthPx != null && state.widthPx > officialMaxFor(t.column))
|
|
1754
|
+
officialDragAdopt = !!(t && t.column > 0 && state.widthPx != null && state.widthPx > officialMaxFor(t.column))
|
|
972
1755
|
}
|
|
973
1756
|
if (state.open && !panel.contains(target) && !fab.contains(target)) {
|
|
974
1757
|
togglePanel(false)
|
|
@@ -987,29 +1770,38 @@ window.__ModuleLoader__.load({
|
|
|
987
1770
|
}
|
|
988
1771
|
function onDocPointerUp() {
|
|
989
1772
|
if (!officialDragging) return
|
|
990
|
-
const
|
|
1773
|
+
const moved = officialDragMoved
|
|
991
1774
|
officialDragMoved = false
|
|
992
1775
|
officialDragStart = null
|
|
993
1776
|
officialDragAdopt = false
|
|
994
|
-
if (!
|
|
1777
|
+
if (!moved) {
|
|
995
1778
|
officialDragging = false
|
|
1779
|
+
refreshWidthTargets()
|
|
996
1780
|
return
|
|
997
1781
|
}
|
|
998
|
-
//
|
|
1782
|
+
// 任何一次官方手柄拖动松手后都采纳官方最终值(含范围内拖动);
|
|
1783
|
+
// 等待 0ms 让官方先提交,期间保持"官方拖动中"避免漂移修复抢跑。
|
|
999
1784
|
setTimeout(() => {
|
|
1785
|
+
if (disposed) return
|
|
1000
1786
|
officialDragging = false
|
|
1001
1787
|
const pref = readWidthPreference()
|
|
1002
1788
|
state.widthPx = pref != null ? pref : null
|
|
1003
1789
|
applyWidthState()
|
|
1004
1790
|
writeState({ widthPx: state.widthPx })
|
|
1005
|
-
|
|
1791
|
+
refreshWidthTargets()
|
|
1006
1792
|
}, 0)
|
|
1007
1793
|
}
|
|
1008
1794
|
function onDocPointerCancel() {
|
|
1795
|
+
const restore = officialDragAdopt && state.widthPx != null
|
|
1009
1796
|
officialDragging = false
|
|
1010
1797
|
officialDragMoved = false
|
|
1011
1798
|
officialDragStart = null
|
|
1012
1799
|
officialDragAdopt = false
|
|
1800
|
+
if (restore) {
|
|
1801
|
+
// 拖动被系统取消(未松手):把交还官方时移除的扩展覆盖装回去
|
|
1802
|
+
applyWidthState()
|
|
1803
|
+
}
|
|
1804
|
+
refreshWidthTargets() // 释放拖动期临时挂上的观察器,并按当前宽度归位
|
|
1013
1805
|
}
|
|
1014
1806
|
document.addEventListener('pointerdown', onDocPointerDown, true)
|
|
1015
1807
|
document.addEventListener('pointermove', onDocPointerMove, true)
|
|
@@ -1019,7 +1811,7 @@ window.__ModuleLoader__.load({
|
|
|
1019
1811
|
// 官方手柄拖动 / 窗口尺寸变化 → 目标元素 style 变化 → 反向同步面板
|
|
1020
1812
|
let watchedTarget = null
|
|
1021
1813
|
const widthObserver = new MutationObserver(() => {
|
|
1022
|
-
if (!widthDragging) renderWidthRow()
|
|
1814
|
+
if (!disposed && state.open && !widthDragging) renderWidthRow()
|
|
1023
1815
|
})
|
|
1024
1816
|
function attachWidthObserver() {
|
|
1025
1817
|
const t = widthTargets()
|
|
@@ -1035,7 +1827,11 @@ window.__ModuleLoader__.load({
|
|
|
1035
1827
|
// 官方键/官方变量的收敛压到同一批回调内,开合过程不再出现滞留窄值的中间态。
|
|
1036
1828
|
let watchedColumn = null
|
|
1037
1829
|
const columnObserver = new ResizeObserver(() => {
|
|
1830
|
+
if (disposed) return
|
|
1831
|
+
adoptOfficialWidthOnStart()
|
|
1038
1832
|
normalizeWidthDrift()
|
|
1833
|
+
if (state.widthPx != null && !widthTargets()) scheduleWidthTargetRetry()
|
|
1834
|
+
else widthTargetRetryCount = 0
|
|
1039
1835
|
if (state.open && !officialDragging && !widthDragging) renderWidthRow()
|
|
1040
1836
|
})
|
|
1041
1837
|
function attachColumnObserver() {
|
|
@@ -1047,84 +1843,58 @@ window.__ModuleLoader__.load({
|
|
|
1047
1843
|
if (container) columnObserver.observe(container)
|
|
1048
1844
|
}
|
|
1049
1845
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
firstSync &&
|
|
1060
|
-
typeof state.fontPx === 'number' &&
|
|
1061
|
-
state.fontPx <= FONT_OFFICIAL_MAX &&
|
|
1062
|
-
state.fontPx !== fontState.official
|
|
1063
|
-
) {
|
|
1064
|
-
// 启动恢复:面板上次设定的字号(范围内)直写回官方设置,
|
|
1065
|
-
// 保证"范围内 ⇔ 官方值 === 面板值"的稳定关系
|
|
1066
|
-
firstSync = false
|
|
1067
|
-
applyFont()
|
|
1068
|
-
writeOfficialFont(state.fontPx) // 官方更新后会再次进入本函数(自写标记路径)
|
|
1069
|
-
return
|
|
1070
|
-
}
|
|
1071
|
-
firstSync = false
|
|
1072
|
-
if (typeof state.fontPx !== 'number') state.fontPx = fontState.official
|
|
1073
|
-
else if (state.fontPx <= FONT_OFFICIAL_MAX) state.fontPx = fontState.official // 范围内以官方为准
|
|
1074
|
-
// 超出 22 的绝对目标保持不动,倍率自动适配
|
|
1075
|
-
writeState({ fontPx: state.fontPx })
|
|
1076
|
-
renderFontRow()
|
|
1077
|
-
}
|
|
1078
|
-
applyFont()
|
|
1846
|
+
/** 只在明确事件(启动/会话切换/面板打开/窗口变化/官方手柄)后复核宽度。 */
|
|
1847
|
+
function refreshWidthTargets() {
|
|
1848
|
+
adoptOfficialWidthOnStart()
|
|
1849
|
+
// 只有面板打开时才需要监听官方手柄写变量(用于滑杆实时回显);
|
|
1850
|
+
// 列观察仅在扩展宽度生效或面板打开时需要;其余时候不挂任何后台观察器。
|
|
1851
|
+
if (state.open) attachWidthObserver()
|
|
1852
|
+
else {
|
|
1853
|
+
widthObserver.disconnect()
|
|
1854
|
+
watchedTarget = null
|
|
1079
1855
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
sync(tctx.theme.getTheme())
|
|
1085
|
-
} catch {
|
|
1086
|
-
applyFont() // 服务首读异常时至少按兜底基准绘制一次
|
|
1856
|
+
if (state.open || state.widthPx != null) attachColumnObserver()
|
|
1857
|
+
else {
|
|
1858
|
+
columnObserver.disconnect()
|
|
1859
|
+
watchedColumn = null
|
|
1087
1860
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
state.
|
|
1093
|
-
}
|
|
1094
|
-
|
|
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)
|
|
1095
1886
|
}
|
|
1096
|
-
if (typeof state.widthPx !== 'number' || !(state.widthPx > 0)) state.widthPx = null
|
|
1097
|
-
if (typeof state.lineHeight === 'number') {
|
|
1098
|
-
const lh = Math.min(LINEH_MAX, Math.max(LINEH_MIN, Math.round(state.lineHeight * 10) / 10))
|
|
1099
|
-
state.lineHeight = lh > LINEH_MIN ? lh : null
|
|
1100
|
-
} else state.lineHeight = null
|
|
1101
|
-
if (typeof state.letterSpacing === 'number') {
|
|
1102
|
-
const ls = Math.min(LETTERSP_MAX, Math.max(LETTERSP_MIN, Math.round(state.letterSpacing * 10) / 10))
|
|
1103
|
-
state.letterSpacing = ls !== 0 ? ls : null
|
|
1104
|
-
} else state.letterSpacing = null
|
|
1105
|
-
state.panelX = typeof state.panelX === 'number' ? state.panelX : null
|
|
1106
|
-
state.panelY = typeof state.panelY === 'number' ? state.panelY : null
|
|
1107
|
-
state.panelH = typeof state.panelH === 'number' && state.panelH > 0 ? state.panelH : null
|
|
1108
1887
|
|
|
1109
1888
|
// ── 字体选择:本机可用性过滤 + 选择恢复 + 覆盖层生成 ────────────
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
const availCjk = TEXT_FONTS_CJK.filter((f) => fontAvailable(f.name)) // 只保留本机已装
|
|
1122
|
-
const availLatin = TEXT_FONTS_LATIN.filter((f) => fontAvailable(f.name))
|
|
1123
|
-
const availCode = CODE_FONTS.filter((f) => fontAvailable(f.name))
|
|
1124
|
-
fillFontSelect(textCjkSelect, availCjk)
|
|
1125
|
-
fillFontSelect(textLatinSelect, availLatin)
|
|
1126
|
-
fillFontSelect(codeFontSelect, availCode)
|
|
1127
|
-
// 已保存的选择若已不在本机(被卸载 / 换机器)→ 回默认
|
|
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
|
+
// 已保存的正文字体选择若已不在本机(被卸载 / 换机器)→ 回默认
|
|
1128
1898
|
let fontsReset = false
|
|
1129
1899
|
if (state.textFontCjk && !fontChoice(availCjk, state.textFontCjk)) {
|
|
1130
1900
|
state.textFontCjk = null
|
|
@@ -1134,29 +1904,61 @@ window.__ModuleLoader__.load({
|
|
|
1134
1904
|
state.textFontLatin = null
|
|
1135
1905
|
fontsReset = true
|
|
1136
1906
|
}
|
|
1137
|
-
if (state.
|
|
1138
|
-
state.codeFont = null
|
|
1139
|
-
fontsReset = true
|
|
1140
|
-
}
|
|
1141
|
-
if (fontsReset) writeState({ textFontCjk: state.textFontCjk, textFontLatin: state.textFontLatin, codeFont: state.codeFont })
|
|
1907
|
+
if (fontsReset) writeState({ textFontCjk: state.textFontCjk, textFontLatin: state.textFontLatin })
|
|
1142
1908
|
let fontPickBuilt = false
|
|
1143
|
-
|
|
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
|
+
/** 生成 / 刷新字体与排版覆盖层;主题未就绪时保持停用并安排有限重试。 */
|
|
1144
1932
|
function rebuildFontPick() {
|
|
1145
|
-
if (!
|
|
1933
|
+
if (!hasFontFeatures()) {
|
|
1934
|
+
cancelFontRetry()
|
|
1146
1935
|
fontPickTag.disabled = true
|
|
1147
1936
|
fontPickBuilt = false
|
|
1148
1937
|
return
|
|
1149
1938
|
}
|
|
1150
1939
|
if (fontPickBuilt) return
|
|
1151
1940
|
const css = buildFontPickCss()
|
|
1152
|
-
if (!css)
|
|
1941
|
+
if (!css) {
|
|
1942
|
+
scheduleFontRetry()
|
|
1943
|
+
return
|
|
1944
|
+
}
|
|
1945
|
+
cancelFontRetry()
|
|
1946
|
+
fontRetryStep = 0
|
|
1153
1947
|
fontPickTag.textContent = css
|
|
1154
1948
|
fontPickTag.disabled = false
|
|
1155
1949
|
fontPickBuilt = true
|
|
1156
1950
|
}
|
|
1951
|
+
/** 用户主动改字体/行距:重开一轮有限重试。 */
|
|
1952
|
+
function kickFontPick() {
|
|
1953
|
+
fontRetryStep = 0
|
|
1954
|
+
cancelFontRetry()
|
|
1955
|
+
fontPickBuilt = false
|
|
1956
|
+
rebuildFontPick()
|
|
1957
|
+
}
|
|
1157
1958
|
|
|
1158
1959
|
applyFont()
|
|
1159
1960
|
renderAll()
|
|
1961
|
+
adoptOfficialWidthOnStart()
|
|
1160
1962
|
applyWidthState()
|
|
1161
1963
|
rebuildFontPick()
|
|
1162
1964
|
// 面板开着启动(上次退出未关):补挂载定位——不经 togglePanel 时面板没有
|
|
@@ -1199,6 +2001,11 @@ window.__ModuleLoader__.load({
|
|
|
1199
2001
|
fab.addEventListener('pointermove', onPointerMove)
|
|
1200
2002
|
fab.addEventListener('pointerup', onPointerUp)
|
|
1201
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
|
+
})
|
|
1202
2009
|
closeBtn.addEventListener('click', () => togglePanel(false))
|
|
1203
2010
|
|
|
1204
2011
|
// ── 功能 8:面板拖动 / 拉拽高度 ──────────────────────────────────
|
|
@@ -1307,7 +2114,7 @@ window.__ModuleLoader__.load({
|
|
|
1307
2114
|
window.addEventListener('pointermove', onPanelPointerMove)
|
|
1308
2115
|
window.addEventListener('pointerup', onPanelPointerUp)
|
|
1309
2116
|
window.addEventListener('pointercancel', onPanelPointerUp)
|
|
1310
|
-
headRow.title = '
|
|
2117
|
+
headRow.title = t('dragHint')
|
|
1311
2118
|
headRow.addEventListener('dblclick', () => {
|
|
1312
2119
|
// 复位:回到自动布局(位置 + 高度一起,回到"高度锁"状态)
|
|
1313
2120
|
state.panelX = null
|
|
@@ -1354,16 +2161,46 @@ window.__ModuleLoader__.load({
|
|
|
1354
2161
|
})
|
|
1355
2162
|
return turndownSvc
|
|
1356
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
|
+
}
|
|
1357
2188
|
function onCopy(e) {
|
|
1358
2189
|
try {
|
|
1359
2190
|
const sel = document.getSelection()
|
|
1360
2191
|
if (!sel || sel.isCollapsed || !sel.rangeCount) return
|
|
1361
2192
|
// 选区两端都要在消息行内才接管(跨区选择保持系统默认行为)
|
|
1362
2193
|
const inChat = (n) => {
|
|
1363
|
-
const
|
|
1364
|
-
return !!(
|
|
2194
|
+
const node = n && (n.nodeType === 3 ? n.parentElement : n)
|
|
2195
|
+
return !!(node && node.closest && node.closest('[data-chat-node-key]'))
|
|
1365
2196
|
}
|
|
1366
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
|
|
1367
2204
|
const elOf = (n) => (n && n.nodeType === 3 ? n.parentElement : n)
|
|
1368
2205
|
const ka = elOf(sel.anchorNode).closest('.katex')
|
|
1369
2206
|
const kf = elOf(sel.focusNode).closest('.katex')
|
|
@@ -1378,10 +2215,17 @@ window.__ModuleLoader__.load({
|
|
|
1378
2215
|
}
|
|
1379
2216
|
}
|
|
1380
2217
|
// 与 KaTeX 官方 copy-tex 同策略:端点落在公式内时先把工作范围扩到整个公式,
|
|
1381
|
-
// "只碰一角"也能拿到完整 LaTeX
|
|
2218
|
+
// "只碰一角"也能拿到完整 LaTeX。必须按 Range 的实际起止端判断,
|
|
2219
|
+
// 不能按 anchor/focus —— 从右向左拖选时方向相反,按方向扩会截掉选区左侧。
|
|
1382
2220
|
const work = sel.getRangeAt(0).cloneRange()
|
|
1383
|
-
|
|
1384
|
-
|
|
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)
|
|
1385
2229
|
const box = document.createElement('div')
|
|
1386
2230
|
// 选区克隆丢掉共同祖先往上的层级(代码块外层的 <pre> 即如此丢的),转换器的
|
|
1387
2231
|
// 空白折叠随即把换行折掉;按现场回收"预格式化"祖先链(判据=实际生效的
|
|
@@ -1407,8 +2251,7 @@ window.__ModuleLoader__.load({
|
|
|
1407
2251
|
nestAt.append(work.cloneContents())
|
|
1408
2252
|
// 公式场景先留存原始 HTML(含 <math>):粘进 Word 等富目标仍可显示公式
|
|
1409
2253
|
const html = box.querySelector('.katex') ? box.innerHTML : null
|
|
1410
|
-
|
|
1411
|
-
const md = getTurndown().turndown(box).replace(/\n{3,}/g, '\n\n').trim()
|
|
2254
|
+
const md = tidyMarkdown(getTurndown().turndown(box))
|
|
1412
2255
|
if (!md) return
|
|
1413
2256
|
e.clipboardData.setData('text/plain', md)
|
|
1414
2257
|
if (html) e.clipboardData.setData('text/html', html)
|
|
@@ -1419,43 +2262,54 @@ window.__ModuleLoader__.load({
|
|
|
1419
2262
|
}
|
|
1420
2263
|
}
|
|
1421
2264
|
document.addEventListener('copy', onCopy, true)
|
|
2265
|
+
/** 文本编辑类控件里的 Esc 先交给控件(取消输入/收起下拉),不关面板。 */
|
|
2266
|
+
function isTextEditingTarget(n) {
|
|
2267
|
+
if (!n || !n.closest) return false
|
|
2268
|
+
if (n.closest('textarea, [contenteditable="true"], [contenteditable=""], [contenteditable="plaintext-only"]')) return true
|
|
2269
|
+
return n.tagName === 'INPUT' && /^(text|search|email|url|tel|password|number)$/i.test(String(n.type || ''))
|
|
2270
|
+
}
|
|
2271
|
+
function onDocKeyDown(e) {
|
|
2272
|
+
if (e.key !== 'Escape' || !state.open || e.defaultPrevented) return
|
|
2273
|
+
if (document.fullscreenElement) return // 全屏下 Esc 先交给浏览器退出全屏
|
|
2274
|
+
// 只保护插件面板内的文本编辑控件;其它区域照常 Esc 收起面板
|
|
2275
|
+
if (e.target && e.target.closest && e.target.closest('.dsrb-panel') && isTextEditingTarget(e.target)) return
|
|
2276
|
+
// 官方对话框 / 菜单里按 Esc:让官方先处理,不抢关面板
|
|
2277
|
+
const inOverlay = (n) => !!(n && n.closest && n.closest('[role="dialog"], [role="menu"], [role="listbox"]'))
|
|
2278
|
+
if (inOverlay(e.target) || inOverlay(document.activeElement)) return
|
|
2279
|
+
togglePanel(false) // 不 preventDefault:不吞官方自己的 Esc 行为
|
|
2280
|
+
}
|
|
2281
|
+
document.addEventListener('keydown', onDocKeyDown)
|
|
1422
2282
|
|
|
1423
2283
|
function onResize() {
|
|
1424
2284
|
const [x, y] = clampPos(fab.offsetLeft, fab.offsetTop, FAB_SIZE, FAB_SIZE)
|
|
1425
2285
|
fab.style.left = x + 'px'
|
|
1426
2286
|
fab.style.top = y + 'px'
|
|
1427
|
-
|
|
1428
|
-
if (state.open)
|
|
1429
|
-
renderWidthRow()
|
|
1430
|
-
placePanel()
|
|
1431
|
-
}
|
|
2287
|
+
refreshWidthTargets()
|
|
2288
|
+
if (state.open) placePanel()
|
|
1432
2289
|
}
|
|
1433
2290
|
window.addEventListener('resize', onResize)
|
|
1434
2291
|
|
|
1435
|
-
//
|
|
1436
|
-
|
|
1437
|
-
attachWidthObserver()
|
|
1438
|
-
attachColumnObserver()
|
|
1439
|
-
const retryTimer = setInterval(() => {
|
|
1440
|
-
attachWidthObserver()
|
|
1441
|
-
attachColumnObserver()
|
|
1442
|
-
normalizeWidthDrift()
|
|
1443
|
-
rebuildFontPick() // 官方主题样式稍后就绪时补建覆盖层(已建则零开销早退)
|
|
1444
|
-
if (state.open && !officialDragging && !widthDragging) renderWidthRow()
|
|
1445
|
-
}, 2000)
|
|
2292
|
+
// 宽度目标只随事件变化:启动、面板打开、窗口尺寸、会话切换、官方手柄操作。
|
|
2293
|
+
refreshWidthTargets()
|
|
1446
2294
|
|
|
1447
2295
|
// 启动标记:记录加载时刻与版本(供诊断确认插件已加载;不影响功能)
|
|
1448
2296
|
writeState({ lastBootAt: Date.now(), lastBootVersion: VERSION })
|
|
1449
|
-
console.log('[dsh-happy-reader] 客户端半边已加载 v' + VERSION)
|
|
1450
2297
|
|
|
1451
|
-
// ──
|
|
1452
|
-
//
|
|
2298
|
+
// ── 卸载清理(React 槽位卸载 / 插件热重载 / ctx.effect 三路都会触发;
|
|
2299
|
+
// 幂等,可重复调用) ──
|
|
1453
2300
|
const dispose = () => {
|
|
1454
|
-
|
|
2301
|
+
if (disposed) return // 幂等:热重载/槽位卸载可能重复调用,不能重复抹掉新实例的全局变量
|
|
2302
|
+
disposed = true
|
|
2303
|
+
overlayAlive = false
|
|
2304
|
+
ui = null
|
|
2305
|
+
if (widthRefreshTimer) { clearTimeout(widthRefreshTimer); widthRefreshTimer = null }
|
|
2306
|
+
if (widthTargetRetry) { clearTimeout(widthTargetRetry); widthTargetRetry = null }
|
|
2307
|
+
cancelFontRetry()
|
|
1455
2308
|
widthObserver.disconnect()
|
|
1456
2309
|
columnObserver.disconnect()
|
|
1457
2310
|
window.removeEventListener('resize', onResize)
|
|
1458
2311
|
window.removeEventListener('pointerup', onWindowPointerUp)
|
|
2312
|
+
window.removeEventListener('pointercancel', onWindowPointerUp)
|
|
1459
2313
|
window.removeEventListener('pointermove', onPanelPointerMove)
|
|
1460
2314
|
window.removeEventListener('pointerup', onPanelPointerUp)
|
|
1461
2315
|
window.removeEventListener('pointercancel', onPanelPointerUp)
|
|
@@ -1464,6 +2318,7 @@ window.__ModuleLoader__.load({
|
|
|
1464
2318
|
document.removeEventListener('pointerup', onDocPointerUp, true)
|
|
1465
2319
|
document.removeEventListener('pointercancel', onDocPointerCancel, true)
|
|
1466
2320
|
document.removeEventListener('copy', onCopy, true)
|
|
2321
|
+
document.removeEventListener('keydown', onDocKeyDown)
|
|
1467
2322
|
fab.remove()
|
|
1468
2323
|
panel.remove()
|
|
1469
2324
|
style.remove()
|
|
@@ -1476,14 +2331,62 @@ window.__ModuleLoader__.load({
|
|
|
1476
2331
|
document.documentElement.removeAttribute(FS_ATTR)
|
|
1477
2332
|
document.documentElement.style.removeProperty('--dsrb-font-base')
|
|
1478
2333
|
document.documentElement.style.removeProperty('--dsrb-font-scale')
|
|
1479
|
-
|
|
1480
|
-
|
|
2334
|
+
turndownSvc = null
|
|
2335
|
+
const target = widthTargets()
|
|
2336
|
+
if (target) target.container.style.removeProperty('--dsh-chat-content-width')
|
|
2337
|
+
// 仅当全局句柄仍属于本实例时才释放,避免抹掉新实例的热重载句柄
|
|
2338
|
+
if (window.__dsrbDisposeOwner === INSTANCE_TOKEN) {
|
|
2339
|
+
window.__dsrbDispose = null
|
|
2340
|
+
window.__dsrbDisposeOwner = null
|
|
2341
|
+
}
|
|
2342
|
+
// 插件被停用/卸载(语言切换重挂除外)时退出本插件进入的全屏,恢复窗口态
|
|
2343
|
+
if (!remounting && document.fullscreenElement === document.documentElement) {
|
|
2344
|
+
try {
|
|
2345
|
+
const p = document.exitFullscreen()
|
|
2346
|
+
if (p && typeof p.catch === 'function') p.catch(() => {})
|
|
2347
|
+
} catch { /* 退出失败不阻塞拆除 */ }
|
|
2348
|
+
}
|
|
1481
2349
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
2350
|
+
// 主题/语言事件通过该句柄刷新本层 UI(apply 级用到)
|
|
2351
|
+
function applyTextFonts() {
|
|
2352
|
+
textCjkSelect.value = state.textFontCjk ?? ''
|
|
2353
|
+
textLatinSelect.value = state.textFontLatin ?? ''
|
|
2354
|
+
kickFontPick()
|
|
2355
|
+
renderGroupSummaries()
|
|
2356
|
+
}
|
|
2357
|
+
function refreshFontPick() { kickFontPick() }
|
|
2358
|
+
ui = {
|
|
2359
|
+
applyFont,
|
|
2360
|
+
renderFontRow,
|
|
2361
|
+
renderCodeSizeRow,
|
|
2362
|
+
renderCodeFontRow,
|
|
2363
|
+
renderTerminalSizeRow,
|
|
2364
|
+
renderTerminalFontRow,
|
|
2365
|
+
renderTextBaseRow,
|
|
2366
|
+
renderGroupSummaries,
|
|
2367
|
+
refreshFontPick,
|
|
2368
|
+
applyTextFonts,
|
|
2369
|
+
scheduleWidthRefresh,
|
|
2370
|
+
}
|
|
2371
|
+
// 热重载句柄必须连同设置页一起拆:设置行不在 mount 的 DOM 树里
|
|
2372
|
+
window.__dsrbDispose = () => {
|
|
2373
|
+
dispose()
|
|
2374
|
+
if (settingsDispose) {
|
|
2375
|
+
try { settingsDispose() } catch { /* 忽略 */ }
|
|
2376
|
+
settingsDispose = null
|
|
2377
|
+
}
|
|
2378
|
+
settingsHost = null
|
|
2379
|
+
settingsRefresh = null
|
|
2380
|
+
settingsAlive = false
|
|
2381
|
+
}
|
|
2382
|
+
window.__dsrbDisposeOwner = INSTANCE_TOKEN
|
|
2383
|
+
return dispose
|
|
1484
2384
|
} catch (err) {
|
|
1485
|
-
//
|
|
1486
|
-
console.error('[dsh-happy-reader]
|
|
2385
|
+
// 任何意外都只记日志并清掉半成品,不影响应用本体
|
|
2386
|
+
console.error('[dsh-happy-reader] 面板实体初始化失败:', err)
|
|
2387
|
+
overlayAlive = false
|
|
2388
|
+
try { host.textContent = '' } catch { /* 忽略 */ }
|
|
2389
|
+
return () => {}
|
|
1487
2390
|
}
|
|
1488
2391
|
}
|
|
1489
2392
|
|
|
@@ -2511,6 +3414,7 @@ return exports;
|
|
|
2511
3414
|
// ══ vendored: turndown-plugin-gfm end ══
|
|
2512
3415
|
|
|
2513
3416
|
exports.apply = apply
|
|
3417
|
+
exports.inject = ['slots']
|
|
2514
3418
|
|
|
2515
3419
|
return module.exports
|
|
2516
3420
|
},
|