@lengduan/dsh-client-ui-skin-815 0.1.21 → 0.1.22
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/CHANGELOG.md +177 -167
- package/LICENSE +21 -21
- package/NOTICE +11 -11
- package/README.md +128 -128
- package/cordis.patch.yml +4 -4
- package/package.json +55 -51
- package/skin.json +22 -22
- package/src/client/index.ts +300 -300
- package/src/client/titlebar-brand.ts +8 -8
- package/src/client/vj815.module.css +971 -971
- package/src/index.ts +4 -4
- package/tests/apply.spec.ts +59 -59
package/src/client/index.ts
CHANGED
|
@@ -1,300 +1,300 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 1945 终战皮肤。客户端入口只做展示层:南京受降油画背景、token 覆盖。
|
|
3
|
-
* effect 销毁器还原全部 CSS/DOM 写入。
|
|
4
|
-
*/
|
|
5
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
6
|
-
import { SURRENDER_PHOTO_2K, SURRENDER_PHOTO_4K } from './art.generated.ts'
|
|
7
|
-
import { MAID_ATELIER_TITLEBAR_BRAND } from './titlebar-brand.ts'
|
|
8
|
-
import './vj815.module.css'
|
|
9
|
-
|
|
10
|
-
const SKIN_TITLE = '一九四五年八月十五日 · DeepSeek Harness'
|
|
11
|
-
const SKIN_OWNER = '815'
|
|
12
|
-
const SKIN_SYSTEM_CHROME_COLOR = '#080a06'
|
|
13
|
-
const SIDEBAR_COLUMN_SELECTOR = ":is([data-pane='sidebar'], [class*='sidebarCol'])"
|
|
14
|
-
|
|
15
|
-
const BACKDROP_PROPERTIES = [
|
|
16
|
-
'background-image',
|
|
17
|
-
'background-position',
|
|
18
|
-
'background-size',
|
|
19
|
-
'background-attachment',
|
|
20
|
-
'background-repeat',
|
|
21
|
-
'background-color',
|
|
22
|
-
'--vj-photo',
|
|
23
|
-
'--vj-sidebar-width',
|
|
24
|
-
'--vj-titlebar-height',
|
|
25
|
-
] as const
|
|
26
|
-
|
|
27
|
-
function decorateTitlebarBrand(ownedNodes: Set<Element>): void {
|
|
28
|
-
const titlebar = document.querySelector<HTMLElement>("[class*='titlebar']")
|
|
29
|
-
if (!titlebar) return
|
|
30
|
-
if (titlebar.querySelector("[data-skin-chrome='titlebar-brand']")) return
|
|
31
|
-
const brand = document.createElement('span')
|
|
32
|
-
brand.dataset.skinChrome = 'titlebar-brand'
|
|
33
|
-
brand.dataset.skinOwner = SKIN_OWNER
|
|
34
|
-
brand.setAttribute('aria-hidden', 'true')
|
|
35
|
-
brand.innerHTML = MAID_ATELIER_TITLEBAR_BRAND
|
|
36
|
-
ownedNodes.add(brand)
|
|
37
|
-
titlebar.prepend(brand)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 从女仆皮 copy:给侧栏 treeitem 打会话标记,选中行才能挂青天白日小旗。
|
|
42
|
-
* @param decoratedElements - 卸载时清除 data-* 的节点集合
|
|
43
|
-
*/
|
|
44
|
-
function decorateWorkspaceTree(decoratedElements: Set<HTMLElement>): void {
|
|
45
|
-
const sidebar = document.querySelector<HTMLElement>(SIDEBAR_COLUMN_SELECTOR)
|
|
46
|
-
if (!sidebar) return
|
|
47
|
-
|
|
48
|
-
sidebar.querySelectorAll<HTMLElement>(
|
|
49
|
-
'[data-vj-session], [data-vj-session-flat]',
|
|
50
|
-
).forEach((element) => {
|
|
51
|
-
delete element.dataset.vjSession
|
|
52
|
-
delete element.dataset.vjSessionFlat
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
sidebar.querySelectorAll<HTMLElement>("[role='tree']").forEach((tree) => {
|
|
56
|
-
const rows = [...tree.querySelectorAll<HTMLElement>("[role='treeitem']")]
|
|
57
|
-
if (tree.matches("[class*='flatList']") && !rows.some(row => row.hasAttribute('aria-expanded'))) {
|
|
58
|
-
rows.filter(row => row.hasAttribute('aria-selected')).forEach((sessionRow) => {
|
|
59
|
-
sessionRow.dataset.vjSession = ''
|
|
60
|
-
sessionRow.dataset.vjSessionFlat = ''
|
|
61
|
-
decoratedElements.add(sessionRow)
|
|
62
|
-
})
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let workspaceRow: HTMLElement | undefined
|
|
67
|
-
let sessionRows: HTMLElement[] = []
|
|
68
|
-
const decorateGroup = (): void => {
|
|
69
|
-
if (!workspaceRow) return
|
|
70
|
-
sessionRows.forEach((sessionRow) => {
|
|
71
|
-
sessionRow.dataset.vjSession = ''
|
|
72
|
-
decoratedElements.add(sessionRow)
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
rows.forEach((row) => {
|
|
77
|
-
if (row.hasAttribute('aria-expanded')) {
|
|
78
|
-
decorateGroup()
|
|
79
|
-
workspaceRow = row
|
|
80
|
-
sessionRows = []
|
|
81
|
-
} else if (workspaceRow && row.hasAttribute('aria-selected')) {
|
|
82
|
-
sessionRows.push(row)
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
decorateGroup()
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 写入皮肤背景,并由 effect 在卸载时整份撤回。
|
|
91
|
-
* @param ctx - 拥有效果生命周期的 Cordis 上下文
|
|
92
|
-
*/
|
|
93
|
-
export function apply(ctx: Context): void {
|
|
94
|
-
const body = document.body
|
|
95
|
-
const originalTitle = document.title
|
|
96
|
-
const previous = new Map<string, string>()
|
|
97
|
-
for (const property of BACKDROP_PROPERTIES) {
|
|
98
|
-
previous.set(property, body.style.getPropertyValue(property))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const ownedNodes = new Set<Element>()
|
|
102
|
-
const decoratedElements = new Set<HTMLElement>()
|
|
103
|
-
let themeColorMeta: HTMLMetaElement | null = null
|
|
104
|
-
let previousThemeColor: string | undefined
|
|
105
|
-
let themeColorObserver: MutationObserver | undefined
|
|
106
|
-
let observer: MutationObserver | undefined
|
|
107
|
-
let titlebarOverlay: WindowControlsOverlay | undefined
|
|
108
|
-
let syncTitlebarHeight: (() => void) | undefined
|
|
109
|
-
let observedSidebar: HTMLElement | undefined
|
|
110
|
-
let resizeObserver: ResizeObserver | undefined
|
|
111
|
-
let syncBackdrop: (() => void) | undefined
|
|
112
|
-
|
|
113
|
-
ctx.effect(() => () => {
|
|
114
|
-
body.removeAttribute('data-dsh-815')
|
|
115
|
-
observer?.disconnect()
|
|
116
|
-
themeColorObserver?.disconnect()
|
|
117
|
-
resizeObserver?.disconnect()
|
|
118
|
-
if (syncBackdrop !== undefined) {
|
|
119
|
-
window.removeEventListener('resize', syncBackdrop)
|
|
120
|
-
}
|
|
121
|
-
if (titlebarOverlay !== undefined && syncTitlebarHeight !== undefined) {
|
|
122
|
-
titlebarOverlay.removeEventListener('geometrychange', syncTitlebarHeight)
|
|
123
|
-
}
|
|
124
|
-
for (const [property, value] of previous) {
|
|
125
|
-
body.style.setProperty(property, value)
|
|
126
|
-
}
|
|
127
|
-
ownedNodes.forEach(element => element.remove())
|
|
128
|
-
decoratedElements.forEach((element) => {
|
|
129
|
-
delete element.dataset.vjSession
|
|
130
|
-
delete element.dataset.vjSessionFlat
|
|
131
|
-
})
|
|
132
|
-
if (themeColorMeta?.isConnected && themeColorMeta.content === SKIN_SYSTEM_CHROME_COLOR) {
|
|
133
|
-
themeColorMeta.content = previousThemeColor ?? ''
|
|
134
|
-
}
|
|
135
|
-
if (document.title === SKIN_TITLE) document.title = originalTitle
|
|
136
|
-
}, 'ui-skin-815: surrender photograph backdrop')
|
|
137
|
-
|
|
138
|
-
const syncSystemChrome = (): void => {
|
|
139
|
-
const meta = document.head.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
|
|
140
|
-
if (meta === null) return
|
|
141
|
-
if (meta !== themeColorMeta) {
|
|
142
|
-
themeColorMeta = meta
|
|
143
|
-
previousThemeColor = meta.content
|
|
144
|
-
}
|
|
145
|
-
if (meta.content !== SKIN_SYSTEM_CHROME_COLOR) meta.content = SKIN_SYSTEM_CHROME_COLOR
|
|
146
|
-
}
|
|
147
|
-
themeColorObserver = new MutationObserver(syncSystemChrome)
|
|
148
|
-
themeColorObserver.observe(document.head, {
|
|
149
|
-
attributes: true,
|
|
150
|
-
attributeFilter: ['content'],
|
|
151
|
-
childList: true,
|
|
152
|
-
subtree: true,
|
|
153
|
-
})
|
|
154
|
-
syncSystemChrome()
|
|
155
|
-
|
|
156
|
-
body.setAttribute('data-dsh-815', '')
|
|
157
|
-
body.style.setProperty('background-position', 'center 42%')
|
|
158
|
-
body.style.setProperty('background-size', 'cover')
|
|
159
|
-
body.style.setProperty('background-attachment', 'fixed')
|
|
160
|
-
body.style.setProperty('background-repeat', 'no-repeat')
|
|
161
|
-
body.style.setProperty('background-color', '#080a06')
|
|
162
|
-
|
|
163
|
-
/** 设备像素宽 >= 2560 用 4K,否则 2K。 */
|
|
164
|
-
syncBackdrop = (): void => {
|
|
165
|
-
const deviceWidth = window.innerWidth * (window.devicePixelRatio || 1)
|
|
166
|
-
const photo = deviceWidth >= 2560 ? SURRENDER_PHOTO_4K : SURRENDER_PHOTO_2K
|
|
167
|
-
const url = `url(${photo})`
|
|
168
|
-
body.style.setProperty('--vj-photo', url)
|
|
169
|
-
body.style.setProperty('background-image', url)
|
|
170
|
-
}
|
|
171
|
-
window.addEventListener('resize', syncBackdrop)
|
|
172
|
-
syncBackdrop()
|
|
173
|
-
|
|
174
|
-
const widthSheet = document.createElement('style')
|
|
175
|
-
widthSheet.dataset.skinChrome = 'sidebar-width-rule'
|
|
176
|
-
widthSheet.dataset.skinOwner = SKIN_OWNER
|
|
177
|
-
ownedNodes.add(widthSheet)
|
|
178
|
-
document.head.append(widthSheet)
|
|
179
|
-
widthSheet.sheet!.insertRule('html, body { background-color: #080a06; --vj-sidebar-width: 280px; --vj-titlebar-height: 0px; }')
|
|
180
|
-
const appendRule = (rule: string): void => {
|
|
181
|
-
widthSheet.sheet!.insertRule(rule, widthSheet.sheet!.cssRules.length)
|
|
182
|
-
}
|
|
183
|
-
appendRule('body[data-dsh-815] [class*="frame"][data-wco] { grid-template-rows: env(titlebar-area-height, 40px) 1fr; }')
|
|
184
|
-
appendRule('body[data-dsh-815] [class*="frame"][data-desktop] { grid-template-rows: 32px 1fr; }')
|
|
185
|
-
const widthRule = widthSheet.sheet!.cssRules[0] as CSSStyleRule
|
|
186
|
-
|
|
187
|
-
syncTitlebarHeight = (): void => {
|
|
188
|
-
const columns = document.querySelector<HTMLElement>(SIDEBAR_COLUMN_SELECTOR)
|
|
189
|
-
if (columns !== null) {
|
|
190
|
-
const top = columns.getBoundingClientRect().top
|
|
191
|
-
if (top > 0) {
|
|
192
|
-
widthRule.style.setProperty('--vj-titlebar-height', `${top}px`)
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (document.querySelector("[class*='frame'][data-desktop]") !== null) {
|
|
197
|
-
widthRule.style.setProperty('--vj-titlebar-height', '32px')
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
widthRule.style.setProperty('--vj-titlebar-height', '0px')
|
|
201
|
-
}
|
|
202
|
-
titlebarOverlay = navigator.windowControlsOverlay
|
|
203
|
-
titlebarOverlay?.addEventListener('geometrychange', syncTitlebarHeight)
|
|
204
|
-
syncTitlebarHeight()
|
|
205
|
-
|
|
206
|
-
const applySidebarWidth = (width: number): void => {
|
|
207
|
-
if (width <= 0) return
|
|
208
|
-
widthRule.style.setProperty('--vj-sidebar-width', `${Math.round(width * 100) / 100}px`)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
212
|
-
resizeObserver = new ResizeObserver((entries) => {
|
|
213
|
-
const entry = entries.at(-1)
|
|
214
|
-
if (!entry) return
|
|
215
|
-
applySidebarWidth(entry.contentRect.width)
|
|
216
|
-
})
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const ensureSidebarObserved = (): void => {
|
|
220
|
-
const sidebar = document.querySelector<HTMLElement>(SIDEBAR_COLUMN_SELECTOR)
|
|
221
|
-
if (!resizeObserver || sidebar === observedSidebar) return
|
|
222
|
-
if (!sidebar) {
|
|
223
|
-
if (observedSidebar) resizeObserver.unobserve(observedSidebar)
|
|
224
|
-
observedSidebar = undefined
|
|
225
|
-
return
|
|
226
|
-
}
|
|
227
|
-
if (observedSidebar) resizeObserver.unobserve(observedSidebar)
|
|
228
|
-
observedSidebar = sidebar
|
|
229
|
-
resizeObserver.observe(sidebar)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
decorateTitlebarBrand(ownedNodes)
|
|
233
|
-
decorateWorkspaceTree(decoratedElements)
|
|
234
|
-
ensureSidebarObserved()
|
|
235
|
-
const initialSidebar = document.querySelector<HTMLElement>(SIDEBAR_COLUMN_SELECTOR)
|
|
236
|
-
if (initialSidebar) applySidebarWidth(initialSidebar.getBoundingClientRect().width)
|
|
237
|
-
|
|
238
|
-
observer = new MutationObserver((records) => {
|
|
239
|
-
let sidebarStructureChanged = false
|
|
240
|
-
let workspaceStateChanged = false
|
|
241
|
-
for (const record of records) {
|
|
242
|
-
if (record.type === 'attributes') {
|
|
243
|
-
const target = record.target instanceof Element ? record.target : undefined
|
|
244
|
-
if ((record.attributeName === 'aria-expanded' || record.attributeName === 'aria-selected')
|
|
245
|
-
&& target !== undefined && target.closest(SIDEBAR_COLUMN_SELECTOR) !== null) {
|
|
246
|
-
workspaceStateChanged = true
|
|
247
|
-
}
|
|
248
|
-
continue
|
|
249
|
-
}
|
|
250
|
-
const appNodes = [...record.addedNodes].filter(node => node instanceof Element)
|
|
251
|
-
if (appNodes.some(node =>
|
|
252
|
-
node instanceof Element
|
|
253
|
-
&& (node.matches(SIDEBAR_COLUMN_SELECTOR)
|
|
254
|
-
|| node.querySelector(SIDEBAR_COLUMN_SELECTOR) !== null
|
|
255
|
-
|| node.matches("[class*='titlebar']")
|
|
256
|
-
|| node.querySelector("[class*='titlebar']") !== null))) {
|
|
257
|
-
sidebarStructureChanged = true
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (sidebarStructureChanged) {
|
|
261
|
-
decorateTitlebarBrand(ownedNodes)
|
|
262
|
-
decorateWorkspaceTree(decoratedElements)
|
|
263
|
-
ensureSidebarObserved()
|
|
264
|
-
syncTitlebarHeight?.()
|
|
265
|
-
} else if (workspaceStateChanged) {
|
|
266
|
-
decorateWorkspaceTree(decoratedElements)
|
|
267
|
-
}
|
|
268
|
-
})
|
|
269
|
-
observer.observe(body, {
|
|
270
|
-
childList: true,
|
|
271
|
-
subtree: true,
|
|
272
|
-
attributes: true,
|
|
273
|
-
attributeFilter: ['aria-expanded', 'aria-selected'],
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
const caption = document.createElement('div')
|
|
277
|
-
caption.dataset.skinChrome = 'caption'
|
|
278
|
-
caption.dataset.skinOwner = SKIN_OWNER
|
|
279
|
-
/* 展签两行:主行画名,副行时间地点事件。 */
|
|
280
|
-
const captionTitle = document.createElement('span')
|
|
281
|
-
captionTitle.dataset.skinChrome = 'caption-title'
|
|
282
|
-
captionTitle.textContent = '陈坚《公元一九四五年九月九日九时》'
|
|
283
|
-
const captionSub = document.createElement('span')
|
|
284
|
-
captionSub.dataset.skinChrome = 'caption-sub'
|
|
285
|
-
captionSub.textContent = '1945.9.9 九时 · 南京 中国战区日军投降签字'
|
|
286
|
-
caption.append(captionTitle, captionSub)
|
|
287
|
-
ownedNodes.add(caption)
|
|
288
|
-
body.append(caption)
|
|
289
|
-
|
|
290
|
-
const favicon = document.createElement('link')
|
|
291
|
-
favicon.rel = 'icon'
|
|
292
|
-
favicon.type = 'image/jpeg'
|
|
293
|
-
favicon.href = SURRENDER_PHOTO_2K
|
|
294
|
-
favicon.dataset.skinChrome = 'favicon'
|
|
295
|
-
favicon.dataset.skinOwner = SKIN_OWNER
|
|
296
|
-
ownedNodes.add(favicon)
|
|
297
|
-
document.head.append(favicon)
|
|
298
|
-
|
|
299
|
-
document.title = SKIN_TITLE
|
|
300
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 1945 终战皮肤。客户端入口只做展示层:南京受降油画背景、token 覆盖。
|
|
3
|
+
* effect 销毁器还原全部 CSS/DOM 写入。
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
6
|
+
import { SURRENDER_PHOTO_2K, SURRENDER_PHOTO_4K } from './art.generated.ts'
|
|
7
|
+
import { MAID_ATELIER_TITLEBAR_BRAND } from './titlebar-brand.ts'
|
|
8
|
+
import './vj815.module.css'
|
|
9
|
+
|
|
10
|
+
const SKIN_TITLE = '一九四五年八月十五日 · DeepSeek Harness'
|
|
11
|
+
const SKIN_OWNER = '815'
|
|
12
|
+
const SKIN_SYSTEM_CHROME_COLOR = '#080a06'
|
|
13
|
+
const SIDEBAR_COLUMN_SELECTOR = ":is([data-pane='sidebar'], [class*='sidebarCol'])"
|
|
14
|
+
|
|
15
|
+
const BACKDROP_PROPERTIES = [
|
|
16
|
+
'background-image',
|
|
17
|
+
'background-position',
|
|
18
|
+
'background-size',
|
|
19
|
+
'background-attachment',
|
|
20
|
+
'background-repeat',
|
|
21
|
+
'background-color',
|
|
22
|
+
'--vj-photo',
|
|
23
|
+
'--vj-sidebar-width',
|
|
24
|
+
'--vj-titlebar-height',
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
function decorateTitlebarBrand(ownedNodes: Set<Element>): void {
|
|
28
|
+
const titlebar = document.querySelector<HTMLElement>("[class*='titlebar']")
|
|
29
|
+
if (!titlebar) return
|
|
30
|
+
if (titlebar.querySelector("[data-skin-chrome='titlebar-brand']")) return
|
|
31
|
+
const brand = document.createElement('span')
|
|
32
|
+
brand.dataset.skinChrome = 'titlebar-brand'
|
|
33
|
+
brand.dataset.skinOwner = SKIN_OWNER
|
|
34
|
+
brand.setAttribute('aria-hidden', 'true')
|
|
35
|
+
brand.innerHTML = MAID_ATELIER_TITLEBAR_BRAND
|
|
36
|
+
ownedNodes.add(brand)
|
|
37
|
+
titlebar.prepend(brand)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 从女仆皮 copy:给侧栏 treeitem 打会话标记,选中行才能挂青天白日小旗。
|
|
42
|
+
* @param decoratedElements - 卸载时清除 data-* 的节点集合
|
|
43
|
+
*/
|
|
44
|
+
function decorateWorkspaceTree(decoratedElements: Set<HTMLElement>): void {
|
|
45
|
+
const sidebar = document.querySelector<HTMLElement>(SIDEBAR_COLUMN_SELECTOR)
|
|
46
|
+
if (!sidebar) return
|
|
47
|
+
|
|
48
|
+
sidebar.querySelectorAll<HTMLElement>(
|
|
49
|
+
'[data-vj-session], [data-vj-session-flat]',
|
|
50
|
+
).forEach((element) => {
|
|
51
|
+
delete element.dataset.vjSession
|
|
52
|
+
delete element.dataset.vjSessionFlat
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
sidebar.querySelectorAll<HTMLElement>("[role='tree']").forEach((tree) => {
|
|
56
|
+
const rows = [...tree.querySelectorAll<HTMLElement>("[role='treeitem']")]
|
|
57
|
+
if (tree.matches("[class*='flatList']") && !rows.some(row => row.hasAttribute('aria-expanded'))) {
|
|
58
|
+
rows.filter(row => row.hasAttribute('aria-selected')).forEach((sessionRow) => {
|
|
59
|
+
sessionRow.dataset.vjSession = ''
|
|
60
|
+
sessionRow.dataset.vjSessionFlat = ''
|
|
61
|
+
decoratedElements.add(sessionRow)
|
|
62
|
+
})
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let workspaceRow: HTMLElement | undefined
|
|
67
|
+
let sessionRows: HTMLElement[] = []
|
|
68
|
+
const decorateGroup = (): void => {
|
|
69
|
+
if (!workspaceRow) return
|
|
70
|
+
sessionRows.forEach((sessionRow) => {
|
|
71
|
+
sessionRow.dataset.vjSession = ''
|
|
72
|
+
decoratedElements.add(sessionRow)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
rows.forEach((row) => {
|
|
77
|
+
if (row.hasAttribute('aria-expanded')) {
|
|
78
|
+
decorateGroup()
|
|
79
|
+
workspaceRow = row
|
|
80
|
+
sessionRows = []
|
|
81
|
+
} else if (workspaceRow && row.hasAttribute('aria-selected')) {
|
|
82
|
+
sessionRows.push(row)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
decorateGroup()
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 写入皮肤背景,并由 effect 在卸载时整份撤回。
|
|
91
|
+
* @param ctx - 拥有效果生命周期的 Cordis 上下文
|
|
92
|
+
*/
|
|
93
|
+
export function apply(ctx: Context): void {
|
|
94
|
+
const body = document.body
|
|
95
|
+
const originalTitle = document.title
|
|
96
|
+
const previous = new Map<string, string>()
|
|
97
|
+
for (const property of BACKDROP_PROPERTIES) {
|
|
98
|
+
previous.set(property, body.style.getPropertyValue(property))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ownedNodes = new Set<Element>()
|
|
102
|
+
const decoratedElements = new Set<HTMLElement>()
|
|
103
|
+
let themeColorMeta: HTMLMetaElement | null = null
|
|
104
|
+
let previousThemeColor: string | undefined
|
|
105
|
+
let themeColorObserver: MutationObserver | undefined
|
|
106
|
+
let observer: MutationObserver | undefined
|
|
107
|
+
let titlebarOverlay: WindowControlsOverlay | undefined
|
|
108
|
+
let syncTitlebarHeight: (() => void) | undefined
|
|
109
|
+
let observedSidebar: HTMLElement | undefined
|
|
110
|
+
let resizeObserver: ResizeObserver | undefined
|
|
111
|
+
let syncBackdrop: (() => void) | undefined
|
|
112
|
+
|
|
113
|
+
ctx.effect(() => () => {
|
|
114
|
+
body.removeAttribute('data-dsh-815')
|
|
115
|
+
observer?.disconnect()
|
|
116
|
+
themeColorObserver?.disconnect()
|
|
117
|
+
resizeObserver?.disconnect()
|
|
118
|
+
if (syncBackdrop !== undefined) {
|
|
119
|
+
window.removeEventListener('resize', syncBackdrop)
|
|
120
|
+
}
|
|
121
|
+
if (titlebarOverlay !== undefined && syncTitlebarHeight !== undefined) {
|
|
122
|
+
titlebarOverlay.removeEventListener('geometrychange', syncTitlebarHeight)
|
|
123
|
+
}
|
|
124
|
+
for (const [property, value] of previous) {
|
|
125
|
+
body.style.setProperty(property, value)
|
|
126
|
+
}
|
|
127
|
+
ownedNodes.forEach(element => element.remove())
|
|
128
|
+
decoratedElements.forEach((element) => {
|
|
129
|
+
delete element.dataset.vjSession
|
|
130
|
+
delete element.dataset.vjSessionFlat
|
|
131
|
+
})
|
|
132
|
+
if (themeColorMeta?.isConnected && themeColorMeta.content === SKIN_SYSTEM_CHROME_COLOR) {
|
|
133
|
+
themeColorMeta.content = previousThemeColor ?? ''
|
|
134
|
+
}
|
|
135
|
+
if (document.title === SKIN_TITLE) document.title = originalTitle
|
|
136
|
+
}, 'ui-skin-815: surrender photograph backdrop')
|
|
137
|
+
|
|
138
|
+
const syncSystemChrome = (): void => {
|
|
139
|
+
const meta = document.head.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
|
|
140
|
+
if (meta === null) return
|
|
141
|
+
if (meta !== themeColorMeta) {
|
|
142
|
+
themeColorMeta = meta
|
|
143
|
+
previousThemeColor = meta.content
|
|
144
|
+
}
|
|
145
|
+
if (meta.content !== SKIN_SYSTEM_CHROME_COLOR) meta.content = SKIN_SYSTEM_CHROME_COLOR
|
|
146
|
+
}
|
|
147
|
+
themeColorObserver = new MutationObserver(syncSystemChrome)
|
|
148
|
+
themeColorObserver.observe(document.head, {
|
|
149
|
+
attributes: true,
|
|
150
|
+
attributeFilter: ['content'],
|
|
151
|
+
childList: true,
|
|
152
|
+
subtree: true,
|
|
153
|
+
})
|
|
154
|
+
syncSystemChrome()
|
|
155
|
+
|
|
156
|
+
body.setAttribute('data-dsh-815', '')
|
|
157
|
+
body.style.setProperty('background-position', 'center 42%')
|
|
158
|
+
body.style.setProperty('background-size', 'cover')
|
|
159
|
+
body.style.setProperty('background-attachment', 'fixed')
|
|
160
|
+
body.style.setProperty('background-repeat', 'no-repeat')
|
|
161
|
+
body.style.setProperty('background-color', '#080a06')
|
|
162
|
+
|
|
163
|
+
/** 设备像素宽 >= 2560 用 4K,否则 2K。 */
|
|
164
|
+
syncBackdrop = (): void => {
|
|
165
|
+
const deviceWidth = window.innerWidth * (window.devicePixelRatio || 1)
|
|
166
|
+
const photo = deviceWidth >= 2560 ? SURRENDER_PHOTO_4K : SURRENDER_PHOTO_2K
|
|
167
|
+
const url = `url(${photo})`
|
|
168
|
+
body.style.setProperty('--vj-photo', url)
|
|
169
|
+
body.style.setProperty('background-image', url)
|
|
170
|
+
}
|
|
171
|
+
window.addEventListener('resize', syncBackdrop)
|
|
172
|
+
syncBackdrop()
|
|
173
|
+
|
|
174
|
+
const widthSheet = document.createElement('style')
|
|
175
|
+
widthSheet.dataset.skinChrome = 'sidebar-width-rule'
|
|
176
|
+
widthSheet.dataset.skinOwner = SKIN_OWNER
|
|
177
|
+
ownedNodes.add(widthSheet)
|
|
178
|
+
document.head.append(widthSheet)
|
|
179
|
+
widthSheet.sheet!.insertRule('html, body { background-color: #080a06; --vj-sidebar-width: 280px; --vj-titlebar-height: 0px; }')
|
|
180
|
+
const appendRule = (rule: string): void => {
|
|
181
|
+
widthSheet.sheet!.insertRule(rule, widthSheet.sheet!.cssRules.length)
|
|
182
|
+
}
|
|
183
|
+
appendRule('body[data-dsh-815] [class*="frame"][data-wco] { grid-template-rows: env(titlebar-area-height, 40px) 1fr; }')
|
|
184
|
+
appendRule('body[data-dsh-815] [class*="frame"][data-desktop] { grid-template-rows: 32px 1fr; }')
|
|
185
|
+
const widthRule = widthSheet.sheet!.cssRules[0] as CSSStyleRule
|
|
186
|
+
|
|
187
|
+
syncTitlebarHeight = (): void => {
|
|
188
|
+
const columns = document.querySelector<HTMLElement>(SIDEBAR_COLUMN_SELECTOR)
|
|
189
|
+
if (columns !== null) {
|
|
190
|
+
const top = columns.getBoundingClientRect().top
|
|
191
|
+
if (top > 0) {
|
|
192
|
+
widthRule.style.setProperty('--vj-titlebar-height', `${top}px`)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (document.querySelector("[class*='frame'][data-desktop]") !== null) {
|
|
197
|
+
widthRule.style.setProperty('--vj-titlebar-height', '32px')
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
widthRule.style.setProperty('--vj-titlebar-height', '0px')
|
|
201
|
+
}
|
|
202
|
+
titlebarOverlay = navigator.windowControlsOverlay
|
|
203
|
+
titlebarOverlay?.addEventListener('geometrychange', syncTitlebarHeight)
|
|
204
|
+
syncTitlebarHeight()
|
|
205
|
+
|
|
206
|
+
const applySidebarWidth = (width: number): void => {
|
|
207
|
+
if (width <= 0) return
|
|
208
|
+
widthRule.style.setProperty('--vj-sidebar-width', `${Math.round(width * 100) / 100}px`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
212
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
213
|
+
const entry = entries.at(-1)
|
|
214
|
+
if (!entry) return
|
|
215
|
+
applySidebarWidth(entry.contentRect.width)
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ensureSidebarObserved = (): void => {
|
|
220
|
+
const sidebar = document.querySelector<HTMLElement>(SIDEBAR_COLUMN_SELECTOR)
|
|
221
|
+
if (!resizeObserver || sidebar === observedSidebar) return
|
|
222
|
+
if (!sidebar) {
|
|
223
|
+
if (observedSidebar) resizeObserver.unobserve(observedSidebar)
|
|
224
|
+
observedSidebar = undefined
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
if (observedSidebar) resizeObserver.unobserve(observedSidebar)
|
|
228
|
+
observedSidebar = sidebar
|
|
229
|
+
resizeObserver.observe(sidebar)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
decorateTitlebarBrand(ownedNodes)
|
|
233
|
+
decorateWorkspaceTree(decoratedElements)
|
|
234
|
+
ensureSidebarObserved()
|
|
235
|
+
const initialSidebar = document.querySelector<HTMLElement>(SIDEBAR_COLUMN_SELECTOR)
|
|
236
|
+
if (initialSidebar) applySidebarWidth(initialSidebar.getBoundingClientRect().width)
|
|
237
|
+
|
|
238
|
+
observer = new MutationObserver((records) => {
|
|
239
|
+
let sidebarStructureChanged = false
|
|
240
|
+
let workspaceStateChanged = false
|
|
241
|
+
for (const record of records) {
|
|
242
|
+
if (record.type === 'attributes') {
|
|
243
|
+
const target = record.target instanceof Element ? record.target : undefined
|
|
244
|
+
if ((record.attributeName === 'aria-expanded' || record.attributeName === 'aria-selected')
|
|
245
|
+
&& target !== undefined && target.closest(SIDEBAR_COLUMN_SELECTOR) !== null) {
|
|
246
|
+
workspaceStateChanged = true
|
|
247
|
+
}
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
const appNodes = [...record.addedNodes].filter(node => node instanceof Element)
|
|
251
|
+
if (appNodes.some(node =>
|
|
252
|
+
node instanceof Element
|
|
253
|
+
&& (node.matches(SIDEBAR_COLUMN_SELECTOR)
|
|
254
|
+
|| node.querySelector(SIDEBAR_COLUMN_SELECTOR) !== null
|
|
255
|
+
|| node.matches("[class*='titlebar']")
|
|
256
|
+
|| node.querySelector("[class*='titlebar']") !== null))) {
|
|
257
|
+
sidebarStructureChanged = true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (sidebarStructureChanged) {
|
|
261
|
+
decorateTitlebarBrand(ownedNodes)
|
|
262
|
+
decorateWorkspaceTree(decoratedElements)
|
|
263
|
+
ensureSidebarObserved()
|
|
264
|
+
syncTitlebarHeight?.()
|
|
265
|
+
} else if (workspaceStateChanged) {
|
|
266
|
+
decorateWorkspaceTree(decoratedElements)
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
observer.observe(body, {
|
|
270
|
+
childList: true,
|
|
271
|
+
subtree: true,
|
|
272
|
+
attributes: true,
|
|
273
|
+
attributeFilter: ['aria-expanded', 'aria-selected'],
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const caption = document.createElement('div')
|
|
277
|
+
caption.dataset.skinChrome = 'caption'
|
|
278
|
+
caption.dataset.skinOwner = SKIN_OWNER
|
|
279
|
+
/* 展签两行:主行画名,副行时间地点事件。 */
|
|
280
|
+
const captionTitle = document.createElement('span')
|
|
281
|
+
captionTitle.dataset.skinChrome = 'caption-title'
|
|
282
|
+
captionTitle.textContent = '陈坚《公元一九四五年九月九日九时》'
|
|
283
|
+
const captionSub = document.createElement('span')
|
|
284
|
+
captionSub.dataset.skinChrome = 'caption-sub'
|
|
285
|
+
captionSub.textContent = '1945.9.9 九时 · 南京 中国战区日军投降签字'
|
|
286
|
+
caption.append(captionTitle, captionSub)
|
|
287
|
+
ownedNodes.add(caption)
|
|
288
|
+
body.append(caption)
|
|
289
|
+
|
|
290
|
+
const favicon = document.createElement('link')
|
|
291
|
+
favicon.rel = 'icon'
|
|
292
|
+
favicon.type = 'image/jpeg'
|
|
293
|
+
favicon.href = SURRENDER_PHOTO_2K
|
|
294
|
+
favicon.dataset.skinChrome = 'favicon'
|
|
295
|
+
favicon.dataset.skinOwner = SKIN_OWNER
|
|
296
|
+
ownedNodes.add(favicon)
|
|
297
|
+
document.head.append(favicon)
|
|
298
|
+
|
|
299
|
+
document.title = SKIN_TITLE
|
|
300
|
+
}
|