@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Ring geometry adapted from React Bits CometDial (David Haz).
|
|
2
|
+
// See src/vendor/react-bits/LICENSE.md and THIRD_PARTY_NOTICES.md.
|
|
3
|
+
export const BACKGROUND_OPACITY_CONTROLS = Object.freeze([
|
|
4
|
+
{ key: 'sidebarOpacity', label: '会话列表侧栏', property: '--prts-sidebar-opacity', value: 94, blurKey: 'sidebarBlur', blurProperty: '--prts-sidebar-blur', blurDefault: 8 },
|
|
5
|
+
{ key: 'railOpacity', label: '设置侧栏', property: '--prts-rail-opacity', value: 70, blurKey: 'railBlur', blurProperty: '--prts-rail-blur', blurDefault: 12, fullBackgroundOnly: true },
|
|
6
|
+
{ key: 'bakerOpacity', label: 'Baker 气泡', property: '--prts-baker-opacity', value: 50, blurKey: 'bakerBlur', blurProperty: '--prts-baker-blur', blurDefault: 8, theme: 'deck-chat' },
|
|
7
|
+
{ key: 'classicUserOpacity', label: '经典用户消息', property: '--prts-classic-user-opacity', value: 50, blurKey: 'classicUserBlur', blurProperty: '--prts-classic-user-blur', blurDefault: 8, theme: 'native' },
|
|
8
|
+
])
|
|
9
|
+
const dialClamp = value => Math.min(100, Math.max(0, Math.round(value)))
|
|
10
|
+
const dialPoint = angle => [100 + Math.cos(angle * Math.PI / 180) * 80, 100 + Math.sin(angle * Math.PI / 180) * 80]
|
|
11
|
+
const dialArc = (start, end) => {
|
|
12
|
+
const [x, y] = dialPoint(start), [ex, ey] = dialPoint(end)
|
|
13
|
+
return `M ${x} ${y} A 80 80 0 ${end - start > 180 ? 1 : 0} 1 ${ex} ${ey}`
|
|
14
|
+
}
|
|
15
|
+
export function opacityDialMarkup() {
|
|
16
|
+
const controls = BACKGROUND_OPACITY_CONTROLS.map(({key,label,value,blurDefault}) => {
|
|
17
|
+
const sample = key === 'bakerOpacity' ? '<span>这是一条回复</span><i></i>'
|
|
18
|
+
: key === 'classicUserOpacity' ? '<span>这是我发送的消息</span><i></i>'
|
|
19
|
+
: key === 'railOpacity' ? '<b>◇</b><b>☼</b><b>⚙</b>' : '<span>工作区</span><i></i><i></i>'
|
|
20
|
+
return `<div data-prts-opacity-control="${key}">
|
|
21
|
+
<div data-prts-opacity-heading><strong>${label}</strong><button type="button" data-prts-opacity-reset data-prts-opacity-reset-tooltip="恢复到 ${value}%" aria-label="恢复${label}透明度到 ${value}%"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 10a7.5 7.5 0 1 1 .7 5.2M4.5 4.5V10H10"/></svg></button></div>
|
|
22
|
+
<div data-prts-opacity-dial><svg viewBox="0 0 200 200" role="slider" tabindex="0" aria-label="${label}透明度" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${value}" aria-valuetext="${value}%"><circle data-dial-focus cx="100" cy="100" r="97"/><path data-dial-track d="${dialArc(110,430)}"/><path data-dial-lit/><path data-dial-tail/><circle data-dial-head r="8"/></svg><label data-prts-opacity-number><input type="number" min="0" max="100" step="1" value="${value}" aria-label="${label}透明度百分比"><span>%</span></label></div>
|
|
23
|
+
<div data-prts-material-preview="${key}" aria-hidden="true"><div data-prts-material-preview-surface>${sample}</div></div>
|
|
24
|
+
<div data-prts-blur-control hidden><label><span>模糊</span><input type="range" min="0" max="32" step="1" value="${blurDefault}" aria-label="${label}模糊度"></label><output>${blurDefault}px</output><button type="button" data-prts-blur-reset title="恢复到 ${blurDefault}px" aria-label="恢复${label}模糊度到 ${blurDefault}px"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 10a7.5 7.5 0 1 1 .7 5.2M4.5 4.5V10H10"/></svg></button></div>
|
|
25
|
+
<small data-prts-opacity-note></small>
|
|
26
|
+
</div>`
|
|
27
|
+
}).join('')
|
|
28
|
+
return `<fieldset data-prts-opacity-settings hidden><legend>材质透明度与模糊度</legend><small>透明度数值越高越透明;模糊度以像素计。调整时预览,松手后保存。</small><div data-prts-opacity-grid>${controls}</div></fieldset>`
|
|
29
|
+
}
|
|
30
|
+
export function mountOpacityDials({ section, window, onChange }) {
|
|
31
|
+
const root = section.ownerDocument.documentElement
|
|
32
|
+
const controls = BACKGROUND_OPACITY_CONTROLS.map(definition => {
|
|
33
|
+
const container = section.querySelector(`[data-prts-opacity-control="${definition.key}"]`)
|
|
34
|
+
const svg = container.querySelector('[data-prts-opacity-dial] svg'), input = container.querySelector('[data-prts-opacity-number] input'), tail = svg.querySelector('[data-dial-tail]')
|
|
35
|
+
const blurControl = container.querySelector('[data-prts-blur-control]')
|
|
36
|
+
const blurInput = blurControl.querySelector('input'), blurOutput = blurControl.querySelector('output')
|
|
37
|
+
const materialPreview = container.querySelector('[data-prts-material-preview]')
|
|
38
|
+
let value = definition.value, saved = value, grip = null, timer
|
|
39
|
+
let blurValue = definition.blurDefault, savedBlur = blurValue
|
|
40
|
+
const paint = next => {
|
|
41
|
+
const previous = value
|
|
42
|
+
value = dialClamp(next)
|
|
43
|
+
const angle = 110 + value * 3.2, [x,y] = dialPoint(angle)
|
|
44
|
+
svg.querySelector('[data-dial-lit]').setAttribute('d', value ? dialArc(110,angle) : '')
|
|
45
|
+
const head = svg.querySelector('[data-dial-head]');head.setAttribute('cx', x);head.setAttribute('cy', y)
|
|
46
|
+
svg.setAttribute('aria-valuenow', value);svg.setAttribute('aria-valuetext', `${value}%`)
|
|
47
|
+
input.value = value
|
|
48
|
+
window.clearTimeout(timer)
|
|
49
|
+
const reach = Math.min(65, Math.abs(value - previous) * 3.2)
|
|
50
|
+
tail.setAttribute('d', reach && value !== previous ? dialArc(Math.max(110, value > previous ? angle-reach : angle), Math.min(430, value > previous ? angle : angle+reach)) : '')
|
|
51
|
+
tail.style.opacity = reach ? '.5' : '0'
|
|
52
|
+
if (reach) timer = window.setTimeout(() => {tail.style.opacity = '0'}, 160)
|
|
53
|
+
}
|
|
54
|
+
const preview = next => { paint(next);root.style.setProperty(definition.property, `${100 - value}%`) }
|
|
55
|
+
const commit = () => { if (value !== saved) { saved = value;onChange(definition.key, (100 - value) / 100) } }
|
|
56
|
+
const position = event => {
|
|
57
|
+
const b = svg.getBoundingClientRect()
|
|
58
|
+
const x = (event.clientX-b.left)/b.width*200-100, y = (event.clientY-b.top)/b.height*200-100
|
|
59
|
+
if (Math.hypot(x,y) < 35) return value
|
|
60
|
+
let angle = (Math.atan2(y,x)*180/Math.PI-110+720)%360
|
|
61
|
+
// Keep a drag on its own side of the dead zone, avoiding 100 -> 0 jumps.
|
|
62
|
+
if (angle > 320) angle = grip?.side === 'high' ? 320 : grip?.side === 'low' ? 0 : angle < 340 ? 320 : 0
|
|
63
|
+
else if (grip) grip.side = angle > 160 ? 'high' : 'low'
|
|
64
|
+
return angle/3.2
|
|
65
|
+
}
|
|
66
|
+
const down = event => {
|
|
67
|
+
if (event.button > 0 || grip) return
|
|
68
|
+
event.preventDefault();svg.focus({preventScroll:true})
|
|
69
|
+
grip = { id: event.pointerId, side: null }
|
|
70
|
+
svg.setPointerCapture?.(event.pointerId);container.setAttribute('data-dragging','')
|
|
71
|
+
preview(position(event))
|
|
72
|
+
}
|
|
73
|
+
const move = event => { if (grip?.id === event.pointerId) preview(position(event)) }
|
|
74
|
+
const finish = (event, cancel = false) => {
|
|
75
|
+
if (!grip || (event.pointerId !== undefined && grip.id !== event.pointerId)) return
|
|
76
|
+
const id = grip.id;grip = null;container.removeAttribute('data-dragging')
|
|
77
|
+
if (svg.hasPointerCapture?.(id)) svg.releasePointerCapture(id)
|
|
78
|
+
if (cancel) preview(saved);else commit()
|
|
79
|
+
}
|
|
80
|
+
const up = event => finish(event)
|
|
81
|
+
const cancel = event => finish(event,true)
|
|
82
|
+
const key = event => {
|
|
83
|
+
if (event.key === 'Escape') { if(grip){event.preventDefault();event.stopPropagation();finish(event,true)}return }
|
|
84
|
+
const step = event.shiftKey ? 10 : 1
|
|
85
|
+
const next = {ArrowRight:value+step,ArrowUp:value+step,ArrowLeft:value-step,ArrowDown:value-step,PageUp:value+10,PageDown:value-10,Home:0,End:100}[event.key]
|
|
86
|
+
if (next === undefined) return
|
|
87
|
+
event.preventDefault();preview(next);commit()
|
|
88
|
+
}
|
|
89
|
+
const change = () => { const n = input.valueAsNumber;if(!Number.isFinite(n)){paint(saved);return}preview(n);commit() }
|
|
90
|
+
const reset = () => { preview(definition.value);commit() }
|
|
91
|
+
const paintBlur = next => {
|
|
92
|
+
blurValue = Math.min(32, Math.max(0, Math.round(next)))
|
|
93
|
+
blurInput.value = blurValue
|
|
94
|
+
blurOutput.value = `${blurValue}px`
|
|
95
|
+
materialPreview.style.setProperty(definition.blurProperty, `${blurValue}px`)
|
|
96
|
+
}
|
|
97
|
+
const previewBlur = next => {
|
|
98
|
+
paintBlur(next)
|
|
99
|
+
if (root.hasAttribute('data-dsh-prts')) root.style.setProperty(definition.blurProperty, `${blurValue}px`)
|
|
100
|
+
}
|
|
101
|
+
const commitBlur = () => { if (blurValue !== savedBlur) { savedBlur = blurValue;onChange(definition.blurKey, blurValue) } }
|
|
102
|
+
const blurChange = () => { previewBlur(blurInput.valueAsNumber);commitBlur() }
|
|
103
|
+
const blurCancel = event => { if (event.key === 'Escape') { event.preventDefault();previewBlur(savedBlur);blurInput.blur() } }
|
|
104
|
+
const resetBlur = () => { previewBlur(definition.blurDefault);commitBlur() }
|
|
105
|
+
const resetButton = container.querySelector('[data-prts-opacity-reset]')
|
|
106
|
+
const blurResetButton = blurControl.querySelector('[data-prts-blur-reset]')
|
|
107
|
+
const bindings = [[svg,'pointerdown',down],[svg,'pointermove',move],[svg,'pointerup',up],[svg,'pointercancel',cancel],[svg,'lostpointercapture',cancel],[svg,'keydown',key],[input,'change',change],[resetButton,'click',reset],[blurInput,'input',event=>previewBlur(event.target.valueAsNumber)],[blurInput,'change',blurChange],[blurInput,'keydown',blurCancel],[blurResetButton,'click',resetBlur],[window,'blur',cancel]]
|
|
108
|
+
for(const [el,type,fn] of bindings)el.addEventListener(type,fn)
|
|
109
|
+
paint(value)
|
|
110
|
+
return {
|
|
111
|
+
update(preferences, active) {
|
|
112
|
+
container.hidden = Boolean((definition.theme && preferences.conversationStyle !== definition.theme)
|
|
113
|
+
|| (definition.fullBackgroundOnly && preferences.backgroundMode !== 'full'))
|
|
114
|
+
saved = 100 - Math.round((preferences[definition.key] ?? (100 - definition.value) / 100) * 100)
|
|
115
|
+
if (!grip && section.ownerDocument.activeElement !== input) paint(saved)
|
|
116
|
+
savedBlur = preferences[definition.blurKey] ?? definition.blurDefault
|
|
117
|
+
if (section.ownerDocument.activeElement !== blurInput) paintBlur(savedBlur)
|
|
118
|
+
blurControl.hidden = !active
|
|
119
|
+
container.querySelector('[data-prts-opacity-note]').textContent = active ? '' : '当前未启用此材质'
|
|
120
|
+
},
|
|
121
|
+
dispose() { if(grip)finish({},true);window.clearTimeout(timer);for(const [el,type,fn] of bindings)el.removeEventListener(type,fn) },
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
return {
|
|
125
|
+
update(preferences, state) {
|
|
126
|
+
const full = preferences.backgroundMode === 'full' && Boolean(state.full)
|
|
127
|
+
const split = preferences.backgroundMode === 'split'
|
|
128
|
+
section.querySelector('[data-prts-opacity-settings]').hidden = preferences.backgroundMode === 'off'
|
|
129
|
+
const conversationImage = full || (split && Boolean(state.conversation))
|
|
130
|
+
const active = [full || (split && Boolean(state.sidebar)), full,
|
|
131
|
+
conversationImage && preferences.conversationStyle === 'deck-chat',
|
|
132
|
+
conversationImage && preferences.conversationStyle === 'native']
|
|
133
|
+
controls.forEach((control,i)=>control.update(preferences,active[i]))
|
|
134
|
+
},
|
|
135
|
+
dispose() {for(const control of controls)control.dispose()},
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -45,7 +45,7 @@ export function createOperationsShell({
|
|
|
45
45
|
onSchemeToggle = () => {},
|
|
46
46
|
onSchemeDragStart = () => undefined,
|
|
47
47
|
onThemeDisable = () => {},
|
|
48
|
-
customEmblemStore, avatarStore,
|
|
48
|
+
customEmblemStore, avatarStore, backgroundStore,
|
|
49
49
|
onPreferenceChange = () => {},
|
|
50
50
|
onPreferencePreview = () => {},
|
|
51
51
|
onVisualReset = () => {},
|
|
@@ -455,7 +455,7 @@ export function createOperationsShell({
|
|
|
455
455
|
trigger: shell.querySelector('[data-prts-rail-brand]'),
|
|
456
456
|
panel: shell.querySelector('[data-prts-theme-settings]'),
|
|
457
457
|
backdrop: shell.querySelector('[data-prts-settings-backdrop]'),
|
|
458
|
-
customEmblemStore, avatarStore,
|
|
458
|
+
customEmblemStore, avatarStore, backgroundStore,
|
|
459
459
|
onPreferenceChange,
|
|
460
460
|
onPreferencePreview,
|
|
461
461
|
onResetVisual: onVisualReset,
|
|
@@ -24,6 +24,17 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
24
24
|
glass: 'standard',
|
|
25
25
|
motion: 'system',
|
|
26
26
|
bootAnimation: true,
|
|
27
|
+
composerGlass: false,
|
|
28
|
+
backgroundMode: 'off',
|
|
29
|
+
backgroundDim: .45,
|
|
30
|
+
sidebarOpacity: .06,
|
|
31
|
+
railOpacity: .3,
|
|
32
|
+
bakerOpacity: .5,
|
|
33
|
+
classicUserOpacity: .5,
|
|
34
|
+
sidebarBlur: 8,
|
|
35
|
+
railBlur: 12,
|
|
36
|
+
bakerBlur: 8,
|
|
37
|
+
classicUserBlur: 8,
|
|
27
38
|
cursor: 'prts',
|
|
28
39
|
conversationParticleDensity: 'sparse',
|
|
29
40
|
heroParticleDensity: 'light',
|
|
@@ -48,6 +59,7 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
48
59
|
sessionFlowSpeed: SESSION_FLOW_SPEED_DEFAULT,
|
|
49
60
|
conversationStyle: 'native',
|
|
50
61
|
bakerGroupAvatars: true,
|
|
62
|
+
bakerLoading: false,
|
|
51
63
|
conversationEmblem: true,
|
|
52
64
|
})
|
|
53
65
|
|
|
@@ -85,12 +97,12 @@ export const PARTICLE_DETAIL_LEVELS = Object.freeze({
|
|
|
85
97
|
precise: Object.freeze({ conversationParticleDensity: 'dense', heroParticleDensity: 'ultra' }),
|
|
86
98
|
})
|
|
87
99
|
export const PREFERENCE_GROUPS = Object.freeze({
|
|
88
|
-
background: Object.freeze(['texture']),
|
|
100
|
+
background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'sidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
|
|
89
101
|
particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
|
|
90
|
-
material: Object.freeze(['glass']),
|
|
102
|
+
material: Object.freeze(['glass', 'composerGlass']),
|
|
91
103
|
accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
|
|
92
104
|
navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
|
|
93
|
-
conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'conversationEmblem']),
|
|
105
|
+
conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'bakerLoading', 'conversationEmblem']),
|
|
94
106
|
})
|
|
95
107
|
|
|
96
108
|
const TEXTURES = new Set(['off', 'restrained', 'full'])
|
|
@@ -134,6 +146,12 @@ function colorOr(value, fallback) {
|
|
|
134
146
|
return typeof value === 'string' && /^#[\da-f]{6}$/i.test(value) ? value.toLowerCase() : fallback
|
|
135
147
|
}
|
|
136
148
|
|
|
149
|
+
function materialBlurOr(value, fallback) {
|
|
150
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
151
|
+
? Math.min(32, Math.max(0, Math.round(value)))
|
|
152
|
+
: fallback
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
function particleTraversalSpeedOr(value, fallback) {
|
|
138
156
|
const numeric = Number(value)
|
|
139
157
|
if (!Number.isFinite(numeric)) return fallback
|
|
@@ -189,6 +207,18 @@ export function normalizePreferences(value) {
|
|
|
189
207
|
texture: enumOr(input.texture, TEXTURES, DEFAULT_PREFERENCES.texture),
|
|
190
208
|
glass: normalizeGlass(input),
|
|
191
209
|
motion: input.motion === 'full' ? 'system' : enumOr(input.motion, MOTIONS, DEFAULT_PREFERENCES.motion),
|
|
210
|
+
bakerLoading: input.bakerLoading === true,
|
|
211
|
+
backgroundMode: ['off', 'split', 'full'].includes(input.backgroundMode) ? input.backgroundMode : 'off',
|
|
212
|
+
sidebarOpacity: typeof input.sidebarOpacity === 'number' && Number.isFinite(input.sidebarOpacity) ? Math.min(1, Math.max(0, input.sidebarOpacity)) : .06,
|
|
213
|
+
railOpacity: typeof input.railOpacity === 'number' && Number.isFinite(input.railOpacity) ? Math.min(1, Math.max(0, input.railOpacity)) : .3,
|
|
214
|
+
bakerOpacity: typeof input.bakerOpacity === 'number' && Number.isFinite(input.bakerOpacity) ? Math.min(1, Math.max(0, input.bakerOpacity)) : .5,
|
|
215
|
+
classicUserOpacity: typeof input.classicUserOpacity === 'number' && Number.isFinite(input.classicUserOpacity) ? Math.min(1, Math.max(0, input.classicUserOpacity)) : .5,
|
|
216
|
+
sidebarBlur: materialBlurOr(input.sidebarBlur, DEFAULT_PREFERENCES.sidebarBlur),
|
|
217
|
+
railBlur: materialBlurOr(input.railBlur, DEFAULT_PREFERENCES.railBlur),
|
|
218
|
+
bakerBlur: materialBlurOr(input.bakerBlur, DEFAULT_PREFERENCES.bakerBlur),
|
|
219
|
+
classicUserBlur: materialBlurOr(input.classicUserBlur, DEFAULT_PREFERENCES.classicUserBlur),
|
|
220
|
+
backgroundDim: typeof input.backgroundDim === 'number' && Number.isFinite(input.backgroundDim) ? Math.min(.9, Math.max(0, input.backgroundDim)) : .45,
|
|
221
|
+
composerGlass: booleanOr(input.composerGlass, DEFAULT_PREFERENCES.composerGlass),
|
|
192
222
|
bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
|
|
193
223
|
cursor: input.cursor === 'system' ? 'system' : DEFAULT_PREFERENCES.cursor,
|
|
194
224
|
conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function createSettingsPage(React, meta = {}) {
|
|
2
2
|
const h = React.createElement
|
|
3
3
|
|
|
4
|
-
return function PrtsPluginSettingsPage({ useStore, updatePreference }) {
|
|
5
|
-
const { preferences } = useStore(state => ({ preferences: state.preferences }))
|
|
4
|
+
return function PrtsPluginSettingsPage({ useStore, updatePreference, retryPersistence }) {
|
|
5
|
+
const { preferences, persistence } = useStore(state => ({ preferences: state.preferences, persistence: state.status?.settingsPersistence }))
|
|
6
6
|
const safeMode = Boolean(meta.safeMode)
|
|
7
7
|
const enabled = Boolean(preferences.enabled) && !safeMode
|
|
8
8
|
const bootAnimation = preferences.bootAnimation !== false
|
|
@@ -40,6 +40,11 @@ export function createSettingsPage(React, meta = {}) {
|
|
|
40
40
|
safeMode && h('p', { className: 'prts-plugin-settings__safe', role: 'status' },
|
|
41
41
|
'安全模式已暂停主题。移除地址中的 ?prts-safe=1 后即可恢复。',
|
|
42
42
|
),
|
|
43
|
+
persistence?.phase === 'error' && h('div', { role: 'alert' },
|
|
44
|
+
h('p', null, '设置未保存到 DSH。请检查连接或持久化支持;重试会重新提交本次修改。'),
|
|
45
|
+
h('button', { type: 'button', onClick: retryPersistence }, '重试保存'),
|
|
46
|
+
),
|
|
47
|
+
persistence?.phase === 'saving' && h('p', { role: 'status' }, '正在保存…'),
|
|
43
48
|
h('div', { className: 'prts-plugin-settings__list' },
|
|
44
49
|
settingRow({
|
|
45
50
|
key: 'enabled',
|
|
@@ -14,6 +14,7 @@ const ROOT_ATTRIBUTES = [
|
|
|
14
14
|
'data-prts-session-flow-palette',
|
|
15
15
|
'data-prts-session-flow-speed',
|
|
16
16
|
'data-prts-conversation-style',
|
|
17
|
+
'data-prts-background-mode',
|
|
17
18
|
]
|
|
18
19
|
const SESSION_FLOW_COLOR_PROPERTIES = {
|
|
19
20
|
'--prts-session-flow-back': 'sessionFlowColorBack',
|
|
@@ -112,6 +113,12 @@ export function createThemeController({ document, window, cssText, service, curs
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
function clearThemeState() {
|
|
116
|
+
root.removeAttribute('data-prts-baker-loading')
|
|
117
|
+
root.style.removeProperty('--prts-background-shade')
|
|
118
|
+
for (const name of ['sidebar', 'rail', 'baker']) root.style.removeProperty(`--prts-${name}-opacity`)
|
|
119
|
+
root.style.removeProperty('--prts-classic-user-opacity')
|
|
120
|
+
for (const name of ['sidebar', 'rail', 'baker']) root.style.removeProperty(`--prts-${name}-blur`)
|
|
121
|
+
root.style.removeProperty('--prts-classic-user-blur')
|
|
115
122
|
for (const attribute of ROOT_ATTRIBUTES) root.removeAttribute(attribute)
|
|
116
123
|
for (const property of Object.keys(SESSION_FLOW_COLOR_PROPERTIES)) root.style.removeProperty(property)
|
|
117
124
|
clearTransitionState()
|
|
@@ -734,6 +741,13 @@ export function createThemeController({ document, window, cssText, service, curs
|
|
|
734
741
|
root.dataset.prtsSessionFlowSpeed = String(preferences.sessionFlowSpeed)
|
|
735
742
|
for (const [property, key] of Object.entries(SESSION_FLOW_COLOR_PROPERTIES)) root.style.setProperty(property, preferences[key])
|
|
736
743
|
root.dataset.prtsConversationStyle = preferences.conversationStyle
|
|
744
|
+
root.toggleAttribute('data-prts-baker-loading', preferences.bakerLoading === true)
|
|
745
|
+
root.dataset.prtsBackgroundMode = preferences.backgroundMode
|
|
746
|
+
root.style.setProperty('--prts-background-shade', `${preferences.backgroundDim * 100}%`)
|
|
747
|
+
for (const name of ['sidebar', 'rail', 'baker']) root.style.setProperty(`--prts-${name}-opacity`, `${preferences[`${name}Opacity`] * 100}%`)
|
|
748
|
+
root.style.setProperty('--prts-classic-user-opacity', `${preferences.classicUserOpacity * 100}%`)
|
|
749
|
+
for (const name of ['sidebar', 'rail', 'baker']) root.style.setProperty(`--prts-${name}-blur`, `${preferences[`${name}Blur`]}px`)
|
|
750
|
+
root.style.setProperty('--prts-classic-user-blur', `${preferences.classicUserBlur}px`)
|
|
737
751
|
refresh()
|
|
738
752
|
},
|
|
739
753
|
sync,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { backgroundMarkup, mountBackgroundSettings } from './custom-background.js'
|
|
1
2
|
import { avatarMarkup, mountAvatarSettings } from './custom-avatar.js'
|
|
2
3
|
import { customEmblemMarkup, mountCustomEmblemEditor } from './custom-emblem.js'
|
|
3
4
|
import { PARTICLE_TRAVERSAL_SPEED_DEFAULT, PARTICLE_TRAVERSAL_SPEED_MAX, PARTICLE_TRAVERSAL_SPEED_MIN, PARTICLE_TRAVERSAL_SPEED_STEP, SESSION_FLOW_SPEED_DEFAULT, SESSION_FLOW_SPEED_MAX, SESSION_FLOW_SPEED_MIN, SESSION_FLOW_SPEED_STEP, resolveParticleDetail } from './preferences.js'
|
|
@@ -37,7 +38,11 @@ function previewSetting(key, label, description) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function settingButtons(key, label, options) {
|
|
40
|
-
const note = key === '
|
|
41
|
+
const note = key === 'composerGlass'
|
|
42
|
+
? '<small data-prts-setting-note>增强输入框的背景模糊与轻微折射,关闭时使用原有材质</small>'
|
|
43
|
+
: key === 'bakerLoading'
|
|
44
|
+
? '<small data-prts-setting-note>每轮正文生成时显示三点动画,完成后显示全文;中断时保留已收到的内容</small>'
|
|
45
|
+
: key === 'motion'
|
|
41
46
|
? '<small data-prts-setting-note>关闭后减少装饰动效,保留运行状态等必要反馈</small><small data-prts-setting-note data-prts-effective-motion hidden>系统当前要求减少动态</small>'
|
|
42
47
|
: key === 'railDefaultHidden'
|
|
43
48
|
? '<small data-prts-setting-note>在任意窗口宽度下默认收起 P.R.T.S. 导航,可从左侧按钮临时展开</small>'
|
|
@@ -54,7 +59,7 @@ function settingButtons(key, label, options) {
|
|
|
54
59
|
: key === 'sessionFlowPalette'
|
|
55
60
|
? '<small data-prts-setting-note>同时适配日间与夜间模式</small>'
|
|
56
61
|
: ''
|
|
57
|
-
if (['motion', 'cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow', 'bakerGroupAvatars', 'conversationEmblem', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning'].includes(key)) return `<div data-prts-setting-row="${key}">
|
|
62
|
+
if (['composerGlass', 'bakerLoading', 'motion', 'cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow', 'bakerGroupAvatars', 'conversationEmblem', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning'].includes(key)) return `<div data-prts-setting-row="${key}">
|
|
58
63
|
<span><strong>${label}</strong>${note}</span>
|
|
59
64
|
<button type="button" role="switch" aria-label="${label}" aria-checked="false" data-prts-setting-switch="${key}" ${key === 'cursor' ? 'data-prts-cursor-switch' : ''}><span aria-hidden="true"></span></button>
|
|
60
65
|
</div>`
|
|
@@ -116,7 +121,8 @@ export function themeSettingsMarkup() {
|
|
|
116
121
|
<div data-prts-settings-body>
|
|
117
122
|
<div data-prts-settings-sidebar>
|
|
118
123
|
<nav data-prts-settings-nav role="tablist" aria-label="设置分区">
|
|
119
|
-
<button type="button" data-prts-settings-jump="appearance" role="tab" id="prts-tab-appearance" aria-controls="prts-page-appearance" aria-selected="true" tabindex="0"
|
|
124
|
+
<button type="button" data-prts-settings-jump="appearance" role="tab" id="prts-tab-appearance" aria-controls="prts-page-appearance" aria-selected="true" tabindex="0">会话主题</button>
|
|
125
|
+
<button type="button" data-prts-settings-jump="background" role="tab" id="prts-tab-background" aria-controls="prts-page-background" aria-selected="false" tabindex="-1">会话背景</button>
|
|
120
126
|
<button type="button" data-prts-settings-jump="interaction" role="tab" id="prts-tab-interaction" aria-controls="prts-page-interaction" aria-selected="false" tabindex="-1">交互</button>
|
|
121
127
|
<button type="button" data-prts-settings-jump="flow" role="tab" id="prts-tab-flow" aria-controls="prts-page-flow" aria-selected="false" tabindex="-1">会话流线</button>
|
|
122
128
|
<button type="button" data-prts-settings-jump="particles" role="tab" id="prts-tab-particles" aria-controls="prts-page-particles" aria-selected="false" tabindex="-1">粒子徽记</button>
|
|
@@ -124,7 +130,7 @@ export function themeSettingsMarkup() {
|
|
|
124
130
|
</nav>
|
|
125
131
|
<footer data-prts-settings-actions aria-label="全局主题设置操作">
|
|
126
132
|
<div data-prts-persistence-error hidden role="alert">
|
|
127
|
-
<span><strong
|
|
133
|
+
<span><strong>设置未保存</strong><small>未能保存到 DSH,可能存在连接中断、配置冲突或当前访问不支持持久保存。重试将重新提交本次修改。</small></span>
|
|
128
134
|
<button type="button" data-prts-retry-save>重试保存</button>
|
|
129
135
|
</div>
|
|
130
136
|
<div data-prts-reset-zone>
|
|
@@ -148,9 +154,14 @@ export function themeSettingsMarkup() {
|
|
|
148
154
|
</div>
|
|
149
155
|
<div data-prts-classic-options>${settingButtons('conversationEmblem', '正文徽记', [])}</div>
|
|
150
156
|
${avatarMarkup()}
|
|
151
|
-
<div data-prts-baker-options>${settingButtons('bakerGroupAvatars', '合并过程回复头像', [])}</div>
|
|
157
|
+
<div data-prts-baker-options>${settingButtons('bakerGroupAvatars', '合并过程回复头像', [])}${settingButtons('bakerLoading', '回复完成前显示加载动画', [])}</div>
|
|
152
158
|
</section>
|
|
153
159
|
</section>
|
|
160
|
+
<section data-prts-settings-section="background" role="tabpanel" id="prts-page-background" aria-labelledby="prts-tab-background" tabindex="0" hidden>
|
|
161
|
+
${backgroundMarkup()}
|
|
162
|
+
<div data-prts-composer-glass-setting>${settingButtons('composerGlass', '输入框增强毛玻璃', [])}</div>
|
|
163
|
+
<small data-prts-composer-glass-required hidden>自定义背景下,输入框增强毛玻璃始终开启。</small>
|
|
164
|
+
</section>
|
|
154
165
|
<section data-prts-settings-common data-prts-settings-section="interaction" role="tabpanel" id="prts-page-interaction" aria-labelledby="prts-tab-interaction" tabindex="0" hidden>
|
|
155
166
|
<div data-prts-settings-common-grid>
|
|
156
167
|
${COMMON_CONTROLS.filter(([key]) => ['cursor', 'railDefaultHidden', 'motion'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
|
|
@@ -222,7 +233,7 @@ export function createThemeSettingsOverlay({
|
|
|
222
233
|
trigger,
|
|
223
234
|
panel,
|
|
224
235
|
backdrop,
|
|
225
|
-
customEmblemStore, avatarStore,
|
|
236
|
+
customEmblemStore, avatarStore, backgroundStore,
|
|
226
237
|
onOpen = () => {},
|
|
227
238
|
onPreferenceChange = () => {},
|
|
228
239
|
onPreferencePreview = () => {},
|
|
@@ -230,6 +241,7 @@ export function createThemeSettingsOverlay({
|
|
|
230
241
|
onRetrySave = () => {},
|
|
231
242
|
}) {
|
|
232
243
|
const window = document?.defaultView
|
|
244
|
+
const backgroundEditor = mountBackgroundSettings({panel,window,store:backgroundStore,onChange:onPreferenceChange})
|
|
233
245
|
const avatarEditor = mountAvatarSettings({panel,window,store:avatarStore})
|
|
234
246
|
const customEditor = mountCustomEmblemEditor({ panel, window, store: customEmblemStore })
|
|
235
247
|
const transparencyMedia = window?.matchMedia?.('(prefers-reduced-transparency: reduce)')
|
|
@@ -474,8 +486,8 @@ export function createThemeSettingsOverlay({
|
|
|
474
486
|
if (phase === 'saved' && revision === renderedPersistenceRevision) return
|
|
475
487
|
if (savedTimer !== undefined) window?.clearTimeout?.(savedTimer)
|
|
476
488
|
savedTimer = undefined
|
|
477
|
-
status.hidden = phase !== 'saved'
|
|
478
|
-
status.textContent = phase === 'saved' ? '已保存' : ''
|
|
489
|
+
status.hidden = phase !== 'saved' && phase !== 'saving'
|
|
490
|
+
status.textContent = phase === 'saved' ? '已保存' : phase === 'saving' ? '正在保存…' : ''
|
|
479
491
|
if (phase === 'saved') {
|
|
480
492
|
renderedPersistenceRevision = revision
|
|
481
493
|
savedTimer = window?.setTimeout?.(() => { status.hidden = true; savedTimer = undefined }, reducedMotion() ? 500 : 1200)
|
|
@@ -707,6 +719,10 @@ export function createThemeSettingsOverlay({
|
|
|
707
719
|
panel.querySelector('[data-prts-classic-options]').hidden = preferences?.conversationStyle === 'deck-chat'
|
|
708
720
|
panel.querySelector('[data-prts-baker-options]').hidden = preferences?.conversationStyle !== 'deck-chat'
|
|
709
721
|
panel.querySelector('[data-prts-avatars]').hidden = preferences?.conversationStyle !== 'deck-chat'
|
|
722
|
+
const customBackground = preferences.backgroundMode !== 'off'
|
|
723
|
+
panel.querySelector('[data-prts-composer-glass-setting]').hidden = customBackground
|
|
724
|
+
panel.querySelector('[data-prts-composer-glass-required]').hidden = !customBackground
|
|
725
|
+
backgroundEditor.update(preferences)
|
|
710
726
|
currentPreferences = preferences
|
|
711
727
|
currentStatus = status
|
|
712
728
|
currentPersistence = persistenceState ?? { phase: 'idle', revision: 0 }
|
|
@@ -723,6 +739,7 @@ export function createThemeSettingsOverlay({
|
|
|
723
739
|
dispose() {
|
|
724
740
|
customEditor.dispose()
|
|
725
741
|
avatarEditor.dispose()
|
|
742
|
+
backgroundEditor.dispose()
|
|
726
743
|
isOpen = false
|
|
727
744
|
hidePanel()
|
|
728
745
|
if (savedTimer !== undefined) window?.clearTimeout?.(savedTimer)
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createBackgroundService, registerBackgroundApi } from './server/backgrounds.js'
|
|
2
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
3
|
+
import { DEFAULT_PREFERENCES } from './client/preferences.js'
|
|
1
4
|
import { createAvatarService, registerAvatarApi } from './server/avatars.js'
|
|
2
5
|
import { join } from 'node:path'
|
|
3
6
|
import { homedir } from 'node:os'
|
|
@@ -9,11 +12,22 @@ export const PACKAGE_ID = '@ash-qw/dsh-theme-prts'
|
|
|
9
12
|
export const THEME_PLUGIN_ID = 'dsh-theme-prts'
|
|
10
13
|
export const inject = ['tools', 'attachments', 'connection', 'webServer']
|
|
11
14
|
|
|
15
|
+
export const Config = Schema.object(Object.fromEntries([
|
|
16
|
+
...Object.entries(DEFAULT_PREFERENCES).map(([key, value]) => [key,
|
|
17
|
+
(typeof value === 'boolean' ? Schema.boolean() : typeof value === 'number' ? Schema.number() : Schema.string()).default(value).volatile(),
|
|
18
|
+
]),
|
|
19
|
+
['preferencesInitialized', Schema.boolean().default(false).volatile()],
|
|
20
|
+
]))
|
|
21
|
+
|
|
12
22
|
export async function apply(ctx) {
|
|
23
|
+
ctx.inject(['settings'], settingsCtx => {
|
|
24
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber))
|
|
25
|
+
})
|
|
13
26
|
const directory=join(process.env.DSH_HOME?.trim() || join(homedir(),'.dsh'),'plugins','dsh-theme-prts','emblems')
|
|
14
27
|
const service=await createEmblemService(directory)
|
|
28
|
+
const backgrounds=await createBackgroundService(join(directory,'..','backgrounds'))
|
|
15
29
|
const avatars=await createAvatarService(join(directory,'..','avatars'))
|
|
16
30
|
const disposers=[...emblemTools(service,ctx.attachments),...avatarTools(avatars,ctx.attachments)].map(tool=>ctx.tools.register(tool))
|
|
17
|
-
disposers.push(registerEmblemApi(ctx,service),registerAvatarApi(ctx,avatars))
|
|
18
|
-
ctx.effect(()=>async()=>{for(const dispose of disposers.reverse())dispose();await service.dispose()})
|
|
31
|
+
disposers.push(registerEmblemApi(ctx,service),registerAvatarApi(ctx,avatars),registerBackgroundApi(ctx,backgrounds))
|
|
32
|
+
ctx.effect(()=>async()=>{for(const dispose of disposers.reverse())dispose();await service.dispose();await backgrounds.dispose()})
|
|
19
33
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, rename, rm } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import sharp from 'sharp'
|
|
5
|
+
import { body, json, isBase64 } from './api.js'
|
|
6
|
+
import { fault } from './emblems.js'
|
|
7
|
+
|
|
8
|
+
export const BACKGROUND_API = '/prts-theme-api/backgrounds'
|
|
9
|
+
const BACKGROUND_SLOTS = ['full', 'sidebar', 'conversation']
|
|
10
|
+
export async function createBackgroundService(directory) {
|
|
11
|
+
await mkdir(directory, { recursive: true, mode: 0o700 })
|
|
12
|
+
const configurationPath = join(directory, 'configuration.json')
|
|
13
|
+
let state = { revision: 0, full: null, sidebar: null, conversation: null }
|
|
14
|
+
try { state = JSON.parse(await readFile(configurationPath, 'utf8')) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
15
|
+
let queue = Promise.resolve()
|
|
16
|
+
const listeners = new Set()
|
|
17
|
+
const snapshot = () => structuredClone(state)
|
|
18
|
+
return {
|
|
19
|
+
snapshot,
|
|
20
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) },
|
|
21
|
+
async image(id) {
|
|
22
|
+
if (!BACKGROUND_SLOTS.some(slot => state[slot]?.id === id)) throw fault(404, '背景不存在')
|
|
23
|
+
return readFile(join(directory, id + '.webp'))
|
|
24
|
+
},
|
|
25
|
+
save(data) {
|
|
26
|
+
const task = queue.then(async () => {
|
|
27
|
+
if (!BACKGROUND_SLOTS.includes(data?.slot)) throw fault(400, '背景区域无效')
|
|
28
|
+
if (data.revision !== state.revision) throw fault(409, '背景已在其他页面更新,请重新操作')
|
|
29
|
+
let entry = null, output
|
|
30
|
+
if (data.image !== null) {
|
|
31
|
+
if (!isBase64(data.image) || data.image.length > 14 * 1024 * 1024) throw fault(400, '请上传不超过 10 MB 的图片')
|
|
32
|
+
const bytes = Buffer.from(data.image, 'base64')
|
|
33
|
+
if (!bytes.length || bytes.length > 10 * 1024 * 1024) throw fault(400, '请上传不超过 10 MB 的图片')
|
|
34
|
+
try {
|
|
35
|
+
const input = sharp(bytes, { limitInputPixels: 40000000, animated: false })
|
|
36
|
+
const metadata = await input.metadata()
|
|
37
|
+
if (!['png', 'jpeg', 'webp', 'avif'].includes(metadata.format)) throw Error('format')
|
|
38
|
+
const result = await input.rotate().resize({ width: 3840, height: 2160, fit: 'inside', withoutEnlargement: true }).webp({ quality: 86 }).toBuffer({ resolveWithObject: true })
|
|
39
|
+
output = result.data
|
|
40
|
+
entry = { id: randomUUID(), width: result.info.width, height: result.info.height }
|
|
41
|
+
} catch { throw fault(400, '无法读取图片,请使用 PNG、JPEG、WebP 或 AVIF(最多 4000 万像素)') }
|
|
42
|
+
}
|
|
43
|
+
const next = { ...state, revision: state.revision + 1, [data.slot]: entry }
|
|
44
|
+
const temporary = configurationPath + '.' + randomUUID() + '.tmp'
|
|
45
|
+
try {
|
|
46
|
+
if (entry) await writeFile(join(directory, entry.id + '.webp'), output, { mode: 0o600 })
|
|
47
|
+
await writeFile(temporary, JSON.stringify(next), { mode: 0o600 })
|
|
48
|
+
await rename(temporary, configurationPath)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (entry) await rm(join(directory, entry.id + '.webp'), { force: true }).catch(() => {})
|
|
51
|
+
throw error
|
|
52
|
+
} finally { await rm(temporary, { force: true }).catch(() => {}) }
|
|
53
|
+
const previous = state[data.slot]
|
|
54
|
+
state = next
|
|
55
|
+
if (previous) await rm(join(directory, previous.id + '.webp'), { force: true }).catch(() => {})
|
|
56
|
+
for (const listener of listeners) { try { listener(state.revision) } catch {} }
|
|
57
|
+
return snapshot()
|
|
58
|
+
})
|
|
59
|
+
queue = task.catch(() => {})
|
|
60
|
+
return task
|
|
61
|
+
},
|
|
62
|
+
async dispose() { listeners.clear(); await queue },
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerBackgroundApi(ctx, service) {
|
|
67
|
+
const connection = ctx.connection
|
|
68
|
+
let disposed = false
|
|
69
|
+
const streams = new Set()
|
|
70
|
+
const unregister = ctx.webServer.register({ kind: 'prefix', path: BACKGROUND_API, handler: async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
if (disposed) throw fault(503, '主题服务正在重新加载')
|
|
73
|
+
if ('rejection' in connection.admit(req)) throw fault(401, '请先登录 DSH')
|
|
74
|
+
const path = new URL(req.url, 'http://localhost').pathname.slice(BACKGROUND_API.length)
|
|
75
|
+
if (req.method === 'GET' && path === '/events') {
|
|
76
|
+
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', 'x-accel-buffering': 'no' })
|
|
77
|
+
streams.add(res)
|
|
78
|
+
const send = revision => res.write('data: ' + JSON.stringify({ revision }) + '\n\n')
|
|
79
|
+
const unsubscribe = service.subscribe(send)
|
|
80
|
+
send(service.snapshot().revision)
|
|
81
|
+
const timer = setInterval(() => res.write(': heartbeat\n\n'), 25000)
|
|
82
|
+
res.on('close', () => { clearInterval(timer); unsubscribe(); streams.delete(res) })
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
if (req.method === 'GET' && /^\/image\/[a-f0-9-]{36}$/.test(path)) {
|
|
86
|
+
const bytes = await service.image(path.slice(7))
|
|
87
|
+
res.writeHead(200, { 'content-type': 'image/webp', 'cache-control': 'private, max-age=31536000, immutable', 'x-content-type-options': 'nosniff' }); res.end(bytes); return
|
|
88
|
+
}
|
|
89
|
+
if (path !== '/configuration') throw fault(404, '接口不存在')
|
|
90
|
+
if (req.method === 'GET') { json(res, 200, service.snapshot()); return }
|
|
91
|
+
if (req.method !== 'POST') throw fault(405, '请求方法不支持')
|
|
92
|
+
if (req.headers['x-prts-request'] !== '1') throw fault(403, '缺少请求标识')
|
|
93
|
+
json(res, 200, await service.save(await body(req)))
|
|
94
|
+
} catch (error) { if (!res.headersSent) json(res, error.status || 500, { error: error.message }); else res.end() }
|
|
95
|
+
} })
|
|
96
|
+
return () => { disposed = true; unregister(); for (const stream of streams) stream.end(); streams.clear() }
|
|
97
|
+
}
|
package/src/styles/cursor.css
CHANGED
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
html[data-dsh-prts][data-prts-cursor="prts"] :is(textarea, input:not([type]), input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], input[type="number"], input[type="tel"], [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]) {
|
|
17
17
|
cursor: var(--prts-text-cursor), text;
|
|
18
18
|
}
|
|
19
|
+
html[data-dsh-prts][data-prts-cursor="prts"] [data-prts-opacity-dial] svg,
|
|
19
20
|
html[data-dsh-prts][data-prts-cursor="prts"] [data-emblem-key],
|
|
20
21
|
html[data-dsh-prts][data-prts-cursor="prts"] :is(.dsh-partner-pendant-hit, .dsh-partner-pendant-anchor, [data-prts-emblem-reorder]):enabled {
|
|
21
22
|
cursor: url("__PRTS_GRAB__") 22 19, grab;
|
|
22
23
|
}
|
|
24
|
+
html[data-dsh-prts][data-prts-cursor="prts"] [data-prts-opacity-control][data-dragging] svg,
|
|
23
25
|
html[data-dsh-prts][data-prts-cursor="prts"] :is(.dsh-partner-pendant-hit, .dsh-partner-pendant-anchor, [data-prts-emblem-reorder]):enabled:active {
|
|
24
26
|
cursor: var(--prts-sort-cursor);
|
|
25
27
|
}
|