dsh-coding-sidebar 1.0.3 → 1.0.5
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 +27 -42
- package/lib/client-editor.js +315 -4179
- package/lib/client-registry.js +913 -1303
- package/lib/client-terminal.js +189 -261
- package/lib/client.js +911 -1301
- package/lib/index.js +13 -110
- package/lib/types/bundle-route.d.ts +1 -1
- package/lib/types/client/LiveView.d.ts +1 -0
- package/lib/types/client/SideCardSection.d.ts +3 -3
- package/lib/types/client/TextEditor.d.ts +20 -10
- package/lib/types/client/add-plugin-modal.d.ts +0 -4
- package/lib/types/client/api.d.ts +16 -11
- package/lib/types/client/builtins/index.d.ts +10 -10
- package/lib/types/client/chunk-loader.d.ts +7 -8
- package/lib/types/client/locales.d.ts +6 -26
- package/lib/types/client/native-avoid.d.ts +2 -0
- package/lib/types/client/plugins-shared.d.ts +6 -6
- package/lib/types/client/service.d.ts +13 -83
- package/lib/types/index.d.ts +1 -1
- package/lib/types/prefs-shared.d.ts +1 -24
- package/lib/types/wire.d.ts +1 -1
- package/package.json +26 -29
- package/src/bundle-route.ts +1 -1
- package/src/client/BrowserView.tsx +18 -2
- package/src/client/EditorHost.tsx +52 -122
- package/src/client/LiveView.tsx +229 -0
- package/src/client/SandboxStatusBar.tsx +12 -9
- package/src/client/SideCardSection.tsx +28 -81
- package/src/client/TextEditor.tsx +89 -266
- package/src/client/add-plugin-modal.tsx +16 -24
- package/src/client/api.ts +4 -25
- package/src/client/builtins/index.ts +10 -14
- package/src/client/chunk-loader.ts +7 -8
- package/src/client/index.tsx +12 -3
- package/src/client/locales-ar.ts +0 -6
- package/src/client/locales-de.ts +0 -6
- package/src/client/locales-fr.ts +0 -6
- package/src/client/locales-hi.ts +0 -6
- package/src/client/locales-id.ts +0 -6
- package/src/client/locales-it.ts +0 -6
- package/src/client/locales-ja.ts +0 -6
- package/src/client/locales-ko.ts +0 -6
- package/src/client/locales-nl.ts +0 -6
- package/src/client/locales-pl.ts +0 -6
- package/src/client/locales-pt.ts +0 -6
- package/src/client/locales-ru.ts +0 -6
- package/src/client/locales-sv.ts +0 -6
- package/src/client/locales-th.ts +0 -6
- package/src/client/locales-tr.ts +0 -6
- package/src/client/locales-vi.ts +0 -6
- package/src/client/locales-zh-HK.ts +0 -6
- package/src/client/locales-zh-MO.ts +0 -6
- package/src/client/locales-zh-TW.ts +0 -6
- package/src/client/locales.ts +12 -52
- package/src/client/native-avoid.ts +155 -0
- package/src/client/plugins-shared.ts +6 -6
- package/src/client/prefs.ts +0 -7
- package/src/client/service.ts +13 -150
- package/src/client/sidebar.module.css +28 -420
- package/src/config.ts +1 -4
- package/src/index.ts +17 -74
- package/src/prefs-shared.ts +1 -27
- package/src/wire.ts +1 -0
- package/lib/client-mermaid.js +0 -200776
- package/lib/types/client/MarkdownHtml.d.ts +0 -32
- package/lib/types/client/PdfView.d.ts +0 -6
- package/lib/types/client/builtins/viewers.d.ts +0 -3
- package/lib/types/client/chunks/mermaid.d.ts +0 -10
- package/lib/types/client/editor-load.d.ts +0 -66
- package/lib/types/client/image-types.d.ts +0 -3
- package/lib/types/client/markdown-html.d.ts +0 -96
- package/lib/types/client/markdown-images.d.ts +0 -45
- package/lib/types/client/md-toc.d.ts +0 -4
- package/lib/types/client/mermaid-blocks.d.ts +0 -47
- package/lib/types/client/mermaid-sanitize.d.ts +0 -2
- package/lib/types/client/mermaid.d.ts +0 -12
- package/lib/types/client/pdf-types.d.ts +0 -2
- package/lib/types/client/plugins-viewers.d.ts +0 -3
- package/lib/types/html-route.d.ts +0 -59
- package/src/client/MarkdownHtml.tsx +0 -296
- package/src/client/PdfView.tsx +0 -110
- package/src/client/builtins/viewers.tsx +0 -125
- package/src/client/chunks/mermaid.tsx +0 -10
- package/src/client/editor-load.ts +0 -92
- package/src/client/image-types.ts +0 -8
- package/src/client/markdown-html.ts +0 -331
- package/src/client/markdown-images.ts +0 -137
- package/src/client/md-toc.tsx +0 -127
- package/src/client/mermaid-blocks.ts +0 -110
- package/src/client/mermaid-sanitize.ts +0 -93
- package/src/client/mermaid.tsx +0 -406
- package/src/client/pdf-types.ts +0 -4
- package/src/client/plugins-viewers.ts +0 -30
- package/src/html-route.ts +0 -106
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 浏览器实况(CDP screencast + 输入回传,2026-09-12 方案 2)。
|
|
3
|
+
*
|
|
4
|
+
* 数据面:KCoder 桌面端 browser-host 维护一个无头 Chromium(固定 CDP
|
|
5
|
+
* 转发地址 127.0.0.1:9223,与 playwright MCP `--cdp-endpoint` 共用)。
|
|
6
|
+
* 本组件作为第二个 CDP 客户端:
|
|
7
|
+
* - target 列表经宿主代理 `/sidebar/api`(CDP HTTP 无 CORS 头);
|
|
8
|
+
* - screencast/输入走 renderer 直连 `ws://127.0.0.1:9223/devtools/page/<id>`
|
|
9
|
+
* (WebSocket 不受 CORS 约束,宿主未设 CSP)。
|
|
10
|
+
*
|
|
11
|
+
* 连接模型:`attachedId`(用户显式选择)优先,否则自动跟随「最新创建的
|
|
12
|
+
* page target」;连接按 effectiveId 建立与重建,列表仅在内容变化时更新
|
|
13
|
+
* state(JSON 比对),避免轮询引起重连抖动。
|
|
14
|
+
*
|
|
15
|
+
* 交互:screencast 帧绘到 canvas;指针/滚轮按 canvas→viewport 比例回传
|
|
16
|
+
* (Input.dispatchMouseEvent);可打印字符经 Input.insertText,控制键走
|
|
17
|
+
* rawKeyDown/keyUp。视口统一 1280×800(Emulation.setDeviceMetricsOverride),
|
|
18
|
+
* canvas 等比缩放显示。
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
21
|
+
import { api } from './api.ts'
|
|
22
|
+
import { t } from './locales.ts'
|
|
23
|
+
|
|
24
|
+
/** 与 KCoder desktop/main/browser-host.ts 的 BROWSER_HOST_PORT 一致。 */
|
|
25
|
+
const CDP_WS = 'ws://127.0.0.1:9223'
|
|
26
|
+
/** 实况视口(CDP 侧强制,帧与输入坐标以此为基准)。 */
|
|
27
|
+
const VIEW_W = 1280
|
|
28
|
+
const VIEW_H = 800
|
|
29
|
+
|
|
30
|
+
type Target = { id: string; url: string; title: string }
|
|
31
|
+
type Status = 'connecting' | 'live' | 'down' | 'idle'
|
|
32
|
+
|
|
33
|
+
export function LiveView(): React.JSX.Element {
|
|
34
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
35
|
+
const wsRef = useRef<WebSocket | null>(null)
|
|
36
|
+
const [status, setStatus] = useState<Status>('connecting')
|
|
37
|
+
const [targets, setTargets] = useState<Target[]>([])
|
|
38
|
+
const [hostDown, setHostDown] = useState(false)
|
|
39
|
+
/** 用户显式钉住的 target(null = 自动跟随最新)。 */
|
|
40
|
+
const [pinnedId, setPinnedId] = useState<string | null>(null)
|
|
41
|
+
/** 连接代数:手动重连/换 target 时 +1。 */
|
|
42
|
+
const [epoch, setEpoch] = useState(0)
|
|
43
|
+
|
|
44
|
+
/* target 列表轮询(3s):内容变化才更新 state,避免无谓重连 */
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let alive = true
|
|
47
|
+
const poll = async (): Promise<void> => {
|
|
48
|
+
try {
|
|
49
|
+
const { targets: list } = await api.cdpTargets()
|
|
50
|
+
if (!alive) return
|
|
51
|
+
setHostDown(false)
|
|
52
|
+
setTargets((prev) => (JSON.stringify(prev) === JSON.stringify(list) ? prev : list))
|
|
53
|
+
} catch {
|
|
54
|
+
if (alive) setHostDown(true)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
void poll()
|
|
58
|
+
const timer = setInterval(() => { void poll() }, 3000)
|
|
59
|
+
return () => { alive = false; clearInterval(timer) }
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
/** 实际附加的 target:id 钉住且存活 → 它;否则自动跟随最新页。 */
|
|
63
|
+
const effectiveId = useMemo<string | null>(() => {
|
|
64
|
+
if (pinnedId !== null && targets.some((tg) => tg.id === pinnedId)) return pinnedId
|
|
65
|
+
return targets.length > 0 ? targets[targets.length - 1]!.id : null
|
|
66
|
+
}, [pinnedId, targets])
|
|
67
|
+
|
|
68
|
+
/* 连接与 screencast(按 effectiveId 建立,变化即重连) */
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (effectiveId === null) { setStatus('idle'); return }
|
|
71
|
+
let cancelled = false
|
|
72
|
+
let socket: WebSocket | null = null
|
|
73
|
+
let msgId = 0
|
|
74
|
+
|
|
75
|
+
setStatus('connecting')
|
|
76
|
+
socket = new WebSocket(`${CDP_WS}/devtools/page/${effectiveId}`)
|
|
77
|
+
wsRef.current = socket
|
|
78
|
+
|
|
79
|
+
const send = (method: string, params?: Record<string, unknown>): void => {
|
|
80
|
+
if (socket !== null && socket.readyState === WebSocket.OPEN) {
|
|
81
|
+
socket.send(JSON.stringify({ id: ++msgId, method, params: params ?? {} }))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const draw = (data: string): void => {
|
|
86
|
+
const el = canvasRef.current
|
|
87
|
+
if (el === null || cancelled) return
|
|
88
|
+
const img = new Image()
|
|
89
|
+
img.onload = () => {
|
|
90
|
+
if (cancelled) return
|
|
91
|
+
el.width = img.naturalWidth
|
|
92
|
+
el.height = img.naturalHeight
|
|
93
|
+
el.getContext('2d')?.drawImage(img, 0, 0)
|
|
94
|
+
}
|
|
95
|
+
img.src = `data:image/jpeg;base64,${data}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
socket.onopen = () => {
|
|
99
|
+
if (cancelled) return
|
|
100
|
+
setStatus('live')
|
|
101
|
+
send('Page.enable')
|
|
102
|
+
send('Runtime.enable')
|
|
103
|
+
send('Emulation.setDeviceMetricsOverride', {
|
|
104
|
+
width: VIEW_W, height: VIEW_H, deviceScaleFactor: 1, mobile: false,
|
|
105
|
+
})
|
|
106
|
+
send('Page.startScreencast', {
|
|
107
|
+
format: 'jpeg', quality: 62, maxWidth: VIEW_W, maxHeight: VIEW_H, everyNthFrame: 1,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
socket.onmessage = (event) => {
|
|
111
|
+
const msg = JSON.parse(event.data as string) as {
|
|
112
|
+
id?: number
|
|
113
|
+
method?: string
|
|
114
|
+
params?: { data?: string; screencastFrameId?: string }
|
|
115
|
+
}
|
|
116
|
+
if (msg.method === 'Page.screencastFrame') {
|
|
117
|
+
const p = msg.params ?? {}
|
|
118
|
+
if (typeof p.data === 'string') draw(p.data)
|
|
119
|
+
if (typeof p.screencastFrameId === 'string') {
|
|
120
|
+
send('Page.screencastFrameAck', { screencastFrameId: p.screencastFrameId })
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
socket.onclose = () => { if (!cancelled) setStatus('connecting') }
|
|
125
|
+
socket.onerror = () => { if (!cancelled) setStatus('down') }
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
cancelled = true
|
|
129
|
+
socket?.close()
|
|
130
|
+
wsRef.current = null
|
|
131
|
+
}
|
|
132
|
+
}, [effectiveId])
|
|
133
|
+
|
|
134
|
+
/* 输入回传:坐标按 canvas 显示比例换算到 1280×800 视口 */
|
|
135
|
+
const sendInput = (method: string, params: Record<string, unknown>): void => {
|
|
136
|
+
wsRef.current?.send(JSON.stringify({ id: Math.floor(Math.random() * 1e9), method, params }))
|
|
137
|
+
}
|
|
138
|
+
const scaleOf = (el: HTMLCanvasElement): number => {
|
|
139
|
+
const shown = el.getBoundingClientRect().width
|
|
140
|
+
return shown > 0 ? VIEW_W / shown : 1
|
|
141
|
+
}
|
|
142
|
+
const posOf = (e: { clientX: number; clientY: number; currentTarget: HTMLCanvasElement }): { x: number; y: number } => {
|
|
143
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
144
|
+
const k = scaleOf(e.currentTarget)
|
|
145
|
+
return { x: (e.clientX - rect.left) * k, y: (e.clientY - rect.top) * k }
|
|
146
|
+
}
|
|
147
|
+
const posOfWheel = (e: React.WheelEvent<HTMLCanvasElement>): { x: number; y: number } => {
|
|
148
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
149
|
+
const k = scaleOf(e.currentTarget)
|
|
150
|
+
return { x: (e.clientX - rect.left) * k, y: (e.clientY - rect.top) * k }
|
|
151
|
+
}
|
|
152
|
+
const onPointerDown = (e: React.PointerEvent<HTMLCanvasElement>): void => {
|
|
153
|
+
const p = posOf(e)
|
|
154
|
+
sendInput('Input.dispatchMouseEvent', { type: 'mousePressed', x: p.x, y: p.y, button: 'left', clickCount: 1 })
|
|
155
|
+
}
|
|
156
|
+
const onPointerUp = (e: React.PointerEvent<HTMLCanvasElement>): void => {
|
|
157
|
+
const p = posOf(e)
|
|
158
|
+
sendInput('Input.dispatchMouseEvent', { type: 'mouseReleased', x: p.x, y: p.y, button: 'left', clickCount: 1 })
|
|
159
|
+
}
|
|
160
|
+
const onPointerMove = (e: React.PointerEvent<HTMLCanvasElement>): void => {
|
|
161
|
+
if (e.buttons === 0) return
|
|
162
|
+
const p = posOf(e)
|
|
163
|
+
sendInput('Input.dispatchMouseEvent', { type: 'mouseMoved', x: p.x, y: p.y, button: 'left' })
|
|
164
|
+
}
|
|
165
|
+
const onWheel = (e: React.WheelEvent<HTMLCanvasElement>): void => {
|
|
166
|
+
const p = posOf(e)
|
|
167
|
+
sendInput('Input.dispatchMouseEvent', { type: 'mouseWheel', x: p.x, y: p.y, deltaX: e.deltaX, deltaY: e.deltaY })
|
|
168
|
+
}
|
|
169
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLCanvasElement>): void => {
|
|
170
|
+
const vk: Record<string, number> = {
|
|
171
|
+
Enter: 13, Backspace: 8, Tab: 9, Escape: 27,
|
|
172
|
+
ArrowUp: 38, ArrowDown: 40, ArrowLeft: 37, ArrowRight: 39,
|
|
173
|
+
}
|
|
174
|
+
const code = vk[e.key]
|
|
175
|
+
if (code !== undefined) {
|
|
176
|
+
sendInput('Input.dispatchKeyEvent', { type: 'rawKeyDown', windowsVirtualKeyCode: code, key: e.key })
|
|
177
|
+
sendInput('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: code, key: e.key })
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
if (e.key.length === 1) sendInput('Input.insertText', { text: e.key })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const statusText = hostDown || status === 'down'
|
|
184
|
+
? t('browserLiveDown')
|
|
185
|
+
: status === 'live'
|
|
186
|
+
? (targets.find((tg) => tg.id === effectiveId)?.url ?? t('browserLiveTargetNone'))
|
|
187
|
+
: effectiveId === null
|
|
188
|
+
? t('browserLiveTargetNone')
|
|
189
|
+
: t('browserLiveConnecting')
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div className="browserLive">
|
|
193
|
+
<div className="browserLiveStatus">
|
|
194
|
+
<button type="button" onClick={() => { setEpoch(e => e + 1) }}>{t('refresh')}</button>
|
|
195
|
+
<select
|
|
196
|
+
className="browserLiveTarget"
|
|
197
|
+
value={effectiveId ?? ''}
|
|
198
|
+
onChange={(e) => { setPinnedId(e.target.value === '' ? null : e.target.value) }}
|
|
199
|
+
>
|
|
200
|
+
<option value="">{t('browserLiveFollowLatest')}</option>
|
|
201
|
+
{targets.map((tg) => (
|
|
202
|
+
<option key={tg.id} value={tg.id}>{tg.title || tg.url}</option>
|
|
203
|
+
))}
|
|
204
|
+
</select>
|
|
205
|
+
<span>{statusText}</span>
|
|
206
|
+
</div>
|
|
207
|
+
{effectiveId !== null && status !== 'idle'
|
|
208
|
+
? (
|
|
209
|
+
<canvas
|
|
210
|
+
ref={canvasRef}
|
|
211
|
+
className="browserLiveCanvas"
|
|
212
|
+
width={VIEW_W}
|
|
213
|
+
height={VIEW_H}
|
|
214
|
+
tabIndex={0}
|
|
215
|
+
onPointerDown={onPointerDown}
|
|
216
|
+
onPointerUp={onPointerUp}
|
|
217
|
+
onPointerMove={onPointerMove}
|
|
218
|
+
onWheel={onWheel}
|
|
219
|
+
onKeyDown={onKeyDown}
|
|
220
|
+
/>
|
|
221
|
+
)
|
|
222
|
+
: (
|
|
223
|
+
<div className="browserStart">
|
|
224
|
+
{hostDown ? t('browserLiveDown') : t('browserLiveTargetNone')}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The live sandbox status row of the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The live sandbox status row of the built-in browser tab: a green
|
|
3
|
+
* "sandbox on" state with a one-tap TEMPORARY unlock, or a RED "sandbox
|
|
4
|
+
* off" state (global setting or the temporary unlock) with a restore
|
|
5
|
+
* action.
|
|
6
6
|
*
|
|
7
7
|
* The temporary unlock is component state only — it never writes the
|
|
8
|
-
* global side card setting (`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* global side card setting (`browserNoSandbox`); it lasts until the
|
|
9
|
+
* surface unmounts (tab switch) or the user restores the sandbox from the
|
|
10
|
+
* row. When the global setting already drops the sandbox, no
|
|
11
|
+
* unlock/restore action is offered (changing the global setting is the
|
|
12
|
+
* settings page's job) — the red warning stands.
|
|
13
|
+
*
|
|
14
|
+
* (v1.0.4: the HTML preview surface was retired with the file-viewer line;
|
|
15
|
+
* this row now serves the browser tab only.)
|
|
13
16
|
*/
|
|
14
17
|
import clsx from 'clsx'
|
|
15
18
|
import { t } from './locales.ts'
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* external plugins alike), laid out in a responsive grid that wraps
|
|
13
13
|
* several cards per row — icon chip + title + type id, clicked to toggle
|
|
14
14
|
* the switch persisted in `prefs.tabsEnabled[id]`.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
*
|
|
16
|
+
* (v1.0.4: the 文件预览 viewer inventory was retired with the file-viewer
|
|
17
|
+
* registry — file preview is the host's job now.)
|
|
17
18
|
*
|
|
18
19
|
* Every group lives in a container card (the DSH PluginCard recipe: l2
|
|
19
20
|
* hairline, 16px radius, layer-3 fill) with a heading and an inventory count
|
|
@@ -64,14 +65,13 @@ import {
|
|
|
64
65
|
} from '../prefs-shared.ts'
|
|
65
66
|
import { api } from './api.ts'
|
|
66
67
|
import { parsePrefs } from './prefs.ts'
|
|
67
|
-
import { AddPluginModal
|
|
68
|
+
import { AddPluginModal } from './add-plugin-modal.tsx'
|
|
68
69
|
import { t } from './locales.ts'
|
|
69
70
|
import { parseDesktopEnv } from './desktop-env.ts'
|
|
70
71
|
import { getShellPreset, getShellPresets } from './shell-presets.ts'
|
|
71
72
|
import type { SidebarStore } from './state.ts'
|
|
72
73
|
import type {
|
|
73
74
|
BetterSidebarService,
|
|
74
|
-
FileViewerDescriptor,
|
|
75
75
|
SidebarSettingsRenderProps,
|
|
76
76
|
SidebarSettingToggle,
|
|
77
77
|
TabDescriptor,
|
|
@@ -124,13 +124,8 @@ function titleBarSchemeValue(prefs: SidebarPrefs): string {
|
|
|
124
124
|
return preset !== undefined ? `preset:${preset.id}` : 'auto'
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/** Viewer inventory order: priority desc (the catch-all `code` comes last). */
|
|
128
|
-
function viewerOrder(a: FileViewerDescriptor, b: FileViewerDescriptor): number {
|
|
129
|
-
return (b.priority ?? 0) - (a.priority ?? 0)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
127
|
/** Whether a feature declares any secondary settings (gear button shows). */
|
|
133
|
-
function hasSettings(feature: TabDescriptor
|
|
128
|
+
function hasSettings(feature: TabDescriptor): boolean {
|
|
134
129
|
const settings = feature.settings
|
|
135
130
|
return settings !== undefined && (
|
|
136
131
|
(settings.toggles?.length ?? 0) > 0
|
|
@@ -139,9 +134,9 @@ function hasSettings(feature: TabDescriptor | FileViewerDescriptor): boolean {
|
|
|
139
134
|
)
|
|
140
135
|
}
|
|
141
136
|
|
|
142
|
-
/** A feature's display name
|
|
143
|
-
function featureNameOf(feature: TabDescriptor
|
|
144
|
-
return textOf(
|
|
137
|
+
/** A feature's display name. */
|
|
138
|
+
function featureNameOf(feature: TabDescriptor): string {
|
|
139
|
+
return textOf(feature.title) || feature.id
|
|
145
140
|
}
|
|
146
141
|
|
|
147
142
|
/**
|
|
@@ -491,7 +486,7 @@ function SelectRow(props: {
|
|
|
491
486
|
}
|
|
492
487
|
|
|
493
488
|
/**
|
|
494
|
-
* The secondary settings popup body of one
|
|
489
|
+
* The secondary settings popup body of one registered tab:
|
|
495
490
|
* - the host-prefs `toggles` rows, then the plugin-owned `pluginToggles`
|
|
496
491
|
* rows (their values live in `pluginSettings[feature.id]`, projected onto
|
|
497
492
|
* the prefs face so the shared row renderer reads them);
|
|
@@ -501,7 +496,7 @@ function SelectRow(props: {
|
|
|
501
496
|
* open-behavior picker) and still ship a custom configuration area.
|
|
502
497
|
*/
|
|
503
498
|
export function SettingsBody(props: {
|
|
504
|
-
feature: TabDescriptor
|
|
499
|
+
feature: TabDescriptor
|
|
505
500
|
prefs: SidebarPrefs
|
|
506
501
|
store: SidebarStore
|
|
507
502
|
service: BetterSidebarService
|
|
@@ -576,19 +571,18 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|
|
576
571
|
const [widthDraft, setWidthDraft] = useState<string>(String(store.getPrefs().defaultWidthPercent))
|
|
577
572
|
const [error, setError] = useState<string | null>(null)
|
|
578
573
|
// Which feature's secondary settings popup is open (null = closed).
|
|
579
|
-
const [settingsFor, setSettingsFor] = useState<TabDescriptor |
|
|
574
|
+
const [settingsFor, setSettingsFor] = useState<TabDescriptor | null>(null)
|
|
580
575
|
// Whether the position-compat strip popup (the gear on the 常规 row) is open.
|
|
581
576
|
const [stripSettingsOpen, setStripSettingsOpen] = useState(false)
|
|
582
577
|
// The parsed desktop environment (URL stamps — see desktop-env.ts). Used
|
|
583
578
|
// ONLY to badge matching presets in the scheme dropdown ("已检测");
|
|
584
579
|
// nothing is auto-applied.
|
|
585
580
|
const detectedEnv = useMemo(() => parseDesktopEnv(), [])
|
|
586
|
-
// Whether the "add plugin" modal (
|
|
587
|
-
// 侧边栏内容
|
|
588
|
-
|
|
589
|
-
const [addPluginsOpen, setAddPluginsOpen] = useState<PluginKind | null>(null)
|
|
581
|
+
// Whether the "add plugin" modal (the dashed card at the end of the
|
|
582
|
+
// 侧边栏内容 grid) is open.
|
|
583
|
+
const [addPluginsOpen, setAddPluginsOpen] = useState(false)
|
|
590
584
|
// The LATEST optimistic prefs, kept in sync with the state. Nested-map
|
|
591
|
-
// merges (tabsEnabled /
|
|
585
|
+
// merges (tabsEnabled / pluginSettings) MUST build from
|
|
592
586
|
// this ref, not from the render-time `prefs`: two same-tick writes (e.g.
|
|
593
587
|
// a settings panel updating several plugin keys at once) would otherwise
|
|
594
588
|
// both spread the stale map and the later patch would drop the earlier
|
|
@@ -596,14 +590,12 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|
|
596
590
|
const optimisticRef = useRef(prefs)
|
|
597
591
|
useEffect(() => { optimisticRef.current = prefs }, [prefs])
|
|
598
592
|
|
|
599
|
-
// The declarative inventory: the registered tab types
|
|
600
|
-
//
|
|
601
|
-
//
|
|
593
|
+
// The declarative inventory: the registered tab types. Local state +
|
|
594
|
+
// service.subscribe (registry changes are rare — plugin load/unload — so
|
|
595
|
+
// a plain effect is enough; no external-store ceremony).
|
|
602
596
|
const [tabs, setTabs] = useState<TabDescriptor[]>(() => [...service.getTabs()].sort(tabOrder))
|
|
603
|
-
const [viewers, setViewers] = useState<FileViewerDescriptor[]>(() => [...service.getFileViewers()].sort(viewerOrder))
|
|
604
597
|
useEffect(() => service.subscribe(() => {
|
|
605
598
|
setTabs([...service.getTabs()].sort(tabOrder))
|
|
606
|
-
setViewers([...service.getFileViewers()].sort(viewerOrder))
|
|
607
599
|
}), [service])
|
|
608
600
|
|
|
609
601
|
// The settings document revision (guards concurrent writes). A ref: commits
|
|
@@ -682,11 +674,6 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|
|
682
674
|
applyPref({ tabsEnabled: { ...optimisticRef.current.tabsEnabled, [id]: next } })
|
|
683
675
|
}
|
|
684
676
|
|
|
685
|
-
/** Flip one per-viewer enable switch (merge into the viewersEnabled map). */
|
|
686
|
-
const onToggleViewer = (id: string, next: boolean): void => {
|
|
687
|
-
applyPref({ viewersEnabled: { ...optimisticRef.current.viewersEnabled, [id]: next } })
|
|
688
|
-
}
|
|
689
|
-
|
|
690
677
|
/** Flip one declaratively-declared toggle (a SidebarPrefs boolean field). */
|
|
691
678
|
const onToggleSetting = (toggle: SidebarSettingToggle, next: boolean): void => {
|
|
692
679
|
applyPref({ [toggle.key]: next })
|
|
@@ -1001,7 +988,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|
|
1001
988
|
<button
|
|
1002
989
|
type="button"
|
|
1003
990
|
className={clsx(css.card, css.addCard)}
|
|
1004
|
-
onClick={() => { setAddPluginsOpen(
|
|
991
|
+
onClick={() => { setAddPluginsOpen(true) }}
|
|
1005
992
|
>
|
|
1006
993
|
<span className={css.cardTop}>
|
|
1007
994
|
<span className={css.cardIconChip}>
|
|
@@ -1014,45 +1001,6 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|
|
1014
1001
|
</div>
|
|
1015
1002
|
</div>
|
|
1016
1003
|
|
|
1017
|
-
{/* 文件预览: one small card per registered file viewer. */}
|
|
1018
|
-
<div className={css.group}>
|
|
1019
|
-
<div className={css.groupHeading}>
|
|
1020
|
-
<span>{t('settingsViewersTitle')}</span>
|
|
1021
|
-
<span className={css.count}>{viewers.length}</span>
|
|
1022
|
-
</div>
|
|
1023
|
-
<div className={css.grid}>
|
|
1024
|
-
{viewers.map(viewer => (
|
|
1025
|
-
<Fragment key={viewer.id}>
|
|
1026
|
-
{renderCard({
|
|
1027
|
-
title: textOf(viewer.title) || viewer.id,
|
|
1028
|
-
desc: viewer.exts.length === 0 ? t('settingsViewerCatchAll') : viewer.exts.join(' · '),
|
|
1029
|
-
icon: iconOf(viewer.icon, 16),
|
|
1030
|
-
enabled: prefs.viewersEnabled[viewer.id] !== false,
|
|
1031
|
-
onToggle: (next) => { onToggleViewer(viewer.id, next) },
|
|
1032
|
-
onOpenSettings: prefs.viewersEnabled[viewer.id] !== false && hasSettings(viewer)
|
|
1033
|
-
? () => { setSettingsFor(viewer) }
|
|
1034
|
-
: undefined,
|
|
1035
|
-
})}
|
|
1036
|
-
</Fragment>
|
|
1037
|
-
))}
|
|
1038
|
-
{/* The "add preview plugin" entry: dashed card opening the
|
|
1039
|
-
FILE-PREVIEWER registration modal. */}
|
|
1040
|
-
<button
|
|
1041
|
-
type="button"
|
|
1042
|
-
className={clsx(css.card, css.addCard)}
|
|
1043
|
-
onClick={() => { setAddPluginsOpen('viewer') }}
|
|
1044
|
-
>
|
|
1045
|
-
<span className={css.cardTop}>
|
|
1046
|
-
<span className={css.cardIconChip}>
|
|
1047
|
-
<IconPlusOutline16 size={16} />
|
|
1048
|
-
</span>
|
|
1049
|
-
<span className={css.cardTitle}>{t('addPluginsViewerCard')}</span>
|
|
1050
|
-
</span>
|
|
1051
|
-
<span className={css.cardDesc}>{t('addPluginsViewerCardDesc')}</span>
|
|
1052
|
-
</button>
|
|
1053
|
-
</div>
|
|
1054
|
-
</div>
|
|
1055
|
-
|
|
1056
1004
|
{/* The secondary settings popup: a feature's declared related settings
|
|
1057
1005
|
as title/desc + switch rows in a wider-than-default Modal with a
|
|
1058
1006
|
Done footer (Modal chrome is the app's own). Mounted only while a
|
|
@@ -1138,18 +1086,17 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|
|
1138
1086
|
</Modal>
|
|
1139
1087
|
)}
|
|
1140
1088
|
|
|
1141
|
-
{/* The "add plugin" modal (opened by the dashed
|
|
1142
|
-
the extension point
|
|
1143
|
-
|
|
1144
|
-
install
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
{addPluginsOpen
|
|
1089
|
+
{/* The "add plugin" modal (opened by the dashed card above): declares
|
|
1090
|
+
the tab extension point, opens the GitHub topic, and lists the
|
|
1091
|
+
recommended plugin catalog with per-entry install buttons (the
|
|
1092
|
+
install flow opens a ~/.dsh terminal with the command pre-typed;
|
|
1093
|
+
failures render inline here, in settings only). Mounted only while
|
|
1094
|
+
open (Modal runs hooks unconditionally — same SSR rule as the
|
|
1095
|
+
settings popup above). */}
|
|
1096
|
+
{addPluginsOpen && (
|
|
1149
1097
|
<AddPluginModal
|
|
1150
1098
|
service={service}
|
|
1151
|
-
onClose={() => { setAddPluginsOpen(
|
|
1152
|
-
kind={addPluginsOpen}
|
|
1099
|
+
onClose={() => { setAddPluginsOpen(false) }}
|
|
1153
1100
|
/>
|
|
1154
1101
|
)}
|
|
1155
1102
|
|