mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
feat(music): support album subscription semantics
This commit is contained in:
@@ -25,6 +25,7 @@ const userStore = useUserStore()
|
||||
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
||||
const canSearch = computed(() => hasPermission(userPermissions.value, 'search'))
|
||||
const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscribe'))
|
||||
const isActionableMedia = computed(() => props.music?.music_type !== 'artist')
|
||||
|
||||
// 封面加载失败后改用专辑占位图标
|
||||
const imageLoadError = ref(false)
|
||||
@@ -90,7 +91,7 @@ const { openMusicSiteSearch } = useMusicSiteSearch(sites =>
|
||||
|
||||
/** 查询当前音乐是否已订阅,用于决定心形图标是实心还是空心。 */
|
||||
async function checkSubscribeStatus() {
|
||||
if (!canSubscribe.value || !props.music?.media_id) return
|
||||
if (!isActionableMedia.value || !canSubscribe.value || !props.music?.media_id) return
|
||||
try {
|
||||
isSubscribed.value = await getCachedMediaSubscribeStatus(getSubscribeStatusKey(), () =>
|
||||
subscribeActions.checkSubscribe(null),
|
||||
@@ -161,7 +162,7 @@ onMounted(checkSubscribeStatus)
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<div class="music-card-supporting text-medium-emphasis">
|
||||
<div v-if="props.music?.music_type !== 'artist'" class="music-card-supporting text-medium-emphasis">
|
||||
<VIcon icon="mdi-account-music" size="16" />
|
||||
<div class="music-card-artists">
|
||||
<template v-if="artistLinks.length">
|
||||
@@ -183,7 +184,10 @@ onMounted(checkSubscribeStatus)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.music?.album" class="music-card-supporting text-medium-emphasis">
|
||||
<div
|
||||
v-if="props.music?.album && props.music?.music_type !== 'album'"
|
||||
class="music-card-supporting text-medium-emphasis"
|
||||
>
|
||||
<VIcon icon="mdi-album" size="16" />
|
||||
<div class="music-card-album">
|
||||
<span>{{ t('music.album') }}:</span>
|
||||
@@ -216,7 +220,7 @@ onMounted(checkSubscribeStatus)
|
||||
|
||||
<div class="music-card-actions">
|
||||
<IconBtn
|
||||
v-if="canSubscribe"
|
||||
v-if="canSubscribe && isActionableMedia"
|
||||
:icon="isSubscribed ? 'mdi-heart' : 'mdi-heart-outline'"
|
||||
:color="isSubscribed ? 'error' : 'medium-emphasis'"
|
||||
:aria-label="isSubscribed ? t('music.unsubscribe') : t('music.subscribe')"
|
||||
@@ -226,7 +230,7 @@ onMounted(checkSubscribeStatus)
|
||||
@click.stop="subscribeActions.handleSubscribe()"
|
||||
/>
|
||||
<IconBtn
|
||||
v-if="canSearch"
|
||||
v-if="canSearch && isActionableMedia"
|
||||
icon="mdi-magnify"
|
||||
color="primary"
|
||||
:aria-label="t('music.searchResources')"
|
||||
|
||||
@@ -136,6 +136,12 @@ const subscribeProgressText = computed(() => {
|
||||
return `${downloadedEpisode.value} / ${total}`
|
||||
})
|
||||
|
||||
// 专辑订阅按整包完成,不伪造分集进度;这里只展示整专规模,提醒用户该目标包含多首曲目。
|
||||
const musicAlbumTrackText = computed(() => {
|
||||
if (props.media?.type !== '音乐' || props.media?.music_type !== 'album' || !props.media?.total_tracks) return ''
|
||||
return `${t('music.entityAlbum')} · ${t('music.trackCount', { count: props.media.total_tracks })}`
|
||||
})
|
||||
|
||||
// 订阅卡片 hover 文案:
|
||||
// - 普通订阅:「已下载 X · 共 Y 集」
|
||||
// - 洗版订阅:「已下载 X · 已洗版 N · 共 Y 集」
|
||||
@@ -634,8 +640,11 @@ function handleCardClick() {
|
||||
:data-subscribe-state-icon="compactStateDisplay.icon"
|
||||
size="16"
|
||||
/>
|
||||
<span v-if="subscribeProgressText" class="subscribe-card-mobile-progress-text">
|
||||
{{ subscribeProgressText }}
|
||||
<span
|
||||
v-if="subscribeProgressText || musicAlbumTrackText"
|
||||
class="subscribe-card-mobile-progress-text"
|
||||
>
|
||||
{{ subscribeProgressText || musicAlbumTrackText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -699,7 +708,9 @@ function handleCardClick() {
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="absolute inset-x-0 bottom-2 z-10 flex min-w-0 justify-space-between align-center flex-wrap px-3">
|
||||
<VCardText
|
||||
class="absolute inset-x-0 bottom-2 z-10 flex min-w-0 justify-space-between align-center flex-wrap px-3"
|
||||
>
|
||||
<div class="flex min-w-0 max-w-full align-center">
|
||||
<VIcon
|
||||
v-if="props.media?.total_episode && props.sortable"
|
||||
@@ -722,6 +733,13 @@ function handleCardClick() {
|
||||
{{ subscribeProgressTooltip }}
|
||||
</VTooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="musicAlbumTrackText"
|
||||
class="flex flex-shrink-0 align-center text-subtitle-2 me-2 text-white"
|
||||
>
|
||||
<VIcon icon="mdi-album" size="small" class="me-1" />
|
||||
{{ musicAlbumTrackText }}
|
||||
</div>
|
||||
<VIcon
|
||||
v-if="props.media?.username && props.sortable"
|
||||
icon="mdi-account"
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('SubscribeCard display and progress', () => {
|
||||
expect((image as HTMLImageElement).src).toContain('system/cache/image?url=')
|
||||
expect((image as HTMLImageElement).src).toContain(encodeURIComponent(media.backdrop || ''))
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/\d+ \/ \d+/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/^\d{1,4} \/ \d{1,4}$/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses the poster as the background fallback and replaces failed images with the placeholder', async () => {
|
||||
@@ -172,6 +172,21 @@ describe('SubscribeCard display and progress', () => {
|
||||
expect(container.querySelector('img')).toBeNull()
|
||||
})
|
||||
|
||||
it.each([480, 1024])('shows whole-album track count without fake episode progress at %ipx', async width => {
|
||||
setViewport(width)
|
||||
await renderCard({
|
||||
music_type: 'album',
|
||||
name: '叶惠美',
|
||||
total_episode: undefined,
|
||||
total_tracks: 11,
|
||||
type: '音乐',
|
||||
})
|
||||
|
||||
expect(screen.getByText('专辑 · 11 首')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/^\d{1,4} \/ \d{1,4}$/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['regular progress', 10, 4, '6 / 10', '60'],
|
||||
['negative missing episodes', 10, -2, '10 / 10', '100'],
|
||||
@@ -181,7 +196,7 @@ describe('SubscribeCard display and progress', () => {
|
||||
await renderCard({ lack_episode: lackEpisode, season: 2, total_episode: totalEpisode, type: '电视剧' })
|
||||
|
||||
if (expectedText) expect(screen.getByText(expectedText)).toBeInTheDocument()
|
||||
else expect(screen.queryByText(/\d+ \/ \d+/)).not.toBeInTheDocument()
|
||||
else expect(screen.queryByText(/^\d{1,4} \/ \d{1,4}$/)).not.toBeInTheDocument()
|
||||
|
||||
if (expectedProgress) expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', expectedProgress)
|
||||
else expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
||||
|
||||
Reference in New Issue
Block a user