This commit is contained in:
truewhile
2026-09-16 21:44:42 +08:00
parent f0a6b630c6
commit 1f14a1e85b
6 changed files with 178 additions and 51 deletions
+92
View File
@@ -0,0 +1,92 @@
import {
DEFAULT_LIBRARY_LIST_SORT_FIELD,
DEFAULT_LIBRARY_LIST_SORT_ORDER,
readLibraryListSort,
sortLibrariesByField,
sortLibraryPreviewsByField,
} from './libraryListSort.ts'
import type { Library } from '../types/index.ts'
function check(name: string, condition: boolean) {
if (!condition) throw new Error(`libraryListSort: ${name}`)
}
function lib(id: string, name: string, sortOrder = 0): Library {
return { id, name, sort_order: sortOrder } as Library
}
const ordered = (libs: Library[]) => libs.map((item) => item.id).join(',')
// 首页默认就是「库名倒序」,所以没写过偏好时要拿到 name/desc。
const fallback = readLibraryListSort()
check(
'default preference is name descending',
fallback.field === 'name' && fallback.order === 'desc',
)
check(
'default constants stay in sync with the fallback',
DEFAULT_LIBRARY_LIST_SORT_FIELD === 'name' && DEFAULT_LIBRARY_LIST_SORT_ORDER === 'desc',
)
const alpha = [lib('a', '动画'), lib('b', '电影'), lib('c', '纪录片')]
// zh-CN 拼音升序是 电影(b) → 动画(a) → 纪录片(c)。
check(
'name descending is the default order',
ordered(sortLibrariesByField(alpha, [], 'name', 'desc')) === 'c,a,b',
)
check(
'name ascending reverses it',
ordered(sortLibrariesByField(alpha, [], 'name', 'asc')) === 'b,a,c',
)
const manual = [lib('a', '动画', 2), lib('b', '电影', 0), lib('c', '纪录片', 1)]
check(
'manual order ascending follows sort_order',
ordered(sortLibrariesByField(manual, [], 'sort_order', 'asc')) === 'b,c,a',
)
check(
'manual order descending flips sort_order',
ordered(sortLibrariesByField(manual, [], 'sort_order', 'desc')) === 'a,c,b',
)
// 置顶只决定分组:置顶库整体在前,组内仍然按当前字段/方向排。
check(
'pinned libraries always come first',
ordered(sortLibrariesByField(alpha, ['c'], 'name', 'desc')) === 'c,a,b',
)
check(
'pinned group is sorted by the active field too',
ordered(sortLibrariesByField(alpha, ['a', 'c'], 'name', 'desc')) === 'c,a,b',
)
check(
'pinned ids that no longer exist are ignored',
ordered(sortLibrariesByField(alpha, ['missing'], 'name', 'desc')) === 'c,a,b',
)
// 同名时按 sort_order 兜底,再按 id 兜底,保证顺序稳定。
const sameName = [lib('b', '动画', 1), lib('a', '动画', 1), lib('c', '动画', 0)]
check(
'equal names fall back to sort_order then id',
ordered(sortLibrariesByField(sameName, [], 'name', 'desc')) === 'c,a,b',
)
check('single library is returned as-is', sortLibrariesByField([lib('a', '动画')], [], 'name', 'desc').length === 1)
const previews = [
{ library: lib('a', '动画'), total: 1 },
{ library: lib('b', '电影'), total: 2 },
]
check(
'previews follow the library order',
sortLibraryPreviewsByField(previews, [], 'name', 'desc').map((item) => item.library.id).join(',') === 'a,b',
)
check(
'previews keep their payload after sorting',
sortLibraryPreviewsByField(previews, [], 'name', 'desc')[0].total === 1,
)
check(
'previews respect pinning',
sortLibraryPreviewsByField(previews, ['b'], 'name', 'desc').map((item) => item.library.id).join(',') === 'b,a',
)
console.log('libraryListSort.test.ts ok')
+35 -13
View File
@@ -76,7 +76,32 @@ function compareLibraries(a: Library, b: Library, field: LibraryListSortField, o
/**
* 置顶组始终在前;组内按 field/order 排序。
* pinnedIds 的相对顺序只决定「是否置顶」,组内不再保留置顶拖拽序。
*
* 首页与 /libraries 共用这份排序,保证两处的媒体库顺序一致。
*/
export function sortLibrariesByField(
libraries: Library[],
pinnedIds: string[],
field: LibraryListSortField,
order: LibraryListSortOrder,
): Library[] {
if (libraries.length < 2) return libraries
const pinnedRank = new Map(pinnedIds.map((id, index) => [id, index]))
const pinned: Library[] = []
const rest: Library[] = []
for (const library of libraries) {
if (pinnedRank.has(library.id)) pinned.push(library)
else rest.push(library)
}
const byField = (a: Library, b: Library) => compareLibraries(a, b, field, order)
pinned.sort(byField)
rest.sort(byField)
return [...pinned, ...rest]
}
/** 预览列表按库排序:先排库,再按排好的库顺序取回对应预览。 */
export function sortLibraryPreviewsByField<T extends { library: Library }>(
items: T[],
pinnedIds: string[],
@@ -84,17 +109,14 @@ export function sortLibraryPreviewsByField<T extends { library: Library }>(
order: LibraryListSortOrder,
): T[] {
if (items.length < 2) return items
const pinnedRank = new Map(pinnedIds.map((id, index) => [id, index]))
const pinned: T[] = []
const rest: T[] = []
for (const item of items) {
if (pinnedRank.has(item.library.id)) pinned.push(item)
else rest.push(item)
}
const byField = (a: T, b: T) => compareLibraries(a.library, b.library, field, order)
pinned.sort(byField)
rest.sort(byField)
return [...pinned, ...rest]
const ordered = sortLibrariesByField(
items.map((item) => item.library),
pinnedIds,
field,
order,
)
const byId = new Map(items.map((item) => [item.library.id, item]))
return ordered
.map((library) => byId.get(library.id))
.filter((item): item is T => item !== undefined)
}
-28
View File
@@ -67,31 +67,3 @@ export function togglePinnedLibraryId(current: string[], id: string): string[] {
export function isLibraryPinned(id: string, pinnedIds: string[]): boolean {
return pinnedIds.includes(id)
}
export function sortByPinnedIds<T extends { id: string }>(items: T[], pinnedIds: string[]): T[] {
if (pinnedIds.length === 0) return items
const rank = new Map(pinnedIds.map((pinnedId, index) => [pinnedId, index]))
return [...items].sort((a, b) => {
const aRank = rank.get(a.id)
const bRank = rank.get(b.id)
const aPinned = aRank !== undefined
const bPinned = bRank !== undefined
if (aPinned !== bPinned) return aPinned ? -1 : 1
if (aPinned && bPinned) return aRank - bRank
return 0
})
}
export function sortLibraryPreviews<T extends { library: { id: string } }>(items: T[], pinnedIds: string[]): T[] {
if (pinnedIds.length === 0) return items
const rank = new Map(pinnedIds.map((id, index) => [id, index]))
return [...items].sort((a, b) => {
const aRank = rank.get(a.library.id)
const bRank = rank.get(b.library.id)
const aPinned = aRank !== undefined
const bPinned = bRank !== undefined
if (aPinned !== bPinned) return aPinned ? -1 : 1
if (aPinned && bPinned) return aRank - bRank
return 0
})
}