mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-20 15:19:41 +08:00
fix: prevent duplicate manual reorganize requests in filtered directories (#469)
This commit is contained in:
@@ -89,6 +89,27 @@ const previewLoaded = ref(false)
|
||||
// 预览数据
|
||||
const previewData = ref<ManualTransferPreviewData>()
|
||||
|
||||
function getFileItemKey(item?: FileItem) {
|
||||
return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
|
||||
}
|
||||
|
||||
function dedupeFileItems(fileItems?: FileItem[]) {
|
||||
if (!fileItems?.length) return []
|
||||
|
||||
const uniqueItems = new Map<string, FileItem>()
|
||||
fileItems.forEach(item => {
|
||||
uniqueItems.set(getFileItemKey(item), item)
|
||||
})
|
||||
|
||||
return Array.from(uniqueItems.values())
|
||||
}
|
||||
|
||||
function getPreviewItemKey(item: ManualTransferPreviewItem) {
|
||||
return [item.source ?? '', item.target ?? '', item.success === false ? 'failed' : 'success'].join('|')
|
||||
}
|
||||
|
||||
const normalizedItems = computed(() => dedupeFileItems(props.items))
|
||||
|
||||
// 分页
|
||||
const previewPage = ref(1)
|
||||
const previewPageSize = ref(10)
|
||||
@@ -128,18 +149,21 @@ const dialogTitle = computed(() => {
|
||||
|
||||
// 副标题
|
||||
const dialogSubtitle = computed(() => {
|
||||
if (props.items) {
|
||||
if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length })
|
||||
return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path })
|
||||
if (normalizedItems.value.length) {
|
||||
if (normalizedItems.value.length > 1) {
|
||||
return t('dialog.reorganize.multipleItemsTitle', { count: normalizedItems.value.length })
|
||||
}
|
||||
|
||||
return t('dialog.reorganize.singleItemTitle', { path: normalizedItems.value[0].path })
|
||||
} else if (props.logids) {
|
||||
return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length })
|
||||
}
|
||||
})
|
||||
// 禁用指定集数
|
||||
const disableEpisodeDetail = computed(() => {
|
||||
if (props.items) {
|
||||
if (normalizedItems.value.length) {
|
||||
if (transferForm.episode_format) return false
|
||||
return !(props.items.length === 1 && props.items[0].type !== 'dir')
|
||||
return !(normalizedItems.value.length === 1 && normalizedItems.value[0].type !== 'dir')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -447,17 +471,21 @@ function getPreviewFailureMessage(data?: ManualTransferPreviewData) {
|
||||
function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTransferPreviewData) {
|
||||
if (!incoming) return
|
||||
|
||||
const incomingItems = incoming.items ?? []
|
||||
const incomingSummary = incoming.summary ?? {
|
||||
total: incomingItems.length,
|
||||
success: incomingItems.filter(item => item.success).length,
|
||||
failed: incomingItems.filter(item => item.success === false).length,
|
||||
}
|
||||
const mergedItems = [...(target.items ?? [])]
|
||||
const existingItemKeys = new Set(mergedItems.map(item => getPreviewItemKey(item)))
|
||||
|
||||
target.summary.total += incomingSummary.total ?? 0
|
||||
target.summary.success += incomingSummary.success ?? 0
|
||||
target.summary.failed += incomingSummary.failed ?? 0
|
||||
target.items.push(...incomingItems)
|
||||
;(incoming.items ?? []).forEach(item => {
|
||||
const itemKey = getPreviewItemKey(item)
|
||||
if (existingItemKeys.has(itemKey)) return
|
||||
|
||||
existingItemKeys.add(itemKey)
|
||||
mergedItems.push(item)
|
||||
})
|
||||
|
||||
target.items = mergedItems
|
||||
target.summary.total = mergedItems.length
|
||||
target.summary.success = mergedItems.filter(item => item.success !== false).length
|
||||
target.summary.failed = mergedItems.filter(item => item.success === false).length
|
||||
|
||||
if (incoming.message) {
|
||||
target.message = [target.message, incoming.message].filter(Boolean).join(';')
|
||||
@@ -466,7 +494,7 @@ function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTr
|
||||
|
||||
// 预览整理结果
|
||||
async function previewTransfer() {
|
||||
if (!props.logids && !props.items) return
|
||||
if (!props.logids && !normalizedItems.value.length) return
|
||||
|
||||
previewLoading.value = true
|
||||
resetPreviewState()
|
||||
@@ -476,9 +504,9 @@ async function previewTransfer() {
|
||||
try {
|
||||
const tasks: Promise<void>[] = []
|
||||
|
||||
if (props.items) {
|
||||
if (normalizedItems.value.length) {
|
||||
tasks.push(
|
||||
...props.items.map(async item => {
|
||||
...normalizedItems.value.map(async item => {
|
||||
try {
|
||||
const result = await requestManualTransfer<ManualTransferPreviewData>(
|
||||
createTransferPayload({ item, preview: true }),
|
||||
@@ -642,14 +670,14 @@ function stopLoadingProgress() {
|
||||
|
||||
// 整理文件
|
||||
async function transfer(background: boolean = false) {
|
||||
if (!props.logids && !props.items) return
|
||||
if (!props.logids && !normalizedItems.value.length) return
|
||||
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
|
||||
// 文件整理
|
||||
if (props.items) {
|
||||
for (const item of props.items) {
|
||||
if (normalizedItems.value.length) {
|
||||
for (const item of normalizedItems.value) {
|
||||
if (!background) {
|
||||
// 如果是文件,计算MD5
|
||||
const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString()
|
||||
|
||||
@@ -107,6 +107,47 @@ const currentItem = ref<FileItem>()
|
||||
// 选中的项目
|
||||
const selected = ref<FileItem[]>([])
|
||||
|
||||
function getFileItemKey(item?: FileItem) {
|
||||
return [item?.storage ?? inProps.item.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
|
||||
}
|
||||
|
||||
function dedupeFileItems(fileItems: FileItem[]) {
|
||||
const uniqueItems = new Map<string, FileItem>()
|
||||
fileItems.forEach(item => {
|
||||
uniqueItems.set(getFileItemKey(item), item)
|
||||
})
|
||||
|
||||
return Array.from(uniqueItems.values())
|
||||
}
|
||||
|
||||
function syncSelectedItems(nextItems: FileItem[] = items.value) {
|
||||
if (!selected.value.length) return
|
||||
|
||||
const currentItemMap = new Map(nextItems.map(item => [getFileItemKey(item), item]))
|
||||
selected.value = dedupeFileItems(selected.value)
|
||||
.map(item => currentItemMap.get(getFileItemKey(item)))
|
||||
.filter((item): item is FileItem => !!item)
|
||||
}
|
||||
|
||||
const selectedKeys = computed(() => new Set(selected.value.map(item => getFileItemKey(item))))
|
||||
|
||||
function isSelected(item: FileItem) {
|
||||
return selectedKeys.value.has(getFileItemKey(item))
|
||||
}
|
||||
|
||||
function setItemSelected(item: FileItem, checked: boolean) {
|
||||
const itemKey = getFileItemKey(item)
|
||||
|
||||
if (checked) {
|
||||
if (!selectedKeys.value.has(itemKey)) {
|
||||
selected.value = [...selected.value, item]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
selected.value = selected.value.filter(selectedItem => getFileItemKey(selectedItem) !== itemKey)
|
||||
}
|
||||
|
||||
// 识别结果
|
||||
const nameTestResult = ref<Context>()
|
||||
|
||||
@@ -210,6 +251,7 @@ async function list_files() {
|
||||
return;
|
||||
}
|
||||
items.value = data
|
||||
syncSelectedItems(data)
|
||||
emit('loading', false)
|
||||
loading.value = false
|
||||
|
||||
@@ -285,13 +327,7 @@ function changePath(item: FileItem) {
|
||||
// 点击列表项
|
||||
function listItemClick(item: FileItem) {
|
||||
if (selectMode.value) {
|
||||
if (selected.value.includes(item)) {
|
||||
selected.value = selected.value.filter(i => i !== item)
|
||||
} else {
|
||||
selected.value.push(item)
|
||||
}
|
||||
// 去重
|
||||
selected.value = Array.from(new Set(selected.value))
|
||||
setItemSelected(item, !isSelected(item))
|
||||
return false
|
||||
}
|
||||
changePath(item)
|
||||
@@ -436,7 +472,7 @@ function showTransfer(item: FileItem) {
|
||||
|
||||
// 显示批量整理对话框
|
||||
function showBatchTransfer() {
|
||||
transferItems.value = selected.value
|
||||
transferItems.value = dedupeFileItems(selected.value)
|
||||
transferPopper.value = true
|
||||
}
|
||||
|
||||
@@ -473,6 +509,7 @@ watch(
|
||||
async () => {
|
||||
// 清空列表
|
||||
items.value = []
|
||||
selected.value = []
|
||||
// 关闭弹窗
|
||||
nameTestResult.value = undefined
|
||||
nameTestDialog.value = false
|
||||
@@ -726,7 +763,11 @@ onUnmounted(() => {
|
||||
<VListItem v-bind="hover.props" class="px-3 pe-1" @click="listItemClick(item)">
|
||||
<template #prepend>
|
||||
<VListItemAction v-if="selectMode">
|
||||
<VCheckbox v-model="selected" :value="item" />
|
||||
<VCheckbox
|
||||
:model-value="isSelected(item)"
|
||||
@update:model-value="setItemSelected(item, !!$event)"
|
||||
@click.stop
|
||||
/>
|
||||
</VListItemAction>
|
||||
<template v-else>
|
||||
<VIcon
|
||||
|
||||
Reference in New Issue
Block a user