mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 08:06:43 +08:00
fix(reorganize): refine transfer dialog layouts
This commit is contained in:
+1
-1
@@ -1871,7 +1871,7 @@ export interface TransferForm {
|
|||||||
// 目标路径
|
// 目标路径
|
||||||
target_path: string | null
|
target_path: string | null
|
||||||
// 媒体数据源
|
// 媒体数据源
|
||||||
media_source?: MediaDataSource
|
media_source?: MediaDataSource | null
|
||||||
// 数据源原生ID
|
// 数据源原生ID
|
||||||
media_id?: string | null
|
media_id?: string | null
|
||||||
// 音乐实体类型
|
// 音乐实体类型
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ const props = defineProps({
|
|||||||
const globalSettingsStore = useGlobalSettingsStore()
|
const globalSettingsStore = useGlobalSettingsStore()
|
||||||
const globalSettings = globalSettingsStore.globalSettings
|
const globalSettings = globalSettingsStore.globalSettings
|
||||||
|
|
||||||
const mediaSourceItems = computed<{ title: string; value: MediaDataSource }[]>(() => [
|
const mediaSourceItems = computed<{ title: string; value: MediaDataSource | null }[]>(() => [
|
||||||
|
{ title: t('dialog.reorganize.auto'), value: null },
|
||||||
{ title: t('setting.cache.recognitionSource.themoviedb'), value: 'themoviedb' },
|
{ title: t('setting.cache.recognitionSource.themoviedb'), value: 'themoviedb' },
|
||||||
{ title: t('setting.cache.recognitionSource.douban'), value: 'douban' },
|
{ title: t('setting.cache.recognitionSource.douban'), value: 'douban' },
|
||||||
{ title: t('setting.cache.recognitionSource.bangumi'), value: 'bangumi' },
|
{ title: t('setting.cache.recognitionSource.bangumi'), value: 'bangumi' },
|
||||||
@@ -54,7 +55,7 @@ const mediaSourceItems = computed<{ title: string; value: MediaDataSource }[]>((
|
|||||||
{ title: t('setting.cache.recognitionSource.doubanmusic'), value: 'doubanmusic' },
|
{ title: t('setting.cache.recognitionSource.doubanmusic'), value: 'doubanmusic' },
|
||||||
])
|
])
|
||||||
|
|
||||||
// 获取后台设置中的默认识别数据源,未知值兼容回退到TheMovieDb。
|
/** 获取后台设置中的默认识别数据源,未知值兼容回退到 TheMovieDb。 */
|
||||||
function getDefaultMediaSource(): MediaDataSource {
|
function getDefaultMediaSource(): MediaDataSource {
|
||||||
const configuredSource = globalSettings.RECOGNIZE_SOURCE as MediaDataSource
|
const configuredSource = globalSettings.RECOGNIZE_SOURCE as MediaDataSource
|
||||||
return mediaSourceItems.value.some(item => item.value === configuredSource) ? configuredSource : 'themoviedb'
|
return mediaSourceItems.value.some(item => item.value === configuredSource) ? configuredSource : 'themoviedb'
|
||||||
@@ -329,7 +330,8 @@ const transferForm = reactive<TransferForm>({
|
|||||||
logid: 0,
|
logid: 0,
|
||||||
target_storage: initialTargetPath ? (props.target_storage ?? 'local') : null,
|
target_storage: initialTargetPath ? (props.target_storage ?? 'local') : null,
|
||||||
target_path: initialTargetPath,
|
target_path: initialTargetPath,
|
||||||
media_source: getDefaultMediaSource(),
|
type_name: '',
|
||||||
|
media_source: null,
|
||||||
media_id: null,
|
media_id: null,
|
||||||
music_type: null,
|
music_type: null,
|
||||||
transfer_type: null,
|
transfer_type: null,
|
||||||
@@ -345,8 +347,11 @@ const transferForm = reactive<TransferForm>({
|
|||||||
// 历史记录入口和文件浏览器命中的成功历史都属于重新整理。
|
// 历史记录入口和文件浏览器命中的成功历史都属于重新整理。
|
||||||
const isReorganize = computed(() => Boolean(props.logids?.length || transferForm.reorganize))
|
const isReorganize = computed(() => Boolean(props.logids?.length || transferForm.reorganize))
|
||||||
|
|
||||||
// 当前手动识别与刮削数据源。
|
// 当前手动识别与刮削数据源;自动模式按媒体类型解析实际来源。
|
||||||
const mediaSource = computed(() => transferForm.media_source ?? 'themoviedb')
|
const mediaSource = computed(() => {
|
||||||
|
if (transferForm.media_source) return transferForm.media_source
|
||||||
|
return transferForm.type_name === '音乐' ? 'musicbrainz' : getDefaultMediaSource()
|
||||||
|
})
|
||||||
|
|
||||||
// 当前数据源对应的原生ID标签。
|
// 当前数据源对应的原生ID标签。
|
||||||
const mediaIdLabel = computed(() => {
|
const mediaIdLabel = computed(() => {
|
||||||
@@ -481,7 +486,7 @@ watch([() => transferForm.type_name, () => mediaSource.value], ([typeName, sourc
|
|||||||
watch(
|
watch(
|
||||||
() => transferForm.type_name,
|
() => transferForm.type_name,
|
||||||
typeName => {
|
typeName => {
|
||||||
if (typeName === '音乐' && !isMusicMediaSource(transferForm.media_source)) {
|
if (typeName === '音乐' && !isMusicMediaSource(transferForm.media_source ?? undefined)) {
|
||||||
transferForm.media_source = 'musicbrainz'
|
transferForm.media_source = 'musicbrainz'
|
||||||
}
|
}
|
||||||
transferForm.music_type = typeName === '音乐' ? (transferForm.music_type ?? 'recording') : null
|
transferForm.music_type = typeName === '音乐' ? (transferForm.music_type ?? 'recording') : null
|
||||||
@@ -492,11 +497,11 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => transferForm.media_source,
|
() => transferForm.media_source,
|
||||||
(source, previousSource) => {
|
(source, previousSource) => {
|
||||||
if (previousSource && source !== previousSource) {
|
if (source !== previousSource) {
|
||||||
transferForm.media_id = null
|
transferForm.media_id = null
|
||||||
mediaSelectorDialog.value = false
|
mediaSelectorDialog.value = false
|
||||||
}
|
}
|
||||||
if (isMusicMediaSource(source) && transferForm.type_name !== '音乐') {
|
if (isMusicMediaSource(source ?? undefined) && transferForm.type_name !== '音乐') {
|
||||||
transferForm.type_name = '音乐'
|
transferForm.type_name = '音乐'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -442,6 +442,7 @@ onUnmounted(() => {
|
|||||||
:src="getPosterUrl(group.media)"
|
:src="getPosterUrl(group.media)"
|
||||||
:alt="getMediaTitle(group.media, group.season)"
|
:alt="getMediaTitle(group.media, group.season)"
|
||||||
cover
|
cover
|
||||||
|
rounded="md"
|
||||||
/>
|
/>
|
||||||
<div class="media-selector__info">
|
<div class="media-selector__info">
|
||||||
<div class="media-selector__title">{{ getMediaTitle(group.media, group.season) }}</div>
|
<div class="media-selector__title">{{ getMediaTitle(group.media, group.season) }}</div>
|
||||||
@@ -467,6 +468,7 @@ onUnmounted(() => {
|
|||||||
:src="getPosterUrl(activeMediaGroup?.media)"
|
:src="getPosterUrl(activeMediaGroup?.media)"
|
||||||
:alt="getMediaTitle(activeMediaGroup?.media, activeMediaGroup?.season)"
|
:alt="getMediaTitle(activeMediaGroup?.media, activeMediaGroup?.season)"
|
||||||
cover
|
cover
|
||||||
|
rounded="md"
|
||||||
/>
|
/>
|
||||||
<div class="active-media__info">
|
<div class="active-media__info">
|
||||||
<h3 class="active-media__title">
|
<h3 class="active-media__title">
|
||||||
@@ -633,20 +635,6 @@ onUnmounted(() => {
|
|||||||
border-block-start: 0;
|
border-block-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-selector__item::before {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: var(--app-vuetify-rounded-pill);
|
|
||||||
background: rgb(var(--v-theme-primary));
|
|
||||||
block-size: 0;
|
|
||||||
content: '';
|
|
||||||
inline-size: 3px;
|
|
||||||
inset-block-start: 50%;
|
|
||||||
inset-inline-start: 0.25rem;
|
|
||||||
transition:
|
|
||||||
block-size 0.18s ease,
|
|
||||||
inset-block-start 0.18s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-selector__item:hover {
|
.media-selector__item:hover {
|
||||||
background: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
|
background: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
|
||||||
}
|
}
|
||||||
@@ -655,14 +643,8 @@ onUnmounted(() => {
|
|||||||
background: rgba(var(--v-theme-primary), 0.08);
|
background: rgba(var(--v-theme-primary), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-selector__item--active::before {
|
|
||||||
block-size: calc(100% - 1rem);
|
|
||||||
inset-block-start: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-selector__poster,
|
.media-selector__poster,
|
||||||
.active-media__poster {
|
.active-media__poster {
|
||||||
border-radius: var(--app-control-radius);
|
|
||||||
background: rgba(var(--v-theme-on-surface), 0.06);
|
background: rgba(var(--v-theme-on-surface), 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -470,6 +470,13 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
|||||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows automatic defaults when media type and source are unset', async () => {
|
||||||
|
await renderDialog()
|
||||||
|
|
||||||
|
expect(screen.getByLabelText<HTMLSelectElement>('类型')).toHaveDisplayValue('自动')
|
||||||
|
expect(screen.getByLabelText<HTMLSelectElement>('数据源')).toHaveDisplayValue('自动')
|
||||||
|
})
|
||||||
|
|
||||||
it('deduplicates selected files and submits nullable automatic target fields in one background request', async () => {
|
it('deduplicates selected files and submits nullable automatic target fields in one background request', async () => {
|
||||||
const bodies: unknown[] = []
|
const bodies: unknown[] = []
|
||||||
const backgrounds: string[] = []
|
const backgrounds: string[] = []
|
||||||
@@ -730,7 +737,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
await selectOption('数据源', 1)
|
await selectOption('数据源', 2)
|
||||||
await user.click(screen.getByRole('button', { name: '加入整理队列' }))
|
await user.click(screen.getByRole('button', { name: '加入整理队列' }))
|
||||||
await waitFor(() => expect(bodies).toHaveLength(2))
|
await waitFor(() => expect(bodies).toHaveLength(2))
|
||||||
expect(bodies[1]).toEqual(
|
expect(bodies[1]).toEqual(
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ import { screen, waitFor, within } from '@testing-library/vue'
|
|||||||
import { renderWithProviders } from '@tests/support/render'
|
import { renderWithProviders } from '@tests/support/render'
|
||||||
import { flushPromises } from '@vue/test-utils'
|
import { flushPromises } from '@vue/test-utils'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import { cwd } from 'node:process'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const transferQueueSource = readFileSync(resolve(cwd(), 'src/components/dialog/TransferQueueDialog.vue'), 'utf8')
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
apiDelete: vi.fn(),
|
apiDelete: vi.fn(),
|
||||||
apiGet: vi.fn(),
|
apiGet: vi.fn(),
|
||||||
@@ -197,6 +202,19 @@ describe('TransferQueueDialog', () => {
|
|||||||
expect(screen.queryByText('来源 A.mkv')).not.toBeInTheDocument()
|
expect(screen.queryByText('来源 A.mkv')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses fixed medium poster rounding without an active-card accent strip', async () => {
|
||||||
|
mocks.apiGet.mockResolvedValue(createQueue('圆角媒体', '/downloads/rounded.mkv'))
|
||||||
|
|
||||||
|
const { container } = await renderDialog()
|
||||||
|
|
||||||
|
await screen.findByRole('navigation', { name: '媒体队列' })
|
||||||
|
expect(container.querySelector('.media-selector__poster')).toHaveClass('rounded-md')
|
||||||
|
expect(container.querySelector('.active-media__poster')).toHaveClass('rounded-md')
|
||||||
|
expect(transferQueueSource).not.toContain('.media-selector__item::before')
|
||||||
|
expect(transferQueueSource).not.toContain('.media-selector__item--active::before')
|
||||||
|
expect(transferQueueSource).not.toContain('border-radius: var(--app-control-radius)')
|
||||||
|
})
|
||||||
|
|
||||||
it('uses canonical built-in identities before falling back to the title', async () => {
|
it('uses canonical built-in identities before falling back to the title', async () => {
|
||||||
const builtIn = createQueueItem({
|
const builtIn = createQueueItem({
|
||||||
id: 7301,
|
id: 7301,
|
||||||
|
|||||||
@@ -1489,7 +1489,7 @@ onUnmounted(() => {
|
|||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<template #item.title="{ item }">
|
<template #item.title="{ item }">
|
||||||
<div class="d-flex align-center">
|
<div class="transfer-history-desktop-media-cell">
|
||||||
<div class="transfer-history-desktop-poster-frame">
|
<div class="transfer-history-desktop-poster-frame">
|
||||||
<VImg
|
<VImg
|
||||||
v-if="getHistoryPosterUrl(item)"
|
v-if="getHistoryPosterUrl(item)"
|
||||||
@@ -1500,15 +1500,15 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<template #error>
|
<template #error>
|
||||||
<div class="transfer-history-desktop-poster-placeholder">
|
<div class="transfer-history-desktop-poster-placeholder">
|
||||||
<VIcon :icon="getPlaceholderIcon(item.type || '')" size="24" color="medium-emphasis" />
|
<VIcon :icon="getPlaceholderIcon(item.type || '')" size="20" color="medium-emphasis" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VImg>
|
</VImg>
|
||||||
<div v-else class="transfer-history-desktop-poster-placeholder">
|
<div v-else class="transfer-history-desktop-poster-placeholder">
|
||||||
<VIcon :icon="getPlaceholderIcon(item.type || '')" size="24" color="medium-emphasis" />
|
<VIcon :icon="getPlaceholderIcon(item.type || '')" size="20" color="medium-emphasis" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column ms-1">
|
<div class="d-flex flex-column">
|
||||||
<span v-if="item.type === '电视剧'" class="d-block text-high-emphasis min-w-20">
|
<span v-if="item.type === '电视剧'" class="d-block text-high-emphasis min-w-20">
|
||||||
{{ item?.seasons }}{{ item?.episodes }}
|
{{ item?.seasons }}{{ item?.episodes }}
|
||||||
</span>
|
</span>
|
||||||
@@ -1592,7 +1592,7 @@ onUnmounted(() => {
|
|||||||
:style="{ height: `${availableHeight}px` }"
|
:style="{ height: `${availableHeight}px` }"
|
||||||
>
|
>
|
||||||
<template #item.title="{ item }">
|
<template #item.title="{ item }">
|
||||||
<div class="d-flex align-center">
|
<div class="transfer-history-desktop-media-cell">
|
||||||
<div class="transfer-history-desktop-poster-frame">
|
<div class="transfer-history-desktop-poster-frame">
|
||||||
<VImg
|
<VImg
|
||||||
v-if="getHistoryPosterUrl(item)"
|
v-if="getHistoryPosterUrl(item)"
|
||||||
@@ -1603,15 +1603,15 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<template #error>
|
<template #error>
|
||||||
<div class="transfer-history-desktop-poster-placeholder">
|
<div class="transfer-history-desktop-poster-placeholder">
|
||||||
<VIcon :icon="getPlaceholderIcon(item.type || '')" size="24" color="medium-emphasis" />
|
<VIcon :icon="getPlaceholderIcon(item.type || '')" size="20" color="medium-emphasis" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VImg>
|
</VImg>
|
||||||
<div v-else class="transfer-history-desktop-poster-placeholder">
|
<div v-else class="transfer-history-desktop-poster-placeholder">
|
||||||
<VIcon :icon="getPlaceholderIcon(item.type || '')" size="24" color="medium-emphasis" />
|
<VIcon :icon="getPlaceholderIcon(item.type || '')" size="20" color="medium-emphasis" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column ms-1">
|
<div class="d-flex flex-column">
|
||||||
<span v-if="item.type === '电视剧'" class="d-block text-high-emphasis min-w-20">
|
<span v-if="item.type === '电视剧'" class="d-block text-high-emphasis min-w-20">
|
||||||
{{ item?.title }} {{ item?.seasons }}{{ item?.episodes }}
|
{{ item?.title }} {{ item?.seasons }}{{ item?.episodes }}
|
||||||
</span>
|
</span>
|
||||||
@@ -1962,14 +1962,21 @@ onUnmounted(() => {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transfer-history-desktop-media-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-block: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.transfer-history-desktop-poster-frame {
|
.transfer-history-desktop-poster-frame {
|
||||||
flex: 0 0 44px;
|
flex: 0 0 36px;
|
||||||
inline-size: 44px;
|
inline-size: 36px;
|
||||||
block-size: 66px;
|
block-size: 54px;
|
||||||
min-inline-size: 44px;
|
min-inline-size: 36px;
|
||||||
min-block-size: 66px;
|
min-block-size: 54px;
|
||||||
max-inline-size: 44px;
|
max-inline-size: 36px;
|
||||||
max-block-size: 66px;
|
max-block-size: 54px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
aspect-ratio: 2 / 3;
|
aspect-ratio: 2 / 3;
|
||||||
|
|||||||
@@ -448,11 +448,13 @@ describe('TransferHistoryView', () => {
|
|||||||
expect(poster).toBeInTheDocument()
|
expect(poster).toBeInTheDocument()
|
||||||
expect(poster).toHaveAttribute('data-cover', 'true')
|
expect(poster).toHaveAttribute('data-cover', 'true')
|
||||||
expect(image).toHaveAttribute('src', expect.stringContaining('system/img/0?imgurl=%2Fposter.jpg'))
|
expect(image).toHaveAttribute('src', expect.stringContaining('system/img/0?imgurl=%2Fposter.jpg'))
|
||||||
expect(transferHistorySource).toContain('flex: 0 0 44px;')
|
expect(transferHistorySource).toContain('gap: 10px;')
|
||||||
expect(transferHistorySource).toContain('inline-size: 44px;')
|
expect(transferHistorySource).toContain('padding-block: 6px;')
|
||||||
expect(transferHistorySource).toContain('block-size: 66px;')
|
expect(transferHistorySource).toContain('flex: 0 0 36px;')
|
||||||
expect(transferHistorySource).toContain('max-inline-size: 44px;')
|
expect(transferHistorySource).toContain('inline-size: 36px;')
|
||||||
expect(transferHistorySource).toContain('max-block-size: 66px;')
|
expect(transferHistorySource).toContain('block-size: 54px;')
|
||||||
|
expect(transferHistorySource).toContain('max-inline-size: 36px;')
|
||||||
|
expect(transferHistorySource).toContain('max-block-size: 54px;')
|
||||||
expect(transferHistorySource).toContain('overflow: hidden;')
|
expect(transferHistorySource).toContain('overflow: hidden;')
|
||||||
expect(transferHistorySource).toContain('aspect-ratio: 2 / 3;')
|
expect(transferHistorySource).toContain('aspect-ratio: 2 / 3;')
|
||||||
expect(transferHistorySource).toContain(
|
expect(transferHistorySource).toContain(
|
||||||
|
|||||||
Reference in New Issue
Block a user