feat(console): 媒体库抓手排序——每行缩略图与垃圾桶之间加六点抓手,按住上下拖实时重排、松手持久化(photoStore.reorder + env)
This commit is contained in:
@@ -1425,6 +1425,7 @@ function WorktableSection(props: any) {
|
||||
},
|
||||
setPhotoId: (id: string) => persistView({ consoleBgPhotoId: id }),
|
||||
removePhoto: async (id: string) => { await photoStore.remove(id) },
|
||||
reorderPhotos: async (ids: string[]) => { await photoStore.reorder(ids) },
|
||||
getPhotoGrid: () => viewRef.current.consoleBgPhotoGrid !== false,
|
||||
setPhotoGrid: (v: boolean) => persistView({ consoleBgPhotoGrid: v }),
|
||||
getGridOpacity: () => viewRef.current.consoleBgGridOpacity ?? 8,
|
||||
|
||||
@@ -134,6 +134,7 @@ export const zh = {
|
||||
'console.bgPhotoUse': '使用此照片',
|
||||
'console.bgPhotoCurrent': '当前使用',
|
||||
'console.bgPhotoNone': '还没有上传过照片',
|
||||
'console.bgMediaDrag': '拖动调整顺序',
|
||||
'console.bgPhotoDelete': '删除这条媒体',
|
||||
'console.bgGridLabel': '网格背景',
|
||||
'console.bgTipB': '贴片模糊程度',
|
||||
@@ -353,6 +354,7 @@ export const en = {
|
||||
'console.bgPhotoUse': 'Use this photo',
|
||||
'console.bgPhotoCurrent': 'In use',
|
||||
'console.bgPhotoNone': 'No photos yet',
|
||||
'console.bgMediaDrag': 'Drag to reorder',
|
||||
'console.bgPhotoDelete': 'Delete this media',
|
||||
'console.bgGridLabel': 'Background grid',
|
||||
'console.bgTipB': 'Card blur',
|
||||
|
||||
@@ -7,7 +7,7 @@ const LEGACY_STORE = 'consoleBgPhoto'
|
||||
const LEGACY_KEY = 'original'
|
||||
|
||||
export type MediaKind = 'photo' | 'video'
|
||||
export type PhotoRecord = { id: string; createdAt: number; kind: MediaKind; blob: Blob }
|
||||
export type PhotoRecord = { id: string; createdAt: number; kind: MediaKind; blob: Blob; order?: number }
|
||||
|
||||
export const kindOf = (blob: Blob): MediaKind => (blob.type && blob.type.indexOf('video/') === 0 ? 'video' : 'photo')
|
||||
|
||||
@@ -35,7 +35,9 @@ function getAll(db: IDBDatabase): Promise<PhotoRecord[]> {
|
||||
arr.push({ id: r.id, createdAt: r.createdAt, kind: kindOf(r.blob), blob: r.blob })
|
||||
}
|
||||
}
|
||||
arr.sort((a, b) => b.createdAt - a.createdAt)
|
||||
const allOrdered = arr.every((r) => typeof r.order === 'number')
|
||||
if (allOrdered) arr.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
else arr.sort((a, b) => b.createdAt - a.createdAt)
|
||||
db.close()
|
||||
resolve(arr)
|
||||
}
|
||||
@@ -87,6 +89,31 @@ export const photoStore = {
|
||||
tx.onabort = () => { db.close(); reject(tx.error) }
|
||||
})
|
||||
},
|
||||
/** 排序:ids 按给定顺序写入 order(0..n),其余保持原顺序追加在后 */
|
||||
async reorder(ids: string[]): Promise<void> {
|
||||
const db = await openDb()
|
||||
const cur = await getAll(db)
|
||||
const idx = new Map<string, number>()
|
||||
ids.forEach((id, i) => idx.set(id, i))
|
||||
const ordered: PhotoRecord[] = []
|
||||
const restSorted = [...cur].sort((a, b) => b.createdAt - a.createdAt)
|
||||
for (const r of cur) {
|
||||
const n = idx.get(r.id)
|
||||
if (typeof n === 'number') ordered.push({ ...r, order: n })
|
||||
}
|
||||
let tail = ordered.length
|
||||
for (const r of restSorted) {
|
||||
if (!idx.has(r.id)) ordered.push({ ...r, order: tail++ })
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, 'readwrite')
|
||||
const store = tx.objectStore(STORE)
|
||||
for (const r of ordered) store.put(r, r.id)
|
||||
tx.oncomplete = () => { db.close(); resolve() }
|
||||
tx.onerror = () => { db.close(); reject(tx.error) }
|
||||
tx.onabort = () => { db.close(); reject(tx.error) }
|
||||
})
|
||||
},
|
||||
/** 删除一张记录 */
|
||||
async remove(id: string): Promise<void> {
|
||||
const db = await openDb()
|
||||
|
||||
@@ -255,6 +255,7 @@ type SplitEnv = {
|
||||
getPhotoHsl: (id: string) => { h: number; s: number; l: number }
|
||||
setPhotoHsl: (id: string, v: { h: number; s: number; l: number }) => void
|
||||
removePhoto: (id: string) => Promise<void>
|
||||
reorderPhotos: (ids: string[]) => Promise<void>
|
||||
getPhotoGrid: () => boolean
|
||||
setPhotoGrid: (v: boolean) => void
|
||||
getGridOpacity: () => number
|
||||
@@ -1571,6 +1572,42 @@ function ConsolePane() {
|
||||
storeAutoCheck(next)
|
||||
if (next) void runUpdateCheck(false)
|
||||
}
|
||||
/** 抓手拖拽排序:按住抓手上下移动 → 行实时重排,松手持久化 */
|
||||
const dragRowRef = useRef<string | null>(null)
|
||||
const [dragRowId, setDragRowId] = useState<string | null>(null)
|
||||
const onHandleDown = (id: string, e: { button: number; preventDefault: () => void; stopPropagation: () => void }) => {
|
||||
if (e.button !== 0) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
dragRowRef.current = id
|
||||
setDragRowId(id)
|
||||
const onMove = (ev: PointerEvent) => {
|
||||
if (dragRowRef.current !== id) return
|
||||
const rows = Array.from(document.querySelectorAll<HTMLElement>('.dsh-wt_photoRow'))
|
||||
let target = 0
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i].getBoundingClientRect()
|
||||
if (ev.clientY > r.top + r.height / 2) target = i + 1
|
||||
}
|
||||
const from = photoList.findIndex((x) => x.id === id)
|
||||
if (from < 0) return
|
||||
const next = [...photoList]
|
||||
const moved = next.splice(from, 1)[0]
|
||||
const to = Math.max(0, Math.min(target > from ? target - 1 : target, next.length))
|
||||
next.splice(to, 0, moved)
|
||||
setPhotoList(next)
|
||||
}
|
||||
const onUp = () => {
|
||||
dragRowRef.current = null
|
||||
setDragRowId(null)
|
||||
window.removeEventListener('pointermove', onMove)
|
||||
window.removeEventListener('pointerup', onUp)
|
||||
const ids = photoList.map((x) => x.id)
|
||||
void splitEnv?.console?.reorderPhotos?.(ids).catch(() => {})
|
||||
}
|
||||
window.addEventListener('pointermove', onMove)
|
||||
window.addEventListener('pointerup', onUp)
|
||||
}
|
||||
/** 在媒体库中选中某条 */
|
||||
const selectPhoto = (p: { id: string; kind: 'photo' | 'video'; url: string }) => {
|
||||
setPhotoIdLocal(p.id)
|
||||
@@ -1889,6 +1926,7 @@ function ConsolePane() {
|
||||
: <svg width="8" height="8" viewBox="0 0 16 16"><rect x="2.5" y="3.5" width="11" height="9" rx="1.5" fill="none" stroke="currentColor" strokeWidth="1.5" /><path d="M3.4 11l2.6-2.6 2.1 2.1 1.7-1.7 2.8 2.4" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>}</span>
|
||||
</span>
|
||||
<span className="dsh-wt_photoName">{photoId === p.id ? T('console.bgPhotoCurrent') : T('console.bgPhotoUse')}</span>
|
||||
<button type="button" className={'dsh-wt_dragHandle' + (dragRowId === p.id ? ' dsh-wt_dragHandleOn' : '')} title={T('console.bgMediaDrag')} aria-label={T('console.bgMediaDrag')} onPointerDown={(e) => onHandleDown(p.id, e)}><svg width="10" height="12" viewBox="0 0 10 12" aria-hidden><circle cx="2.5" cy="2" r="1.1" fill="currentColor" /><circle cx="7.5" cy="2" r="1.1" fill="currentColor" /><circle cx="2.5" cy="6" r="1.1" fill="currentColor" /><circle cx="7.5" cy="6" r="1.1" fill="currentColor" /><circle cx="2.5" cy="10" r="1.1" fill="currentColor" /><circle cx="7.5" cy="10" r="1.1" fill="currentColor" /></svg></button>
|
||||
<button type="button" className="dsh-wt_dropTrash" title={T('console.bgPhotoDelete')} aria-label={T('console.bgPhotoDelete')} onClick={(e) => { e.stopPropagation(); removePhotoById(p) }}><svg width="11" height="11" viewBox="0 0 16 16" aria-hidden><path d="M2.5 4.2h11M6.5 4.2V2.9c0-.6.4-1 .9-1h1.2c.5 0 .9.4.9 1v1.3M4.2 4.2l.5 8.1c0 .7.5 1.2 1.2 1.2h4.2c.7 0 1.2-.5 1.2-1.2l.5-8.1" fill="none" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" /></svg></button>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -467,6 +467,9 @@ export const css = xtermCss + '\n' + [
|
||||
'.dsh-wt_dropGear:hover{background:var(--wt-chip);color:var(--wt-text)}',
|
||||
'.dsh-wt_dropWide{width:264px}',
|
||||
'.dsh-wt_dropTrash{flex:none;width:22px;height:22px;margin-right:2px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease,color .15s ease}',
|
||||
'.dsh-wt_dragHandle{flex:none;width:18px;height:22px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);cursor:grab;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease,color .15s ease;touch-action:none}',
|
||||
'.dsh-wt_dragHandle:hover{background:var(--wt-chip);color:var(--wt-text2)}',
|
||||
'.dsh-wt_dragHandleOn{background:var(--wt-chip);color:var(--wt-text);cursor:grabbing}',
|
||||
'.dsh-wt_dropTrash:hover{background:rgba(244,63,94,.16);color:#f87171}',
|
||||
'.dsh-wt_gridLabel{flex:none;white-space:nowrap;font-size:12px;line-height:17px;color:var(--wt-text2);padding:2px 2px 2px 4px}',
|
||||
'.dsh-wt_gridHalf .dsh-wt_switch{margin-right:2px}',
|
||||
|
||||
Reference in New Issue
Block a user