dsh-declutter 0.0.0-stage → 0.1.3
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/LICENSE +21 -0
- package/README.en.md +81 -0
- package/README.md +259 -2
- package/cordis.patch.yml +3 -0
- package/icon.svg +8 -0
- package/locale/en.json +5 -0
- package/locale/zh.json +5 -0
- package/package.json +52 -4
- package/src/client.js +1689 -0
- package/src/index.js +2 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,1689 @@
|
|
|
1
|
+
// dsh-sidebar-declutter — client half.
|
|
2
|
+
//
|
|
3
|
+
// Each row is rendered straight into the layout by a shipped package, so none of
|
|
4
|
+
// them is a slot registration and no config switch exists for any of them:
|
|
5
|
+
//
|
|
6
|
+
// * `_newSession` — the sidebar's "New session" button (ui-sidebar). It sits in
|
|
7
|
+
// the normal column flow with no wrapper (its Tooltip clones the button), so
|
|
8
|
+
// `display:none` takes it — and its margin — out of layout and the panel list
|
|
9
|
+
// below moves up into the freed row.
|
|
10
|
+
// * `_moreAnchor` in `_panelList` — the sidebar panel list's "More" launcher. It
|
|
11
|
+
// renders only while `sidebar.panellist.actions` has registrations (today one,
|
|
12
|
+
// from entry-customization), and that menu is the only place a panel can be
|
|
13
|
+
// pinned or unpinned, so hiding it retires that control too.
|
|
14
|
+
// * `_moreAnchor` in `[data-sidebar-right-guide]` — the right column's start-page
|
|
15
|
+
// "More". Same local class name as the sidebar's, in a different CSS module
|
|
16
|
+
// (`F6Huca_moreAnchor` vs `kB3qhq_moreAnchor`), which is why every rule here is
|
|
17
|
+
// scoped by an ancestor instead of matching the bare suffix.
|
|
18
|
+
// * `_viewsMore` — the Session header View row's launcher; an ellipsis icon, so
|
|
19
|
+
// its aria-label says "More" while the page shows no such text.
|
|
20
|
+
//
|
|
21
|
+
// Selectors key on CSS-module local suffixes, never on the build-hashed prefix
|
|
22
|
+
// (`kB3qhq_newSession` today): the prefix changes on every build.
|
|
23
|
+
//
|
|
24
|
+
// Settings live in localStorage rather than the loader Config: the entry needs no
|
|
25
|
+
// Host schema, changes apply without a restart, and every surface reads one store.
|
|
26
|
+
window.__ModuleLoader__.load({
|
|
27
|
+
id: 'dsh-sidebar-declutter',
|
|
28
|
+
factory: (require) => {
|
|
29
|
+
const React = require('react')
|
|
30
|
+
const h = React.createElement
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bisect switch: the settings card is the only thing this bundle contributes
|
|
34
|
+
* to the plugins page, so it can be taken out while that page is investigated.
|
|
35
|
+
*/
|
|
36
|
+
const ENABLE_ROW_CONFIG = true
|
|
37
|
+
|
|
38
|
+
/** Row-config key: `<bundle package>#<row id from the bundle patch>`. */
|
|
39
|
+
/** Sidebar entry that opens this plugin's settings page. */
|
|
40
|
+
const PANEL_ENTRY_LABEL = '隐藏与整理'
|
|
41
|
+
/** Panel id: the URL segment (#/panel/<id>) and the main slot key. */
|
|
42
|
+
const PANEL_ENTRY_ID = 'hide-rows'
|
|
43
|
+
/** The sidebar draws the row; this slot contributes the icon in front of it. */
|
|
44
|
+
function PanelEntry() {
|
|
45
|
+
return h(
|
|
46
|
+
'svg',
|
|
47
|
+
{
|
|
48
|
+
width: 16,
|
|
49
|
+
height: 16,
|
|
50
|
+
viewBox: '0 0 16 16',
|
|
51
|
+
fill: 'none',
|
|
52
|
+
'aria-hidden': 'true',
|
|
53
|
+
},
|
|
54
|
+
h('path', {
|
|
55
|
+
d: 'M1.8 8c1.6-2.5 3.8-3.9 6.2-3.9S12.6 5.5 14.2 8c-1.6 2.5-3.8 3.9-6.2 3.9S3.4 10.5 1.8 8Z',
|
|
56
|
+
stroke: 'currentColor',
|
|
57
|
+
strokeWidth: 1.3,
|
|
58
|
+
strokeLinejoin: 'round',
|
|
59
|
+
}),
|
|
60
|
+
h('circle', { cx: 8, cy: 8, r: 1.9, stroke: 'currentColor', strokeWidth: 1.3 }),
|
|
61
|
+
h('path', {
|
|
62
|
+
d: 'M3 13 13 3',
|
|
63
|
+
stroke: 'currentColor',
|
|
64
|
+
strokeWidth: 1.3,
|
|
65
|
+
strokeLinecap: 'round',
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ROW_CONFIG_KEY = 'dsh-declutter#hide-rows'
|
|
71
|
+
const STORE_KEY = 'dsh-sidebar-declutter'
|
|
72
|
+
/** Key this bundle stored its settings under before the rename. */
|
|
73
|
+
const LEGACY_STORE_KEY = 'dsh-hide-new-session'
|
|
74
|
+
const STYLE_ATTR = 'data-dsh-sidebar-declutter'
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The page groups its switches under headings; the flat `FIELDS` below is
|
|
78
|
+
* derived from them, so the store, the summary and the rules keep iterating
|
|
79
|
+
* one list while the page renders sections.
|
|
80
|
+
*/
|
|
81
|
+
const SECTIONS = [
|
|
82
|
+
{
|
|
83
|
+
kind: 'hide',
|
|
84
|
+
title: '左栏',
|
|
85
|
+
panels: true,
|
|
86
|
+
columns: 4,
|
|
87
|
+
fields: [
|
|
88
|
+
{ key: 'brand', title: '顶部 logo' },
|
|
89
|
+
{ key: 'newSession', title: '「新会话」按钮' },
|
|
90
|
+
{ key: 'sidebarMore', title: '「更多」按钮' },
|
|
91
|
+
{ key: 'topGap', title: '顶部空隙' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
kind: 'hide',
|
|
96
|
+
title: '其它位置',
|
|
97
|
+
columns: 2,
|
|
98
|
+
fields: [
|
|
99
|
+
{ key: 'rightMore', title: '右侧栏起始页的「更多」' },
|
|
100
|
+
{ key: 'headerMore', title: '会话头部 View 行的「更多」' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
kind: 'layout',
|
|
105
|
+
title: '排布',
|
|
106
|
+
fields: [
|
|
107
|
+
{
|
|
108
|
+
key: 'panelLayout',
|
|
109
|
+
type: 'choice',
|
|
110
|
+
inline: true,
|
|
111
|
+
options: [
|
|
112
|
+
{ value: 'stack', title: '一列' },
|
|
113
|
+
{ value: 'two', title: '两列' },
|
|
114
|
+
{ value: 'three', title: '三列' },
|
|
115
|
+
{ value: 'wrap', title: '自动紧凑' },
|
|
116
|
+
{ value: 'icon', title: '图标显示' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
kind: 'menu',
|
|
123
|
+
title: '会话菜单',
|
|
124
|
+
hint: '打开一次会话菜单(右键会话行)后,这里会列出这份构建真正渲染过的条目;勾上即隐藏,取消即恢复。',
|
|
125
|
+
fields: [],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
kind: 'picked',
|
|
129
|
+
title: '自由隐藏',
|
|
130
|
+
hint: '点「开始点选」,再点界面上任意一个元素——它会作为一个条目出现在下面,勾上才隐藏。',
|
|
131
|
+
fields: [],
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
/** Flat view of every switch, in page order, tagged with its section. */
|
|
136
|
+
const FIELDS = SECTIONS.flatMap((section) =>
|
|
137
|
+
section.fields.map((field) => ({ ...field, kind: section.kind })),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
/** Defaults match the state this plugin shipped with before the switches. */
|
|
141
|
+
const DEFAULTS = {
|
|
142
|
+
brand: true,
|
|
143
|
+
newSession: true,
|
|
144
|
+
panelLayout: 'two',
|
|
145
|
+
sidebarMore: true,
|
|
146
|
+
rightMore: true,
|
|
147
|
+
headerMore: false,
|
|
148
|
+
/** The bulk layer the footer button drives; never persisted. */
|
|
149
|
+
collapsedAll: false,
|
|
150
|
+
/** Elements picked by hand: `[{ selector, label }]`. */
|
|
151
|
+
picked: [],
|
|
152
|
+
/** Panel ids (from `sidebar.panellist`) the user ticked off. */
|
|
153
|
+
hiddenPanels: [],
|
|
154
|
+
/** Panel ids in the order the user dragged them into. */
|
|
155
|
+
panelOrder: [],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Accepted values of the layout choice, keyed by field. */
|
|
159
|
+
const CHOICES = {}
|
|
160
|
+
for (const field of FIELDS) {
|
|
161
|
+
if (field.type === 'choice') CHOICES[field.key] = field.options.map((option) => option.value)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let values = { ...DEFAULTS }
|
|
165
|
+
const listeners = new Set()
|
|
166
|
+
|
|
167
|
+
function readStore() {
|
|
168
|
+
const next = { ...DEFAULTS }
|
|
169
|
+
try {
|
|
170
|
+
const raw = localStorage.getItem(STORE_KEY) ?? localStorage.getItem(LEGACY_STORE_KEY)
|
|
171
|
+
if (raw === null) return next
|
|
172
|
+
const parsed = JSON.parse(raw)
|
|
173
|
+
for (const field of FIELDS) {
|
|
174
|
+
const stored = parsed?.[field.key]
|
|
175
|
+
if (field.type === 'choice') {
|
|
176
|
+
if (CHOICES[field.key].includes(stored)) next[field.key] = stored
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if (typeof stored === 'boolean') next[field.key] = stored
|
|
180
|
+
}
|
|
181
|
+
// `panelGrid` was the boolean this setting shipped as before it became a
|
|
182
|
+
// three-way choice; keep an existing preference instead of resetting it.
|
|
183
|
+
if (Array.isArray(parsed?.picked)) {
|
|
184
|
+
next.picked = parsed.picked.filter(
|
|
185
|
+
(one) => one && typeof one.selector === 'string' && one.selector.trim() !== '',
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(parsed?.hiddenPanels)) {
|
|
189
|
+
next.hiddenPanels = parsed.hiddenPanels.filter((one) => typeof one === 'string')
|
|
190
|
+
}
|
|
191
|
+
if (Array.isArray(parsed?.panelOrder)) {
|
|
192
|
+
next.panelOrder = parsed.panelOrder.filter((one) => typeof one === 'string')
|
|
193
|
+
}
|
|
194
|
+
if (parsed?.panelLayout === undefined && typeof parsed?.panelGrid === 'boolean') {
|
|
195
|
+
// `panelGrid` was introduced meaning "two per row" before it briefly
|
|
196
|
+
// meant "wrap"; `true` keeps the two-column reading.
|
|
197
|
+
next.panelLayout = parsed.panelGrid ? 'two' : 'stack'
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Stored junk falls back to the defaults instead of breaking boot.
|
|
201
|
+
}
|
|
202
|
+
return next
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function writeStore() {
|
|
206
|
+
try {
|
|
207
|
+
localStorage.setItem(STORE_KEY, JSON.stringify(values))
|
|
208
|
+
} catch {
|
|
209
|
+
// Private mode: the switch still applies for this page.
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- menu rows: free hiding -------------------------------------------------
|
|
214
|
+
//
|
|
215
|
+
// Menu rows are not slots a plugin can drop; the shipped ones are rendered by
|
|
216
|
+
// the product and only `display: none` takes them away. Which rows exist is
|
|
217
|
+
// discovered from real menu opens (label + accelerator), so the page lists
|
|
218
|
+
// what this build really renders. Nothing is hidden until it is ticked.
|
|
219
|
+
|
|
220
|
+
const MENU_STORE_KEY = 'dsh-sidebar-declutter.menu'
|
|
221
|
+
const MENU_STORE_VERSION = 2
|
|
222
|
+
const MENU_HIDDEN_ATTR = 'data-dsd-menu-hidden'
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The Session menu's shipped rows, listed up front so nothing has to be
|
|
226
|
+
* opened once before it can be ticked. Each rule matches the row's text at
|
|
227
|
+
* its start, so a trailing accelerator or a language switch still hits.
|
|
228
|
+
* Rows this build renders that are not listed here are appended by discovery.
|
|
229
|
+
*/
|
|
230
|
+
const MENU_PRESETS = [
|
|
231
|
+
{ key: '重命名', accel: 'Ctrl+Alt+G', rule: /^(重命名|Rename)/, aria: ['Control+Alt+G', 'Ctrl+Alt+G', '⌘+⌥+G'] },
|
|
232
|
+
{ key: '归档会话', accel: '', rule: /^(归档会话|Archive)/, aria: ['Control+Shift+A', 'Ctrl+Shift+A', '⌘+⇧+A'] },
|
|
233
|
+
{ key: '取消归档', accel: '', rule: /^(取消归档|Unarchive)/, aria: [] },
|
|
234
|
+
{ key: '置顶会话', accel: '', rule: /^(置顶会话|Pin)/, aria: [] },
|
|
235
|
+
{ key: '取消置顶', accel: '', rule: /^(取消置顶|Unpin)/, aria: [] },
|
|
236
|
+
// Also shipped by the product: menu.fork / menu.sessionId / menu.openIn
|
|
237
|
+
// in the shell's own locale. They used to fall through to discovery and
|
|
238
|
+
// were mislabelled as community rows.
|
|
239
|
+
{ key: '分叉会话', accel: 'Alt+Meta+F', rule: /^(分叉会话|Fork)/, aria: ['Alt+Meta+F'] },
|
|
240
|
+
{ key: '复制 Session ID', accel: '', rule: /^(复制 Session ID|Copy Session ID)/, aria: [] },
|
|
241
|
+
{ key: '在应用中打开', accel: '', rule: /^(在应用中打开|Open in App)/, aria: [] },
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
/** Fold a stored/discovered label onto its official key when it matches. */
|
|
245
|
+
function canonicalMenuKey(key) {
|
|
246
|
+
const preset = MENU_PRESETS.find((one) => one.rule.test(key))
|
|
247
|
+
return preset === undefined ? key : preset.key
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Rows that carry an accelerator can be hidden by CSS, before the menu is ever
|
|
252
|
+
* painted — no timing to lose. Rows without one still go through the DOM sweep.
|
|
253
|
+
*/
|
|
254
|
+
function menuCssRules() {
|
|
255
|
+
const rules = []
|
|
256
|
+
for (const preset of MENU_PRESETS) {
|
|
257
|
+
if (!menuState.hidden.includes(preset.key)) continue
|
|
258
|
+
for (const aria of preset.aria) {
|
|
259
|
+
rules.push(`[role="menuitem"][aria-keyshortcuts="${aria}"]{display:none!important}`)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return rules
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** @type {{ hidden: string[], seen: { key: string, accel: string }[] }} */
|
|
266
|
+
let menuState = { hidden: [], seen: [] }
|
|
267
|
+
|
|
268
|
+
function readMenuState() {
|
|
269
|
+
try {
|
|
270
|
+
const parsed = JSON.parse(localStorage.getItem(MENU_STORE_KEY) || 'null')
|
|
271
|
+
if (!parsed) return
|
|
272
|
+
const hidden = Array.isArray(parsed.hidden)
|
|
273
|
+
? [...new Set(parsed.hidden.filter((one) => typeof one === 'string').map(canonicalMenuKey))]
|
|
274
|
+
: []
|
|
275
|
+
// v1 recorded rows from every menu on the desktop — including the system
|
|
276
|
+
// "open with" list — so its `seen` is thrown away, keeping the switches.
|
|
277
|
+
const seen = parsed.version === MENU_STORE_VERSION && Array.isArray(parsed.seen)
|
|
278
|
+
? parsed.seen
|
|
279
|
+
.filter((one) => one && typeof one.key === 'string')
|
|
280
|
+
.reduce((list, one) => {
|
|
281
|
+
const key = canonicalMenuKey(one.key)
|
|
282
|
+
return list.some((entry) => entry.key === key) ? list : [...list, { ...one, key }]
|
|
283
|
+
}, [])
|
|
284
|
+
: []
|
|
285
|
+
menuState = { hidden, seen }
|
|
286
|
+
if (parsed.version !== MENU_STORE_VERSION) writeMenuState()
|
|
287
|
+
} catch {
|
|
288
|
+
// Private mode: the switches still apply for this page.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function writeMenuState() {
|
|
293
|
+
try {
|
|
294
|
+
localStorage.setItem(
|
|
295
|
+
MENU_STORE_KEY,
|
|
296
|
+
JSON.stringify({ version: MENU_STORE_VERSION, hidden: menuState.hidden, seen: menuState.seen }),
|
|
297
|
+
)
|
|
298
|
+
} catch {
|
|
299
|
+
// Same.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** True when a stored row id is really one of the presets (a legacy record). */
|
|
304
|
+
function isPresetMenuKey(key) {
|
|
305
|
+
return MENU_PRESETS.some((one) => one.rule.test(key))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** A row's identity: a preset name when one matches, else its normalized text. */
|
|
309
|
+
function menuRowKey(item) {
|
|
310
|
+
const text = (item.textContent || '').replace(/\s+/g, ' ').trim()
|
|
311
|
+
for (const preset of MENU_PRESETS) {
|
|
312
|
+
if (preset.rule.test(text)) return preset.key
|
|
313
|
+
}
|
|
314
|
+
return text
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** True when this row is one of the shipped Session actions. */
|
|
318
|
+
function isSessionMenuItem(item) {
|
|
319
|
+
const text = (item.textContent || '').replace(/\s+/g, ' ').trim()
|
|
320
|
+
return MENU_PRESETS.some((one) => one.rule.test(text))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function menuSweep() {
|
|
324
|
+
// `[role="menuitem"]` is used by every menu on the desktop — the system
|
|
325
|
+
// "open with" list included. A menu only counts when it carries a shipped
|
|
326
|
+
// Session action, so unrelated lists are never read and never recorded.
|
|
327
|
+
const groups = new Map()
|
|
328
|
+
for (const item of document.querySelectorAll('[role="menuitem"]')) {
|
|
329
|
+
const root = item.closest('[role="menu"]') || item.parentElement
|
|
330
|
+
if (root === null) continue
|
|
331
|
+
const list = groups.get(root)
|
|
332
|
+
if (list === undefined) groups.set(root, [item])
|
|
333
|
+
else list.push(item)
|
|
334
|
+
}
|
|
335
|
+
let added = false
|
|
336
|
+
for (const items of groups.values()) {
|
|
337
|
+
if (!items.some((item) => isSessionMenuItem(item))) continue
|
|
338
|
+
for (const item of items) {
|
|
339
|
+
// A marked node was already handled in this batch. The next menu open
|
|
340
|
+
// mounts a fresh set, so "unhide after unticking" needs no bookkeeping.
|
|
341
|
+
if (item.hasAttribute(MENU_HIDDEN_ATTR)) continue
|
|
342
|
+
const key = menuRowKey(item)
|
|
343
|
+
if (!key) continue
|
|
344
|
+
// Only a menu opened from a Session row is recorded: that is what keeps
|
|
345
|
+
// unrelated lists (the system "open with" menu) out of the settings page.
|
|
346
|
+
if (menuFromSessionRow && !menuState.seen.some((one) => one.key === key)) {
|
|
347
|
+
menuState.seen.push({ key, accel: item.getAttribute('aria-keyshortcuts') || '' })
|
|
348
|
+
added = true
|
|
349
|
+
}
|
|
350
|
+
if (!menuState.hidden.includes(key)) continue
|
|
351
|
+
item.setAttribute(MENU_HIDDEN_ATTR, '')
|
|
352
|
+
item.style.display = 'none'
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (added) {
|
|
356
|
+
writeMenuState()
|
|
357
|
+
notify()
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let menuObserver = null
|
|
362
|
+
let menuObserverTimer = 0
|
|
363
|
+
let menuSweepQueued = false
|
|
364
|
+
|
|
365
|
+
/** Coalesce a burst of mutations into one sweep, before the next paint. */
|
|
366
|
+
function menuQueueSweep(records) {
|
|
367
|
+
// The 400ms watch overlaps the chat stream. Only a mutation that actually
|
|
368
|
+
// mounted a menu node deserves a document-wide sweep; per-token text and
|
|
369
|
+
// message-row updates do not. The timed sweeps below still cover menus
|
|
370
|
+
// that mount without adding a role-bearing node.
|
|
371
|
+
if (records && records.length) {
|
|
372
|
+
let menuish = false
|
|
373
|
+
for (const rec of records) {
|
|
374
|
+
for (const node of rec.addedNodes || []) {
|
|
375
|
+
if (node.nodeType !== 1) continue
|
|
376
|
+
if (node.matches?.('[role="menu"],[role="menuitem"]') ||
|
|
377
|
+
node.querySelector?.('[role="menu"],[role="menuitem"]')) {
|
|
378
|
+
menuish = true
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (menuish) break
|
|
383
|
+
}
|
|
384
|
+
if (!menuish) return
|
|
385
|
+
}
|
|
386
|
+
if (menuSweepQueued) return
|
|
387
|
+
menuSweepQueued = true
|
|
388
|
+
const run = () => {
|
|
389
|
+
menuSweepQueued = false
|
|
390
|
+
menuSweep()
|
|
391
|
+
}
|
|
392
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(run)
|
|
393
|
+
else setTimeout(run, 0)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Watch for the menu's first insertion instead of waiting for it. The observer
|
|
398
|
+
* runs only inside a short window after a click, and the sweep itself is queued
|
|
399
|
+
* on the next frame — which lands before that frame paints, so the rows never
|
|
400
|
+
* appear unhidden first. A standing body-wide observer is what would be
|
|
401
|
+
* expensive; a 400ms window is not.
|
|
402
|
+
*/
|
|
403
|
+
function menuWatchForMount() {
|
|
404
|
+
if (typeof MutationObserver !== 'function') return
|
|
405
|
+
if (menuObserver === null) {
|
|
406
|
+
menuObserver = new MutationObserver(menuQueueSweep)
|
|
407
|
+
menuObserver.observe(document.documentElement, { childList: true, subtree: true })
|
|
408
|
+
}
|
|
409
|
+
clearTimeout(menuObserverTimer)
|
|
410
|
+
menuObserverTimer = setTimeout(() => {
|
|
411
|
+
menuObserver.disconnect()
|
|
412
|
+
menuObserver = null
|
|
413
|
+
}, 400)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Set while the click that opens a menu came from a Session row. */
|
|
417
|
+
let menuFromSessionRow = false
|
|
418
|
+
|
|
419
|
+
/** Menus mount a frame or two after the click that opens them. */
|
|
420
|
+
function menuScheduleSweep(fromSessionRow) {
|
|
421
|
+
// A click that cannot open a session menu and has no hidden rows to
|
|
422
|
+
// enforce makes menuSweep() a no-op: added stays false and every item
|
|
423
|
+
// hits the hidden guard. Skip the four document-wide scans, the 400ms
|
|
424
|
+
// watch and all timers -- this is the hot path on every ordinary click.
|
|
425
|
+
if (!fromSessionRow && !menuFromSessionRow && menuState.hidden.length === 0) return
|
|
426
|
+
if (fromSessionRow) menuFromSessionRow = true
|
|
427
|
+
menuSweep()
|
|
428
|
+
menuWatchForMount()
|
|
429
|
+
for (const delay of [40, 120, 260]) setTimeout(menuSweep, delay)
|
|
430
|
+
setTimeout(() => {
|
|
431
|
+
menuFromSessionRow = false
|
|
432
|
+
}, 600)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Drop the watch when the plugin goes away. */
|
|
436
|
+
function menuStopWatch() {
|
|
437
|
+
clearTimeout(menuObserverTimer)
|
|
438
|
+
if (menuObserver !== null) {
|
|
439
|
+
menuObserver.disconnect()
|
|
440
|
+
menuObserver = null
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function setMenuRowHidden(key, hide) {
|
|
445
|
+
const next = menuState.hidden.filter((one) => one !== key)
|
|
446
|
+
if (hide) next.push(key)
|
|
447
|
+
menuState = { ...menuState, hidden: next }
|
|
448
|
+
writeMenuState()
|
|
449
|
+
syncStyle()
|
|
450
|
+
notify()
|
|
451
|
+
menuSweep()
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function setValue(key, value) {
|
|
455
|
+
if (values[key] === value) return
|
|
456
|
+
values = { ...values, [key]: value }
|
|
457
|
+
writeStore()
|
|
458
|
+
syncStyle()
|
|
459
|
+
for (const listener of [...listeners]) {
|
|
460
|
+
try {
|
|
461
|
+
listener()
|
|
462
|
+
} catch {
|
|
463
|
+
// One broken subscriber must not stop the rest.
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function subscribe(listener) {
|
|
469
|
+
listeners.add(listener)
|
|
470
|
+
return () => {
|
|
471
|
+
listeners.delete(listener)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function hiddenRules() {
|
|
476
|
+
const rules = []
|
|
477
|
+
/**
|
|
478
|
+
* Platform split. The desktop shell marks <html data-platform>: darwin on
|
|
479
|
+
* macOS, win32 on Windows; a plain browser carries no marker at all (see
|
|
480
|
+
* the shell's own macDesktopWindow / dsh-power-menu-mac). Linux desktop is
|
|
481
|
+
* neither darwin nor a browser and stays on the generic branch. macOS is
|
|
482
|
+
* the only build with native traffic lights floating over the sidebar and
|
|
483
|
+
* no in-page title bar strip.
|
|
484
|
+
*/
|
|
485
|
+
const isMacDesktop = () =>
|
|
486
|
+
typeof document !== 'undefined' &&
|
|
487
|
+
document.documentElement?.getAttribute('data-platform') === 'darwin'
|
|
488
|
+
if (values.brand) {
|
|
489
|
+
if (isMacDesktop()) {
|
|
490
|
+
// macOS has native traffic lights and no in-page title bar: the
|
|
491
|
+
// _topStrip is the only thing keeping sidebar content below them.
|
|
492
|
+
// Collapse the _logoRow (that is the space we reclaim) but leave the
|
|
493
|
+
// strip alone -- dropping both, as the Windows/browser branch does,
|
|
494
|
+
// is what pulled "New session" into the traffic lights. No global
|
|
495
|
+
// padding fallback here: [class*="_root"] is reused by many shell
|
|
496
|
+
// components and padding every one of them tears the column apart.
|
|
497
|
+
rules.push(
|
|
498
|
+
'[class*="_logoRow"] [class*="_brand"]{display:none!important}',
|
|
499
|
+
'[class*="_topStrip"] [class*="_brand"]{display:none!important}',
|
|
500
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_logoRow"]:not(:has([data-sidebar-toggle]))'
|
|
501
|
+
+ '{display:none!important}',
|
|
502
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_logoRow"]:has([data-sidebar-toggle])'
|
|
503
|
+
+ '{height:0!important;min-height:0!important;padding:0!important;overflow:visible!important}',
|
|
504
|
+
)
|
|
505
|
+
} else {
|
|
506
|
+
// The logo row is a flex child of the column, so dropping it closes the
|
|
507
|
+
// gap. It holds the collapse toggle on Windows and Linux, though, and that
|
|
508
|
+
// toggle must survive: hide the whole row only where it has no toggle, and
|
|
509
|
+
// otherwise hide just the brand inside it. `data-sidebar-toggle` is the
|
|
510
|
+
// sidebar's own attribute, stabler than any CSS-module class name.
|
|
511
|
+
rules.push(
|
|
512
|
+
'[class*="_logoRow"]:not(:has([data-sidebar-toggle])){display:none!important}',
|
|
513
|
+
'[class*="_logoRow"]:has([data-sidebar-toggle]) [class*="_brand"]{display:none!important}',
|
|
514
|
+
// Where the row does hold the toggle, drop its height instead of the
|
|
515
|
+
// row: zero height, no padding, and the button keeps the same place,
|
|
516
|
+
// drawn on top of whatever now starts where the row used to be. Pure
|
|
517
|
+
// CSS — no probe, no timer, no forced layout.
|
|
518
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_logoRow"]:has([data-sidebar-toggle])'
|
|
519
|
+
+ '{height:0!important;min-height:0!important;padding:0!important;overflow:visible!important'
|
|
520
|
+
+ ';align-items:flex-start!important;justify-content:flex-end!important}',
|
|
521
|
+
// Never macOS: its native traffic lights sit over the sidebar, so
|
|
522
|
+
// the toggle must stay in the shell's own layout. Shells that draw
|
|
523
|
+
// a Windows title bar are excluded by the first guard; everything
|
|
524
|
+
// else keeps the gutter behaviour.
|
|
525
|
+
'html:not([data-windows-titlebar]):not([data-platform="darwin"]) [class*="_root"]:not([class*="_collapsed"])'
|
|
526
|
+
+ ' [class*="_logoRow"]:has([data-sidebar-toggle]) [class*="_toggle"]'
|
|
527
|
+
+ '{position:fixed;top:6px;left:calc(var(--dsh-sidebar-width,280px) + 8px);z-index:30;border-radius:8px}',
|
|
528
|
+
// The collapsed rail keeps a normal row, or there is no way back out.
|
|
529
|
+
'[class*="_root"][class*="_collapsed"] [class*="_logoRow"]'
|
|
530
|
+
+ '{height:28px!important;min-height:0!important;padding-block:0!important;align-items:center!important}',
|
|
531
|
+
// Newer builds put the collapse toggle in a 52px `topStrip` above the
|
|
532
|
+
// logo row, so hiding the row alone leaves that strip showing as a band
|
|
533
|
+
// of empty space. Drop it too: on Windows the toggle is fixed inside the
|
|
534
|
+
// title bar anyway, and the browser build re-parks it in the gutter.
|
|
535
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_topStrip"]'
|
|
536
|
+
+ '{height:0!important;min-height:0!important;margin:0!important;padding:0!important'
|
|
537
|
+
+ ';overflow:visible!important;border:0!important}',
|
|
538
|
+
'html:not([data-windows-titlebar]):not([data-platform="darwin"]) [class*="_root"]:not([class*="_collapsed"])'
|
|
539
|
+
+ ' [class*="_topStrip"] [class*="_toggle"]'
|
|
540
|
+
+ '{position:fixed;top:6px;left:calc(var(--dsh-sidebar-width,280px) + 8px);z-index:30;border-radius:8px}',
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// 顶部空隙: ONE checkbox on all three platforms, different behaviour.
|
|
545
|
+
// Windows / web keep the original three rules verbatim. On macOS the
|
|
546
|
+
// topStrip is 52px tall with a 28px toggle centred in it, so its lower
|
|
547
|
+
// part is empty: shrink it to 38px (reclaim 14px) and keep the toggle
|
|
548
|
+
// 11px from the top -- the same offset centring gave it -- so the button
|
|
549
|
+
// stays at exactly the same screen position while everything below moves
|
|
550
|
+
// up 14px. Zeroing _root padding is never done on macOS: that would
|
|
551
|
+
// shove the toggle into the native traffic lights.
|
|
552
|
+
if (values.topGap) {
|
|
553
|
+
if (isMacDesktop()) {
|
|
554
|
+
rules.push(
|
|
555
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_topStrip"]'
|
|
556
|
+
+ '{height:38px!important;min-height:38px!important;align-items:flex-start!important'
|
|
557
|
+
+ ';padding-top:11px!important;padding-bottom:0!important;box-sizing:border-box!important}',
|
|
558
|
+
)
|
|
559
|
+
} else {
|
|
560
|
+
rules.push(
|
|
561
|
+
'[class*="_root"]{padding-top:0!important}',
|
|
562
|
+
'[class*="_scrollRegion"]{margin-top:0!important}',
|
|
563
|
+
'[class*="_scrollArea"]{grid-template-rows:0 max-content 2px max-content!important}',
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (values.newSession) {
|
|
568
|
+
rules.push('button[class*="_newSession"]{display:none!important}')
|
|
569
|
+
}
|
|
570
|
+
if (values.panelLayout === 'two') {
|
|
571
|
+
// Fixed two-column grid: columns share one width, so rows line up
|
|
572
|
+
// exactly, and a longer label is truncated by the shipped ellipsis.
|
|
573
|
+
// The `:not([class*="_collapsed"])` guard is load-bearing here too — the
|
|
574
|
+
// shipped stylesheet hides the whole panel list in the collapsed rail
|
|
575
|
+
// (`.collapsed .panelList{display:none}`), and an unscoped `!important`
|
|
576
|
+
// grid would resurrect it there.
|
|
577
|
+
rules.push(
|
|
578
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
|
|
579
|
+
+ '{display:grid!important;grid-template-columns:1fr 1fr;gap:2px}',
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
if (values.panelLayout === 'three') {
|
|
583
|
+
// Three fixed columns: same idea as the two-column grid, one more lane.
|
|
584
|
+
// Long labels keep the shipped ellipsis, and rows still line up.
|
|
585
|
+
rules.push(
|
|
586
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
|
|
587
|
+
+ '{display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));gap:2px}',
|
|
588
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelEntry"]'
|
|
589
|
+
+ '{width:auto!important;min-width:0!important;max-width:100%}',
|
|
590
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelRow"]'
|
|
591
|
+
+ '{padding-left:6px!important;padding-right:6px!important}',
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
if (values.panelLayout === 'icon') {
|
|
595
|
+
// Icons only: the label is dropped, rows shrink to their glyph, and the
|
|
596
|
+
// list packs them side by side. The unpin control is hidden too, or it
|
|
597
|
+
// would show up as a second glyph per entry.
|
|
598
|
+
rules.push(
|
|
599
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
|
|
600
|
+
+ '{flex-direction:row!important;flex-wrap:wrap!important;gap:4px!important}',
|
|
601
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelEntry"]'
|
|
602
|
+
+ '{flex:0 0 auto!important;width:auto!important;min-width:0!important;max-width:100%}',
|
|
603
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelTitle"]{display:none!important}',
|
|
604
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelRow"]'
|
|
605
|
+
+ '{width:auto!important;min-width:0!important;padding-left:8px!important;'
|
|
606
|
+
+ 'padding-right:8px!important;justify-content:center!important;gap:0!important}',
|
|
607
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelUnpin"]{display:none!important}',
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
if (values.panelLayout === 'wrap') {
|
|
611
|
+
// Content-sized chips that wrap greedily, not fixed columns: two short
|
|
612
|
+
// names (插件 / 鱼塘) pack three or four to a line while a long one
|
|
613
|
+
// (自动化任务) takes what it needs, and nothing is ever truncated. Fixed
|
|
614
|
+
// `grid-template-columns` cannot do this — every column shares one width.
|
|
615
|
+
rules.push(
|
|
616
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
|
|
617
|
+
+ '{flex-direction:row!important;flex-wrap:wrap!important;'
|
|
618
|
+
+ 'column-gap:6px!important;row-gap:2px!important}',
|
|
619
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
|
|
620
|
+
+ ' > [class*="_panelRow"]{flex:0 0 auto!important;width:auto!important;'
|
|
621
|
+
+ 'max-width:100%}',
|
|
622
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
|
|
623
|
+
+ ' > button:nth-of-type(n+3){order:1}',
|
|
624
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
|
|
625
|
+
+ ' > [class*="_moreAnchor"]{order:0}',
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
if (values.panelOrder.length > 0) {
|
|
629
|
+
// The user's arrangement becomes the real order, and the More launcher
|
|
630
|
+
// keeps trailing it. `!important` beats the wrap-mode rule above.
|
|
631
|
+
const ordered = orderedPanelEntries()
|
|
632
|
+
ordered.forEach((entry, index) => {
|
|
633
|
+
rules.push(`[class*="_panelEntry"]:has([${PANEL_ATTR}="${entry.id}"]){order:${index + 1}!important}`)
|
|
634
|
+
})
|
|
635
|
+
rules.push(
|
|
636
|
+
'[class*="_root"]:not([class*="_collapsed"]) [class*="_panelList"]'
|
|
637
|
+
+ ` > [class*="_moreAnchor"]{order:${ordered.length + 1}!important}`,
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
if (values.sidebarMore) {
|
|
641
|
+
rules.push(
|
|
642
|
+
'[class*="_panelList"] [class*="_moreAnchor"]{display:none!important}',
|
|
643
|
+
'[class*="_panelList"] button[aria-haspopup="menu"]{display:none!important}',
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
if (values.rightMore) {
|
|
647
|
+
rules.push(
|
|
648
|
+
'[data-sidebar-right-guide] [class*="_moreAnchor"]{display:none!important}',
|
|
649
|
+
'[data-sidebar-right-guide] button[aria-haspopup="menu"]{display:none!important}',
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
if (values.headerMore) {
|
|
653
|
+
rules.push(
|
|
654
|
+
'[class*="_viewsMoreAnchor"]{display:none!important}',
|
|
655
|
+
'button[class*="_viewsMore"]{display:none!important}',
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
if (values.collapsedAll === true) {
|
|
659
|
+
// The bulk layer only: every panel row plus the list's More launcher. The
|
|
660
|
+
// individual switches and the checkbox list stay untouched.
|
|
661
|
+
rules.push(
|
|
662
|
+
'[class*="_panelList"] [class*="_panelEntry"]{display:none!important}',
|
|
663
|
+
'[class*="_panelList"] > button[aria-label]{display:none!important}',
|
|
664
|
+
'[class*="_panelList"] [class*="_moreAnchor"]{display:none!important}',
|
|
665
|
+
'[class*="_panelList"] button[aria-haspopup="menu"]{display:none!important}',
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
for (const id of values.hiddenPanels) {
|
|
669
|
+
const entry = panelEntries.find((one) => one.id === id)
|
|
670
|
+
if (entry === undefined) continue
|
|
671
|
+
// Primary: the id stamped on the row, which keeps two same-named panels
|
|
672
|
+
// apart. Fallback: the name, used only before the first stamping pass and
|
|
673
|
+
// only when no other panel carries it.
|
|
674
|
+
// Newer builds wrap every row in a `_panelEntry` box; hiding only the
|
|
675
|
+
// button leaves that box occupying its grid cell, which is the empty slot
|
|
676
|
+
// seen when a two-column list loses one panel. Hide the box itself.
|
|
677
|
+
rules.push(
|
|
678
|
+
'[class*="_panelEntry"]:has(button[' + PANEL_ATTR + '="' + cssAttrValue(id) + '"])'
|
|
679
|
+
+ '{display:none!important}',
|
|
680
|
+
'button[' + PANEL_ATTR + '="' + cssAttrValue(id) + '"]{display:none!important}',
|
|
681
|
+
)
|
|
682
|
+
const shared = panelEntries.some((one) => one.id !== id && one.label === entry.label)
|
|
683
|
+
if (!shared) {
|
|
684
|
+
rules.push(
|
|
685
|
+
'[class*="_panelEntry"]:has(button[aria-label="' + cssAttrValue(entry.label) + '"])'
|
|
686
|
+
+ '{display:none!important}',
|
|
687
|
+
)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
for (const one of values.picked) {
|
|
691
|
+
// Entries written before this flag existed were already hidden; keep them.
|
|
692
|
+
if (one.enabled === false) continue
|
|
693
|
+
rules.push(one.selector + '{display:none!important}')
|
|
694
|
+
}
|
|
695
|
+
return rules
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Styles for the plugin-page card; static, kept beside the hide rules. */
|
|
699
|
+
const FORM_CSS = [
|
|
700
|
+
'.dhns-form{display:flex;flex-direction:column;gap:22px;padding:12px 0}',
|
|
701
|
+
'.dhns-section{display:flex;flex-direction:column;gap:8px}',
|
|
702
|
+
'.dhns-sectionHead{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}',
|
|
703
|
+
'.dhns-sectionTitle{margin:0;font-size:13px;font-weight:600;',
|
|
704
|
+
'letter-spacing:.02em;color:var(--dsw-alias-label-primary)}',
|
|
705
|
+
'.dhns-sectionHint{font-size:11px;line-height:1.4;color:var(--dsw-alias-label-tertiary)}',
|
|
706
|
+
'.dhns-rows{display:flex;flex-direction:column;gap:8px}',
|
|
707
|
+
'.dhns-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}',
|
|
708
|
+
'.dhns-grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}',
|
|
709
|
+
'.dhns-grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}',
|
|
710
|
+
'.dhns-grid .dhns-row{padding:6px 8px;align-items:center;border-radius:6px}',
|
|
711
|
+
'.dhns-grid .dhns-title{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
712
|
+
'.dhns-grid .dhns-hint{font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
713
|
+
'.dhns-grid .dhns-text{gap:0}',
|
|
714
|
+
'.dhns-footer-btn{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;',
|
|
715
|
+
'padding:7px 8px;margin:0 0 2px;border:0;border-radius:var(--dsw-radius-md,8px);',
|
|
716
|
+
'background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer}',
|
|
717
|
+
'.dhns-footer-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
718
|
+
'.dhns-footer-btn[data-rail=true]{justify-content:center;width:36px;height:36px;padding:0;margin:0 auto 2px}',
|
|
719
|
+
'.dhns-footer-icon{flex:none;width:14px;text-align:center;opacity:.75}',
|
|
720
|
+
'.dhns-footer-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
721
|
+
'.dhns-row{display:flex;align-items:flex-start;gap:10px;cursor:pointer;padding:8px 10px;',
|
|
722
|
+
'border-radius:var(--dsw-radius-md,8px);border:1px solid var(--dsw-alias-border-secondary,rgba(128,128,128,.18))}',
|
|
723
|
+
'.dhns-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
724
|
+
'.dhns-row input{margin:2px 0 0;flex:none}',
|
|
725
|
+
'.dhns-grip{flex:none;cursor:grab;user-select:none;line-height:1;font-size:13px;',
|
|
726
|
+
'color:var(--dsw-alias-label-tertiary,#9aa0a6);padding:1px 0}',
|
|
727
|
+
'.dhns-grip:active{cursor:grabbing}',
|
|
728
|
+
'.dhns-rowDragging{opacity:.45}',
|
|
729
|
+
'.dhns-choice{margin:0;padding:0;border:0;display:flex;flex-direction:column;gap:8px}',
|
|
730
|
+
'.dhns-choiceInline{flex-direction:row;flex-wrap:wrap;gap:6px}',
|
|
731
|
+
'.dhns-choiceInline .dhns-row{flex:1 1 0;min-width:0;padding:6px 10px}',
|
|
732
|
+
'.dhns-choiceInline .dhns-title{white-space:nowrap}',
|
|
733
|
+
'.dhns-legend{padding:0;font-size:13px;font-weight:500;',
|
|
734
|
+
'color:var(--dsw-alias-label-primary);margin-bottom:2px}',
|
|
735
|
+
'.dhns-option[data-selected=true]{border-color:var(--dsw-alias-state-business-primary,rgba(44,123,229,.55));',
|
|
736
|
+
'background:var(--dsw-alias-interactive-bg-hover)}',
|
|
737
|
+
'.dhns-text{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}',
|
|
738
|
+
'.dhns-title{font-size:13px;color:var(--dsw-alias-label-primary)}',
|
|
739
|
+
'.dhns-hint{font-size:11px;line-height:1.45;color:var(--dsw-alias-label-tertiary)}',
|
|
740
|
+
'.dhns-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:2px}',
|
|
741
|
+
'.dhns-picked{display:flex;flex-direction:column;gap:6px;margin-top:4px}',
|
|
742
|
+
'.dhns-pickedRow{display:flex;align-items:center;gap:10px;padding:6px 10px;',
|
|
743
|
+
'border-radius:var(--dsw-radius-md,8px);border:1px solid var(--dsw-alias-border-secondary,rgba(128,128,128,.18))}',
|
|
744
|
+
'.dhns-pickedRow .dhns-hint{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
745
|
+
'.dhns-sel{font-size:11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;',
|
|
746
|
+
'color:var(--dsw-alias-label-secondary);max-width:46%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
747
|
+
'.dhns-btn{font:inherit;font-size:12px;line-height:18px;padding:5px 12px;cursor:pointer;',
|
|
748
|
+
'border-radius:var(--dsw-radius-md,8px);border:1px solid var(--dsw-alias-border-secondary,rgba(128,128,128,.3));',
|
|
749
|
+
'background:transparent;color:var(--dsw-alias-label-primary)}',
|
|
750
|
+
'.dhns-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
751
|
+
'.dhns-btn[data-tone=primary]{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-primary,#fff);',
|
|
752
|
+
'border-color:var(--dsw-alias-label-primary)}',
|
|
753
|
+
'.dhns-footer-btn{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;',
|
|
754
|
+
'padding:7px 8px;margin:0 0 2px;border:0;border-radius:var(--dsw-radius-md,8px);',
|
|
755
|
+
'background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer}',
|
|
756
|
+
'.dhns-footer-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
757
|
+
'.dhns-footer-btn[data-rail=true]{justify-content:center;width:36px;height:36px;padding:0;margin:0 auto 2px}',
|
|
758
|
+
'.dhns-footer-icon{flex:none;width:14px;text-align:center;opacity:.75}',
|
|
759
|
+
'.dhns-footer-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
760
|
+
'.dhns-subhead{font-size:12px;font-weight:500;letter-spacing:.02em;',
|
|
761
|
+
'color:var(--dsw-alias-label-secondary);margin-top:4px}',
|
|
762
|
+
'.dhns-subheadRow{display:flex;align-items:center;justify-content:space-between;',
|
|
763
|
+
'gap:8px;margin-top:4px}',
|
|
764
|
+
'.dhns-subheadRow .dhns-subhead{margin-top:0}',
|
|
765
|
+
'.dhns-subheadBtn{font:inherit;font-size:11px;line-height:16px;padding:1px 8px;cursor:pointer;',
|
|
766
|
+
'border-radius:6px;border:1px solid var(--dsw-alias-border-secondary,rgba(128,128,128,.3));',
|
|
767
|
+
'background:transparent;color:var(--dsw-alias-label-secondary)}',
|
|
768
|
+
'.dhns-subheadBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
769
|
+
'.dhns-subheadBtn:disabled{opacity:.4;cursor:default}',
|
|
770
|
+
'.dhns-page{padding:24px 28px;max-width:820px}',
|
|
771
|
+
'.dhns-pageTitle{margin:0 0 16px;font-size:16px;font-weight:600}',
|
|
772
|
+
].join('')
|
|
773
|
+
|
|
774
|
+
function syncStyle() {
|
|
775
|
+
if (typeof document === 'undefined') return null
|
|
776
|
+
let tag = document.querySelector('style[' + STYLE_ATTR + ']')
|
|
777
|
+
if (tag === null) {
|
|
778
|
+
tag = document.createElement('style')
|
|
779
|
+
tag.setAttribute(STYLE_ATTR, '1')
|
|
780
|
+
document.head.appendChild(tag)
|
|
781
|
+
}
|
|
782
|
+
const css = '/* dsh-sidebar-declutter */\n'
|
|
783
|
+
+ hiddenRules().concat(menuCssRules()).join('\n')
|
|
784
|
+
+ '\n' + FORM_CSS
|
|
785
|
+
// The tag outlives the module instance: a newer build re-executing in the
|
|
786
|
+
// same document must rewrite it rather than keep the old stylesheet.
|
|
787
|
+
if (tag.textContent !== css) tag.textContent = css
|
|
788
|
+
return tag
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Live view of what is registered into the sidebar's panel list. */
|
|
792
|
+
let panelEntries = []
|
|
793
|
+
|
|
794
|
+
/** Escape a value for a double-quoted CSS attribute selector. */
|
|
795
|
+
function cssAttrValue(value) {
|
|
796
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// --- collapse control -------------------------------------------------------
|
|
800
|
+
// `sidebar.panellist` rows are navigation by contract, so an action has no
|
|
801
|
+
// seat there. `sidebar.footer.action` is an official list slot the sidebar
|
|
802
|
+
// renders itself — no DOM injection, and a re-render cannot remove it.
|
|
803
|
+
|
|
804
|
+
function toggleCollapse() {
|
|
805
|
+
values = { ...values, collapsedAll: values.collapsedAll !== true }
|
|
806
|
+
writeStore()
|
|
807
|
+
syncStyle()
|
|
808
|
+
notify()
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function collapseLabel() {
|
|
812
|
+
return values.collapsedAll === true ? '展开面板列表' : '收起面板列表'
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* The footer action: an official list slot the sidebar renders itself, so it
|
|
817
|
+
* cannot be removed by a re-render. `wide` collapses it to an icon in the rail.
|
|
818
|
+
*/
|
|
819
|
+
function CollapseAction(props) {
|
|
820
|
+
const [, force] = React.useReducer((count) => count + 1, 0)
|
|
821
|
+
React.useEffect(() => subscribe(force), [])
|
|
822
|
+
const wide = props?.wide !== false
|
|
823
|
+
const label = collapseLabel()
|
|
824
|
+
return h(
|
|
825
|
+
'button',
|
|
826
|
+
{
|
|
827
|
+
type: 'button',
|
|
828
|
+
className: 'dhns-footer-btn',
|
|
829
|
+
'data-rail': wide ? undefined : 'true',
|
|
830
|
+
'aria-label': label,
|
|
831
|
+
'aria-pressed': values.collapsedAll === true ? 'true' : 'false',
|
|
832
|
+
title: label,
|
|
833
|
+
onClick: () => toggleCollapse(),
|
|
834
|
+
},
|
|
835
|
+
h('span', { className: 'dhns-footer-icon', 'aria-hidden': 'true' }, values.collapsedAll ? '▸' : '▾'),
|
|
836
|
+
wide ? h('span', { className: 'dhns-footer-label' }, label) : null,
|
|
837
|
+
)
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// --- per-row id stamping -----------------------------------------------------
|
|
841
|
+
// The list exposes no id in the DOM, only the accessible name, so two panels
|
|
842
|
+
// sharing a name would hide together. Each rendered row is therefore stamped
|
|
843
|
+
// with the registration id it belongs to, and the hide rule names the id.
|
|
844
|
+
// Stamping only sets attributes; it never inserts or moves a node.
|
|
845
|
+
|
|
846
|
+
const PANEL_ATTR = 'data-dsh-panel'
|
|
847
|
+
|
|
848
|
+
/** The panel list inside the sidebar column, located from its own anchor. */
|
|
849
|
+
function findPanelNav() {
|
|
850
|
+
if (typeof document === 'undefined') return null
|
|
851
|
+
const toggle = document.querySelector('[data-sidebar-toggle]')
|
|
852
|
+
if (toggle === null) return null
|
|
853
|
+
let node = toggle.parentElement
|
|
854
|
+
while (node !== null && node !== document.body) {
|
|
855
|
+
const nav = node.querySelector('[class*="_panelList"]')
|
|
856
|
+
if (nav !== null) {
|
|
857
|
+
const rect = nav.getBoundingClientRect()
|
|
858
|
+
// A sidebar column is a narrow strip; anything wider is another
|
|
859
|
+
// component whose CSS-module local name happens to match.
|
|
860
|
+
if (rect.width > 0 && rect.width < 520) return nav
|
|
861
|
+
}
|
|
862
|
+
node = node.parentElement
|
|
863
|
+
}
|
|
864
|
+
return null
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Pair each rendered row with a registered entry: by name first, then by
|
|
869
|
+
* render order among the rest. A failed pass hides nothing, which is the safe
|
|
870
|
+
* direction — every panel stays visible and can be re-ticked.
|
|
871
|
+
*/
|
|
872
|
+
/**
|
|
873
|
+
* Registered panels in the order the user dragged them into; anything they
|
|
874
|
+
* have not arranged yet keeps its registration order behind them.
|
|
875
|
+
*/
|
|
876
|
+
function orderedPanelEntries() {
|
|
877
|
+
if (values.panelOrder.length === 0) return [...panelEntries]
|
|
878
|
+
const rank = new Map(values.panelOrder.map((id, index) => [id, index]))
|
|
879
|
+
const base = new Map(panelEntries.map((entry, index) => [entry.id, index]))
|
|
880
|
+
return [...panelEntries].sort((a, b) => {
|
|
881
|
+
const ra = rank.has(a.id) ? rank.get(a.id) : Number.MAX_SAFE_INTEGER
|
|
882
|
+
const rb = rank.has(b.id) ? rank.get(b.id) : Number.MAX_SAFE_INTEGER
|
|
883
|
+
if (ra !== rb) return ra - rb
|
|
884
|
+
return (base.get(a.id) ?? 0) - (base.get(b.id) ?? 0)
|
|
885
|
+
})
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function stampPanelRows() {
|
|
889
|
+
try {
|
|
890
|
+
const nav = findPanelNav()
|
|
891
|
+
if (nav === null) return
|
|
892
|
+
const rows = Array.from(nav.querySelectorAll('button[aria-label]'))
|
|
893
|
+
if (rows.length === 0) return
|
|
894
|
+
const claimed = new Set()
|
|
895
|
+
const take = (label) => {
|
|
896
|
+
const hit = panelEntries.find((entry) => entry.label === label && !claimed.has(entry.id))
|
|
897
|
+
if (hit === undefined) return null
|
|
898
|
+
claimed.add(hit.id)
|
|
899
|
+
return hit.id
|
|
900
|
+
}
|
|
901
|
+
for (const row of rows) {
|
|
902
|
+
const id = take(row.getAttribute('aria-label') ?? '')
|
|
903
|
+
if (id === null) {
|
|
904
|
+
row.removeAttribute(PANEL_ATTR)
|
|
905
|
+
continue
|
|
906
|
+
}
|
|
907
|
+
if (row.getAttribute(PANEL_ATTR) !== id) row.setAttribute(PANEL_ATTR, id)
|
|
908
|
+
}
|
|
909
|
+
} catch (error) {
|
|
910
|
+
console.warn('[dsh-sidebar-declutter] 面板打标失败:', error)
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Re-stamp whenever the sidebar re-renders its list. The observer watches the
|
|
916
|
+
* list node only — watching the whole document would fire on every chat
|
|
917
|
+
* token. Until the sidebar exists, a short poll looks for it and then stops.
|
|
918
|
+
*/
|
|
919
|
+
function watchPanelRows() {
|
|
920
|
+
if (typeof MutationObserver === 'undefined' || typeof document === 'undefined') return null
|
|
921
|
+
let observer = null
|
|
922
|
+
let target = null
|
|
923
|
+
let scheduled = false
|
|
924
|
+
let poll = null
|
|
925
|
+
|
|
926
|
+
const stampSoon = () => {
|
|
927
|
+
if (scheduled) return
|
|
928
|
+
scheduled = true
|
|
929
|
+
setTimeout(() => {
|
|
930
|
+
scheduled = false
|
|
931
|
+
stampPanelRows()
|
|
932
|
+
}, 16)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const attach = () => {
|
|
936
|
+
const nav = findPanelNav()
|
|
937
|
+
if (nav === null) return false
|
|
938
|
+
if (nav === target) return true
|
|
939
|
+
observer?.disconnect()
|
|
940
|
+
target = nav
|
|
941
|
+
observer = new MutationObserver(stampSoon)
|
|
942
|
+
observer.observe(nav, { childList: true })
|
|
943
|
+
stampPanelRows()
|
|
944
|
+
return true
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (!attach()) {
|
|
948
|
+
poll = setInterval(() => {
|
|
949
|
+
if (attach() && poll !== null) {
|
|
950
|
+
clearInterval(poll)
|
|
951
|
+
poll = null
|
|
952
|
+
}
|
|
953
|
+
}, 500)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return () => {
|
|
957
|
+
observer?.disconnect()
|
|
958
|
+
if (poll !== null) clearInterval(poll)
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// --- pick mode -------------------------------------------------------------
|
|
963
|
+
// User-started only: capture one click, turn the target into a selector and
|
|
964
|
+
// keep it in `picked`. Nothing here runs until the button is pressed.
|
|
965
|
+
|
|
966
|
+
let picker = null
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Teardown handles that outlive one module instance. A hot swap re-runs this
|
|
970
|
+
* module with fresh state while the previous instance's document listeners
|
|
971
|
+
* are still attached; without this the old capture handler keeps swallowing
|
|
972
|
+
* every click in the page.
|
|
973
|
+
*/
|
|
974
|
+
const TEARDOWN_KEY = '__dshSidebarDeclutterTeardown'
|
|
975
|
+
|
|
976
|
+
function registerTeardown(fn) {
|
|
977
|
+
if (typeof window === 'undefined') return
|
|
978
|
+
const previous = window[TEARDOWN_KEY]
|
|
979
|
+
if (typeof previous === 'function') {
|
|
980
|
+
try {
|
|
981
|
+
previous()
|
|
982
|
+
} catch {
|
|
983
|
+
// A broken previous instance must not stop this one.
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
window[TEARDOWN_KEY] = fn
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Live view of what is registered into the sidebar's panel list, mirrored
|
|
991
|
+
* from the slot registry the sidebar itself renders from. Each entry is
|
|
992
|
+
* `{ id, order, label }`; the label is what the rendered button carries as
|
|
993
|
+
* its accessible name.
|
|
994
|
+
*/
|
|
995
|
+
/** Escape a value for a double-quoted CSS attribute selector. */
|
|
996
|
+
/** `kB3qhq_panelRow` → `panelRow`; a class without a module hash stays whole. */
|
|
997
|
+
function localName(cls) {
|
|
998
|
+
const match = /^[A-Za-z0-9_-]+_([A-Za-z][A-Za-z0-9-]*)$/.exec(cls)
|
|
999
|
+
return match === null ? null : match[1]
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function escapeIdent(value) {
|
|
1003
|
+
return typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
|
|
1004
|
+
? CSS.escape(value)
|
|
1005
|
+
: value.replace(/[^A-Za-z0-9_-]/g, (ch) => '\\' + ch)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* A selector that survives the next build. CSS-module class names carry a
|
|
1010
|
+
* per-build hash (`kB3qhq_panelRow`), so they are matched by their local
|
|
1011
|
+
* suffix; an id or a `data-` attribute is preferred when the element has one.
|
|
1012
|
+
*/
|
|
1013
|
+
function buildSelector(el) {
|
|
1014
|
+
if (el.id !== '') return '#' + escapeIdent(el.id)
|
|
1015
|
+
const classes = Array.from(el.classList)
|
|
1016
|
+
const locals = classes.map(localName).filter((one) => one !== null)
|
|
1017
|
+
const plain = classes.filter((one) => localName(one) === null)
|
|
1018
|
+
if (plain.length > 0 || locals.length > 0) {
|
|
1019
|
+
let sel = el.tagName.toLowerCase()
|
|
1020
|
+
if (plain.length > 0) sel += '.' + escapeIdent(plain[0])
|
|
1021
|
+
if (locals.length > 0) sel += '[class*="_' + locals[0] + '"]'
|
|
1022
|
+
return sel
|
|
1023
|
+
}
|
|
1024
|
+
for (const attr of Array.from(el.attributes)) {
|
|
1025
|
+
if (attr.name.startsWith('data-') && attr.name !== 'data-dsh-sidebar-declutter') {
|
|
1026
|
+
return '[' + attr.name + ']'
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const path = []
|
|
1030
|
+
let node = el
|
|
1031
|
+
for (let depth = 0; depth < 5 && node && node !== document.body; depth += 1) {
|
|
1032
|
+
let sel = node.tagName.toLowerCase()
|
|
1033
|
+
const parent = node.parentElement
|
|
1034
|
+
if (parent !== null) sel += ':nth-child(' + (Array.from(parent.children).indexOf(node) + 1) + ')'
|
|
1035
|
+
path.unshift(sel)
|
|
1036
|
+
node = node.parentElement
|
|
1037
|
+
}
|
|
1038
|
+
return path.join(' > ')
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function labelFor(el) {
|
|
1042
|
+
const named = el.getAttribute('aria-label') ?? el.getAttribute('title') ?? ''
|
|
1043
|
+
const text = (named !== '' ? named : el.textContent ?? '').replace(/\s+/g, ' ').trim()
|
|
1044
|
+
return text === '' ? el.tagName.toLowerCase() : text.slice(0, 24)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** Guards that keep the way back open. */
|
|
1048
|
+
function isProtected(el) {
|
|
1049
|
+
if (typeof document === 'undefined') return true
|
|
1050
|
+
if (el === document.body || el === document.documentElement) return true
|
|
1051
|
+
if (el.closest('.dhns-form') !== null) return true
|
|
1052
|
+
// The settings entry is the only door to the plugin page that holds the
|
|
1053
|
+
// restore list; hiding it — directly or through an ancestor — locks the
|
|
1054
|
+
// user out of the very screen that could undo it.
|
|
1055
|
+
if (el.matches('[class*="_triggerRow"]') || el.querySelector('[class*="_triggerRow"]') !== null) return true
|
|
1056
|
+
// A container, not a row. One stray click on a panel-sized box can blank a
|
|
1057
|
+
// whole area — the plugin page itself was hidden this way once — so
|
|
1058
|
+
// anything covering a large share of the viewport is refused.
|
|
1059
|
+
const rect = el.getBoundingClientRect()
|
|
1060
|
+
const viewport = window.innerWidth * window.innerHeight
|
|
1061
|
+
if (viewport > 0 && rect.width * rect.height > viewport * 0.4) return true
|
|
1062
|
+
return false
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function addPicked(el) {
|
|
1066
|
+
const selector = buildSelector(el)
|
|
1067
|
+
if (selector.trim() === '') return null
|
|
1068
|
+
if (values.picked.some((one) => one.selector === selector)) return { selector, duplicate: true }
|
|
1069
|
+
// Added as a candidate: it hides only once its checkbox is ticked, so a
|
|
1070
|
+
// stray click can never take something off the screen by itself.
|
|
1071
|
+
const entry = { selector, label: labelFor(el), enabled: false }
|
|
1072
|
+
values = { ...values, picked: [...values.picked, entry] }
|
|
1073
|
+
writeStore()
|
|
1074
|
+
syncStyle()
|
|
1075
|
+
notify()
|
|
1076
|
+
return { selector, duplicate: false }
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function removePickedAt(index) {
|
|
1080
|
+
const next = values.picked.filter((_, position) => position !== index)
|
|
1081
|
+
values = { ...values, picked: next }
|
|
1082
|
+
writeStore()
|
|
1083
|
+
syncStyle()
|
|
1084
|
+
notify()
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function stopPicker() {
|
|
1088
|
+
if (picker === null) return
|
|
1089
|
+
document.removeEventListener('click', picker.onClick, true)
|
|
1090
|
+
document.removeEventListener('mousemove', picker.onMove, true)
|
|
1091
|
+
document.removeEventListener('keydown', picker.onKeyDown, true)
|
|
1092
|
+
if (picker.outlined !== null) picker.outlined.style.outline = picker.savedOutline
|
|
1093
|
+
picker.bar.remove()
|
|
1094
|
+
picker = null
|
|
1095
|
+
notify()
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function startPicker() {
|
|
1099
|
+
if (typeof document === 'undefined' || picker !== null) return
|
|
1100
|
+
const bar = document.createElement('div')
|
|
1101
|
+
bar.setAttribute('data-dsh-picker', '1')
|
|
1102
|
+
bar.style.cssText = [
|
|
1103
|
+
'position:fixed', 'left:50%', 'bottom:24px', 'transform:translateX(-50%)',
|
|
1104
|
+
'z-index:2147483000', 'display:flex', 'align-items:center', 'gap:10px',
|
|
1105
|
+
'padding:8px 12px', 'border-radius:12px', 'font:13px/1.4 system-ui,sans-serif',
|
|
1106
|
+
'background:#1f2328', 'color:#fff', 'box-shadow:0 8px 24px rgba(0,0,0,.35)',
|
|
1107
|
+
].join(';')
|
|
1108
|
+
|
|
1109
|
+
const text = document.createElement('span')
|
|
1110
|
+
text.setAttribute('data-role', 'text')
|
|
1111
|
+
const undo = document.createElement('button')
|
|
1112
|
+
undo.type = 'button'
|
|
1113
|
+
undo.textContent = '撤销'
|
|
1114
|
+
undo.style.cssText = 'font:inherit;padding:4px 10px;border-radius:8px;border:1px solid #ffffff44;background:transparent;color:#fff;cursor:pointer'
|
|
1115
|
+
const done = document.createElement('button')
|
|
1116
|
+
done.type = 'button'
|
|
1117
|
+
done.textContent = '完成'
|
|
1118
|
+
done.style.cssText = 'font:inherit;padding:4px 10px;border-radius:8px;border:1px solid #ffffff44;background:#fff;color:#1f2328;cursor:pointer'
|
|
1119
|
+
bar.append(text, undo, done)
|
|
1120
|
+
document.body.appendChild(bar)
|
|
1121
|
+
|
|
1122
|
+
const refreshBar = () => {
|
|
1123
|
+
text.textContent = '点选要隐藏的元素 · 已隐藏 ' + values.picked.length + ' 个'
|
|
1124
|
+
undo.disabled = values.picked.length === 0
|
|
1125
|
+
}
|
|
1126
|
+
refreshBar()
|
|
1127
|
+
|
|
1128
|
+
undo.addEventListener('click', () => {
|
|
1129
|
+
if (values.picked.length === 0) return
|
|
1130
|
+
removePickedAt(values.picked.length - 1)
|
|
1131
|
+
refreshBar()
|
|
1132
|
+
})
|
|
1133
|
+
done.addEventListener('click', () => stopPicker())
|
|
1134
|
+
|
|
1135
|
+
const state = { bar, onClick: null, onMove: null, outlined: null, savedOutline: '' }
|
|
1136
|
+
|
|
1137
|
+
state.onMove = (event) => {
|
|
1138
|
+
const el = event.target instanceof Element ? event.target : null
|
|
1139
|
+
if (el === null || el === state.outlined) return
|
|
1140
|
+
if (state.outlined !== null) state.outlined.style.outline = state.savedOutline
|
|
1141
|
+
state.outlined = null
|
|
1142
|
+
if (el === document.body || el === document.documentElement) return
|
|
1143
|
+
if (bar.contains(el)) return
|
|
1144
|
+
state.outlined = el
|
|
1145
|
+
state.savedOutline = el.style.outline
|
|
1146
|
+
el.style.outline = isProtected(el) ? '2px dashed #e5484d' : '2px solid #2c7be5'
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
state.onClick = (event) => {
|
|
1150
|
+
const el = event.target instanceof Element ? event.target : null
|
|
1151
|
+
if (el === null || bar.contains(el)) return
|
|
1152
|
+
// Clicks on this plugin's own card must keep working — that is where
|
|
1153
|
+
// 「结束点选」and the restore rows live.
|
|
1154
|
+
if (el.closest('.dhns-form') !== null) return
|
|
1155
|
+
event.preventDefault()
|
|
1156
|
+
event.stopPropagation()
|
|
1157
|
+
if (isProtected(el)) {
|
|
1158
|
+
text.textContent = '这一项不能隐藏:它会挡住设置入口'
|
|
1159
|
+
setTimeout(refreshBar, 1800)
|
|
1160
|
+
return
|
|
1161
|
+
}
|
|
1162
|
+
const result = addPicked(el)
|
|
1163
|
+
text.textContent =
|
|
1164
|
+
result !== null && result.duplicate
|
|
1165
|
+
? '这一项已经在列表里了'
|
|
1166
|
+
: '已加入列表:' + labelFor(el) + '(去设置里勾选才会隐藏)'
|
|
1167
|
+
setTimeout(refreshBar, 2000)
|
|
1168
|
+
if (state.outlined !== null) {
|
|
1169
|
+
state.outlined.style.outline = state.savedOutline
|
|
1170
|
+
state.outlined = null
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
state.onKeyDown = (event) => {
|
|
1175
|
+
if (event.key === 'Escape') {
|
|
1176
|
+
event.preventDefault()
|
|
1177
|
+
event.stopPropagation()
|
|
1178
|
+
stopPicker()
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
document.addEventListener('mousemove', state.onMove, true)
|
|
1183
|
+
document.addEventListener('click', state.onClick, true)
|
|
1184
|
+
document.addEventListener('keydown', state.onKeyDown, true)
|
|
1185
|
+
picker = state
|
|
1186
|
+
notify()
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function notify() {
|
|
1190
|
+
for (const listener of [...listeners]) {
|
|
1191
|
+
try {
|
|
1192
|
+
listener()
|
|
1193
|
+
} catch {
|
|
1194
|
+
// One broken subscriber must not stop the rest.
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* The plugin page card, registered at `plugins.row.config` for this bundle's
|
|
1201
|
+
* row. `summary` is the one-liner the row shows when collapsed; `page` is the
|
|
1202
|
+
* form the row's configure control opens.
|
|
1203
|
+
*/
|
|
1204
|
+
/** The page #/panel/hide-rows renders: the same form as the plugin row card. */
|
|
1205
|
+
function SettingsPage() {
|
|
1206
|
+
return h(
|
|
1207
|
+
'div',
|
|
1208
|
+
{ className: 'dhns-page' },
|
|
1209
|
+
h('h2', { className: 'dhns-pageTitle' }, PANEL_ENTRY_LABEL),
|
|
1210
|
+
h(SettingsCard, { view: 'full' }),
|
|
1211
|
+
)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function SettingsCard(props) {
|
|
1215
|
+
const view = props?.view
|
|
1216
|
+
const [, forceRender] = React.useReducer((count) => count + 1, 0)
|
|
1217
|
+
const [dragId, setDragId] = React.useState(null)
|
|
1218
|
+
React.useEffect(() => subscribe(forceRender), [])
|
|
1219
|
+
|
|
1220
|
+
// The host renders this card with view="summary" as the one-line
|
|
1221
|
+
// description under the plugin row. Return nothing so no description
|
|
1222
|
+
// is drawn at all; the full form is unchanged.
|
|
1223
|
+
if (view === 'summary') return null
|
|
1224
|
+
|
|
1225
|
+
const rowFor = (field) =>
|
|
1226
|
+
h(
|
|
1227
|
+
'label',
|
|
1228
|
+
{ key: field.key, className: 'dhns-row' },
|
|
1229
|
+
h('input', {
|
|
1230
|
+
type: 'checkbox',
|
|
1231
|
+
checked: values[field.key] === true,
|
|
1232
|
+
onChange: (event) => setValue(field.key, event.target.checked === true),
|
|
1233
|
+
}),
|
|
1234
|
+
h(
|
|
1235
|
+
'span',
|
|
1236
|
+
{ className: 'dhns-text' },
|
|
1237
|
+
h('span', { className: 'dhns-title' }, field.title),
|
|
1238
|
+
h('span', { className: 'dhns-hint' }, field.hint),
|
|
1239
|
+
),
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* A mutually exclusive group. One shared `name` is what makes the browser
|
|
1244
|
+
* keep exactly one option selected, so the three layouts can never be
|
|
1245
|
+
* combined — the CSS rules are all derived from the single stored value.
|
|
1246
|
+
*/
|
|
1247
|
+
const choiceFor = (field) =>
|
|
1248
|
+
h(
|
|
1249
|
+
'fieldset',
|
|
1250
|
+
{
|
|
1251
|
+
key: field.key,
|
|
1252
|
+
className: field.inline === true ? 'dhns-choice dhns-choiceInline' : 'dhns-choice',
|
|
1253
|
+
'aria-label': field.title ?? field.key,
|
|
1254
|
+
},
|
|
1255
|
+
field.title === undefined
|
|
1256
|
+
? null
|
|
1257
|
+
: h('legend', { className: 'dhns-legend' }, field.title),
|
|
1258
|
+
field.options.map((option) =>
|
|
1259
|
+
h(
|
|
1260
|
+
'label',
|
|
1261
|
+
{
|
|
1262
|
+
key: option.value,
|
|
1263
|
+
className: 'dhns-row dhns-option',
|
|
1264
|
+
'data-selected': values[field.key] === option.value ? 'true' : undefined,
|
|
1265
|
+
},
|
|
1266
|
+
h('input', {
|
|
1267
|
+
type: 'radio',
|
|
1268
|
+
name: 'dhns-' + field.key,
|
|
1269
|
+
checked: values[field.key] === option.value,
|
|
1270
|
+
onChange: () => setValue(field.key, option.value),
|
|
1271
|
+
}),
|
|
1272
|
+
h(
|
|
1273
|
+
'span',
|
|
1274
|
+
{ className: 'dhns-text' },
|
|
1275
|
+
h('span', { className: 'dhns-title' }, option.title),
|
|
1276
|
+
h('span', { className: 'dhns-hint' }, option.hint),
|
|
1277
|
+
),
|
|
1278
|
+
),
|
|
1279
|
+
),
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
const renderField = (field) => (field.type === 'choice' ? choiceFor(field) : rowFor(field))
|
|
1283
|
+
|
|
1284
|
+
const togglePanel = (id, on) => {
|
|
1285
|
+
const next = on
|
|
1286
|
+
? values.hiddenPanels.includes(id) ? values.hiddenPanels : [...values.hiddenPanels, id]
|
|
1287
|
+
: values.hiddenPanels.filter((one) => one !== id)
|
|
1288
|
+
values = { ...values, hiddenPanels: next }
|
|
1289
|
+
writeStore()
|
|
1290
|
+
syncStyle()
|
|
1291
|
+
notify()
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** Drop `fromId` where `toId` sits, then persist the whole arrangement. */
|
|
1295
|
+
const movePanel = (fromId, toId) => {
|
|
1296
|
+
const ids = orderedPanelEntries().map((one) => one.id)
|
|
1297
|
+
const from = ids.indexOf(fromId)
|
|
1298
|
+
const to = ids.indexOf(toId)
|
|
1299
|
+
if (from < 0 || to < 0 || from === to) return
|
|
1300
|
+
const [moved] = ids.splice(from, 1)
|
|
1301
|
+
ids.splice(to, 0, moved)
|
|
1302
|
+
values = { ...values, panelOrder: ids }
|
|
1303
|
+
writeStore()
|
|
1304
|
+
syncStyle()
|
|
1305
|
+
notify()
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/** Drop the arrangement and fall back to the registration order. */
|
|
1309
|
+
const resetPanelOrder = () => {
|
|
1310
|
+
if (values.panelOrder.length === 0) return
|
|
1311
|
+
values = { ...values, panelOrder: [] }
|
|
1312
|
+
writeStore()
|
|
1313
|
+
syncStyle()
|
|
1314
|
+
notify()
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const panelBlock = () => {
|
|
1318
|
+
if (panelEntries.length === 0) {
|
|
1319
|
+
return h('p', { className: 'dhns-hint' }, '当前没有检测到已注册的面板。')
|
|
1320
|
+
}
|
|
1321
|
+
return h(
|
|
1322
|
+
'div',
|
|
1323
|
+
null,
|
|
1324
|
+
h(
|
|
1325
|
+
'div',
|
|
1326
|
+
{ className: 'dhns-subheadRow', key: 'subhead' },
|
|
1327
|
+
h('span', { className: 'dhns-subhead' }, '面板'),
|
|
1328
|
+
h(
|
|
1329
|
+
'button',
|
|
1330
|
+
{
|
|
1331
|
+
type: 'button',
|
|
1332
|
+
className: 'dhns-subheadBtn',
|
|
1333
|
+
disabled: values.panelOrder.length === 0,
|
|
1334
|
+
onClick: resetPanelOrder,
|
|
1335
|
+
},
|
|
1336
|
+
'还原默认',
|
|
1337
|
+
),
|
|
1338
|
+
),
|
|
1339
|
+
h(
|
|
1340
|
+
'div',
|
|
1341
|
+
{ className: 'dhns-grid', key: 'grid' },
|
|
1342
|
+
orderedPanelEntries().map((entry) =>
|
|
1343
|
+
h(
|
|
1344
|
+
'label',
|
|
1345
|
+
{
|
|
1346
|
+
key: entry.id,
|
|
1347
|
+
className: dragId === entry.id ? 'dhns-row dhns-rowDragging' : 'dhns-row',
|
|
1348
|
+
onDragOver: (event) => {
|
|
1349
|
+
if (dragId === null) return
|
|
1350
|
+
event.preventDefault()
|
|
1351
|
+
event.dataTransfer.dropEffect = 'move'
|
|
1352
|
+
},
|
|
1353
|
+
onDrop: (event) => {
|
|
1354
|
+
if (dragId === null) return
|
|
1355
|
+
event.preventDefault()
|
|
1356
|
+
movePanel(dragId, entry.id)
|
|
1357
|
+
setDragId(null)
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1360
|
+
h(
|
|
1361
|
+
'span',
|
|
1362
|
+
{
|
|
1363
|
+
className: 'dhns-grip',
|
|
1364
|
+
title: '拖动排序',
|
|
1365
|
+
draggable: true,
|
|
1366
|
+
onDragStart: (event) => {
|
|
1367
|
+
setDragId(entry.id)
|
|
1368
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
1369
|
+
event.dataTransfer.setData('text/plain', entry.id)
|
|
1370
|
+
},
|
|
1371
|
+
onDragEnd: () => setDragId(null),
|
|
1372
|
+
},
|
|
1373
|
+
'⠿',
|
|
1374
|
+
),
|
|
1375
|
+
h('input', {
|
|
1376
|
+
type: 'checkbox',
|
|
1377
|
+
checked: values.hiddenPanels.includes(entry.id),
|
|
1378
|
+
onChange: (event) => togglePanel(entry.id, event.target.checked === true),
|
|
1379
|
+
}),
|
|
1380
|
+
h(
|
|
1381
|
+
'span',
|
|
1382
|
+
{ className: 'dhns-text' },
|
|
1383
|
+
h('span', { className: 'dhns-title' }, entry.label),
|
|
1384
|
+
h('span', { className: 'dhns-hint' }, entry.id),
|
|
1385
|
+
),
|
|
1386
|
+
),
|
|
1387
|
+
),
|
|
1388
|
+
),
|
|
1389
|
+
)
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const togglePicked = (index, on) => {
|
|
1393
|
+
const next = values.picked.map((one, position) =>
|
|
1394
|
+
position === index ? { ...one, enabled: on } : one,
|
|
1395
|
+
)
|
|
1396
|
+
values = { ...values, picked: next }
|
|
1397
|
+
writeStore()
|
|
1398
|
+
syncStyle()
|
|
1399
|
+
notify()
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const pickedBlock = () => {
|
|
1403
|
+
const running = picker !== null
|
|
1404
|
+
return [
|
|
1405
|
+
h(
|
|
1406
|
+
'div',
|
|
1407
|
+
{ className: 'dhns-actions', key: 'actions' },
|
|
1408
|
+
h(
|
|
1409
|
+
'button',
|
|
1410
|
+
{
|
|
1411
|
+
type: 'button',
|
|
1412
|
+
className: 'dhns-btn',
|
|
1413
|
+
'data-tone': running ? undefined : 'primary',
|
|
1414
|
+
onClick: () => (running ? stopPicker() : startPicker()),
|
|
1415
|
+
},
|
|
1416
|
+
running ? '结束点选' : '开始点选',
|
|
1417
|
+
),
|
|
1418
|
+
values.picked.length === 0
|
|
1419
|
+
? null
|
|
1420
|
+
: h(
|
|
1421
|
+
'button',
|
|
1422
|
+
{
|
|
1423
|
+
type: 'button',
|
|
1424
|
+
className: 'dhns-btn',
|
|
1425
|
+
onClick: () => {
|
|
1426
|
+
values = { ...values, picked: [] }
|
|
1427
|
+
writeStore()
|
|
1428
|
+
syncStyle()
|
|
1429
|
+
notify()
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
'清空列表',
|
|
1433
|
+
),
|
|
1434
|
+
),
|
|
1435
|
+
values.picked.length === 0
|
|
1436
|
+
? null
|
|
1437
|
+
: h(
|
|
1438
|
+
'div',
|
|
1439
|
+
{ className: 'dhns-rows', key: 'list' },
|
|
1440
|
+
values.picked.map((one, index) =>
|
|
1441
|
+
h(
|
|
1442
|
+
'label',
|
|
1443
|
+
{ className: 'dhns-row', key: one.selector + ':' + index },
|
|
1444
|
+
h('input', {
|
|
1445
|
+
type: 'checkbox',
|
|
1446
|
+
checked: one.enabled !== false,
|
|
1447
|
+
onChange: (event) => togglePicked(index, event.target.checked === true),
|
|
1448
|
+
}),
|
|
1449
|
+
h(
|
|
1450
|
+
'span',
|
|
1451
|
+
{ className: 'dhns-text' },
|
|
1452
|
+
h('span', { className: 'dhns-title' }, one.label),
|
|
1453
|
+
h(
|
|
1454
|
+
'span',
|
|
1455
|
+
{ className: 'dhns-hint dhns-sel', title: one.selector },
|
|
1456
|
+
one.selector,
|
|
1457
|
+
),
|
|
1458
|
+
),
|
|
1459
|
+
h(
|
|
1460
|
+
'button',
|
|
1461
|
+
{
|
|
1462
|
+
type: 'button',
|
|
1463
|
+
className: 'dhns-btn',
|
|
1464
|
+
onClick: (event) => {
|
|
1465
|
+
event.preventDefault()
|
|
1466
|
+
removePickedAt(index)
|
|
1467
|
+
},
|
|
1468
|
+
},
|
|
1469
|
+
'移除',
|
|
1470
|
+
),
|
|
1471
|
+
),
|
|
1472
|
+
),
|
|
1473
|
+
),
|
|
1474
|
+
]
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const menuBlock = () => {
|
|
1478
|
+
const presetKeys = MENU_PRESETS.map((one) => one.key)
|
|
1479
|
+
const other = menuState.seen
|
|
1480
|
+
.filter((one) => !presetKeys.includes(one.key) && !isPresetMenuKey(one.key))
|
|
1481
|
+
.sort((a, b) => a.key.localeCompare(b.key, 'zh'))
|
|
1482
|
+
const rowOf = (one) =>
|
|
1483
|
+
h(
|
|
1484
|
+
'label',
|
|
1485
|
+
{ key: one.key, className: 'dhns-row' },
|
|
1486
|
+
h('input', {
|
|
1487
|
+
type: 'checkbox',
|
|
1488
|
+
checked: menuState.hidden.includes(one.key),
|
|
1489
|
+
onChange: (event) => setMenuRowHidden(one.key, event.target.checked === true),
|
|
1490
|
+
}),
|
|
1491
|
+
h(
|
|
1492
|
+
'span',
|
|
1493
|
+
{ className: 'dhns-text' },
|
|
1494
|
+
h('span', { className: 'dhns-title' }, one.key),
|
|
1495
|
+
one.accel === '' ? null : h('span', { className: 'dhns-hint' }, one.accel),
|
|
1496
|
+
),
|
|
1497
|
+
)
|
|
1498
|
+
return h(
|
|
1499
|
+
'div',
|
|
1500
|
+
null,
|
|
1501
|
+
h('div', { className: 'dhns-subhead', key: 'official' }, '官方'),
|
|
1502
|
+
h('div', { className: 'dhns-grid', key: 'official-grid' },
|
|
1503
|
+
MENU_PRESETS.map((one) => rowOf({ key: one.key, accel: one.accel }))),
|
|
1504
|
+
other.length === 0
|
|
1505
|
+
? null
|
|
1506
|
+
: h('div', { className: 'dhns-subhead', key: 'other' }, '社区'),
|
|
1507
|
+
other.length === 0
|
|
1508
|
+
? null
|
|
1509
|
+
: h('div', { className: 'dhns-grid', key: 'other-grid' }, other.map(rowOf)),
|
|
1510
|
+
)
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
return h(
|
|
1514
|
+
'div',
|
|
1515
|
+
{ className: 'dhns-form' },
|
|
1516
|
+
SECTIONS.map((section) =>
|
|
1517
|
+
h(
|
|
1518
|
+
'section',
|
|
1519
|
+
{ key: section.title, className: 'dhns-section' },
|
|
1520
|
+
h(
|
|
1521
|
+
'div',
|
|
1522
|
+
{ className: 'dhns-sectionHead' },
|
|
1523
|
+
h('h4', { className: 'dhns-sectionTitle' }, section.title),
|
|
1524
|
+
section.hint === undefined
|
|
1525
|
+
? null
|
|
1526
|
+
: h('span', { className: 'dhns-sectionHint' }, section.hint),
|
|
1527
|
+
),
|
|
1528
|
+
section.fields.length === 0
|
|
1529
|
+
? null
|
|
1530
|
+
: h(
|
|
1531
|
+
'div',
|
|
1532
|
+
{
|
|
1533
|
+
className: section.columns === 2
|
|
1534
|
+
? 'dhns-grid dhns-grid-2'
|
|
1535
|
+
: section.columns === 3
|
|
1536
|
+
? 'dhns-grid'
|
|
1537
|
+
: section.columns === 4
|
|
1538
|
+
? 'dhns-grid dhns-grid-4'
|
|
1539
|
+
: 'dhns-rows',
|
|
1540
|
+
},
|
|
1541
|
+
section.fields.map(renderField),
|
|
1542
|
+
),
|
|
1543
|
+
section.kind === 'picked' ? pickedBlock() : null,
|
|
1544
|
+
section.kind === 'menu' ? menuBlock() : null,
|
|
1545
|
+
section.panels === true ? panelBlock() : null,
|
|
1546
|
+
),
|
|
1547
|
+
),
|
|
1548
|
+
)
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
return {
|
|
1552
|
+
inject: ['slots'],
|
|
1553
|
+
apply(ctx) {
|
|
1554
|
+
values = readStore()
|
|
1555
|
+
readMenuState()
|
|
1556
|
+
|
|
1557
|
+
// Mirror the panel registry the sidebar renders from: it is the only
|
|
1558
|
+
// place that knows every left-column button and its display name, so the
|
|
1559
|
+
// checkboxes follow newly installed plugins without any code change.
|
|
1560
|
+
const syncPanels = () => {
|
|
1561
|
+
try {
|
|
1562
|
+
panelEntries = ctx.slots
|
|
1563
|
+
.entriesOfSlot('sidebar.panellist')
|
|
1564
|
+
.map(({ options }) => ({
|
|
1565
|
+
id: options.id,
|
|
1566
|
+
order: options.order ?? 0,
|
|
1567
|
+
label:
|
|
1568
|
+
typeof options.label === 'function'
|
|
1569
|
+
? options.label()
|
|
1570
|
+
: options.label ?? options.id,
|
|
1571
|
+
}))
|
|
1572
|
+
.sort((a, b) => a.order - b.order)
|
|
1573
|
+
} catch {
|
|
1574
|
+
panelEntries = []
|
|
1575
|
+
}
|
|
1576
|
+
stampPanelRows()
|
|
1577
|
+
syncStyle()
|
|
1578
|
+
notify()
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
ctx.effect(() => ctx.slots.subscribe('sidebar.panellist', syncPanels), 'dsh-sidebar-declutter: panel registry')
|
|
1582
|
+
const locale = (() => {
|
|
1583
|
+
try {
|
|
1584
|
+
return ctx.get('locale')
|
|
1585
|
+
} catch {
|
|
1586
|
+
return null
|
|
1587
|
+
}
|
|
1588
|
+
})()
|
|
1589
|
+
if (locale !== null && typeof locale?.subscribe === 'function') {
|
|
1590
|
+
ctx.effect(() => locale.subscribe(syncPanels), 'dsh-sidebar-declutter: panel labels')
|
|
1591
|
+
}
|
|
1592
|
+
syncPanels()
|
|
1593
|
+
ctx.effect(() => watchPanelRows(), 'dsh-sidebar-declutter: panel rows')
|
|
1594
|
+
|
|
1595
|
+
ctx.effect(() => {
|
|
1596
|
+
registerTeardown(() => {
|
|
1597
|
+
if (picker !== null) stopPicker()
|
|
1598
|
+
})
|
|
1599
|
+
return () => {
|
|
1600
|
+
if (picker !== null) stopPicker()
|
|
1601
|
+
}
|
|
1602
|
+
}, 'dsh-sidebar-declutter: pick teardown')
|
|
1603
|
+
|
|
1604
|
+
ctx.effect(() => {
|
|
1605
|
+
const tag = syncStyle()
|
|
1606
|
+
|
|
1607
|
+
// Diagnose selector drift after an upgrade instead of failing silently:
|
|
1608
|
+
// the sidebar is on screen but nothing matches the button.
|
|
1609
|
+
let timer = null
|
|
1610
|
+
if (typeof document !== 'undefined' && values.newSession) {
|
|
1611
|
+
timer = setTimeout(() => {
|
|
1612
|
+
timer = null
|
|
1613
|
+
if (document.querySelector('[class*="_panelList"]') === null) return
|
|
1614
|
+
if (document.querySelector('button[class*="_newSession"]') !== null) return
|
|
1615
|
+
console.warn(
|
|
1616
|
+
'[dsh-sidebar-declutter] 侧栏已渲染但没匹配到新会话按钮,选择器可能随版本变化。',
|
|
1617
|
+
)
|
|
1618
|
+
}, 2000)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
return () => {
|
|
1622
|
+
if (timer !== null) clearTimeout(timer)
|
|
1623
|
+
if (tag !== null) tag.remove()
|
|
1624
|
+
}
|
|
1625
|
+
}, 'dsh-sidebar-declutter: stylesheet')
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
ctx.effect(() =>
|
|
1629
|
+
ctx.slots.inject('sidebar.footer.action', () =>
|
|
1630
|
+
ctx.slots.register(
|
|
1631
|
+
{ name: 'sidebar.footer.action', id: 'sidebar-declutter-collapse', order: 10 },
|
|
1632
|
+
CollapseAction,
|
|
1633
|
+
),
|
|
1634
|
+
),
|
|
1635
|
+
)
|
|
1636
|
+
|
|
1637
|
+
// Same two-slot pattern the shipped panels use: the sidebar row only
|
|
1638
|
+
// carries the icon and label, and the main slot with the same key is the
|
|
1639
|
+
// page the shell routes #/panel/<id> to. Without the second registration
|
|
1640
|
+
// the click falls back to whatever panel was open before.
|
|
1641
|
+
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
1642
|
+
ctx.effect(() =>
|
|
1643
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
1644
|
+
ctx.slots.register(
|
|
1645
|
+
{
|
|
1646
|
+
name: 'sidebar.panellist',
|
|
1647
|
+
id: PANEL_ENTRY_ID,
|
|
1648
|
+
order: 20,
|
|
1649
|
+
label: () => PANEL_ENTRY_LABEL,
|
|
1650
|
+
},
|
|
1651
|
+
PanelEntry,
|
|
1652
|
+
),
|
|
1653
|
+
),
|
|
1654
|
+
)
|
|
1655
|
+
ctx.effect(() =>
|
|
1656
|
+
ctx.slots.inject('main', () =>
|
|
1657
|
+
ctx.slots.register({ name: 'main', key: PANEL_ENTRY_ID }, SettingsPage),
|
|
1658
|
+
),
|
|
1659
|
+
)
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (ENABLE_ROW_CONFIG) {
|
|
1663
|
+
ctx.effect(() =>
|
|
1664
|
+
ctx.slots.inject('plugins.row.config', () =>
|
|
1665
|
+
ctx.slots.register({ name: 'plugins.row.config', key: ROW_CONFIG_KEY }, SettingsCard),
|
|
1666
|
+
),
|
|
1667
|
+
)
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Menu rows are plain DOM: sweep after every click that could have opened
|
|
1671
|
+
// one, never with a body-level observer (the chat stream mutates per token).
|
|
1672
|
+
ctx.effect(() => {
|
|
1673
|
+
const sweep = (event) => {
|
|
1674
|
+
const target = event && event.target
|
|
1675
|
+
const fromRow = !!(target && target.closest && target.closest('[class*="_sessionRow"]'))
|
|
1676
|
+
menuScheduleSweep(fromRow)
|
|
1677
|
+
}
|
|
1678
|
+
document.addEventListener('click', sweep, true)
|
|
1679
|
+
document.addEventListener('contextmenu', sweep, true)
|
|
1680
|
+
return () => {
|
|
1681
|
+
document.removeEventListener('click', sweep, true)
|
|
1682
|
+
document.removeEventListener('contextmenu', sweep, true)
|
|
1683
|
+
menuStopWatch()
|
|
1684
|
+
}
|
|
1685
|
+
}, 'dsh-sidebar-declutter: menu rows')
|
|
1686
|
+
},
|
|
1687
|
+
}
|
|
1688
|
+
},
|
|
1689
|
+
})
|