mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
优化标签
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import {
|
||||
ALL_TAG_ID,
|
||||
assignLibrariesToTag,
|
||||
attachLibrariesToTag,
|
||||
attachLibraryToTag,
|
||||
buildLibraryTagTabs,
|
||||
dedupeLibraryTags,
|
||||
detachLibrariesFromTag,
|
||||
detachLibraryFromTag,
|
||||
filterLibrariesByTag,
|
||||
filterLibrariesForTagging,
|
||||
normalizeLibraryTags,
|
||||
normalizeTagName,
|
||||
resolveSelectedTagId,
|
||||
@@ -84,6 +88,46 @@ const detached = detachLibraryFromTag(
|
||||
)
|
||||
check('detach removes only the requested library', detached[0].library_ids.join(',') === 'b')
|
||||
|
||||
const batchAttached = attachLibrariesToTag(
|
||||
[
|
||||
{ name: '动画', library_ids: ['a', 'x'] },
|
||||
{ name: '电影', library_ids: ['b'] },
|
||||
],
|
||||
['b', 'x', 'b', ' missing '],
|
||||
'动画',
|
||||
)
|
||||
check('batch attach moves every id out of its old tag', batchAttached[1].library_ids.length === 0)
|
||||
check('batch attach keeps order and dedupes ids', batchAttached[0].library_ids.join(',') === 'a,b,x,missing')
|
||||
check('batch attach ignores unknown target tag', attachLibrariesToTag([{ name: '动画', library_ids: [] }], ['a'], '不存在')[0].library_ids.length === 0)
|
||||
check('batch attach with no ids is a no-op', attachLibrariesToTag([{ name: '动画', library_ids: ['a'] }], [], '动画')[0].library_ids.join(',') === 'a')
|
||||
|
||||
const batchDetached = detachLibrariesFromTag(
|
||||
[
|
||||
{ name: '动画', library_ids: ['a', 'b'] },
|
||||
{ name: '电影', library_ids: ['b', 'c'] },
|
||||
],
|
||||
['b', 'c'],
|
||||
)
|
||||
check('batch detach removes ids from every tag', batchDetached[0].library_ids.join(',') === 'a')
|
||||
check('batch detach clears emptied tags', batchDetached[1].library_ids.length === 0)
|
||||
check(
|
||||
'assign with empty tag name clears everything',
|
||||
assignLibrariesToTag([{ name: '动画', library_ids: ['a'] }], ['a'], '')[0].library_ids.length === 0,
|
||||
)
|
||||
check(
|
||||
'assign with a tag routes to attach',
|
||||
assignLibrariesToTag([{ name: '动画', library_ids: [] }], ['a'], '动画')[0].library_ids.join(',') === 'a',
|
||||
)
|
||||
|
||||
const searchable = [
|
||||
{ id: 'lib-a', name: '电影库', path: '/media/movies', type: 'movie' },
|
||||
{ id: 'lib-b', name: 'TV Shows', path: '/media/tv', type: 'tv' },
|
||||
]
|
||||
check('tagging search matches name', filterLibrariesForTagging(searchable as never, '电影').length === 1)
|
||||
check('tagging search matches path', filterLibrariesForTagging(searchable as never, 'media/tv').length === 1)
|
||||
check('tagging search matches type case-insensitively', filterLibrariesForTagging(searchable as never, 'MOVIE').length === 1)
|
||||
check('tagging search with blank keyword returns all', filterLibrariesForTagging(searchable as never, ' ').length === 2)
|
||||
|
||||
check('blank name is rejected', tagNameError(' ', []) !== '')
|
||||
check('duplicate name is rejected', tagNameError('动画', [{ name: '动画', library_ids: [] }]) !== '')
|
||||
check(
|
||||
|
||||
@@ -232,3 +232,67 @@ export function detachLibraryFromTag(tags: LibraryTag[], libraryId: string, tagN
|
||||
tag.name === tagName ? { ...tag, library_ids: tag.library_ids.filter((id) => id !== libraryId) } : tag,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量把一个或多个媒体库挂到标签下:先从各自原标签移除,再按传入顺序追加,
|
||||
* 保证「一库一标签」。整批只产生一次写入。
|
||||
*/
|
||||
export function attachLibrariesToTag(
|
||||
tags: LibraryTag[],
|
||||
libraryIds: string[],
|
||||
tagName: string,
|
||||
): LibraryTag[] {
|
||||
const target = findTagIdByName(tags, tagName)
|
||||
const moving = normalizeLibraryIds(libraryIds)
|
||||
if (!target || moving.length === 0) return tags
|
||||
const movingSet = new Set(moving)
|
||||
const next = tags.map((tag) => ({
|
||||
...tag,
|
||||
library_ids: tag.library_ids.filter((id) => !movingSet.has(id)),
|
||||
}))
|
||||
return next.map((tag) =>
|
||||
tag.name === target ? { ...tag, library_ids: [...tag.library_ids, ...moving] } : tag,
|
||||
)
|
||||
}
|
||||
|
||||
/** 批量把媒体库移出所有标签(即变为「未分类」)。 */
|
||||
export function detachLibrariesFromTag(tags: LibraryTag[], libraryIds: string[]): LibraryTag[] {
|
||||
const moving = new Set(normalizeLibraryIds(libraryIds))
|
||||
if (moving.size === 0) return tags
|
||||
return tags.map((tag) => ({
|
||||
...tag,
|
||||
library_ids: tag.library_ids.filter((id) => !moving.has(id)),
|
||||
}))
|
||||
}
|
||||
|
||||
/** 批量归类:tagName 为空表示移出所有标签。 */
|
||||
export function assignLibrariesToTag(
|
||||
tags: LibraryTag[],
|
||||
libraryIds: string[],
|
||||
tagName: string,
|
||||
): LibraryTag[] {
|
||||
return tagName ? attachLibrariesToTag(tags, libraryIds, tagName) : detachLibrariesFromTag(tags, libraryIds)
|
||||
}
|
||||
|
||||
/** 去掉空白与重复,保留首次出现的顺序。 */
|
||||
export function normalizeLibraryIds(libraryIds: unknown): string[] {
|
||||
if (!Array.isArray(libraryIds)) return []
|
||||
const seen = new Set<string>()
|
||||
const out: string[] = []
|
||||
for (const raw of libraryIds) {
|
||||
const trimmed = String(raw ?? '').trim()
|
||||
if (!trimmed || seen.has(trimmed)) continue
|
||||
seen.add(trimmed)
|
||||
out.push(trimmed)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 媒体库列表的模糊匹配(名称 / 路径 / 类型),供标签管理里的搜索框使用。 */
|
||||
export function filterLibrariesForTagging(libraries: Library[], keyword: string): Library[] {
|
||||
const needle = keyword.trim().toLowerCase()
|
||||
if (!needle) return libraries
|
||||
return libraries.filter((lib) =>
|
||||
`${lib.name} ${lib.path} ${lib.type}`.toLowerCase().includes(needle),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user