@ash-qw/dsh-theme-prts 1.0.6 → 1.1.1
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 +15 -0
- package/README.en.md +9 -1
- package/README.md +8 -2
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/THIRD_PARTY_NOTICES.zh-CN.md +4 -2
- package/lib/client.js +656 -130
- package/lib/index.js +1 -3
- package/package.json +5 -2
- package/src/assets/cursors/animation.json +56 -0
- package/src/assets/cursors/prts-grab.png +0 -0
- package/src/assets/cursors/prts-grabbing.png +0 -0
- package/src/assets/cursors/prts-not-allowed.png +0 -0
- package/src/client/cursor-adapter.js +2 -2
- package/src/client/custom-emblem.js +404 -0
- package/src/client/emblem-reorder.js +143 -0
- package/src/client/emblem-tool-preview.js +17 -0
- package/src/client/index.js +7 -1
- package/src/client/operations-shell-v2.js +2 -8
- package/src/client/particle-field-adapter.js +32 -7
- package/src/client/theme-settings-workbench.js +64 -48
- package/src/index.js +13 -1
- package/src/server/api.js +54 -0
- package/src/server/emblems.js +153 -0
- package/src/server/image-worker.js +32 -0
- package/src/server/tools.js +67 -0
- package/src/styles/cursor.css +7 -1
- package/src/styles/prts.css +0 -8
- package/src/styles/refinement.css +2 -2
- package/src/styles/theme-settings-workbench.css +82 -85
- package/src/client/resize-shield-adapter.js +0 -69
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Hold anywhere on a card to sort; movement before the hold keeps native touch scrolling.
|
|
2
|
+
export function mountEmblemReorder({ list, window, status, canStart, onStart, onCommit }) {
|
|
3
|
+
let gesture = null, holdTimer, frame, previousTime = 0, suppressClick = false
|
|
4
|
+
const rows = () => [...list.children].filter(row => row.hasAttribute('data-emblem-key'))
|
|
5
|
+
const scroller = list.closest('[data-prts-theme-settings-scroll]')
|
|
6
|
+
const clear = () => {
|
|
7
|
+
window.clearTimeout(holdTimer); window.cancelAnimationFrame(frame)
|
|
8
|
+
holdTimer = frame = undefined; previousTime = 0
|
|
9
|
+
}
|
|
10
|
+
const finish = (commit = false) => {
|
|
11
|
+
const current = gesture
|
|
12
|
+
if (!current) return
|
|
13
|
+
gesture = null; clear()
|
|
14
|
+
current.ghost?.remove();current.row.removeAttribute('data-prts-emblem-dragging')
|
|
15
|
+
list.removeAttribute('data-prts-emblem-sorting')
|
|
16
|
+
current.handle.setAttribute('aria-pressed','false')
|
|
17
|
+
try { if (list.hasPointerCapture?.(current.pointerId)) list.releasePointerCapture(current.pointerId) } catch {}
|
|
18
|
+
const ordered = rows().map(row=>row.dataset.emblemKey)
|
|
19
|
+
const changed = ordered.some((key,index)=>key!==current.original[index].dataset.emblemKey)
|
|
20
|
+
if (!commit) for (const row of current.original) list.append(row)
|
|
21
|
+
if (current.active) {
|
|
22
|
+
status.textContent = commit ? '徽记顺序未改变' : '已取消排序,原顺序保持不变'
|
|
23
|
+
if (commit && changed) void onCommit(ordered,current.row.dataset.emblemKey)
|
|
24
|
+
else current.handle.focus({preventScroll:true})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const place = y => {
|
|
28
|
+
const current = gesture
|
|
29
|
+
if (!current?.active) return
|
|
30
|
+
const others = rows().filter(row=>row!==current.row)
|
|
31
|
+
const next = others.find(row=>{const box=row.getBoundingClientRect();return y < box.top + box.height/2})
|
|
32
|
+
if (next) {
|
|
33
|
+
if (current.row.nextElementSibling !== next) list.insertBefore(current.row,next)
|
|
34
|
+
} else if (others.at(-1)?.nextElementSibling !== current.row) list.insertBefore(current.row,current.ghost)
|
|
35
|
+
current.position = rows().indexOf(current.row)
|
|
36
|
+
}
|
|
37
|
+
const tick = time => {
|
|
38
|
+
if (!gesture?.active || gesture.keyboard) return
|
|
39
|
+
const current = gesture
|
|
40
|
+
if (!list.isConnected || !list.getClientRects().length) { finish();return }
|
|
41
|
+
const delta = previousTime ? Math.min(32,time-previousTime) : 16
|
|
42
|
+
previousTime=time
|
|
43
|
+
if (scroller) {
|
|
44
|
+
const box=scroller.getBoundingClientRect(), edge=48
|
|
45
|
+
const speed=current.y < box.top+edge ? -Math.min(1,(box.top+edge-current.y)/edge)
|
|
46
|
+
: current.y > box.bottom-edge ? Math.min(1,(current.y-box.bottom+edge)/edge) : 0
|
|
47
|
+
if (speed) scroller.scrollTop += speed * delta * .5
|
|
48
|
+
}
|
|
49
|
+
place(current.y)
|
|
50
|
+
current.ghost.style.transform=`translate3d(${current.x-current.startX}px,${current.y-current.startY}px,0)`
|
|
51
|
+
frame=window.requestAnimationFrame(tick)
|
|
52
|
+
}
|
|
53
|
+
const activate = () => {
|
|
54
|
+
const current=gesture
|
|
55
|
+
if (!current || !canStart() || !current.row.isConnected) {finish();return}
|
|
56
|
+
onStart?.()
|
|
57
|
+
current.active=true
|
|
58
|
+
suppressClick=true
|
|
59
|
+
current.handle.setAttribute('aria-pressed','true')
|
|
60
|
+
list.setAttribute('data-prts-emblem-sorting','')
|
|
61
|
+
if (!current.keyboard) {
|
|
62
|
+
const box=current.row.getBoundingClientRect()
|
|
63
|
+
const ghost=current.row.cloneNode(true)
|
|
64
|
+
ghost.removeAttribute('data-emblem-key');ghost.setAttribute('data-prts-emblem-drag-ghost','')
|
|
65
|
+
ghost.setAttribute('aria-hidden','true');ghost.inert=true
|
|
66
|
+
const originals=current.row.querySelectorAll('canvas')
|
|
67
|
+
ghost.querySelectorAll('canvas').forEach((canvas,index)=>canvas.getContext('2d')?.drawImage(originals[index],0,0))
|
|
68
|
+
Object.assign(ghost.style,{position:'fixed',left:`${box.left}px`,top:`${box.top}px`,width:`${box.width}px`,boxSizing:'border-box',margin:'0',pointerEvents:'none',zIndex:'10'})
|
|
69
|
+
current.ghost=ghost;list.append(ghost)
|
|
70
|
+
// Native dialogs can establish a fixed-position containing block. Keep the lifted row under the pointer.
|
|
71
|
+
const positioned=ghost.getBoundingClientRect()
|
|
72
|
+
ghost.style.left=`${2*box.left-positioned.left}px`;ghost.style.top=`${2*box.top-positioned.top}px`
|
|
73
|
+
try {list.setPointerCapture(current.pointerId)}catch{finish();return}
|
|
74
|
+
frame=window.requestAnimationFrame(tick)
|
|
75
|
+
}
|
|
76
|
+
current.row.setAttribute('data-prts-emblem-dragging','')
|
|
77
|
+
status.textContent='正在调整徽记顺序,松开保存;按 Esc 取消'
|
|
78
|
+
}
|
|
79
|
+
const down = event => {
|
|
80
|
+
if (gesture) { if (event.pointerId!==gesture.pointerId) finish();return }
|
|
81
|
+
const row=event.target.closest('[data-emblem-key]')
|
|
82
|
+
const handle=row?.querySelector('[data-prts-emblem-reorder]')
|
|
83
|
+
suppressClick=false
|
|
84
|
+
if (!handle || handle.disabled || event.button!==0 || event.isPrimary===false || !canStart() || rows().length<2) return
|
|
85
|
+
gesture={handle,row,pointerId:event.pointerId,startX:event.clientX,startY:event.clientY,x:event.clientX,y:event.clientY,original:rows(),active:false}
|
|
86
|
+
holdTimer=window.setTimeout(activate,400)
|
|
87
|
+
}
|
|
88
|
+
const move = event => {
|
|
89
|
+
if (!gesture || gesture.pointerId!==event.pointerId) return
|
|
90
|
+
gesture.x=event.clientX;gesture.y=event.clientY
|
|
91
|
+
if (!gesture.active && Math.hypot(gesture.x-gesture.startX,gesture.y-gesture.startY)>8) {finish();return}
|
|
92
|
+
if (gesture.active) event.preventDefault()
|
|
93
|
+
}
|
|
94
|
+
const up = event => {
|
|
95
|
+
if (gesture?.pointerId!==event.pointerId) return
|
|
96
|
+
const box=list.getBoundingClientRect(), viewport=(scroller || list).getBoundingClientRect()
|
|
97
|
+
finish(event.clientX>=box.left && event.clientX<=box.right && event.clientY>=viewport.top && event.clientY<=viewport.bottom)
|
|
98
|
+
}
|
|
99
|
+
const cancelPointer = event => {if (gesture?.pointerId===event.pointerId) finish()}
|
|
100
|
+
const cancel = () => finish()
|
|
101
|
+
const visibility = () => {if (window.document.hidden) finish()}
|
|
102
|
+
const keydown = event => {
|
|
103
|
+
if (event.key==='Escape' && gesture) {event.preventDefault();event.stopPropagation();finish();return}
|
|
104
|
+
const handle=event.target.closest?.('[data-prts-emblem-reorder]')
|
|
105
|
+
if (!handle || handle.disabled || !canStart()) return
|
|
106
|
+
if ([' ','Enter'].includes(event.key)) {
|
|
107
|
+
event.preventDefault()
|
|
108
|
+
if (gesture?.keyboard) {finish(true);return}
|
|
109
|
+
if (gesture) return
|
|
110
|
+
gesture={handle,row:handle.closest('[data-emblem-key]'),original:rows(),keyboard:true,active:false}
|
|
111
|
+
activate();status.textContent='用上下方向键调整顺序,空格或回车保存;Esc 取消'
|
|
112
|
+
} else if (gesture?.keyboard && ['ArrowUp','ArrowDown','Home','End'].includes(event.key)) {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
const current=rows(),index=current.indexOf(gesture.row)
|
|
115
|
+
const target=event.key==='Home'?0:event.key==='End'?current.length-1:Math.max(0,Math.min(current.length-1,index+(event.key==='ArrowDown'?1:-1)))
|
|
116
|
+
if (target!==index)list.insertBefore(gesture.row,target<index?current[target]:current[target].nextSibling)
|
|
117
|
+
gesture.handle.focus({preventScroll:true});gesture.row.scrollIntoView({block:'nearest'})
|
|
118
|
+
status.textContent=`当前位置:第 ${target+1} 项,共 ${current.length} 项;空格保存`
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const touchmove = event => {if(gesture?.active && !gesture.keyboard)event.preventDefault()}
|
|
122
|
+
const context = event => {if(event.target.closest('[data-emblem-key]')||gesture?.active)event.preventDefault()}
|
|
123
|
+
const click = event => {if(suppressClick || gesture?.active || event.target.closest('[data-prts-emblem-reorder]')){event.preventDefault();event.stopPropagation()}}
|
|
124
|
+
list.addEventListener('touchmove',touchmove,{passive:false})
|
|
125
|
+
list.addEventListener('pointerdown',down)
|
|
126
|
+
list.addEventListener('contextmenu',context)
|
|
127
|
+
list.addEventListener('click',click,true)
|
|
128
|
+
window.addEventListener('pointermove',move,{passive:false})
|
|
129
|
+
window.addEventListener('pointerup',up)
|
|
130
|
+
window.addEventListener('pointercancel',cancelPointer)
|
|
131
|
+
list.addEventListener('lostpointercapture',cancelPointer)
|
|
132
|
+
window.addEventListener('keydown',keydown,true)
|
|
133
|
+
window.addEventListener('blur',cancel)
|
|
134
|
+
window.addEventListener('resize',cancel)
|
|
135
|
+
window.document.addEventListener('visibilitychange',visibility)
|
|
136
|
+
return { cancel, get active(){return !!gesture?.active}, dispose() {
|
|
137
|
+
finish()
|
|
138
|
+
list.removeEventListener('touchmove',touchmove);list.removeEventListener('pointerdown',down);list.removeEventListener('contextmenu',context);list.removeEventListener('click',click,true)
|
|
139
|
+
window.removeEventListener('pointermove',move);window.removeEventListener('pointerup',up);window.removeEventListener('pointercancel',cancelPointer)
|
|
140
|
+
list.removeEventListener('lostpointercapture',cancelPointer);window.removeEventListener('keydown',keydown,true);window.removeEventListener('blur',cancel);window.removeEventListener('resize',cancel)
|
|
141
|
+
window.document.removeEventListener('visibilitychange',visibility)
|
|
142
|
+
} }
|
|
143
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The native tool seat keeps previews available even for sessions with a remote filesystem.
|
|
2
|
+
export function registerEmblemToolPreview(ctx, React) {
|
|
3
|
+
if (!ctx.slots?.inject || !React?.createElement) return () => {}
|
|
4
|
+
return ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
5
|
+
name:'tool.call.toolview',key:'prts_emblem_preview',
|
|
6
|
+
}, ({block}) => {
|
|
7
|
+
const h=React.createElement
|
|
8
|
+
let result
|
|
9
|
+
for(const part of block?.content ?? [])if(part.type==='text'){
|
|
10
|
+
try {const value=JSON.parse(part.text);if(value.draftId)result=value}catch{}
|
|
11
|
+
}
|
|
12
|
+
if(!result || !/^[a-zA-Z0-9-]{1,80}$/.test(result.draftId))return h('div',null,block?.isError?'徽记预览失败':'正在生成徽记预览…')
|
|
13
|
+
return h('figure',{'data-prts-tool-preview':'',style:{margin:'8px 0'}},
|
|
14
|
+
h('img',{src:`/prts-theme-api/emblems/draft/${result.draftId}/preview.png`,alt:'粒子徽记预览',width:384,height:384,style:{maxWidth:'100%',height:'auto',display:'block',borderRadius:8}}),
|
|
15
|
+
h('figcaption',null,`${result.label || '自定义徽记'} · 标准密度 ${result.particles?.standard ?? ''} 个粒子 · 未应用`))
|
|
16
|
+
}))
|
|
17
|
+
}
|
package/src/client/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerEmblemToolPreview } from './emblem-tool-preview.js'
|
|
2
|
+
import { createCustomEmblemStore } from './custom-emblem.js'
|
|
1
3
|
import { DEFAULT_PREFERENCES, isSafeMode, loadPreferences, persistPreferences, resetPreferenceGroup, updatePreferenceValue } from './preferences.js'
|
|
2
4
|
import { createHostGeometryAdapter } from './host-geometry-adapter.js'
|
|
3
5
|
import { createComposerGlassAdapter } from './composer-glass-adapter.js'
|
|
@@ -42,6 +44,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
42
44
|
startupTimings,
|
|
43
45
|
} = environment
|
|
44
46
|
const safeMode = isSafeMode(window?.location?.search ?? '')
|
|
47
|
+
const stopEmblemPreview = safeMode ? null : registerEmblemToolPreview(ctx, React)
|
|
45
48
|
const storage = window?.localStorage
|
|
46
49
|
const themeService = contextService(ctx, 'theme')
|
|
47
50
|
const composerGlass = createComposerGlassAdapter({ document, window })
|
|
@@ -61,6 +64,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
61
64
|
sync()
|
|
62
65
|
},
|
|
63
66
|
})
|
|
67
|
+
const customEmblemStore = createCustomEmblemStore({ window, emblems: assets.emblems, preview: entry => particleField.previewEmblem?.(entry), onChange: value => particleField.setCustomEmblemConfiguration?.(value) })
|
|
64
68
|
const startup = createPrtsStartupSequence({
|
|
65
69
|
document,
|
|
66
70
|
window,
|
|
@@ -187,7 +191,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
187
191
|
}
|
|
188
192
|
if (!themeAlreadyApplied) theme.apply(preferences)
|
|
189
193
|
operations ||= createOperationsShell({
|
|
190
|
-
document, window, assets, adapter, hostGeometry,
|
|
194
|
+
document, window, assets, adapter, hostGeometry, customEmblemStore,
|
|
191
195
|
onSchemeToggle(next, interaction) {
|
|
192
196
|
return theme.setTheme(next, { ...interaction, animate: true })
|
|
193
197
|
},
|
|
@@ -339,7 +343,9 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
339
343
|
applyVisualState()
|
|
340
344
|
}
|
|
341
345
|
ctx.effect(() => () => {
|
|
346
|
+
if (typeof stopEmblemPreview === 'function') stopEmblemPreview()
|
|
342
347
|
statusSource.dispose()
|
|
348
|
+
customEmblemStore.dispose()
|
|
343
349
|
particleField.dispose()
|
|
344
350
|
startup.dispose({ restoreFocus: false })
|
|
345
351
|
stopStartupReadyWatch()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createThemeSettingsOverlay, themeSettingsMarkup } from './theme-settings-workbench.js'
|
|
2
|
-
import { createResizeShieldAdapter } from './resize-shield-adapter.js'
|
|
3
2
|
|
|
4
3
|
const PHONE_RAIL_BREAKPOINT = 640
|
|
5
4
|
const RAIL_HOLD_DELAY_MS = 350
|
|
@@ -46,13 +45,13 @@ export function createOperationsShell({
|
|
|
46
45
|
onSchemeToggle = () => {},
|
|
47
46
|
onSchemeDragStart = () => undefined,
|
|
48
47
|
onThemeDisable = () => {},
|
|
48
|
+
customEmblemStore,
|
|
49
49
|
onPreferenceChange = () => {},
|
|
50
50
|
onPreferencePreview = () => {},
|
|
51
51
|
onVisualReset = () => {},
|
|
52
52
|
onRetrySave = () => {},
|
|
53
53
|
}) {
|
|
54
54
|
const root = document.documentElement
|
|
55
|
-
const resizeShield = createResizeShieldAdapter({ window })
|
|
56
55
|
let shell
|
|
57
56
|
let navRail
|
|
58
57
|
let railLauncher
|
|
@@ -456,16 +455,12 @@ export function createOperationsShell({
|
|
|
456
455
|
trigger: shell.querySelector('[data-prts-rail-brand]'),
|
|
457
456
|
panel: shell.querySelector('[data-prts-theme-settings]'),
|
|
458
457
|
backdrop: shell.querySelector('[data-prts-settings-backdrop]'),
|
|
458
|
+
customEmblemStore,
|
|
459
459
|
onPreferenceChange,
|
|
460
460
|
onPreferencePreview,
|
|
461
461
|
onResetVisual: onVisualReset,
|
|
462
462
|
onRetrySave,
|
|
463
463
|
})
|
|
464
|
-
resizeShield.start([
|
|
465
|
-
operationRegion,
|
|
466
|
-
shell.querySelector('[data-prts-theme-settings]'),
|
|
467
|
-
shell.querySelector('[data-prts-settings-backdrop]'),
|
|
468
|
-
])
|
|
469
464
|
conversationObserver = new window.MutationObserver(syncConversationState)
|
|
470
465
|
conversationObserver.observe(operationRegion, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden', 'data-chat-flow-kind', 'data-message-role'] })
|
|
471
466
|
schemeObserver = new window.MutationObserver(syncSchemeToggle)
|
|
@@ -518,7 +513,6 @@ export function createOperationsShell({
|
|
|
518
513
|
railViewportQuery = undefined
|
|
519
514
|
railPointerQuery = undefined
|
|
520
515
|
railHoverQuery = undefined
|
|
521
|
-
resizeShield.dispose()
|
|
522
516
|
settingsOverlay?.dispose()
|
|
523
517
|
conversationObserver?.disconnect()
|
|
524
518
|
schemeObserver?.disconnect()
|
|
@@ -365,6 +365,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
365
365
|
},
|
|
366
366
|
opticalScale: suppliedHeroMask.opticalScale,
|
|
367
367
|
} : maskRecords[rhodesIndex]
|
|
368
|
+
let customEmblemConfiguration = null
|
|
368
369
|
const targetCache = new Map()
|
|
369
370
|
let particles = []
|
|
370
371
|
let pointerActive = false
|
|
@@ -448,35 +449,44 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
448
449
|
}
|
|
449
450
|
|
|
450
451
|
function sequenceIndexForSegment(segmentValue) {
|
|
451
|
-
const length = Math.max(1, emblemSequence.length)
|
|
452
|
+
const length = Math.max(1, (customEmblemConfiguration?.conversation ?? emblemSequence).length)
|
|
452
453
|
return ((segmentValue % length) + length) % length
|
|
453
454
|
}
|
|
454
455
|
|
|
455
456
|
function emblemForSegment(segmentValue) {
|
|
456
|
-
|
|
457
|
+
const sequence = customEmblemConfiguration?.conversation ?? emblemSequence
|
|
458
|
+
return sequence[sequenceIndexForSegment(segmentValue)] ?? sequence[0]
|
|
457
459
|
}
|
|
458
460
|
|
|
459
|
-
function
|
|
460
|
-
const
|
|
461
|
+
function targetsForBuiltin(entry, density = 'standard') {
|
|
462
|
+
const index = emblemSequence.findIndex(item => item.key === entry?.key)
|
|
463
|
+
const key = `builtin:${index}:${density}`
|
|
461
464
|
const cached = targetCache.get(key)
|
|
462
465
|
if (cached) return cached
|
|
463
466
|
const record = maskRecords[index]
|
|
464
467
|
const targets = record ? createOrthogonalTargets(record.mask, density) : []
|
|
465
|
-
if (
|
|
466
|
-
targetCache.set(key, targets)
|
|
468
|
+
if (targets.length) targetCache.set(key, targets)
|
|
467
469
|
return targets
|
|
468
470
|
}
|
|
469
471
|
|
|
472
|
+
function targetsForIndex(index, density = 'standard') {
|
|
473
|
+
const entry = (customEmblemConfiguration?.conversation ?? emblemSequence)[index]
|
|
474
|
+
if (entry?.kind === 'custom') return entry.targets[density] ?? entry.targets.standard
|
|
475
|
+
return targetsForBuiltin(entry, density)
|
|
476
|
+
}
|
|
477
|
+
|
|
470
478
|
function targetsForSegment(segmentValue, density = preferences.conversationParticleDensity) {
|
|
471
479
|
return targetsForIndex(sequenceIndexForSegment(segmentValue), density)
|
|
472
480
|
}
|
|
473
481
|
|
|
474
482
|
function heroEmblem() {
|
|
483
|
+
if (customEmblemConfiguration?.hero) return customEmblemConfiguration.hero
|
|
475
484
|
return emblemSequence[rhodesIndex] ?? emblemSequence[0]
|
|
476
485
|
}
|
|
477
486
|
|
|
478
487
|
function targetsForHero(density = preferences.heroParticleDensity) {
|
|
479
488
|
const effectiveDensity = effectiveHeroDensity(density)
|
|
489
|
+
if (customEmblemConfiguration?.hero) return customEmblemConfiguration.hero.targets[effectiveDensity] ?? customEmblemConfiguration.hero.targets.standard
|
|
480
490
|
const key = `hero:${effectiveDensity}`
|
|
481
491
|
const cached = targetCache.get(key)
|
|
482
492
|
if (cached) return cached
|
|
@@ -688,7 +698,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
688
698
|
function scheduleTargetPrewarm() {
|
|
689
699
|
cancelTargetPrewarm()
|
|
690
700
|
if (typeof window.requestIdleCallback !== 'function' || !particles.length) return
|
|
691
|
-
targetPrewarmQueue = emblemSequence.map((_, index) => index)
|
|
701
|
+
targetPrewarmQueue = (customEmblemConfiguration?.conversation ?? emblemSequence).map((_, index) => index)
|
|
692
702
|
.filter(index => index !== sequenceIndexForSegment(segment))
|
|
693
703
|
const warmNext = () => {
|
|
694
704
|
targetPrewarmHandle = undefined
|
|
@@ -1836,6 +1846,21 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1836
1846
|
schemeTransitionPaused = next
|
|
1837
1847
|
updateFrameGate()
|
|
1838
1848
|
},
|
|
1849
|
+
previewEmblem(entry) {
|
|
1850
|
+
if (entry?.kind === 'custom') return entry
|
|
1851
|
+
const points = entry ? targetsForBuiltin(entry, 'standard') : heroMaskRecord ? createOrthogonalTargets(heroMaskRecord.mask, 'standard') : []
|
|
1852
|
+
return { targets: { standard: points } }
|
|
1853
|
+
},
|
|
1854
|
+
setCustomEmblemConfiguration(value) {
|
|
1855
|
+
const previousSequence = customEmblemConfiguration?.conversation ?? emblemSequence
|
|
1856
|
+
const nextSequence = value?.conversation ?? emblemSequence
|
|
1857
|
+
const changed = heroActive
|
|
1858
|
+
? (customEmblemConfiguration?.hero ?? null) !== (value?.hero ?? null)
|
|
1859
|
+
: previousSequence.length !== nextSequence.length || previousSequence.some((entry, index) =>
|
|
1860
|
+
entry.key !== nextSequence[index]?.key || entry.targets !== nextSequence[index]?.targets)
|
|
1861
|
+
customEmblemConfiguration = value
|
|
1862
|
+
if (canvas && changed) { resetTraversal(); scheduleFrame() }
|
|
1863
|
+
},
|
|
1839
1864
|
update(nextPreferences = {}) {
|
|
1840
1865
|
const previousPreferences = preferences
|
|
1841
1866
|
const traversalSpeedChanged = previousPreferences.particleTraversalSpeed !== undefined
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { customEmblemMarkup, mountCustomEmblemEditor } from './custom-emblem.js'
|
|
1
2
|
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'
|
|
2
3
|
|
|
3
4
|
const SETTINGS_FOCUSABLE = [
|
|
4
5
|
'button:not([disabled])',
|
|
5
6
|
'input:not([disabled])',
|
|
7
|
+
'select:not([disabled])',
|
|
6
8
|
'summary',
|
|
7
9
|
'[href]',
|
|
8
10
|
'[tabindex]:not([tabindex="-1"])',
|
|
@@ -119,14 +121,31 @@ export function themeSettingsMarkup() {
|
|
|
119
121
|
</header>
|
|
120
122
|
|
|
121
123
|
<div data-prts-settings-body>
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
<button type="button" data-prts-settings-jump="
|
|
125
|
-
<button type="button" data-prts-settings-jump="
|
|
126
|
-
<button type="button" data-prts-settings-jump="
|
|
124
|
+
<div data-prts-settings-sidebar>
|
|
125
|
+
<nav data-prts-settings-nav role="tablist" aria-label="设置分区">
|
|
126
|
+
<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>
|
|
127
|
+
<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>
|
|
128
|
+
<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>
|
|
129
|
+
<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>
|
|
127
130
|
</nav>
|
|
131
|
+
<footer data-prts-settings-actions aria-label="全局主题设置操作">
|
|
132
|
+
<div data-prts-persistence-error hidden role="alert">
|
|
133
|
+
<span><strong>仅本次会话生效</strong><small>无法写入本地设置,当前视觉效果仍会保留。</small></span>
|
|
134
|
+
<button type="button" data-prts-retry-save>重试保存</button>
|
|
135
|
+
</div>
|
|
136
|
+
<div data-prts-reset-zone>
|
|
137
|
+
|
|
138
|
+
<button type="button" data-prts-reset-visual title="恢复全部外观设置" aria-label="恢复全部外观设置">恢复默认</button>
|
|
139
|
+
<div data-prts-reset-confirm hidden role="group" aria-label="确认恢复全部外观设置">
|
|
140
|
+
<span>重置四个分类的外观参数,保留主题开关、启动动画和徽记配置。</span>
|
|
141
|
+
<button type="button" data-prts-reset-confirm-action>确认恢复</button>
|
|
142
|
+
<button type="button" data-prts-reset-cancel>取消</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</footer>
|
|
146
|
+
</div>
|
|
128
147
|
<div data-prts-theme-settings-scroll>
|
|
129
|
-
<section data-prts-settings-section="appearance"
|
|
148
|
+
<section data-prts-settings-section="appearance" role="tabpanel" id="prts-page-appearance" aria-labelledby="prts-tab-appearance" tabindex="0">
|
|
130
149
|
<header data-prts-section-heading><strong>外观</strong><small>预设、界面材质与会话样式</small></header>
|
|
131
150
|
<section data-prts-settings-presets>
|
|
132
151
|
<header><span><strong>视觉预设</strong></span></header>
|
|
@@ -141,13 +160,13 @@ export function themeSettingsMarkup() {
|
|
|
141
160
|
</div>
|
|
142
161
|
</section>
|
|
143
162
|
</section>
|
|
144
|
-
<section data-prts-settings-common data-prts-settings-section="interaction"
|
|
163
|
+
<section data-prts-settings-common data-prts-settings-section="interaction" role="tabpanel" id="prts-page-interaction" aria-labelledby="prts-tab-interaction" tabindex="0" hidden>
|
|
145
164
|
<header data-prts-section-heading><strong>交互</strong><small>光标、导航栏与动态反馈</small></header>
|
|
146
165
|
<div data-prts-settings-common-grid>
|
|
147
166
|
${COMMON_CONTROLS.filter(([key]) => ['cursor', 'railDefaultHidden', 'motion'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
|
|
148
167
|
</div>
|
|
149
168
|
</section>
|
|
150
|
-
<section data-prts-settings-common data-prts-settings-section="flow"
|
|
169
|
+
<section data-prts-settings-common data-prts-settings-section="flow" role="tabpanel" id="prts-page-flow" aria-labelledby="prts-tab-flow" tabindex="0" hidden>
|
|
151
170
|
<header data-prts-section-heading><strong>会话流线</strong><small>当前会话的流线与辉光</small></header>
|
|
152
171
|
<div data-prts-settings-common-grid>
|
|
153
172
|
${COMMON_CONTROLS.filter(([key]) => ['sessionFlow', 'sessionGlow', 'sessionFlowPalette'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
|
|
@@ -160,9 +179,10 @@ export function themeSettingsMarkup() {
|
|
|
160
179
|
</div>
|
|
161
180
|
</section>
|
|
162
181
|
|
|
163
|
-
<
|
|
164
|
-
<
|
|
182
|
+
<section data-prts-settings-advanced data-prts-settings-section="particles" role="tabpanel" id="prts-page-particles" aria-labelledby="prts-tab-particles" tabindex="0" hidden>
|
|
183
|
+
<header data-prts-section-heading><span><strong>粒子效果</strong><small>徽记精度与跟随移动速度</small></span><output data-prts-advanced-summary>标准 / 1×</output></header>
|
|
165
184
|
<div data-prts-settings-advanced-content>
|
|
185
|
+
${customEmblemMarkup()}
|
|
166
186
|
<section data-prts-particle-detail>
|
|
167
187
|
<header><strong>粒子精度</strong><span>同时调整会话与新会话徽记</span></header>
|
|
168
188
|
<div role="radiogroup" aria-label="粒子精度">${particleMarkup()}</div>
|
|
@@ -173,25 +193,11 @@ export function themeSettingsMarkup() {
|
|
|
173
193
|
<span data-prts-particle-error role="alert" hidden>粒子资源加载异常,主题已使用静态后备效果</span>
|
|
174
194
|
</section>
|
|
175
195
|
</div>
|
|
176
|
-
</
|
|
196
|
+
</section>
|
|
177
197
|
|
|
178
|
-
<footer data-prts-settings-actions>
|
|
179
|
-
<div data-prts-persistence-error hidden role="alert">
|
|
180
|
-
<span><strong>仅本次会话生效</strong><small>无法写入本地设置,当前视觉效果仍会保留。</small></span>
|
|
181
|
-
<button type="button" data-prts-retry-save>重试保存</button>
|
|
182
|
-
</div>
|
|
183
|
-
<div data-prts-reset-zone>
|
|
184
|
-
<span><strong>恢复主题默认</strong><small>保留主题开关和启动动画设置</small></span>
|
|
185
|
-
<button type="button" data-prts-reset-visual>恢复默认</button>
|
|
186
|
-
<div data-prts-reset-confirm hidden role="group" aria-label="确认恢复主题默认">
|
|
187
|
-
<span>确认恢复全部主题参数?</span>
|
|
188
|
-
<button type="button" data-prts-reset-confirm-action>确认恢复</button>
|
|
189
|
-
<button type="button" data-prts-reset-cancel>取消</button>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
</footer>
|
|
193
198
|
</div>
|
|
194
199
|
</div>
|
|
200
|
+
|
|
195
201
|
</div>
|
|
196
202
|
</dialog>`
|
|
197
203
|
}
|
|
@@ -201,6 +207,7 @@ export function createThemeSettingsOverlay({
|
|
|
201
207
|
trigger,
|
|
202
208
|
panel,
|
|
203
209
|
backdrop,
|
|
210
|
+
customEmblemStore,
|
|
204
211
|
onOpen = () => {},
|
|
205
212
|
onPreferenceChange = () => {},
|
|
206
213
|
onPreferencePreview = () => {},
|
|
@@ -208,6 +215,7 @@ export function createThemeSettingsOverlay({
|
|
|
208
215
|
onRetrySave = () => {},
|
|
209
216
|
}) {
|
|
210
217
|
const window = document?.defaultView
|
|
218
|
+
const customEditor = mountCustomEmblemEditor({ panel, window, store: customEmblemStore })
|
|
211
219
|
const transparencyMedia = window?.matchMedia?.('(prefers-reduced-transparency: reduce)')
|
|
212
220
|
const motionMedia = window?.matchMedia?.('(prefers-reduced-motion: reduce)')
|
|
213
221
|
const nativeModal = typeof panel?.showModal === 'function' && typeof panel?.close === 'function'
|
|
@@ -519,19 +527,25 @@ export function createThemeSettingsOverlay({
|
|
|
519
527
|
}, 300)
|
|
520
528
|
}
|
|
521
529
|
|
|
522
|
-
|
|
530
|
+
let activeSection = 'appearance'
|
|
531
|
+
const sectionScroll = new Map()
|
|
532
|
+
function selectSection(key, focus = false) {
|
|
523
533
|
const scroller = panel?.querySelector('[data-prts-theme-settings-scroll]')
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
534
|
+
const section = panel?.querySelector(`[data-prts-settings-section="${key}"]`)
|
|
535
|
+
if (!scroller || !section) return
|
|
536
|
+
if (key !== activeSection) {
|
|
537
|
+
sectionScroll.set(activeSection, scroller.scrollTop)
|
|
538
|
+
customEditor.cancelReorder?.()
|
|
539
|
+
for (const page of panel.querySelectorAll('[data-prts-settings-section]')) page.hidden = page !== section
|
|
540
|
+
activeSection = key
|
|
541
|
+
scroller.scrollTop = sectionScroll.get(key) || 0
|
|
542
|
+
setResetConfirmation(false)
|
|
530
543
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
544
|
+
for (const tab of panel.querySelectorAll('[data-prts-settings-jump]')) {
|
|
545
|
+
const selected = tab.dataset.prtsSettingsJump === key
|
|
546
|
+
tab.setAttribute('aria-selected', String(selected))
|
|
547
|
+
tab.tabIndex = selected ? 0 : -1
|
|
548
|
+
if (selected && focus) tab.focus({preventScroll:true})
|
|
535
549
|
}
|
|
536
550
|
}
|
|
537
551
|
|
|
@@ -540,14 +554,7 @@ export function createThemeSettingsOverlay({
|
|
|
540
554
|
const target = event.target?.closest?.('button')
|
|
541
555
|
if (!target) return
|
|
542
556
|
if (target.hasAttribute('data-prts-settings-jump')) {
|
|
543
|
-
|
|
544
|
-
const scroller = panel.querySelector('[data-prts-theme-settings-scroll]')
|
|
545
|
-
if (section && scroller) {
|
|
546
|
-
if (section.tagName === 'DETAILS') section.open = true
|
|
547
|
-
scroller.scrollTo?.({ top: target.dataset.prtsSettingsJump === 'appearance' ? 0 : scroller.scrollTop + section.getBoundingClientRect().top - scroller.getBoundingClientRect().top - 16, behavior: 'instant' })
|
|
548
|
-
section.focus?.({ preventScroll: true })
|
|
549
|
-
syncSectionNavigation()
|
|
550
|
-
}
|
|
557
|
+
selectSection(target.dataset.prtsSettingsJump)
|
|
551
558
|
return
|
|
552
559
|
}
|
|
553
560
|
if (target.hasAttribute('data-prts-theme-settings-close')) { setOpen(false); return }
|
|
@@ -615,6 +622,16 @@ export function createThemeSettingsOverlay({
|
|
|
615
622
|
|
|
616
623
|
function onKeydown(event) {
|
|
617
624
|
if (!isOpen) return
|
|
625
|
+
const tab = event.target.closest?.('[data-prts-settings-jump]')
|
|
626
|
+
if (tab && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Home','End'].includes(event.key)) {
|
|
627
|
+
event.preventDefault()
|
|
628
|
+
const tabs = [...panel.querySelectorAll('[data-prts-settings-jump]')]
|
|
629
|
+
const index = tabs.indexOf(tab)
|
|
630
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length-1
|
|
631
|
+
: (index + (['ArrowRight','ArrowDown'].includes(event.key) ? 1 : -1) + tabs.length) % tabs.length
|
|
632
|
+
selectSection(tabs[next].dataset.prtsSettingsJump, true)
|
|
633
|
+
return
|
|
634
|
+
}
|
|
618
635
|
if (event.key === 'Escape') { event.preventDefault(); setOpen(false); return }
|
|
619
636
|
if (nativeModal || event.key !== 'Tab') return
|
|
620
637
|
const nodes = focusable()
|
|
@@ -642,8 +659,7 @@ export function createThemeSettingsOverlay({
|
|
|
642
659
|
|
|
643
660
|
trigger?.addEventListener('click', toggle)
|
|
644
661
|
panel?.addEventListener('click', onPanelClick)
|
|
645
|
-
|
|
646
|
-
panel?.querySelector('[data-prts-settings-jump]')?.setAttribute('aria-current', 'location')
|
|
662
|
+
selectSection('appearance')
|
|
647
663
|
panel?.addEventListener('cancel', onCancel)
|
|
648
664
|
panel?.addEventListener('input', onPanelInput)
|
|
649
665
|
panel?.addEventListener('change', commitFromControl)
|
|
@@ -670,6 +686,7 @@ export function createThemeSettingsOverlay({
|
|
|
670
686
|
close: () => setOpen(false),
|
|
671
687
|
toggle,
|
|
672
688
|
dispose() {
|
|
689
|
+
customEditor.dispose()
|
|
673
690
|
isOpen = false
|
|
674
691
|
hidePanel()
|
|
675
692
|
if (savedTimer !== undefined) window?.clearTimeout?.(savedTimer)
|
|
@@ -678,7 +695,6 @@ export function createThemeSettingsOverlay({
|
|
|
678
695
|
trigger?.setAttribute('aria-expanded', 'false')
|
|
679
696
|
trigger?.removeEventListener('click', toggle)
|
|
680
697
|
panel?.removeEventListener('click', onPanelClick)
|
|
681
|
-
panel?.querySelector('[data-prts-theme-settings-scroll]')?.removeEventListener('scroll', syncSectionNavigation)
|
|
682
698
|
panel?.removeEventListener('cancel', onCancel)
|
|
683
699
|
panel?.removeEventListener('input', onPanelInput)
|
|
684
700
|
panel?.removeEventListener('change', commitFromControl)
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { createEmblemService } from './server/emblems.js'
|
|
4
|
+
import { emblemTools } from './server/tools.js'
|
|
5
|
+
import { registerEmblemApi } from './server/api.js'
|
|
1
6
|
export const PACKAGE_ID = '@ash-qw/dsh-theme-prts'
|
|
2
7
|
export const THEME_PLUGIN_ID = 'dsh-theme-prts'
|
|
8
|
+
export const inject = ['tools', 'attachments', 'connection', 'webServer']
|
|
3
9
|
|
|
4
|
-
export function apply() {
|
|
10
|
+
export async function apply(ctx) {
|
|
11
|
+
const directory=join(process.env.DSH_HOME?.trim() || join(homedir(),'.dsh'),'plugins','dsh-theme-prts','emblems')
|
|
12
|
+
const service=await createEmblemService(directory)
|
|
13
|
+
const disposers=emblemTools(service,ctx.attachments).map(tool=>ctx.tools.register(tool))
|
|
14
|
+
disposers.push(registerEmblemApi(ctx,service))
|
|
15
|
+
ctx.on('dispose',()=>{for(const dispose of disposers.reverse())dispose()})
|
|
16
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { API, draftView, fault } from './emblems.js'
|
|
2
|
+
|
|
3
|
+
async function body(req) {
|
|
4
|
+
const chunks=[];let size=0
|
|
5
|
+
for await(const chunk of req){size+=chunk.length;if(size>16*1024*1024)throw fault(413,'请求过大');chunks.push(chunk)}
|
|
6
|
+
try{return JSON.parse(Buffer.concat(chunks).toString('utf8'))}catch{throw fault(400,'请求数据无效')}
|
|
7
|
+
}
|
|
8
|
+
function json(res,status,value){res.writeHead(status,{'content-type':'application/json; charset=utf-8','cache-control':'no-store'});res.end(JSON.stringify(value))}
|
|
9
|
+
export function registerEmblemApi(ctx,service) {
|
|
10
|
+
const streams=new Set()
|
|
11
|
+
const dispose=ctx.webServer.register({kind:'prefix',path:API,handler:async(req,res)=>{
|
|
12
|
+
try {
|
|
13
|
+
const admission=ctx.connection.admit(req)
|
|
14
|
+
if('rejection' in admission){json(res,401,{error:'请先登录 DSH'});return}
|
|
15
|
+
const path=new URL(req.url,'http://localhost').pathname.slice(API.length)
|
|
16
|
+
if(req.method==='GET'&&path==='/events') {
|
|
17
|
+
res.writeHead(200,{'content-type':'text/event-stream','cache-control':'no-cache','x-accel-buffering':'no'})
|
|
18
|
+
streams.add(res)
|
|
19
|
+
const send=revision=>res.write(`data: ${JSON.stringify({revision})}\n\n`)
|
|
20
|
+
const unsubscribe=service.subscribe(send)
|
|
21
|
+
send(service.snapshot().revision)
|
|
22
|
+
const heartbeat=setInterval(()=>res.write(': heartbeat\n\n'),25000)
|
|
23
|
+
res.on('close',()=>{clearInterval(heartbeat);unsubscribe();streams.delete(res)})
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
if(req.method==='GET'&&path==='/configuration'){json(res,200,service.snapshot());return}
|
|
27
|
+
const exported=path.match(/^\/draft\/([a-zA-Z0-9-]+)\/export$/)
|
|
28
|
+
if(req.method==='GET'&&exported){json(res,200,await service.exportAsset(exported[1]));return}
|
|
29
|
+
const match=path.match(/^\/draft\/([a-zA-Z0-9-]+)(?:\/(preview\.png|source))?$/)
|
|
30
|
+
if(req.method==='GET'&&match){
|
|
31
|
+
if(!match[2]){const d=await service.draft(match[1]);json(res,200,{...draftView(d),value:d.value});return}
|
|
32
|
+
const bytes=await service.image(match[1],match[2]==='source')
|
|
33
|
+
res.writeHead(200,{'content-type':match[2]==='source'?'application/octet-stream':'image/png','cache-control':'private, max-age=3600','x-content-type-options':'nosniff'});res.end(bytes);return
|
|
34
|
+
}
|
|
35
|
+
if(req.method!=='POST')throw fault(404,'接口不存在')
|
|
36
|
+
if(req.headers['x-prts-request']!=='1')throw fault(403,'缺少请求标识')
|
|
37
|
+
const controller=new AbortController()
|
|
38
|
+
res.on('close',()=>{if(!res.writableEnded)controller.abort(new Error('请求已取消'))})
|
|
39
|
+
const data=await body(req)
|
|
40
|
+
if(path==='/configuration'){json(res,200,await service.save(data.configuration,data.revision,controller.signal));return}
|
|
41
|
+
if(path==='/import'){
|
|
42
|
+
const d=await service.importAsset(data,controller.signal)
|
|
43
|
+
json(res,200,{...draftView(d),value:d.value});return
|
|
44
|
+
}
|
|
45
|
+
if(path==='/preview'){
|
|
46
|
+
if(data.image && (typeof data.image!=='string'||! /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(data.image)))throw fault(400,'图片编码无效')
|
|
47
|
+
const d=await service.preview({bytes:data.image?Buffer.from(data.image,'base64'):undefined,baseId:data.draftId,params:data.params,label:data.label},controller.signal)
|
|
48
|
+
json(res,200,{...draftView(d),value:d.value});return
|
|
49
|
+
}
|
|
50
|
+
throw fault(404,'接口不存在')
|
|
51
|
+
}catch(error){if(!res.headersSent)json(res,error.status||500,{error:error.message});else res.end()}
|
|
52
|
+
}})
|
|
53
|
+
return()=>{dispose();for(const res of streams)res.end();streams.clear()}
|
|
54
|
+
}
|