@ash-qw/dsh-theme-prts 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.en.md +12 -2
- package/README.md +16 -4
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/THIRD_PARTY_NOTICES.zh-CN.md +17 -0
- package/lib/client.js +1425 -44
- package/lib/index.js +1 -1
- package/package.json +3 -1
- package/src/client/assistant-glass-adapter.js +45 -2
- package/src/client/composer-glass-adapter.js +20 -1
- package/src/client/composer-glass-surface.js +85 -0
- package/src/client/custom-background.js +140 -0
- package/src/client/index.js +58 -31
- package/src/client/native-preferences.js +92 -0
- package/src/client/opacity-dial.js +137 -0
- package/src/client/operations-shell-v2.js +2 -2
- package/src/client/preferences.js +33 -3
- package/src/client/settings-page.js +7 -2
- package/src/client/theme-controller-v2.js +14 -0
- package/src/client/theme-settings-workbench.js +25 -8
- package/src/index.js +16 -2
- package/src/server/backgrounds.js +97 -0
- package/src/styles/cursor.css +2 -0
- package/src/styles/custom-background.css +326 -0
- package/src/styles/prts.css +90 -0
- package/src/styles/refinement.css +9 -0
- package/src/styles/theme-settings-workbench.css +3 -0
- package/src/vendor/react-bits/GlassSurface.css +168 -0
- package/src/vendor/react-bits/GlassSurface.jsx +818 -0
- package/src/vendor/react-bits/LICENSE.md +25 -0
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { PACKAGE_ID, THEME_PLUGIN_ID, inject, apply } from '../src/index.js'
|
|
1
|
+
export { PACKAGE_ID, THEME_PLUGIN_ID, Config, inject, apply } from '../src/index.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ash-qw/dsh-theme-prts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web and the official desktop client",
|
|
@@ -73,10 +73,12 @@
|
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@playwright/test": "1.55.1",
|
|
76
|
+
"esbuild": "^0.28.2",
|
|
76
77
|
"jsdom": "24.1.3",
|
|
77
78
|
"markdown-it": "^14.3.2"
|
|
78
79
|
},
|
|
79
80
|
"dependencies": {
|
|
81
|
+
"@deepseek-ai/schemastery": "~3.18.4",
|
|
80
82
|
"sharp": "^0.35.5"
|
|
81
83
|
}
|
|
82
84
|
}
|
|
@@ -106,6 +106,36 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
|
|
|
106
106
|
let observationRoot
|
|
107
107
|
let groupAvatars = true
|
|
108
108
|
let avatarsDirty = true
|
|
109
|
+
let loadingTransitions = false
|
|
110
|
+
const streamingSteps = new Set()
|
|
111
|
+
const revealAnimations = new Map()
|
|
112
|
+
function cancelReveal(step) {
|
|
113
|
+
const animations = revealAnimations.get(step)
|
|
114
|
+
revealAnimations.delete(step)
|
|
115
|
+
for (const animation of animations ?? []) animation.cancel()
|
|
116
|
+
}
|
|
117
|
+
function revealResponse(step, surface) {
|
|
118
|
+
cancelReveal(step)
|
|
119
|
+
if (!loadingTransitions || !surface?.animate || !surface.isConnected
|
|
120
|
+
|| document.documentElement.getAttribute('data-prts-motion') === 'reduced'
|
|
121
|
+
|| window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
122
|
+
|| step.closest('[hidden], [data-turn-process-hidden="true"]')) return
|
|
123
|
+
const bounds = surface.getBoundingClientRect()
|
|
124
|
+
if (!bounds.width || !bounds.height) return
|
|
125
|
+
const animations = [surface.animate([
|
|
126
|
+
{ width: '78px', height: '42px', overflow: 'clip' },
|
|
127
|
+
{ width: `${bounds.width}px`, height: `${bounds.height}px`, overflow: 'clip' },
|
|
128
|
+
], { duration: 220, easing: 'cubic-bezier(.2,.7,.2,1)' })]
|
|
129
|
+
for (const child of surface.children) {
|
|
130
|
+
if (!child.animate) continue
|
|
131
|
+
animations.push(child.animate([{ opacity: 0 }, { opacity: 1 }],
|
|
132
|
+
{ duration: 160, delay: 80, fill: 'backwards', easing: 'ease-out' }))
|
|
133
|
+
}
|
|
134
|
+
revealAnimations.set(step, animations)
|
|
135
|
+
void Promise.all(animations.map(animation => animation.finished)).catch(() => {}).then(() => {
|
|
136
|
+
if (revealAnimations.get(step) === animations) revealAnimations.delete(step)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
109
139
|
const avatarSteps = new Set()
|
|
110
140
|
const emptySteps = new Set()
|
|
111
141
|
function refreshAvatars() {
|
|
@@ -210,6 +240,8 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
|
|
|
210
240
|
}
|
|
211
241
|
|
|
212
242
|
function releaseStep(step) {
|
|
243
|
+
streamingSteps.delete(step)
|
|
244
|
+
cancelReveal(step)
|
|
213
245
|
const previousSurface = stepSurfaces.get(step)
|
|
214
246
|
stepSurfaces.delete(step)
|
|
215
247
|
ownedSteps.delete(step)
|
|
@@ -241,6 +273,13 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
|
|
|
241
273
|
surface.setAttribute(ASSISTANT_SURFACE_ATTRIBUTE, kind)
|
|
242
274
|
ownedSurfaces.add(surface)
|
|
243
275
|
stepSurfaces.set(step, surface)
|
|
276
|
+
const streaming = surface.parentElement?.hasAttribute('data-streaming') === true
|
|
277
|
+
const wasStreaming = streamingSteps.has(step)
|
|
278
|
+
if (streaming) { streamingSteps.add(step); cancelReveal(step) }
|
|
279
|
+
else {
|
|
280
|
+
streamingSteps.delete(step)
|
|
281
|
+
if (wasStreaming) revealResponse(step, surface)
|
|
282
|
+
}
|
|
244
283
|
}
|
|
245
284
|
|
|
246
285
|
function scan() {
|
|
@@ -301,7 +340,9 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
|
|
|
301
340
|
}
|
|
302
341
|
|
|
303
342
|
return {
|
|
304
|
-
start({ groupAvatars: enabled = true } = {}) {
|
|
343
|
+
start({ groupAvatars: enabled = true, loading = false } = {}) {
|
|
344
|
+
loadingTransitions = loading
|
|
345
|
+
if (!loading) for (const step of [...revealAnimations.keys()]) cancelReveal(step)
|
|
305
346
|
if (!document) return
|
|
306
347
|
if (groupAvatars !== enabled) {
|
|
307
348
|
groupAvatars = enabled
|
|
@@ -324,7 +365,7 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
|
|
|
324
365
|
childList: true,
|
|
325
366
|
characterData: true,
|
|
326
367
|
attributes: true,
|
|
327
|
-
attributeFilter: ['class', 'data-chat-flow-kind', 'data-chat-group-part', 'data-markdown', 'data-slot', 'data-chat-turn', 'hidden', 'data-turn-process-hidden', 'data-turn-process-member', 'data-turn-process-answer'],
|
|
368
|
+
attributeFilter: ['data-streaming', 'class', 'data-chat-flow-kind', 'data-chat-group-part', 'data-markdown', 'data-slot', 'data-chat-turn', 'hidden', 'data-turn-process-hidden', 'data-turn-process-member', 'data-turn-process-answer'],
|
|
328
369
|
})
|
|
329
370
|
},
|
|
330
371
|
dispose() {
|
|
@@ -335,6 +376,8 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
|
|
|
335
376
|
else window?.clearTimeout?.(scanFrame)
|
|
336
377
|
}
|
|
337
378
|
scanFrame = undefined
|
|
379
|
+
for (const step of [...revealAnimations.keys()]) cancelReveal(step)
|
|
380
|
+
streamingSteps.clear()
|
|
338
381
|
removeMarkers()
|
|
339
382
|
for (const step of avatarSteps) step.removeAttribute('data-prts-avatar-hidden')
|
|
340
383
|
avatarSteps.clear()
|
|
@@ -57,6 +57,13 @@ function markPopup(popup, kind) {
|
|
|
57
57
|
const card = popup.closest?.('[data-trigger-menu], [class*="mufS8W_card"]')
|
|
58
58
|
mark(card, MENU_ATTRIBUTE, kind)
|
|
59
59
|
for (const nested of popup.querySelectorAll?.('[role="menu"], [role="listbox"], [data-trigger-menu], [class*="mufS8W_card"]') ?? []) {
|
|
60
|
+
// The model picker replaces its contents with a scrolling role=menu.
|
|
61
|
+
// That list is content inside the material shell, not another popup.
|
|
62
|
+
if (kind === 'model' && popup.hasAttribute('data-menu-material')
|
|
63
|
+
&& !nested.hasAttribute('data-menu-material')) {
|
|
64
|
+
nested.removeAttribute(MENU_ATTRIBUTE)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
60
67
|
mark(nested, MENU_ATTRIBUTE, kind)
|
|
61
68
|
}
|
|
62
69
|
}
|
|
@@ -111,7 +118,17 @@ export function createComposerGlassAdapter({ document, window }) {
|
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
function onMutations(mutations) {
|
|
114
|
-
if (mutations.some(mutationTouchesComposer))
|
|
121
|
+
if (!mutations.some(mutationTouchesComposer)) return
|
|
122
|
+
const popupSelector = '[role="menu"], [role="listbox"], [data-trigger-menu]'
|
|
123
|
+
const openingPopup = mutations.some(mutation =>
|
|
124
|
+
(mutation.type === 'attributes' && ['aria-expanded', 'aria-controls'].includes(mutation.attributeName))
|
|
125
|
+
|| [...(mutation.addedNodes ?? [])].some(node => node.nodeType === 1
|
|
126
|
+
&& (node.matches?.(popupSelector) || node.querySelector?.(popupSelector))),
|
|
127
|
+
)
|
|
128
|
+
// Classify new popups in the mutation microtask, before the first paint.
|
|
129
|
+
// Waiting for another animation frame can expose their native material.
|
|
130
|
+
if (openingPopup) scan()
|
|
131
|
+
else queueScan()
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
function queueScan() {
|
|
@@ -185,6 +202,7 @@ export function createComposerGlassAdapter({ document, window }) {
|
|
|
185
202
|
if (trigger.getAttribute('aria-expanded') !== 'true') continue
|
|
186
203
|
const popup = controlledPopup(document, trigger)
|
|
187
204
|
?? localPopup(trigger)
|
|
205
|
+
?? document.querySelector('[role="menu"]:has([class*="cubgiG_item"])')
|
|
188
206
|
markPopup(popup, 'preset')
|
|
189
207
|
}
|
|
190
208
|
|
|
@@ -217,6 +235,7 @@ export function createComposerGlassAdapter({ document, window }) {
|
|
|
217
235
|
?? document.querySelector('[data-slot="main.conversation"], [data-slot="conversation"]')
|
|
218
236
|
if (!observationRoot) return
|
|
219
237
|
observer = new Observer(onMutations)
|
|
238
|
+
if (observationRoot !== document.body) observer.observe(document.body, { childList: true })
|
|
220
239
|
observer.observe(observationRoot, {
|
|
221
240
|
subtree: true,
|
|
222
241
|
childList: true,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Keep host content in place; mount upstream GlassSurface only as a background.
|
|
2
|
+
export function createComposerGlassSurface({document, window, mount}) {
|
|
3
|
+
const surfaces = new Map()
|
|
4
|
+
let observer, resize, frame
|
|
5
|
+
function remove(card, entry) {
|
|
6
|
+
window.clearTimeout(entry.resizeTimer)
|
|
7
|
+
resize?.unobserve(card)
|
|
8
|
+
entry.unmount()
|
|
9
|
+
entry.container.remove()
|
|
10
|
+
card.removeAttribute('data-prts-composer-glass-active')
|
|
11
|
+
surfaces.delete(card)
|
|
12
|
+
}
|
|
13
|
+
function sync() {
|
|
14
|
+
frame = undefined
|
|
15
|
+
const cards = new Set(document.querySelectorAll('[data-composer-card], [data-prts-composer-fallback]'))
|
|
16
|
+
for (const [card, entry] of surfaces) if (!cards.has(card)) remove(card, entry)
|
|
17
|
+
for (const card of cards) {
|
|
18
|
+
// Use layout dimensions, unaffected by host entrance/resize transforms.
|
|
19
|
+
const w = card.offsetWidth
|
|
20
|
+
const h = card.offsetHeight
|
|
21
|
+
if (!w || !h) continue
|
|
22
|
+
let entry = surfaces.get(card)
|
|
23
|
+
if (entry?.width === w && entry?.height === h) continue
|
|
24
|
+
const points = [
|
|
25
|
+
[3,0],[w/2-42,0],[w/2-37,5],[w/2+37,5],[w/2+42,0],
|
|
26
|
+
[w-3,0],[w,3],[w,h-3],[w-3,h],[3,h],[0,h-3],[0,3],
|
|
27
|
+
].map(([x,y])=>[x+16,y+16].join(',')).join(' ')
|
|
28
|
+
const outline = '<polygon fill="white" points="'+points+'"/>'
|
|
29
|
+
const shape = 'data:image/svg+xml,' + encodeURIComponent(
|
|
30
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="'+(w+32)+'" height="'+(h+32)+'" viewBox="0 0 '+(w+32)+' '+(h+32)+'">'+outline+'</svg>',
|
|
31
|
+
)
|
|
32
|
+
if (!entry) {
|
|
33
|
+
const container = document.createElement('div')
|
|
34
|
+
container.setAttribute('data-prts-composer-glass-surface', '')
|
|
35
|
+
container.setAttribute('aria-hidden', 'true')
|
|
36
|
+
card.prepend(container)
|
|
37
|
+
entry = {container, shape, width:w, height:h, unmount:mount(container, {shape})}
|
|
38
|
+
surfaces.set(card, entry)
|
|
39
|
+
card.setAttribute('data-prts-composer-glass-active', '')
|
|
40
|
+
resize?.observe(card)
|
|
41
|
+
} else if (entry.shape !== shape) {
|
|
42
|
+
entry.shape = shape
|
|
43
|
+
entry.width = w
|
|
44
|
+
entry.height = h
|
|
45
|
+
// CSS owns the live outline. Rebuild the costly refraction map once
|
|
46
|
+
// resizing settles, instead of decoding a new image on every frame.
|
|
47
|
+
window.clearTimeout(entry.resizeTimer)
|
|
48
|
+
entry.resizeTimer = window.setTimeout(() => {
|
|
49
|
+
entry.resizeTimer = undefined
|
|
50
|
+
entry.unmount.update({shape:entry.shape})
|
|
51
|
+
}, 100)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function queue() {
|
|
56
|
+
if (frame === undefined) frame = window.requestAnimationFrame(sync)
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
start() {
|
|
60
|
+
if (!mount) return
|
|
61
|
+
document.documentElement.setAttribute('data-prts-composer-enhanced', '')
|
|
62
|
+
if (!observer) {
|
|
63
|
+
resize = new window.ResizeObserver(queue)
|
|
64
|
+
observer = new window.MutationObserver(records => {
|
|
65
|
+
const selector = '[data-composer-card], [data-prts-composer-fallback]'
|
|
66
|
+
if (records.some(record => record.type === 'attributes'
|
|
67
|
+
|| [...record.addedNodes, ...record.removedNodes].some(node =>
|
|
68
|
+
node.nodeType === 1 && (node.matches(selector) || node.querySelector(selector))))) queue()
|
|
69
|
+
})
|
|
70
|
+
observer.observe(document.body, {childList:true, subtree:true, attributes:true, attributeFilter:['data-composer-card','data-prts-composer-fallback']})
|
|
71
|
+
}
|
|
72
|
+
sync()
|
|
73
|
+
},
|
|
74
|
+
dispose() {
|
|
75
|
+
document.documentElement.removeAttribute('data-prts-composer-enhanced')
|
|
76
|
+
observer?.disconnect()
|
|
77
|
+
observer = undefined
|
|
78
|
+
resize?.disconnect()
|
|
79
|
+
window.cancelAnimationFrame?.(frame)
|
|
80
|
+
frame = undefined
|
|
81
|
+
for (const [card, entry] of surfaces) remove(card, entry)
|
|
82
|
+
resize = undefined
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { opacityDialMarkup, mountOpacityDials } from './opacity-dial.js'
|
|
2
|
+
const BACKGROUND_ENDPOINT = '/prts-theme-api/backgrounds'
|
|
3
|
+
const backgroundLabels = { sidebar: '侧边栏', conversation: '会话正文', full: '完整背景' }
|
|
4
|
+
function backgroundImageUrl(entry) {
|
|
5
|
+
return /^[a-f0-9-]{36}$/.test(entry?.id ?? '') ? `${BACKGROUND_ENDPOINT}/image/${entry.id}` : null
|
|
6
|
+
}
|
|
7
|
+
export function createBackgroundStore({ window, document }) {
|
|
8
|
+
let state = { revision: -1, sidebar: null, conversation: null, full: null }, disposed = false, events, refreshing
|
|
9
|
+
const listeners = new Set()
|
|
10
|
+
const abort = window.AbortController ? new window.AbortController() : null
|
|
11
|
+
const apply = value => {
|
|
12
|
+
if (disposed || value.revision < state.revision) return
|
|
13
|
+
state = value
|
|
14
|
+
for (const slot of Object.keys(backgroundLabels)) {
|
|
15
|
+
const url = backgroundImageUrl(state[slot])
|
|
16
|
+
const root = document.documentElement
|
|
17
|
+
root.toggleAttribute(`data-prts-background-${slot}`, Boolean(url))
|
|
18
|
+
if (url) root.style.setProperty(`--prts-background-${slot}`, `url("${url}")`)
|
|
19
|
+
else root.style.removeProperty(`--prts-background-${slot}`)
|
|
20
|
+
}
|
|
21
|
+
for (const listener of listeners) listener(state)
|
|
22
|
+
}
|
|
23
|
+
const request = async data => {
|
|
24
|
+
const response = await window.fetch(BACKGROUND_ENDPOINT + '/configuration', {
|
|
25
|
+
credentials: 'same-origin', signal: abort?.signal,
|
|
26
|
+
...(data === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json', 'x-prts-request': '1' }, body: JSON.stringify(data) }),
|
|
27
|
+
})
|
|
28
|
+
const value = await response.json()
|
|
29
|
+
if (!response.ok) throw Error(value.error || '背景服务暂不可用')
|
|
30
|
+
apply(value); return state
|
|
31
|
+
}
|
|
32
|
+
const refresh = () => refreshing ?? (refreshing = request().finally(() => { refreshing = null }))
|
|
33
|
+
if (window.fetch) void refresh().catch(() => {}).then(() => {
|
|
34
|
+
if (disposed || !window.EventSource) return
|
|
35
|
+
events = new window.EventSource(BACKGROUND_ENDPOINT + '/events')
|
|
36
|
+
events.onmessage = event => {
|
|
37
|
+
try {
|
|
38
|
+
const revision = JSON.parse(event.data).revision
|
|
39
|
+
if (revision > state.revision) void refresh().then(() => {
|
|
40
|
+
if (!disposed && revision > state.revision) return refresh()
|
|
41
|
+
}).catch(() => {})
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
const focus = () => { if (window.fetch) void refresh().catch(() => {}) }
|
|
46
|
+
window.addEventListener('focus', focus)
|
|
47
|
+
return {
|
|
48
|
+
snapshot: () => state, refresh,
|
|
49
|
+
subscribe(fn) { listeners.add(fn); fn(state); return () => listeners.delete(fn) },
|
|
50
|
+
async save(slot, image, revision = state.revision) {
|
|
51
|
+
try { return await request({ slot, image, revision }) }
|
|
52
|
+
catch (error) { await refresh().catch(() => {}); throw error }
|
|
53
|
+
},
|
|
54
|
+
dispose() {
|
|
55
|
+
disposed = true; abort?.abort(); events?.close(); listeners.clear(); window.removeEventListener('focus', focus)
|
|
56
|
+
for (const slot of Object.keys(backgroundLabels)) {
|
|
57
|
+
document.documentElement.removeAttribute(`data-prts-background-${slot}`)
|
|
58
|
+
document.documentElement.style.removeProperty(`--prts-background-${slot}`)
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function backgroundMarkup() {
|
|
64
|
+
return `<section data-prts-background-settings>
|
|
65
|
+
<header><strong>会话背景</strong><small>分区使用不同图片,或让一张完整背景贯穿侧边栏与正文。</small></header>
|
|
66
|
+
<div data-prts-background-modes role="group" aria-label="背景模式">
|
|
67
|
+
${[['off','默认背景','使用主题原有底色'],['split','分区背景','侧栏与正文独立配置'],['full','完整背景','一张图片贯穿整个界面']].map(([value,label,hint]) => `<button type="button" data-prts-setting-key="backgroundMode" data-prts-setting-value="${value}" aria-pressed="false"><span data-prts-background-diagram="${value}" aria-hidden="true"><i></i><i></i></span><span><strong>${label}</strong><small>${hint}</small></span></button>`).join('')}
|
|
68
|
+
</div>
|
|
69
|
+
<div data-prts-background-images hidden>${Object.entries(backgroundLabels).map(([slot,label]) => `<article data-prts-background-card="${slot}" hidden>
|
|
70
|
+
<div data-prts-background-card-heading><strong>${label}</strong><button type="button" data-prts-background-remove="${slot}" aria-label="移除${label}图片" hidden>移除</button></div>
|
|
71
|
+
<button type="button" data-prts-background-upload="${slot}" aria-label="上传或替换${label}"><span data-prts-background-thumbnail="${slot}" aria-hidden="true"></span><span data-prts-background-caption="${slot}">+ 选择图片</span></button>
|
|
72
|
+
<input type="file" data-prts-background-file="${slot}" accept="image/png,image/jpeg,image/webp,image/avif" hidden>
|
|
73
|
+
</article>`).join('')}</div>
|
|
74
|
+
<label data-prts-background-shade hidden><span>背景淡化 <output></output></span><input type="range" min="0" max="90" step="5" aria-label="背景淡化程度"><small>调高后背景更淡,接近当前主题底色。</small></label>
|
|
75
|
+
${opacityDialMarkup()}
|
|
76
|
+
<small data-prts-background-help>支持 PNG / JPEG / WebP / AVIF · 最大 10 MB<br>图片居中铺满,切换模式保留素材,并在当前 DSH 实例同步。</small><p data-prts-background-status role="status"></p>
|
|
77
|
+
</section>`
|
|
78
|
+
}
|
|
79
|
+
export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
80
|
+
const section = panel.querySelector('[data-prts-background-settings]')
|
|
81
|
+
if (!section || !store) return { update() {}, dispose() {} }
|
|
82
|
+
let disposed = false, busy = false, preferences = {}, state = store.snapshot()
|
|
83
|
+
const status = section.querySelector('[data-prts-background-status]')
|
|
84
|
+
const range = section.querySelector('input[type="range"]')
|
|
85
|
+
const dials = mountOpacityDials({ section, window, onChange })
|
|
86
|
+
function render() {
|
|
87
|
+
dials.update(preferences, state)
|
|
88
|
+
const mode = preferences.backgroundMode || 'off'
|
|
89
|
+
section.querySelector('[data-prts-background-images]').hidden = mode === 'off'
|
|
90
|
+
section.querySelector('[data-prts-background-shade]').hidden = mode === 'off'
|
|
91
|
+
range.value = String(Math.round((preferences.backgroundDim ?? .45) * 100))
|
|
92
|
+
section.querySelector('output').textContent = `${range.value}%`
|
|
93
|
+
for (const slot of Object.keys(backgroundLabels)) {
|
|
94
|
+
const url = backgroundImageUrl(state[slot])
|
|
95
|
+
section.querySelector(`[data-prts-background-card="${slot}"]`).hidden = mode === 'off' || (mode === 'full') !== (slot === 'full')
|
|
96
|
+
section.querySelector(`[data-prts-background-upload="${slot}"]`).toggleAttribute('data-has-image', Boolean(url))
|
|
97
|
+
section.querySelector(`[data-prts-background-thumbnail="${slot}"]`).style.backgroundImage = url ? `url("${url}")` : ''
|
|
98
|
+
section.querySelector(`[data-prts-background-caption="${slot}"]`).textContent = url ? '替换图片' : '+ 选择图片'
|
|
99
|
+
section.querySelector(`[data-prts-background-remove="${slot}"]`).hidden = !url
|
|
100
|
+
}
|
|
101
|
+
for (const button of section.querySelectorAll('[data-prts-background-upload], [data-prts-background-remove]')) button.disabled = busy
|
|
102
|
+
}
|
|
103
|
+
async function save(slot, file) {
|
|
104
|
+
if (busy) return
|
|
105
|
+
if (file && file.size > 10 * 1024 * 1024) { status.textContent = '请上传不超过 10 MB 的图片'; return }
|
|
106
|
+
const revision = state.revision
|
|
107
|
+
busy = true; status.textContent = '正在保存背景…'; render()
|
|
108
|
+
try {
|
|
109
|
+
let image = null
|
|
110
|
+
if (file) image = await new Promise((resolve, reject) => {
|
|
111
|
+
const reader = new window.FileReader()
|
|
112
|
+
reader.onload = () => resolve(String(reader.result).split(',')[1])
|
|
113
|
+
reader.onerror = () => reject(Error('无法读取图片'))
|
|
114
|
+
reader.readAsDataURL(file)
|
|
115
|
+
})
|
|
116
|
+
if (disposed) return
|
|
117
|
+
await store.save(slot, image, revision)
|
|
118
|
+
if (!disposed) status.textContent = file ? '背景已保存,并同步到其他页面。' : '图片已移除。'
|
|
119
|
+
} catch (error) { if (!disposed) status.textContent = error.message }
|
|
120
|
+
finally { busy = false; if (!disposed) render() }
|
|
121
|
+
}
|
|
122
|
+
const click = event => {
|
|
123
|
+
const upload = event.target.closest('[data-prts-background-upload]')
|
|
124
|
+
if (upload) section.querySelector(`[data-prts-background-file="${upload.dataset.prtsBackgroundUpload}"]`).click()
|
|
125
|
+
const remove = event.target.closest('[data-prts-background-remove]')
|
|
126
|
+
if (remove) void save(remove.dataset.prtsBackgroundRemove, null)
|
|
127
|
+
}
|
|
128
|
+
const change = event => {
|
|
129
|
+
if (event.target === range) onChange('backgroundDim', Number(range.value) / 100)
|
|
130
|
+
const slot = event.target.dataset.prtsBackgroundFile
|
|
131
|
+
if (slot && event.target.files?.[0]) { void save(slot, event.target.files[0]); event.target.value = '' }
|
|
132
|
+
}
|
|
133
|
+
const input = event => { if (event.target === range) section.querySelector('output').textContent = `${range.value}%` }
|
|
134
|
+
section.addEventListener('click', click); section.addEventListener('change', change); section.addEventListener('input', input)
|
|
135
|
+
const unsubscribe = store.subscribe(value => { state = value; render() })
|
|
136
|
+
return {
|
|
137
|
+
update(value) { preferences = value; render() },
|
|
138
|
+
dispose() { disposed = true; dials.dispose(); unsubscribe(); section.removeEventListener('click', click); section.removeEventListener('change', change); section.removeEventListener('input', input) },
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/client/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createBackgroundStore } from './custom-background.js'
|
|
2
|
+
import { createNativePreferences } from './native-preferences.js'
|
|
3
|
+
import { createComposerGlassSurface } from './composer-glass-surface.js'
|
|
1
4
|
import { createAvatarStore } from './custom-avatar.js'
|
|
2
5
|
import { registerEmblemToolPreview } from './emblem-tool-preview.js'
|
|
3
6
|
import { createCustomEmblemStore } from './custom-emblem.js'
|
|
4
|
-
import { DEFAULT_PREFERENCES, isSafeMode, loadPreferences,
|
|
7
|
+
import { DEFAULT_PREFERENCES, isSafeMode, loadPreferences, resetPreferenceGroup, updatePreferenceValue } from './preferences.js'
|
|
5
8
|
import { createHostGeometryAdapter } from './host-geometry-adapter.js'
|
|
6
9
|
import { createComposerGlassAdapter } from './composer-glass-adapter.js'
|
|
7
10
|
import { createAssistantGlassAdapter } from './assistant-glass-adapter.js'
|
|
@@ -44,6 +47,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
44
47
|
version = 'development',
|
|
45
48
|
startupTimings,
|
|
46
49
|
} = environment
|
|
50
|
+
const composerSurface = createComposerGlassSurface({document, window, React, mount: environment.mountComposerGlassSurface})
|
|
47
51
|
const safeMode = isSafeMode(window?.location?.search ?? '')
|
|
48
52
|
const stopEmblemPreview = safeMode ? null : registerEmblemToolPreview(ctx, React)
|
|
49
53
|
const storage = window?.localStorage
|
|
@@ -65,6 +69,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
65
69
|
sync()
|
|
66
70
|
},
|
|
67
71
|
})
|
|
72
|
+
const backgroundStore = createBackgroundStore({window,document})
|
|
68
73
|
const avatarStore = createAvatarStore({window,document})
|
|
69
74
|
const customEmblemStore = createCustomEmblemStore({ window, emblems: assets.emblems, preview: entry => particleField.previewEmblem?.(entry), onChange: value => particleField.setCustomEmblemConfiguration?.(value) })
|
|
70
75
|
const startup = createPrtsStartupSequence({
|
|
@@ -123,7 +128,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
123
128
|
|
|
124
129
|
const sync = () => {
|
|
125
130
|
revision += 1
|
|
126
|
-
for (const actions of bindings) actions.sync({ ...preferences }, { ...status }, revision)
|
|
131
|
+
for (const actions of bindings) actions.sync({ ...preferences }, { ...status, settingsPersistence }, revision)
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
const cancelMountFrame = () => {
|
|
@@ -183,6 +188,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
183
188
|
conversationControls.dispose()
|
|
184
189
|
layoutDiagnostics.dispose()
|
|
185
190
|
sidebarControls.dispose()
|
|
191
|
+
composerSurface.dispose()
|
|
186
192
|
floatingGlass.dispose()
|
|
187
193
|
theme.apply({ ...preferences, enabled: false })
|
|
188
194
|
}
|
|
@@ -194,7 +200,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
194
200
|
}
|
|
195
201
|
if (!themeAlreadyApplied) theme.apply(preferences)
|
|
196
202
|
operations ||= createOperationsShell({
|
|
197
|
-
document, window, assets, adapter, hostGeometry, customEmblemStore, avatarStore,
|
|
203
|
+
document, window, assets, adapter, hostGeometry, customEmblemStore, avatarStore, backgroundStore,
|
|
198
204
|
onSchemeToggle(next, interaction) {
|
|
199
205
|
return theme.setTheme(next, { ...interaction, animate: true })
|
|
200
206
|
},
|
|
@@ -253,41 +259,37 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
253
259
|
}
|
|
254
260
|
particleField.update(preferences)
|
|
255
261
|
composerGlass.start()
|
|
256
|
-
if (preferences.conversationStyle === 'deck-chat') assistantGlass.start({ groupAvatars: preferences.bakerGroupAvatars })
|
|
262
|
+
if (preferences.conversationStyle === 'deck-chat') assistantGlass.start({ groupAvatars: preferences.bakerGroupAvatars, loading: preferences.bakerLoading })
|
|
257
263
|
else assistantGlass.dispose()
|
|
258
264
|
toBottom.start()
|
|
259
265
|
conversationControls.start()
|
|
260
266
|
layoutDiagnostics.start()
|
|
261
267
|
sidebarControls.start(preferences)
|
|
268
|
+
if (preferences.composerGlass || preferences.backgroundMode !== 'off') composerSurface.start()
|
|
269
|
+
else composerSurface.dispose()
|
|
262
270
|
floatingGlass.start()
|
|
263
271
|
}
|
|
264
272
|
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
phase: result.persisted ? 'saved' : 'error',
|
|
286
|
-
revision: settingsPersistence.revision + 1,
|
|
287
|
-
}
|
|
288
|
-
operations?.update(preferences, status, settingsPersistence)
|
|
289
|
-
sync()
|
|
290
|
-
}
|
|
273
|
+
const nativePreferences = createNativePreferences({
|
|
274
|
+
storage,
|
|
275
|
+
onChange(next, persistence) {
|
|
276
|
+
const changed = JSON.stringify(preferences) !== JSON.stringify(next)
|
|
277
|
+
const wasEnabled = preferences.enabled
|
|
278
|
+
preferences = next
|
|
279
|
+
settingsPersistence = persistence
|
|
280
|
+
if (changed) {
|
|
281
|
+
const play = !safeMode && !wasEnabled && preferences.enabled && preferences.bootAnimation
|
|
282
|
+
if (play) particleField.setStartupActive(true)
|
|
283
|
+
applyVisualState()
|
|
284
|
+
if (play && !startup.play()) particleField.setStartupActive(false)
|
|
285
|
+
}
|
|
286
|
+
operations?.update(preferences, status, settingsPersistence)
|
|
287
|
+
sync()
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
preferences = nativePreferences.value
|
|
291
|
+
const persistAndApply = next => nativePreferences.update(next)
|
|
292
|
+
const retryPersistence = () => nativePreferences.retry()
|
|
291
293
|
const statusSource = createHarnessStatusSource({
|
|
292
294
|
connection,
|
|
293
295
|
onChange(next) {
|
|
@@ -345,11 +347,19 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
345
347
|
} else {
|
|
346
348
|
applyVisualState()
|
|
347
349
|
}
|
|
350
|
+
const bindNativePreferences = settingsCtx => {
|
|
351
|
+
const forms = contextService(settingsCtx, 'configForms')
|
|
352
|
+
if (forms) settingsCtx.effect(() => nativePreferences.bind(forms.get('theme-prts')))
|
|
353
|
+
}
|
|
354
|
+
if (typeof ctx.inject === 'function') ctx.inject(['configForms'], bindNativePreferences)
|
|
355
|
+
else bindNativePreferences(ctx)
|
|
348
356
|
ctx.effect(() => () => {
|
|
349
357
|
if (typeof stopEmblemPreview === 'function') stopEmblemPreview()
|
|
358
|
+
nativePreferences.dispose()
|
|
350
359
|
statusSource.dispose()
|
|
351
360
|
customEmblemStore.dispose()
|
|
352
361
|
avatarStore.dispose()
|
|
362
|
+
backgroundStore.dispose()
|
|
353
363
|
particleField.dispose()
|
|
354
364
|
startup.dispose({ restoreFocus: false })
|
|
355
365
|
stopStartupReadyWatch()
|
|
@@ -361,13 +371,14 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
361
371
|
conversationControls.dispose()
|
|
362
372
|
layoutDiagnostics.dispose()
|
|
363
373
|
sidebarControls.dispose()
|
|
374
|
+
composerSurface.dispose()
|
|
364
375
|
floatingGlass.dispose()
|
|
365
376
|
if (typeof stopThemeSync === 'function') stopThemeSync()
|
|
366
377
|
theme.dispose()
|
|
367
378
|
bindings.clear()
|
|
368
379
|
}, 'dsh-theme-prts: visual cleanup')
|
|
369
380
|
|
|
370
|
-
const initialStoreState = { preferences, status, revision }
|
|
381
|
+
const initialStoreState = { preferences, status: { ...status, settingsPersistence }, revision }
|
|
371
382
|
const settingsStore = createPrtsUiStore(defineStore, initialStoreState)
|
|
372
383
|
const PrtsSettingsPage = createSettingsPage(React, {
|
|
373
384
|
emblem: assets.emblem,
|
|
@@ -378,6 +389,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
378
389
|
bindings.add(actions)
|
|
379
390
|
sync()
|
|
380
391
|
return {
|
|
392
|
+
retryPersistence,
|
|
381
393
|
updatePreference(key, value) { persistAndApply(updatePreferenceValue(preferences, key, value)) },
|
|
382
394
|
resetPreferenceGroup(group) {
|
|
383
395
|
persistAndApply(resetPreferenceGroup(preferences, group))
|
|
@@ -410,6 +422,21 @@ export function apply(ctx) {
|
|
|
410
422
|
window,
|
|
411
423
|
React,
|
|
412
424
|
defineStore,
|
|
425
|
+
mountComposerGlassSurface(container, props) {
|
|
426
|
+
const { createRoot } = require('react-dom/client')
|
|
427
|
+
const root = createRoot(container)
|
|
428
|
+
const update = props => root.render(React.createElement(PrtsGlassSurface.default, {
|
|
429
|
+
width: '100%', height: '100%',
|
|
430
|
+
// Gentle edge refraction; a fully calm center avoids shifting input content.
|
|
431
|
+
distortionScale: -18, redOffset: 0, greenOffset: 1, blueOffset: 2,
|
|
432
|
+
opacity: 1, backgroundOpacity: 0.5, displace: 5,
|
|
433
|
+
...props,
|
|
434
|
+
}))
|
|
435
|
+
update(props)
|
|
436
|
+
const dispose = () => root.unmount()
|
|
437
|
+
dispose.update = update
|
|
438
|
+
return dispose
|
|
439
|
+
},
|
|
413
440
|
cssText: PRTS_CSS,
|
|
414
441
|
assets: PRTS_ASSETS,
|
|
415
442
|
version: PRTS_VERSION,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { DEFAULT_PREFERENCES, PRTS_STORAGE_KEY, loadPreferences, normalizePreferences } from './preferences.js'
|
|
2
|
+
|
|
3
|
+
// Host snapshots are authoritative. This cache must never become a migration source.
|
|
4
|
+
const NATIVE_CACHE_KEY = 'dsh.ui.prts.host-cache.v1'
|
|
5
|
+
export function createNativePreferences({ storage, onChange }) {
|
|
6
|
+
let current = loadPreferences(storage)
|
|
7
|
+
let legacy = null
|
|
8
|
+
try {
|
|
9
|
+
const raw = JSON.parse(storage?.getItem(PRTS_STORAGE_KEY) || 'null')
|
|
10
|
+
if (raw && Number.isInteger(raw.version) && raw.version >= 1 && raw.version <= 12) legacy = normalizePreferences(raw)
|
|
11
|
+
} catch {}
|
|
12
|
+
try {
|
|
13
|
+
const cache = JSON.parse(storage?.getItem(NATIVE_CACHE_KEY) || 'null')
|
|
14
|
+
if (cache) { current = normalizePreferences(cache); legacy = null }
|
|
15
|
+
} catch {}
|
|
16
|
+
let form, unsubscribe, disposed = false, busy = false, revision = 0
|
|
17
|
+
let pending = {}, inFlight = {}, failed = {}, phase = 'idle', migrationAttempted = false
|
|
18
|
+
const emit = () => { if (!disposed) onChange(current, { phase, revision: ++revision }) }
|
|
19
|
+
const cache = value => { try { storage?.setItem(NATIVE_CACHE_KEY, JSON.stringify(value)) } catch {} }
|
|
20
|
+
const snapshot = () => { try { return form?.getSnapshot() } catch { return undefined } }
|
|
21
|
+
const writable = s => s?.status === 'ready' && s.writable && s.mode !== 'memory'
|
|
22
|
+
function receive() {
|
|
23
|
+
if (disposed) return
|
|
24
|
+
const s = snapshot()
|
|
25
|
+
if (s?.status !== 'ready') { emit(); return }
|
|
26
|
+
const host = normalizePreferences(s.value)
|
|
27
|
+
if (!s.value?.preferencesInitialized && legacy && !migrationAttempted && writable(s)) {
|
|
28
|
+
migrationAttempted = true
|
|
29
|
+
pending = { ...legacy, ...pending }
|
|
30
|
+
}
|
|
31
|
+
current = normalizePreferences({ ...host, ...inFlight, ...pending })
|
|
32
|
+
if (s.value?.preferencesInitialized) {
|
|
33
|
+
legacy = null
|
|
34
|
+
cache(host)
|
|
35
|
+
// Once the Host has settings, an obsolete browser must not re-migrate them.
|
|
36
|
+
try { storage?.removeItem(PRTS_STORAGE_KEY) } catch {}
|
|
37
|
+
}
|
|
38
|
+
emit()
|
|
39
|
+
void flush()
|
|
40
|
+
}
|
|
41
|
+
async function flush() {
|
|
42
|
+
if (disposed || busy || !Object.keys(pending).length) return
|
|
43
|
+
const s = snapshot()
|
|
44
|
+
if (!writable(s)) { failed = { ...failed, ...pending }; pending = {}; phase = 'error'; emit(); return }
|
|
45
|
+
busy = true
|
|
46
|
+
const patch = pending
|
|
47
|
+
inFlight = patch
|
|
48
|
+
pending = {}
|
|
49
|
+
phase = 'saving'; emit()
|
|
50
|
+
try {
|
|
51
|
+
const ops = Object.entries(patch).map(([key, value]) => ({ op: 'set', path: [key], value }))
|
|
52
|
+
ops.push({ op: 'set', path: ['preferencesInitialized'], value: true })
|
|
53
|
+
// Fixed revision: conflicts are recovered by DSH, never blindly replayed.
|
|
54
|
+
const accepted = await form.mutate(ops, s.revision)
|
|
55
|
+
if (disposed) return
|
|
56
|
+
if (accepted !== true) throw Error('Host refused settings mutation')
|
|
57
|
+
for (const key of Object.keys(patch)) delete failed[key]
|
|
58
|
+
phase = Object.keys(failed).length ? 'error' : 'saved'
|
|
59
|
+
} catch {
|
|
60
|
+
if (disposed) return
|
|
61
|
+
failed = { ...failed, ...patch }
|
|
62
|
+
for (const key of Object.keys(pending)) delete failed[key]
|
|
63
|
+
phase = 'error'
|
|
64
|
+
} finally {
|
|
65
|
+
busy = false
|
|
66
|
+
inFlight = {}
|
|
67
|
+
if (!disposed) receive()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
get value() { return current },
|
|
72
|
+
bind(next) {
|
|
73
|
+
unsubscribe?.(); form = next
|
|
74
|
+
unsubscribe = form.subscribe(receive)
|
|
75
|
+
receive()
|
|
76
|
+
return () => { unsubscribe?.(); unsubscribe = null; form = null }
|
|
77
|
+
},
|
|
78
|
+
update(next) {
|
|
79
|
+
if (disposed) return
|
|
80
|
+
const normalized = normalizePreferences(next)
|
|
81
|
+
for (const key of Object.keys(DEFAULT_PREFERENCES)) {
|
|
82
|
+
if (normalized[key] !== current[key]) { pending[key] = normalized[key]; delete failed[key] }
|
|
83
|
+
}
|
|
84
|
+
if (!Object.keys(pending).length) return
|
|
85
|
+
current = normalized
|
|
86
|
+
phase = 'saving'; emit()
|
|
87
|
+
void flush()
|
|
88
|
+
},
|
|
89
|
+
retry() { pending = { ...failed, ...pending }; failed = {}; void flush() },
|
|
90
|
+
dispose() { disposed = true; unsubscribe?.() },
|
|
91
|
+
}
|
|
92
|
+
}
|