@ash-qw/dsh-theme-prts 2.3.0 → 2.3.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 +10 -0
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/lib/client.js +360 -39
- package/package.json +1 -1
- package/src/client/background-layout.js +59 -0
- package/src/client/background-region-editor.js +103 -0
- package/src/client/cursor-adapter.js +1 -1
- package/src/client/custom-background.js +21 -5
- package/src/client/index.js +2 -0
- package/src/client/session-card-reveal.js +125 -0
- package/src/client/sidebar-control-adapter.js +40 -35
- package/src/client/theme-transfer.js +6 -3
- package/src/server/backgrounds.js +10 -6
- package/src/shared/background-region.js +21 -0
- package/src/styles/cursor.css +8 -0
- package/src/styles/custom-background.css +69 -13
- package/src/styles/prts.css +2 -2
- package/src/styles/refinement.css +6 -0
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { backgroundPlacement } from '../shared/background-region.js'
|
|
2
|
+
|
|
3
|
+
export function createBackgroundLayout({window,document}) {
|
|
4
|
+
const root=document.documentElement
|
|
5
|
+
const raf=window.requestAnimationFrame?.bind(window) ?? (fn=>window.setTimeout(fn,16))
|
|
6
|
+
const caf=window.cancelAnimationFrame?.bind(window) ?? window.clearTimeout.bind(window)
|
|
7
|
+
let state={}, frame, disposed=false, enabled=false, observing=false, operation, sessions
|
|
8
|
+
const set=(name,value)=>{if(root.style.getPropertyValue(name)!==value) root.style.setProperty(name,value)}
|
|
9
|
+
const update=()=>{
|
|
10
|
+
frame=undefined
|
|
11
|
+
if(disposed||!observing)return
|
|
12
|
+
const nextOperation=document.querySelector('[data-prts-region="operation"]')
|
|
13
|
+
const nextSessions=document.querySelector('[data-prts-region="sessions"]')
|
|
14
|
+
if(operation!==nextOperation || sessions!==nextSessions){
|
|
15
|
+
resize?.disconnect(); operation=nextOperation;sessions=nextSessions
|
|
16
|
+
for(const node of [operation,sessions])if(node)resize?.observe(node)
|
|
17
|
+
}
|
|
18
|
+
const railWidth=parseFloat(window.getComputedStyle(root).getPropertyValue('--prts-nav-width'))||0
|
|
19
|
+
for(const slot of ['full','conversation','sidebar']){
|
|
20
|
+
const entry=state[slot]
|
|
21
|
+
const node=slot==='sidebar'?sessions:operation
|
|
22
|
+
const width=slot==='full'?window.innerWidth:(node?.clientWidth||0)+(slot==='sidebar'?railWidth:0)
|
|
23
|
+
const height=slot==='full'?window.innerHeight:node?.clientHeight||0
|
|
24
|
+
const active=entry?.region && width>0 && height>0
|
|
25
|
+
if(active){
|
|
26
|
+
const p=backgroundPlacement(entry.width,entry.height,width,height,entry.region)
|
|
27
|
+
set(`--prts-protected-${slot}-x`,`${p.x}px`)
|
|
28
|
+
set(`--prts-protected-${slot}-y`,`${p.y}px`)
|
|
29
|
+
if(slot==='sidebar')set('--prts-protected-sidebar-width',`${p.width}px`)
|
|
30
|
+
}else{
|
|
31
|
+
root.style.removeProperty(`--prts-protected-${slot}-x`)
|
|
32
|
+
root.style.removeProperty(`--prts-protected-${slot}-y`)
|
|
33
|
+
if(slot==='sidebar')root.style.removeProperty('--prts-protected-sidebar-width')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function schedule(){if(!disposed && observing && frame===undefined)frame=raf(update)}
|
|
38
|
+
const resize=window.ResizeObserver?new window.ResizeObserver(schedule):null
|
|
39
|
+
const mutations=new window.MutationObserver(()=>{
|
|
40
|
+
if(!operation?.isConnected || !sessions?.isConnected)schedule()
|
|
41
|
+
})
|
|
42
|
+
const attributes=new window.MutationObserver(schedule)
|
|
43
|
+
function clear(){
|
|
44
|
+
caf(frame);frame=undefined;resize?.disconnect();mutations.disconnect();attributes.disconnect();window.removeEventListener('resize',schedule)
|
|
45
|
+
operation=undefined;sessions=undefined;observing=false
|
|
46
|
+
for(const slot of ['full','conversation','sidebar'])for(const axis of ['x','y','width'])root.style.removeProperty(`--prts-protected-${slot}-${axis}`)
|
|
47
|
+
}
|
|
48
|
+
function reconcile(){
|
|
49
|
+
if(disposed || !enabled || !['full','conversation','sidebar'].some(slot=>state[slot]?.region)){clear();return}
|
|
50
|
+
if(!observing){
|
|
51
|
+
observing=true
|
|
52
|
+
mutations.observe(document.body||root,{childList:true,subtree:true})
|
|
53
|
+
attributes.observe(root,{attributes:true,attributeFilter:['data-prts-background-mode','data-prts-rail-mode']})
|
|
54
|
+
window.addEventListener('resize',schedule)
|
|
55
|
+
}
|
|
56
|
+
schedule()
|
|
57
|
+
}
|
|
58
|
+
return {update(value){state=value;reconcile()},setEnabled(value){enabled=value;reconcile()},dispose(){disposed=true;clear()}}
|
|
59
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { backgroundPlacement } from '../shared/background-region.js'
|
|
2
|
+
|
|
3
|
+
export function backgroundRegionMarkup(slot) {
|
|
4
|
+
return `<dialog data-prts-region-editor="${slot}" aria-labelledby="prts-region-title-${slot}">
|
|
5
|
+
<header><div><strong id="prts-region-title-${slot}">保护重要区域</strong><p>拖动框移动,拖动四边或四角缩放。横向优先保留中心,纵向优先保留顶端。</p></div><button type="button" data-prts-region-cancel aria-label="关闭保护区域设置">×</button></header>
|
|
6
|
+
<div data-prts-region-workspace><section data-prts-region-original><strong>原图与保护框</strong><div data-prts-region-stage><img alt="背景原图" draggable="false"><div data-prts-region-box tabindex="0" role="group" aria-label="保护区域,方向键移动"><span>保护区域</span>${['nw','ne','sw','se','n','e','s','w'].map((corner,i)=>`<button type="button" data-prts-region-handle="${corner}" aria-label="调整${['左上角','右上角','左下角','右下角','上边','右边','下边','左边'][i]},使用方向键"></button>`).join('')}</div><div data-prts-region-ruler="x"></div><div data-prts-region-ruler="y"></div></div><small>边缘刻度为图片坐标。可用方向键移动,Shift 加速。</small></section>
|
|
7
|
+
<section data-prts-region-samples><strong>窗口裁剪预览</strong><p>${slot==='sidebar' ? '按当前侧栏高度等比例预览,包含导航栏背景。' : '白框标出保护区域,调整时同步更新。'}</p><div data-prts-region-preview-grid>${(slot==='sidebar' ? [['narrow','较窄侧栏 · 264px'],['wide','较宽侧栏 · 420px']] : [['wide','横向窗口 · 16:9'],['narrow','纵向窗口 · 9:16']]).map(([kind,label])=>`<figure><figcaption>${label}</figcaption><div data-prts-region-preview="${kind}"><i></i></div><small data-prts-region-warning hidden>此比例无法完整显示保护区域:横向保中心,纵向保顶端。</small></figure>`).join('')}</div></section></div>
|
|
8
|
+
<footer><button type="button" data-prts-region-create>恢复默认框</button><button type="button" data-prts-region-clear>清除保护区域</button><span></span><button type="button" data-prts-region-cancel>取消</button><button type="button" data-prts-region-save>保存</button></footer><p data-prts-region-status role="status"></p>
|
|
9
|
+
</dialog>`
|
|
10
|
+
}
|
|
11
|
+
export function mountBackgroundRegionEditor({section,window,store}) {
|
|
12
|
+
let state=store.snapshot(), active, draft=null, revision, imageId, busy=false, disposed=false, drag
|
|
13
|
+
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v))
|
|
14
|
+
const defaultRegion=()=>({x:.25,y:.2,width:.5,height:.6})
|
|
15
|
+
const editor=()=>active && section.querySelector(`[data-prts-region-editor="${active}"]`)
|
|
16
|
+
function draw(){
|
|
17
|
+
const el=editor(),entry=state[active];if(!el||!entry)return
|
|
18
|
+
const box=el.querySelector('[data-prts-region-box]')
|
|
19
|
+
box.hidden=!draft
|
|
20
|
+
if(draft)Object.assign(box.style,{left:`${draft.x*100}%`,top:`${draft.y*100}%`,width:`${draft.width*100}%`,height:`${draft.height*100}%`})
|
|
21
|
+
for(const axis of ['x','y']){
|
|
22
|
+
const ruler=el.querySelector(`[data-prts-region-ruler="${axis}"]`)
|
|
23
|
+
ruler.innerHTML=[0,25,50,75,100].filter(v=>!draft||Math.abs(v-draft[axis]*100)>8).map(v=>`<span style="${axis==='x'?'left':'top'}:${v}%">${v}</span>`).join('')
|
|
24
|
+
if(draft)ruler.innerHTML+=`<b style="${axis==='x'?'left':'top'}:${draft[axis]*100}%">${Math.round(draft[axis]*100)}%</b>`
|
|
25
|
+
}
|
|
26
|
+
const previews=[...el.querySelectorAll('[data-prts-region-preview]')]
|
|
27
|
+
const sidebarHeight=section.ownerDocument.querySelector('[data-prts-region="sessions"]')?.clientHeight||window.innerHeight
|
|
28
|
+
const railWidth=parseFloat(window.getComputedStyle(section.ownerDocument.documentElement).getPropertyValue('--prts-nav-width'))||0
|
|
29
|
+
// DSH 0.2.1-rc.2 clamps the sidebar track to 264–420px; use the same scale for both samples.
|
|
30
|
+
const sidebarScale=Math.min(240/Math.max(1,sidebarHeight),...previews.map(preview=>Math.max(1,preview.parentElement.clientWidth)/((preview.dataset.prtsRegionPreview==='narrow'?264:420)+railWidth)))
|
|
31
|
+
for(const preview of previews){
|
|
32
|
+
const narrow=preview.dataset.prtsRegionPreview==='narrow'
|
|
33
|
+
const w=active==='sidebar'?((narrow?264:420)+railWidth)*sidebarScale:Math.max(1,Math.min(narrow?112:200,preview.parentElement.clientWidth))
|
|
34
|
+
const h=active==='sidebar'?sidebarHeight*sidebarScale:w*(narrow?16/9:9/16)
|
|
35
|
+
const p=backgroundPlacement(entry.width,entry.height,w,h,draft)
|
|
36
|
+
Object.assign(preview.style,{width:`${w}px`,height:`${h}px`,backgroundImage:`url("/prts-theme-api/backgrounds/image/${entry.id}")`,backgroundSize:`${p.width}px ${p.height}px`,backgroundPosition:`${p.x}px ${p.y}px`})
|
|
37
|
+
const mark=preview.firstElementChild;mark.hidden=!draft
|
|
38
|
+
if(draft)Object.assign(mark.style,{left:`${p.x+draft.x*p.width}px`,top:`${p.y+draft.y*p.height}px`,width:`${draft.width*p.width}px`,height:`${draft.height*p.height}px`})
|
|
39
|
+
preview.parentElement.querySelector('[data-prts-region-warning]').hidden=!p.clipped
|
|
40
|
+
}
|
|
41
|
+
for(const button of el.querySelectorAll('button'))button.disabled=busy
|
|
42
|
+
}
|
|
43
|
+
const resize=window.ResizeObserver ? new window.ResizeObserver(()=>{if(!disposed)draw()}) : null
|
|
44
|
+
function close(){window.removeEventListener('resize',draw);resize?.disconnect();const el=editor();el?.querySelector('[data-prts-region-stage]').style.removeProperty('cursor');if(el?.open)el.close();if(active)section.querySelector(`[data-prts-background-region-open="${active}"]`)?.focus();active=undefined;drag=undefined}
|
|
45
|
+
function open(slot){
|
|
46
|
+
if(busy)return
|
|
47
|
+
close();const entry=state[slot];if(!entry?.width||!entry?.height)return
|
|
48
|
+
active=slot;revision=state.revision;imageId=entry.id;draft=entry.region?{...entry.region}:defaultRegion()
|
|
49
|
+
const el=editor();el.showModal();resize?.observe(el)
|
|
50
|
+
if(slot==='sidebar'){const sidebar=section.ownerDocument.querySelector('[data-prts-region="sessions"]');if(sidebar)resize?.observe(sidebar)}
|
|
51
|
+
window.addEventListener('resize',draw)
|
|
52
|
+
el.querySelector('[data-prts-region-stage]').style.width=`min(100%, ${Math.min(640,360*entry.width/entry.height)}px)`
|
|
53
|
+
el.querySelector('img').src=`/prts-theme-api/backgrounds/image/${entry.id}`
|
|
54
|
+
el.querySelector('[data-prts-region-status]').textContent=''
|
|
55
|
+
draw()
|
|
56
|
+
}
|
|
57
|
+
async function save(){
|
|
58
|
+
if(busy||!active)return
|
|
59
|
+
const el=editor(),slot=active,region=draft?{...draft}:null
|
|
60
|
+
busy=true;draw();el.querySelector('[data-prts-region-status]').textContent='正在保存…'
|
|
61
|
+
try{await store.saveRegion(slot,region,revision);if(!disposed){close();section.querySelector('[data-prts-background-status]').textContent='保护区域已保存,并同步到其他页面。';section.querySelector(`[data-prts-background-region-open="${slot}"]`).focus()}}
|
|
62
|
+
catch(error){if(!disposed)el.querySelector('[data-prts-region-status]').textContent=error.message+' 请取消后重新打开编辑。'}
|
|
63
|
+
finally{busy=false;if(!disposed)draw()}
|
|
64
|
+
}
|
|
65
|
+
const click=event=>{
|
|
66
|
+
const openButton=event.target.closest('[data-prts-background-region-open]');if(openButton){open(openButton.dataset.prtsBackgroundRegionOpen);return}
|
|
67
|
+
if(!active||busy)return
|
|
68
|
+
if(event.target.closest('[data-prts-region-cancel]'))close()
|
|
69
|
+
else if(event.target.closest('[data-prts-region-save]'))void save()
|
|
70
|
+
else if(event.target.closest('[data-prts-region-create]')){draft=defaultRegion();draw();editor().querySelector('[data-prts-region-box]').focus()}
|
|
71
|
+
else if(event.target.closest('[data-prts-region-clear]')){draft=null;draw()}
|
|
72
|
+
}
|
|
73
|
+
const down=event=>{
|
|
74
|
+
const el=editor(),stage=event.target.closest('[data-prts-region-stage]')
|
|
75
|
+
if(busy||!draft||!stage||!el?.contains(stage)||!event.target.closest('[data-prts-region-box]')||event.button!==0)return
|
|
76
|
+
event.preventDefault();const bounds=stage.getBoundingClientRect();if(!bounds.width||!bounds.height)return
|
|
77
|
+
const x=clamp((event.clientX-bounds.left)/bounds.width,0,1),y=clamp((event.clientY-bounds.top)/bounds.height,0,1)
|
|
78
|
+
drag={x,y,bounds,initial:{...draft},corner:event.target.dataset.prtsRegionHandle,id:event.pointerId}
|
|
79
|
+
stage.style.cursor=window.getComputedStyle(event.target).cursor
|
|
80
|
+
stage.setPointerCapture?.(event.pointerId);draw()
|
|
81
|
+
}
|
|
82
|
+
function adjust(initial,dx,dy,corner){
|
|
83
|
+
if(!corner)return {...initial,x:clamp(initial.x+dx,0,1-initial.width),y:clamp(initial.y+dy,0,1-initial.height)}
|
|
84
|
+
let left=initial.x,top=initial.y,right=left+initial.width,bottom=top+initial.height
|
|
85
|
+
if(corner.includes('w'))left=clamp(left+dx,0,right-.01);else if(corner.includes('e'))right=clamp(right+dx,left+.01,1)
|
|
86
|
+
if(corner.includes('n'))top=clamp(top+dy,0,bottom-.01);else if(corner.includes('s'))bottom=clamp(bottom+dy,top+.01,1)
|
|
87
|
+
return {x:left,y:top,width:/[ew]/.test(corner)?right-left:initial.width,height:/[ns]/.test(corner)?bottom-top:initial.height}
|
|
88
|
+
}
|
|
89
|
+
const move=event=>{if(!drag||event.pointerId!==drag.id)return;draft=adjust(drag.initial,(event.clientX-drag.bounds.left)/drag.bounds.width-drag.x,(event.clientY-drag.bounds.top)/drag.bounds.height-drag.y,drag.corner);draw()}
|
|
90
|
+
const up=event=>{if(drag&&event.pointerId===drag.id){if(event.type==='pointercancel')draft=drag.initial;drag=undefined;editor()?.querySelector('[data-prts-region-stage]').style.removeProperty('cursor');draw()}}
|
|
91
|
+
const key=event=>{
|
|
92
|
+
if(!active||busy)return
|
|
93
|
+
if(event.key==='Escape'){event.preventDefault();event.stopPropagation();close();return}
|
|
94
|
+
if(!draft||!event.target.closest('[data-prts-region-box]')||!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key))return
|
|
95
|
+
event.preventDefault();const step=event.shiftKey ? .05 : .01
|
|
96
|
+
draft=adjust(draft,event.key==='ArrowLeft'?-step:event.key==='ArrowRight'?step:0,event.key==='ArrowUp'?-step:event.key==='ArrowDown'?step:0,event.target.dataset.prtsRegionHandle);draw()
|
|
97
|
+
}
|
|
98
|
+
const cancel=event=>{if(event.target.matches('[data-prts-region-editor]')){event.preventDefault();event.stopPropagation();if(!busy)close()}}
|
|
99
|
+
section.addEventListener('cancel',cancel,true)
|
|
100
|
+
const handlers={click,pointerdown:down,pointermove:move,pointerup:up,pointercancel:up,keydown:key}
|
|
101
|
+
for(const [name,fn] of Object.entries(handlers))section.addEventListener(name,fn)
|
|
102
|
+
return {update(value,mode){state=value;if(active&&(state[active]?.id!==imageId||mode==='off'||(mode==='full')!==(active==='full')))close()},dispose(){disposed=true;close();section.removeEventListener('cancel',cancel,true);for(const [name,fn] of Object.entries(handlers))section.removeEventListener(name,fn)}}
|
|
103
|
+
}
|
|
@@ -69,7 +69,7 @@ export function createCursorAdapter({ document, window, assets }) {
|
|
|
69
69
|
const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
|
|
70
70
|
if (target === resizeTarget && target.style.cursor === resizeApplied) return
|
|
71
71
|
clearResize()
|
|
72
|
-
if (!target) return
|
|
72
|
+
if (!target || target.closest('[data-prts-region-stage]')) return
|
|
73
73
|
const cursor = window.getComputedStyle(target).cursor
|
|
74
74
|
const direction = { 'n-resize': 'ns', 's-resize': 'ns', 'ns-resize': 'ns', 'row-resize': 'ns',
|
|
75
75
|
'e-resize': 'ew', 'w-resize': 'ew', 'ew-resize': 'ew', 'col-resize': 'ew',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createBackgroundLayout } from './background-layout.js'
|
|
2
|
+
import { mountBackgroundRegionEditor, backgroundRegionMarkup } from './background-region-editor.js'
|
|
1
3
|
import { opacityDialMarkup, mountOpacityDials } from './opacity-dial.js'
|
|
2
4
|
const BACKGROUND_ENDPOINT = '/prts-theme-api/backgrounds'
|
|
3
5
|
const backgroundLabels = { sidebar: '侧边栏', conversation: '会话正文', full: '完整背景' }
|
|
@@ -7,10 +9,12 @@ function backgroundImageUrl(entry) {
|
|
|
7
9
|
export function createBackgroundStore({ window, document }) {
|
|
8
10
|
let state = { revision: -1, sidebar: null, conversation: null, full: null }, disposed = false, events, refreshing
|
|
9
11
|
const listeners = new Set()
|
|
12
|
+
const layout = createBackgroundLayout({window,document})
|
|
10
13
|
const abort = window.AbortController ? new window.AbortController() : null
|
|
11
14
|
const apply = value => {
|
|
12
15
|
if (disposed || value.revision < state.revision) return
|
|
13
16
|
state = value
|
|
17
|
+
layout.update(state)
|
|
14
18
|
for (const slot of Object.keys(backgroundLabels)) {
|
|
15
19
|
const url = backgroundImageUrl(state[slot])
|
|
16
20
|
const root = document.documentElement
|
|
@@ -48,12 +52,18 @@ export function createBackgroundStore({ window, document }) {
|
|
|
48
52
|
window.addEventListener('focus', focus)
|
|
49
53
|
return {
|
|
50
54
|
snapshot: () => state, refresh,
|
|
55
|
+
setEnabled: value => layout.setEnabled(value),
|
|
51
56
|
subscribe(fn) { listeners.add(fn); fn(state); return () => listeners.delete(fn) },
|
|
52
57
|
async save(slot, image, revision = state.revision) {
|
|
53
58
|
try { return await request({ slot, image, revision }) }
|
|
54
59
|
catch (error) { await refresh().catch(() => {}); throw error }
|
|
55
60
|
},
|
|
61
|
+
async saveRegion(slot, region, revision = state.revision) {
|
|
62
|
+
try { return await request({ slot, region, revision }) }
|
|
63
|
+
catch (error) { await refresh().catch(() => {}); throw error }
|
|
64
|
+
},
|
|
56
65
|
dispose() {
|
|
66
|
+
layout.dispose()
|
|
57
67
|
document.documentElement.style.removeProperty('--prts-sidebar-image-ratio')
|
|
58
68
|
disposed = true; abort?.abort(); events?.close(); listeners.clear(); window.removeEventListener('focus', focus)
|
|
59
69
|
for (const slot of Object.keys(backgroundLabels)) {
|
|
@@ -71,13 +81,16 @@ export function backgroundMarkup(heroContrastControl = '') {
|
|
|
71
81
|
</div>
|
|
72
82
|
<div data-prts-background-images hidden>${Object.entries(backgroundLabels).map(([slot,label]) => `<article data-prts-background-card="${slot}" hidden>
|
|
73
83
|
<div data-prts-background-card-heading><strong>${label}</strong><button type="button" data-prts-background-remove="${slot}" aria-label="移除${label}图片" hidden>移除</button></div>
|
|
74
|
-
<
|
|
84
|
+
<div data-prts-background-image-controls="${slot}"><span data-prts-background-thumbnail="${slot}" aria-hidden="true"></span><span data-prts-background-empty>尚未选择图片</span>
|
|
85
|
+
<div data-prts-background-image-actions><button type="button" data-prts-background-region-open="${slot}" hidden><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M7 3H3v4m10-4h4v4M3 13v4h4m10-4v4h-4"/><rect x="6.5" y="6.5" width="7" height="7" rx="1.5"/></svg><span>保护区域</span></button><button type="button" data-prts-background-upload="${slot}" aria-label="上传或替换${label}"><svg viewBox="0 0 20 20" aria-hidden="true"><rect x="3" y="3" width="14" height="14" rx="3"/><path d="m4 14 4-4 3 3 2-2 4 4"/><circle cx="12.5" cy="7" r="1"/></svg><span data-prts-background-caption="${slot}">选择图片</span></button></div>
|
|
86
|
+
</div>
|
|
87
|
+
${backgroundRegionMarkup(slot)}
|
|
75
88
|
<input type="file" data-prts-background-file="${slot}" accept="image/png,image/jpeg,image/webp,image/avif" hidden>
|
|
76
89
|
</article>`).join('')}</div>
|
|
77
90
|
<label data-prts-background-shade hidden><span>背景淡化 <output></output></span><input type="range" min="0" max="90" step="5" aria-label="背景淡化程度"><small>调高后背景更淡,接近当前主题底色。</small></label>
|
|
78
91
|
<div data-prts-background-hero-setting hidden>${heroContrastControl}</div>
|
|
79
92
|
${opacityDialMarkup()}
|
|
80
|
-
<small data-prts-background-help>支持 PNG / JPEG / WebP / AVIF · 最大 10 MB<br
|
|
93
|
+
<small data-prts-background-help>支持 PNG / JPEG / WebP / AVIF · 最大 10 MB<br>图片铺满,可框选重要区域优先保留。切换模式保留素材,并在当前 DSH 实例同步。</small><p data-prts-background-status role="status"></p>
|
|
81
94
|
</section>`
|
|
82
95
|
}
|
|
83
96
|
export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
@@ -86,9 +99,11 @@ export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
|
86
99
|
let disposed = false, busy = false, preferences = {}, state = store.snapshot()
|
|
87
100
|
const status = section.querySelector('[data-prts-background-status]')
|
|
88
101
|
const range = section.querySelector('input[type="range"]')
|
|
102
|
+
const regions = mountBackgroundRegionEditor({section,window,store})
|
|
89
103
|
const dials = mountOpacityDials({ section, window, onChange })
|
|
90
104
|
function render() {
|
|
91
105
|
dials.update(preferences, state)
|
|
106
|
+
regions.update(state, preferences.backgroundMode || 'off')
|
|
92
107
|
const mode = preferences.backgroundMode || 'off'
|
|
93
108
|
section.querySelector('[data-prts-background-images]').hidden = mode === 'off'
|
|
94
109
|
section.querySelector('[data-prts-background-shade]').hidden = mode === 'off'
|
|
@@ -97,10 +112,11 @@ export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
|
97
112
|
for (const slot of Object.keys(backgroundLabels)) {
|
|
98
113
|
const url = backgroundImageUrl(state[slot])
|
|
99
114
|
section.querySelector(`[data-prts-background-card="${slot}"]`).hidden = mode === 'off' || (mode === 'full') !== (slot === 'full')
|
|
100
|
-
section.querySelector(`[data-prts-background-
|
|
115
|
+
section.querySelector(`[data-prts-background-image-controls="${slot}"]`).toggleAttribute('data-has-image', Boolean(url))
|
|
101
116
|
section.querySelector(`[data-prts-background-thumbnail="${slot}"]`).style.backgroundImage = url ? `url("${url}")` : ''
|
|
102
|
-
section.querySelector(`[data-prts-background-caption="${slot}"]`).textContent = url ? '替换图片' : '
|
|
117
|
+
section.querySelector(`[data-prts-background-caption="${slot}"]`).textContent = url ? '替换图片' : '选择图片'
|
|
103
118
|
section.querySelector(`[data-prts-background-remove="${slot}"]`).hidden = !url
|
|
119
|
+
section.querySelector(`[data-prts-background-region-open="${slot}"]`).hidden = !url
|
|
104
120
|
}
|
|
105
121
|
for (const button of section.querySelectorAll('[data-prts-background-upload], [data-prts-background-remove]')) button.disabled = busy
|
|
106
122
|
}
|
|
@@ -139,6 +155,6 @@ export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
|
139
155
|
const unsubscribe = store.subscribe(value => { state = value; render() })
|
|
140
156
|
return {
|
|
141
157
|
update(value) { preferences = value; render() },
|
|
142
|
-
dispose() { disposed = true; dials.dispose(); unsubscribe(); section.removeEventListener('click', click); section.removeEventListener('change', change); section.removeEventListener('input', input) },
|
|
158
|
+
dispose() { disposed = true; regions.dispose(); dials.dispose(); unsubscribe(); section.removeEventListener('click', click); section.removeEventListener('change', change); section.removeEventListener('input', input) },
|
|
143
159
|
}
|
|
144
160
|
}
|
package/src/client/index.js
CHANGED
|
@@ -179,6 +179,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
const disposeVisualState = () => {
|
|
182
|
+
backgroundStore.setEnabled(false)
|
|
182
183
|
particleField.update({ ...preferences, enabled: false })
|
|
183
184
|
startup.stop({ restoreFocus: false })
|
|
184
185
|
stopStartupReadyWatch()
|
|
@@ -201,6 +202,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
201
202
|
disposeVisualState()
|
|
202
203
|
return
|
|
203
204
|
}
|
|
205
|
+
backgroundStore.setEnabled(preferences.backgroundMode !== 'off')
|
|
204
206
|
if (!themeAlreadyApplied) theme.apply(preferences)
|
|
205
207
|
operations ||= createOperationsShell({
|
|
206
208
|
document, window, assets, adapter, hostGeometry, customEmblemStore, avatarStore, backgroundStore,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Reveal the temporarily selected card before mounting the destination conversation.
|
|
2
|
+
// Keep the destination stable through unrelated reflow; retarget smoothly only when needed.
|
|
3
|
+
export function createSessionCardReveal({window, root, sessionId, onDone, onStart}) {
|
|
4
|
+
let row, scroller, frame, timer, started = false, disposed = false, geometry, destination, stableSince, lastPosition
|
|
5
|
+
const findRow = () => [...root.querySelectorAll('[data-row-key], [data-session-id]')].find(el =>
|
|
6
|
+
el.getAttribute('data-row-key') === `session:${sessionId}` || el.getAttribute('data-session-id') === sessionId)
|
|
7
|
+
const instant = top => {
|
|
8
|
+
try { scroller.scrollTo({top, behavior: 'instant'}) }
|
|
9
|
+
catch { scroller.scrollTop = top }
|
|
10
|
+
}
|
|
11
|
+
function cancel() {
|
|
12
|
+
if (disposed) return
|
|
13
|
+
disposed = true
|
|
14
|
+
if (scroller && destination !== undefined) instant(scroller.scrollTop)
|
|
15
|
+
destination = undefined
|
|
16
|
+
window.cancelAnimationFrame(frame)
|
|
17
|
+
window.clearTimeout(timer)
|
|
18
|
+
resize?.disconnect()
|
|
19
|
+
mutations.disconnect()
|
|
20
|
+
root.removeEventListener('transitionend', schedule)
|
|
21
|
+
root.removeEventListener('animationend', schedule)
|
|
22
|
+
scroller?.removeEventListener('scrollend', scrollEnded)
|
|
23
|
+
}
|
|
24
|
+
function scrollEnded() {
|
|
25
|
+
if (destination !== undefined && Math.abs(scroller.scrollTop - destination) <= 1) destination = undefined
|
|
26
|
+
schedule()
|
|
27
|
+
}
|
|
28
|
+
function schedule() {
|
|
29
|
+
if (!disposed && frame === undefined) frame = window.requestAnimationFrame(() => { frame = undefined; check() })
|
|
30
|
+
}
|
|
31
|
+
function check(final = false) {
|
|
32
|
+
if (disposed) return
|
|
33
|
+
const nextRow = findRow()
|
|
34
|
+
if (!nextRow || (nextRow.getAttribute('data-prts-selection-target') !== 'true' && nextRow.getAttribute('aria-selected') !== 'true')) {
|
|
35
|
+
stableSince = undefined
|
|
36
|
+
if (started && !final) schedule()
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const remounted = nextRow !== row
|
|
40
|
+
if (remounted) {
|
|
41
|
+
row = nextRow
|
|
42
|
+
const previousScroller = scroller
|
|
43
|
+
scroller?.removeEventListener('scrollend', scrollEnded)
|
|
44
|
+
resize?.disconnect()
|
|
45
|
+
scroller = undefined
|
|
46
|
+
for (let node = row.parentElement; node && node !== root.parentElement; node = node.parentElement) {
|
|
47
|
+
if (/^(auto|scroll)$/.test(window.getComputedStyle(node).overflowY) && node.clientHeight > 0) { scroller = node; break }
|
|
48
|
+
}
|
|
49
|
+
if (scroller !== previousScroller) destination = undefined
|
|
50
|
+
for (let node = row; node && node !== root.parentElement; node = node.parentElement) {
|
|
51
|
+
for (const animation of node.getAnimations?.() ?? []) {
|
|
52
|
+
const timing = animation.effect?.getComputedTiming?.()
|
|
53
|
+
if (animation.playState === 'running' && Number.isFinite(timing?.endTime)) animation.finished.then(schedule, schedule)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
resize?.observe(root)
|
|
57
|
+
if (scroller) { resize?.observe(scroller); scroller.addEventListener('scrollend', scrollEnded) }
|
|
58
|
+
}
|
|
59
|
+
let settled = true
|
|
60
|
+
const first = !started
|
|
61
|
+
const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
62
|
+
|| root.ownerDocument.documentElement.getAttribute('data-prts-motion') === 'reduced'
|
|
63
|
+
if (scroller) {
|
|
64
|
+
// Read all geometry before writing scrollTop. Offset is independent of smooth-scroll progress.
|
|
65
|
+
const card = row.getBoundingClientRect(), box = scroller.getBoundingClientRect()
|
|
66
|
+
const current = scroller.scrollTop, height = scroller.clientHeight
|
|
67
|
+
const top = card.top - box.top - scroller.clientTop, bottom = card.bottom - box.top - scroller.clientTop
|
|
68
|
+
const next = [top + current, card.height, height, scroller.scrollHeight]
|
|
69
|
+
const changed = remounted || !geometry || next.some((v, i) => Math.abs(v - geometry[i]) > 1)
|
|
70
|
+
if (first || changed || final) {
|
|
71
|
+
// Evaluate visibility at the pending destination, not at an intermediate animation frame.
|
|
72
|
+
// Rows below the target may change scrollHeight without invalidating that destination.
|
|
73
|
+
const base = destination ?? current
|
|
74
|
+
const offset = next[0], bottomOffset = offset + card.height
|
|
75
|
+
const limit = Math.max(0, next[3] - height)
|
|
76
|
+
const nearest = position => Math.max(0, Math.min(limit,
|
|
77
|
+
offset < position ? offset : bottomOffset > position + height ? bottomOffset - height : position))
|
|
78
|
+
// A stale endpoint can lie on the other side of a card that is already visible.
|
|
79
|
+
// Preserve valid endpoints, but correct invalid ones from the actual viewport.
|
|
80
|
+
const target = Math.abs(nearest(base) - base) > 1 ? nearest(current) : base
|
|
81
|
+
if (final) {
|
|
82
|
+
// End any native animation at the nearest visible position before disconnecting.
|
|
83
|
+
instant(nearest(current))
|
|
84
|
+
destination = undefined
|
|
85
|
+
} else if (Math.abs(target - base) > 1) {
|
|
86
|
+
destination = target
|
|
87
|
+
if (!reduced && typeof scroller.scrollTo === 'function') scroller.scrollTo({top: target, behavior: 'smooth'})
|
|
88
|
+
else instant(target)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
settled = !changed && Math.abs(current - (lastPosition ?? current)) <= 1
|
|
92
|
+
&& (destination === undefined || Math.abs(scroller.scrollTop - destination) <= 1)
|
|
93
|
+
lastPosition = current
|
|
94
|
+
geometry = next
|
|
95
|
+
} else if (first || remounted || final) {
|
|
96
|
+
row.scrollIntoView?.({block:'nearest', inline:'nearest', behavior:'instant'})
|
|
97
|
+
}
|
|
98
|
+
for (let node = row; node && node !== root.parentElement; node = node.parentElement) {
|
|
99
|
+
if ((node.getAnimations?.() ?? []).some(animation => animation.playState === 'running'
|
|
100
|
+
&& Number.isFinite(animation.effect?.getComputedTiming?.().endTime))) settled = false
|
|
101
|
+
}
|
|
102
|
+
if (first) {
|
|
103
|
+
started = true
|
|
104
|
+
onStart?.()
|
|
105
|
+
// Bound the pre-navigation wait; finish any remaining scroll before opening.
|
|
106
|
+
timer = window.setTimeout(() => { check(true); cancel(); onDone() }, 1000)
|
|
107
|
+
}
|
|
108
|
+
if (!final) {
|
|
109
|
+
const now = window.performance.now()
|
|
110
|
+
if (!settled) stableSince = undefined
|
|
111
|
+
else stableSince ??= now
|
|
112
|
+
if (stableSince !== undefined && now - stableSince >= 80) {
|
|
113
|
+
cancel()
|
|
114
|
+
onDone()
|
|
115
|
+
} else schedule()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const resize = typeof window.ResizeObserver === 'function' ? new window.ResizeObserver(schedule) : undefined
|
|
119
|
+
const mutations = new window.MutationObserver(schedule)
|
|
120
|
+
mutations.observe(root, {childList:true, subtree:true, attributes:true, attributeFilter:['aria-selected','aria-expanded','data-prts-selection-target']})
|
|
121
|
+
root.addEventListener('transitionend', schedule)
|
|
122
|
+
root.addEventListener('animationend', schedule)
|
|
123
|
+
schedule()
|
|
124
|
+
return {cancel, interrupt: cancel}
|
|
125
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSessionCardReveal } from './session-card-reveal.js'
|
|
1
2
|
import { createSessionSummaryPreview, scrollSummaryTitle } from './session-summary-preview.js'
|
|
2
3
|
import { ensureFacilityGraphic, ensureFacilityTextureDefs, renderFacilityGraphic } from './facility-vector.js'
|
|
3
4
|
import { createSessionLifelineAnimator } from './session-lifeline.js'
|
|
@@ -353,7 +354,11 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
353
354
|
queueScan()
|
|
354
355
|
}
|
|
355
356
|
function interruptSelectionTarget(event) {
|
|
356
|
-
if (event.target.closest?.('[data-prts-session-row]'))
|
|
357
|
+
if (event.target.closest?.('[data-prts-session-row]')) {
|
|
358
|
+
cancelPendingNavigation()
|
|
359
|
+
cancelReveal()
|
|
360
|
+
clearSelectionTarget()
|
|
361
|
+
}
|
|
357
362
|
}
|
|
358
363
|
function setSelectionTarget(id) {
|
|
359
364
|
clearSelectionTarget()
|
|
@@ -362,27 +367,41 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
362
367
|
document.addEventListener('pointerdown', interruptSelectionTarget, true)
|
|
363
368
|
}
|
|
364
369
|
|
|
365
|
-
let pendingReveal, revealTimer
|
|
370
|
+
let pendingReveal, revealTimer
|
|
366
371
|
let navigationFrame, navigationGeneration = 0
|
|
367
372
|
function cancelPendingNavigation() {
|
|
368
373
|
navigationGeneration++
|
|
369
374
|
window.cancelAnimationFrame?.(navigationFrame)
|
|
370
375
|
}
|
|
371
376
|
function cancelReveal() {
|
|
377
|
+
pendingReveal?.tracker?.cancel()
|
|
372
378
|
pendingReveal = undefined
|
|
373
379
|
window.clearTimeout(revealTimer)
|
|
374
|
-
|
|
375
|
-
window.cancelAnimationFrame?.(revealFrame)
|
|
380
|
+
for (const type of ['wheel','touchmove','pointerdown','keydown']) document.removeEventListener(type, interruptReveal, true)
|
|
376
381
|
}
|
|
377
|
-
function
|
|
382
|
+
function interruptReveal(event) {
|
|
383
|
+
if (!event.target?.closest?.('[data-prts-region="sessions"]')) return
|
|
384
|
+
if (event.type === 'keydown' && !['ArrowUp','ArrowDown','PageUp','PageDown','Home','End',' '].includes(event.key)) return
|
|
385
|
+
cancelPendingNavigation()
|
|
378
386
|
cancelReveal()
|
|
379
|
-
|
|
380
|
-
|
|
387
|
+
clearSelectionTarget()
|
|
388
|
+
}
|
|
389
|
+
function revealSessionCard(sessionId, onDone) {
|
|
390
|
+
pendingReveal?.tracker?.interrupt()
|
|
391
|
+
cancelReveal()
|
|
392
|
+
pendingReveal = {sessionId, onDone}
|
|
393
|
+
revealTimer = window.setTimeout(finishReveal, 4000)
|
|
394
|
+
for (const type of ['wheel','touchmove','pointerdown','keydown']) document.addEventListener(type, interruptReveal, {capture:true, passive:true})
|
|
381
395
|
const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
|
|
382
396
|
syncSessionReveal(root)
|
|
383
397
|
queueScan()
|
|
384
398
|
return Boolean(pendingReveal?.expanding)
|
|
385
399
|
}
|
|
400
|
+
function finishReveal() {
|
|
401
|
+
const onDone = pendingReveal?.onDone
|
|
402
|
+
cancelReveal()
|
|
403
|
+
onDone?.()
|
|
404
|
+
}
|
|
386
405
|
function syncSessionReveal(root) {
|
|
387
406
|
if (!pendingReveal || !root) return
|
|
388
407
|
const {sessionId} = pendingReveal
|
|
@@ -390,30 +409,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
390
409
|
const row = rows.find(el => el.getAttribute('data-row-key') === `session:${sessionId}`
|
|
391
410
|
|| el.getAttribute('data-session-id') === sessionId)
|
|
392
411
|
if (row) {
|
|
393
|
-
if (pendingReveal.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const animations = []
|
|
400
|
-
if (!reduced) for (let node = row; node && node !== root; node = node.parentElement) {
|
|
401
|
-
for (const animation of node.getAnimations?.() ?? []) {
|
|
402
|
-
const timing = animation.effect?.getComputedTiming?.()
|
|
403
|
-
if (animation.playState === 'running' && Number.isFinite(timing?.endTime)) animations.push(animation.finished)
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
if (animations.length) await Promise.race([
|
|
407
|
-
Promise.allSettled(animations),
|
|
408
|
-
new Promise(resolve => { revealSettleTimer = window.setTimeout(resolve, 300) }),
|
|
409
|
-
])
|
|
410
|
-
if (pendingReveal !== request || !row.isConnected) return
|
|
411
|
-
row.scrollIntoView?.({block:'nearest', inline:'nearest', behavior:reduced ? 'instant' : 'smooth'})
|
|
412
|
-
cancelReveal()
|
|
413
|
-
})
|
|
412
|
+
if (!pendingReveal.tracker) {
|
|
413
|
+
const request = pendingReveal
|
|
414
|
+
request.tracker = createSessionCardReveal({window, root, sessionId, onStart: () => window.clearTimeout(revealTimer), onDone: () => {
|
|
415
|
+
if (pendingReveal === request) finishReveal()
|
|
416
|
+
}})
|
|
417
|
+
}
|
|
414
418
|
return
|
|
415
419
|
}
|
|
416
|
-
if (++pendingReveal.attempts > 40) { cancelReveal(); return }
|
|
417
420
|
const items = snapshotOf(workspaces?.list)?.items ?? []
|
|
418
421
|
const owner = items.find(item => item.sessionIds?.includes(sessionId))
|
|
419
422
|
const key = owner?.workspaceId ?? ''
|
|
@@ -439,7 +442,6 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
439
442
|
closeNavigationPreview()
|
|
440
443
|
cancelPendingNavigation()
|
|
441
444
|
setSelectionTarget(sessionId)
|
|
442
|
-
revealSessionCard(sessionId)
|
|
443
445
|
const generation = navigationGeneration
|
|
444
446
|
const navigate = () => {
|
|
445
447
|
if (generation !== navigationGeneration || !adapterActive) return
|
|
@@ -462,11 +464,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
462
464
|
if (row) row.click?.()
|
|
463
465
|
else clearSelectionTarget()
|
|
464
466
|
}
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
navigationFrame = window.requestAnimationFrame(
|
|
467
|
+
// Finish sidebar motion first, then paint its final position before the host can block.
|
|
468
|
+
revealSessionCard(sessionId, () => {
|
|
469
|
+
if (generation !== navigationGeneration || !adapterActive) return
|
|
470
|
+
setSelectionTarget(sessionId)
|
|
471
|
+
navigationFrame = window.requestAnimationFrame(() => {
|
|
472
|
+
if (generation !== navigationGeneration) return
|
|
473
|
+
navigationFrame = window.requestAnimationFrame(navigate)
|
|
474
|
+
})
|
|
470
475
|
})
|
|
471
476
|
}
|
|
472
477
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validBackgroundRegion } from '../shared/background-region.js'
|
|
1
2
|
import { DEFAULT_PREFERENCES, normalizePreferences } from './preferences.js'
|
|
2
3
|
|
|
3
4
|
export const THEME_PACKAGE_LIMIT = 160 * 1024 * 1024
|
|
@@ -22,6 +23,7 @@ export function validateThemePackage(value) {
|
|
|
22
23
|
const a = value.assets
|
|
23
24
|
if (!record(a) || !record(a.backgrounds) || !record(a.avatars) || !record(a.emblems)) throw Error('主题素材数据不完整')
|
|
24
25
|
for (const slot of transferSlots) if (a.backgrounds[slot] !== null && !encodedImage(a.backgrounds[slot], 10 * 1024 * 1024)) throw Error('背景图片数据无效')
|
|
26
|
+
if (a.backgroundRegions !== undefined && (!record(a.backgroundRegions) || transferSlots.some(slot => !validBackgroundRegion(a.backgroundRegions[slot] ?? null)))) throw Error('背景保护区域无效')
|
|
25
27
|
for (const role of transferRoles) {
|
|
26
28
|
const avatar = a.avatars[role]
|
|
27
29
|
if (avatar === null) continue
|
|
@@ -72,10 +74,11 @@ export function createThemeTransfer({ window, getPreferences, savePreferences, c
|
|
|
72
74
|
const value = { format: 'prts-theme', version: 1, themeVersion, exportedAt: new Date().toISOString(), preferences: { ...getPreferences() } }
|
|
73
75
|
const states = {}
|
|
74
76
|
if (includeAssets) {
|
|
75
|
-
const backgrounds = {}, avatars = {}, items = [], ids = new Map()
|
|
77
|
+
const backgrounds = {}, backgroundRegions = {}, avatars = {}, items = [], ids = new Map()
|
|
76
78
|
states.backgrounds = await request('backgrounds/configuration')
|
|
77
79
|
for (const slot of transferSlots) {
|
|
78
80
|
const entry = states.backgrounds[slot]
|
|
81
|
+
backgroundRegions[slot] = entry?.region ?? null
|
|
79
82
|
backgrounds[slot] = null
|
|
80
83
|
if (entry) {
|
|
81
84
|
if (!/^[a-f0-9-]{36}$/.test(entry.id)) throw Error('背景引用无效')
|
|
@@ -102,7 +105,7 @@ export function createThemeTransfer({ window, getPreferences, savePreferences, c
|
|
|
102
105
|
}
|
|
103
106
|
const hero = await reference(states.emblems.configuration.hero), conversation = []
|
|
104
107
|
for (const entry of states.emblems.configuration.conversation) conversation.push(await reference(entry))
|
|
105
|
-
value.assets = { backgrounds, avatars, emblems: { items, hero, conversation } }
|
|
108
|
+
value.assets = { backgrounds, backgroundRegions, avatars, emblems: { items, hero, conversation } }
|
|
106
109
|
for (const kind of ['backgrounds', 'avatars', 'emblems']) {
|
|
107
110
|
if ((await request(kind + '/configuration')).revision !== states[kind].revision) throw Error('素材在导出期间发生变化,请重试')
|
|
108
111
|
}
|
|
@@ -146,7 +149,7 @@ export function createThemeTransfer({ window, getPreferences, savePreferences, c
|
|
|
146
149
|
for (const item of a.emblems.items) drafts.push(await request('emblems/import', item))
|
|
147
150
|
const entry = (ref, index) => ref === null ? null : ref.kind === 'builtin' ? ref : { kind: 'custom', key: 'import-' + index + '-' + drafts[ref.asset].draftId, label: ref.label, draftId: drafts[ref.asset].draftId }
|
|
148
151
|
const configuration = { version: 2, hero: entry(a.emblems.hero, 'hero'), conversation: a.emblems.conversation.map(entry) }
|
|
149
|
-
for (const slot of transferSlots) await write('backgrounds', { slot, image: a.backgrounds[slot] }, { slot, image: old.backgrounds[slot] })
|
|
152
|
+
for (const slot of transferSlots) await write('backgrounds', { slot, image: a.backgrounds[slot], region: a.backgroundRegions?.[slot] ?? null }, { slot, image: old.backgrounds[slot], region: old.backgroundRegions?.[slot] ?? null })
|
|
150
153
|
for (const role of transferRoles) await write('avatars', { role, image: a.avatars[role]?.image ?? null, crop: a.avatars[role]?.crop }, { role, image: old.avatars[role]?.image ?? null, crop: old.avatars[role]?.crop })
|
|
151
154
|
await write('emblems', { configuration }, { configuration: before.states.emblems.configuration })
|
|
152
155
|
}
|