@@ -215,7 +222,8 @@ function getSeasonEpisode(item: DownloadHistory) {
}
.download-history-item {
- min-block-size: 8.5rem;
+ min-block-size: 7rem;
+ padding-block: 0.5rem !important;
}
.download-history-item__image {
@@ -248,6 +256,11 @@ function getSeasonEpisode(item: DownloadHistory) {
opacity: 1;
}
+.download-history-item__date {
+ font-size: 0.75rem;
+ line-height: 1.3;
+}
+
.download-history-empty {
display: flex;
flex-direction: column;
@@ -277,9 +290,13 @@ function getSeasonEpisode(item: DownloadHistory) {
}
@media (width <= 600px) {
+ .download-history-item {
+ min-block-size: 6.5rem;
+ }
+
.download-history-item__image {
- block-size: 54px !important;
- inline-size: 81px !important;
+ block-size: 84px !important;
+ inline-size: 56px !important;
}
}
diff --git a/src/components/dialog/PluginSearchDialog.vue b/src/components/dialog/PluginSearchDialog.vue
index cd472ba8..833ce404 100644
--- a/src/components/dialog/PluginSearchDialog.vue
+++ b/src/components/dialog/PluginSearchDialog.vue
@@ -86,6 +86,7 @@ function closeDialog() {
variant="solo"
prepend-inner-icon="mdi-magnify"
flat
+ autofocus
class="mx-1"
/>
diff --git a/src/components/dialog/__tests__/DownloadHistoryDialog.spec.ts b/src/components/dialog/__tests__/DownloadHistoryDialog.spec.ts
index 77dd843d..ef771e27 100644
--- a/src/components/dialog/__tests__/DownloadHistoryDialog.spec.ts
+++ b/src/components/dialog/__tests__/DownloadHistoryDialog.spec.ts
@@ -81,6 +81,31 @@ const MenuStub = defineComponent({
},
})
+const ImageStub = defineComponent({
+ name: 'VImg',
+ inheritAttrs: false,
+ props: {
+ aspectRatio: [String, Number],
+ cover: Boolean,
+ height: [String, Number],
+ position: String,
+ src: String,
+ width: [String, Number],
+ },
+ setup(props, { attrs }) {
+ return () =>
+ h('img', {
+ ...attrs,
+ src: props.src,
+ 'data-aspect-ratio': props.aspectRatio,
+ 'data-cover': String(props.cover),
+ 'data-height': props.height,
+ 'data-position': props.position,
+ 'data-width': props.width,
+ })
+ },
+})
+
let historySeed = 5000
function createHistory(overrides: Partial = {}): DownloadHistory {
@@ -90,7 +115,8 @@ function createHistory(overrides: Partial = {}): DownloadHistor
download_hash: `hash-${historySeed}`,
episodes: 'E01-E02',
id: historySeed,
- image: `https://images.example.com/history-${historySeed}.jpg`,
+ image: `https://images.example.com/backdrop-${historySeed}.jpg`,
+ poster: `https://images.example.com/poster-${historySeed}.jpg`,
path: `/downloads/history-${historySeed}`,
seasons: 'S01',
title: `历史媒体 ${historySeed}`,
@@ -123,6 +149,7 @@ async function renderDialog() {
},
stubs: {
VInfiniteScroll: InfiniteScrollStub,
+ VImg: ImageStub,
VMenu: MenuStub,
VVirtualScroll: VirtualScrollStub,
},
@@ -138,9 +165,10 @@ describe('DownloadHistoryDialog', () => {
it('loads and renders download history with page parameters', async () => {
const item = createHistory({ title: '首载剧集' })
+ const backdropItem = createHistory({ poster: undefined, title: '背景图历史' })
const requests: URL[] = []
server.use(
- downloadHistoryHandler([item], 200, url => {
+ downloadHistoryHandler([item, backdropItem], 200, url => {
requests.push(url)
}),
)
@@ -148,9 +176,9 @@ describe('DownloadHistoryDialog', () => {
await renderDialog()
expect(await screen.findByText('首载剧集')).toBeInTheDocument()
- expect(screen.getByText('(2026)')).toBeInTheDocument()
- expect(screen.getByText('S01E01-E02').closest('.v-chip')).toBeInTheDocument()
- expect(screen.getByText('示例站').closest('.v-chip')).toHaveClass('text-info')
+ expect(screen.getAllByText('(2026)')).toHaveLength(2)
+ expect(within(historyRow(item)).getByText('S01E01-E02').closest('.v-chip')).toBeInTheDocument()
+ expect(within(historyRow(item)).getByText('示例站').closest('.v-chip')).toHaveClass('text-info')
const resourceTitle = screen.getByText(item.torrent_name!)
expect(resourceTitle).toHaveClass('download-history-item__torrent', 'download-history-item__meta')
expect(document.querySelector('.download-history-item__date')).toHaveClass('download-history-item__meta')
@@ -159,6 +187,16 @@ describe('DownloadHistoryDialog', () => {
expect(requests).toHaveLength(1)
expect(requests[0].searchParams.get('page')).toBe('1')
expect(requests[0].searchParams.get('count')).toBe('30')
+ const posterImage = historyRow(item).querySelector('img.download-history-item__image')
+ const backdropImage = historyRow(backdropItem).querySelector('img.download-history-item__image')
+
+ expect(posterImage).toHaveAttribute('src', item.poster)
+ expect(backdropImage).toHaveAttribute('src', backdropItem.image)
+ expect(posterImage).toHaveAttribute('data-aspect-ratio', '2/3')
+ expect(posterImage).toHaveAttribute('data-cover', 'true')
+ expect(posterImage).toHaveAttribute('data-height', '96')
+ expect(posterImage).toHaveAttribute('data-position', 'center')
+ expect(posterImage).toHaveAttribute('data-width', '64')
})
it('appends later pages and preserves existing rows at the end', async () => {
diff --git a/src/components/misc/MediaIdSelector.vue b/src/components/misc/MediaIdSelector.vue
index da2ed51a..a73e890c 100644
--- a/src/components/misc/MediaIdSelector.vue
+++ b/src/components/misc/MediaIdSelector.vue
@@ -104,11 +104,12 @@ onMounted(() => {
-
+
{{ t('dialog.reorganize.mediaSearchInput') }}
{
emit('close')
@@ -116,24 +117,25 @@ onMounted(() => {
"
/>
-
+
-
-
+
+
{
+
+
diff --git a/src/components/misc/__tests__/MediaIdSelector.spec.ts b/src/components/misc/__tests__/MediaIdSelector.spec.ts
new file mode 100644
index 00000000..7683ed1e
--- /dev/null
+++ b/src/components/misc/__tests__/MediaIdSelector.spec.ts
@@ -0,0 +1,59 @@
+import MediaIdSelector from '@/components/misc/MediaIdSelector.vue'
+import { fireEvent, screen } from '@testing-library/vue'
+import { renderWithProviders } from '@tests/support/render'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ apiGet: vi.fn(),
+}))
+
+vi.mock('@/api', () => ({
+ default: { get: mocks.apiGet },
+}))
+
+describe('MediaIdSelector layout', () => {
+ beforeEach(() => {
+ mocks.apiGet.mockReset()
+ })
+
+ it('keeps the search field visible while results scroll independently', async () => {
+ mocks.apiGet.mockResolvedValue([
+ {
+ media_id: 'tmdb-1',
+ overview: '测试简介',
+ poster_path: '',
+ title: 'Hello Mini',
+ type: '电视剧',
+ year: '2019',
+ },
+ ])
+
+ const { container } = await renderWithProviders(MediaIdSelector, {
+ global: {
+ stubs: {
+ VDialogCloseBtn: {
+ props: ['innerClass'],
+ template: '',
+ },
+ },
+ },
+ })
+ const searchPanel = container.querySelector('.media-id-selector__search')
+ const closeButton = container.querySelector('.media-id-selector__close')
+
+ expect(container.querySelector('.media-id-selector')).toBeInTheDocument()
+ expect(searchPanel).toBeInstanceOf(HTMLElement)
+ expect(closeButton).toBeInstanceOf(HTMLButtonElement)
+ expect(container.querySelector('.v-input__details')).not.toBeInTheDocument()
+ expect(closeButton).toHaveAttribute('aria-label', '关闭')
+ expect(closeButton).not.toHaveClass('static')
+
+ const input = screen.getByPlaceholderText('输入媒体名称')
+ await fireEvent.update(input, 'hello')
+ await fireEvent.keyDown(input, { key: 'Enter' })
+ expect(await screen.findByText('Hello Mini(2019)')).toBeInTheDocument()
+
+ const results = container.querySelector('.media-id-selector__results')
+ expect(results).toBeInstanceOf(HTMLElement)
+ })
+})