mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
Improve episode group carousel navigation
This commit is contained in:
@@ -67,6 +67,13 @@ const episodeGroups = ref<{ [key: string]: any }[]>([])
|
|||||||
// 当前选择剧集组
|
// 当前选择剧集组
|
||||||
const episodeGroup = ref(props.initialEpisodeGroup ?? '')
|
const episodeGroup = ref(props.initialEpisodeGroup ?? '')
|
||||||
|
|
||||||
|
// 剧集组横向轨道
|
||||||
|
const episodeGroupRail = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 剧集组轨道左右滚动状态
|
||||||
|
const canScrollEpisodeGroupsBackward = ref(false)
|
||||||
|
const canScrollEpisodeGroupsForward = ref(false)
|
||||||
|
|
||||||
const subscribeModeOptions = computed<SubscribeModeOption[]>(() => [
|
const subscribeModeOptions = computed<SubscribeModeOption[]>(() => [
|
||||||
{
|
{
|
||||||
title: t('dialog.subscribeMode.normal'),
|
title: t('dialog.subscribeMode.normal'),
|
||||||
@@ -173,6 +180,8 @@ async function getEpisodeGroups() {
|
|||||||
episodeGroups.value = await api.get(`media/groups/${props.media?.tmdb_id}`)
|
episodeGroups.value = await api.get(`media/groups/${props.media?.tmdb_id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
nextTick(updateEpisodeGroupScrollState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +284,32 @@ function setEpisodeGroup(value: string) {
|
|||||||
seasonsNotExisted.value = {}
|
seasonsNotExisted.value = {}
|
||||||
seasonInfos.value = []
|
seasonInfos.value = []
|
||||||
episodeGroup.value = value
|
episodeGroup.value = value
|
||||||
|
nextTick(updateEpisodeGroupScrollState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新剧集组横向轨道的左右滚动按钮状态。
|
||||||
|
function updateEpisodeGroupScrollState() {
|
||||||
|
const rail = episodeGroupRail.value
|
||||||
|
if (!rail) {
|
||||||
|
canScrollEpisodeGroupsBackward.value = false
|
||||||
|
canScrollEpisodeGroupsForward.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxScrollLeft = Math.max(rail.scrollWidth - rail.clientWidth, 0)
|
||||||
|
canScrollEpisodeGroupsBackward.value = rail.scrollLeft > 4
|
||||||
|
canScrollEpisodeGroupsForward.value = rail.scrollLeft < maxScrollLeft - 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按一屏内可辨识的距离横向滚动剧集组轨道。
|
||||||
|
function scrollEpisodeGroups(direction: 'backward' | 'forward') {
|
||||||
|
const rail = episodeGroupRail.value
|
||||||
|
if (!rail) return
|
||||||
|
|
||||||
|
rail.scrollBy({
|
||||||
|
behavior: 'smooth',
|
||||||
|
left: direction === 'backward' ? -Math.max(rail.clientWidth * 0.72, 240) : Math.max(rail.clientWidth * 0.72, 240),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交当前剧集组下选中的季及其订阅模式。
|
// 提交当前剧集组下选中的季及其订阅模式。
|
||||||
@@ -395,12 +430,19 @@ watch(() => props.subscribedSeasons, syncSelectedSeason)
|
|||||||
|
|
||||||
watch(() => props.subscribedSeasonModes, syncSelectedSeason)
|
watch(() => props.subscribedSeasonModes, syncSelectedSeason)
|
||||||
|
|
||||||
|
watch(episodeGroupOptions, () => nextTick(updateEpisodeGroupScrollState), { flush: 'post' })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
window.addEventListener('resize', updateEpisodeGroupScrollState)
|
||||||
// 自定义剧集组由 watchEffect 首次加载,避免默认季数据异步覆盖它。
|
// 自定义剧集组由 watchEffect 首次加载,避免默认季数据异步覆盖它。
|
||||||
if (!episodeGroup.value) getMediaSeasons()
|
if (!episodeGroup.value) getMediaSeasons()
|
||||||
getEpisodeGroups()
|
getEpisodeGroups()
|
||||||
checkSeasonsNotExists()
|
checkSeasonsNotExists()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateEpisodeGroupScrollState)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -424,20 +466,44 @@ onMounted(async () => {
|
|||||||
<div class="subscribe-season-group-label">
|
<div class="subscribe-season-group-label">
|
||||||
{{ t('dialog.subscribeSeason.selectGroup') }}
|
{{ t('dialog.subscribeSeason.selectGroup') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="subscribe-season-group-options">
|
<div class="subscribe-season-group-rail-shell">
|
||||||
<button
|
<button
|
||||||
v-for="group in episodeGroupOptions"
|
v-if="mdAndUp && canScrollEpisodeGroupsBackward"
|
||||||
:key="group.value || 'default'"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="subscribe-season-group-option"
|
class="subscribe-season-group-nav subscribe-season-group-nav--backward"
|
||||||
:class="{ 'subscribe-season-group-option--active': episodeGroup === group.value }"
|
:aria-label="t('media.episodeGroups.previous')"
|
||||||
@click="setEpisodeGroup(group.value)"
|
@click="scrollEpisodeGroups('backward')"
|
||||||
>
|
>
|
||||||
<VIcon :icon="group.icon" size="small" />
|
<VIcon icon="mdi-chevron-left" />
|
||||||
<span class="subscribe-season-group-text">
|
</button>
|
||||||
<span class="subscribe-season-group-title">{{ group.title }}</span>
|
<div
|
||||||
<span class="subscribe-season-group-subtitle">{{ group.subtitle }}</span>
|
ref="episodeGroupRail"
|
||||||
</span>
|
class="subscribe-season-group-options"
|
||||||
|
@scroll.passive="updateEpisodeGroupScrollState"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="group in episodeGroupOptions"
|
||||||
|
:key="group.value || 'default'"
|
||||||
|
type="button"
|
||||||
|
class="subscribe-season-group-option"
|
||||||
|
:class="{ 'subscribe-season-group-option--active': episodeGroup === group.value }"
|
||||||
|
@click="setEpisodeGroup(group.value)"
|
||||||
|
>
|
||||||
|
<VIcon :icon="group.icon" size="small" />
|
||||||
|
<span class="subscribe-season-group-text">
|
||||||
|
<span class="subscribe-season-group-title">{{ group.title }}</span>
|
||||||
|
<span class="subscribe-season-group-subtitle">{{ group.subtitle }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="mdAndUp && canScrollEpisodeGroupsForward"
|
||||||
|
type="button"
|
||||||
|
class="subscribe-season-group-nav subscribe-season-group-nav--forward"
|
||||||
|
:aria-label="t('media.episodeGroups.next')"
|
||||||
|
@click="scrollEpisodeGroups('forward')"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-chevron-right" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -577,27 +643,39 @@ onMounted(async () => {
|
|||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subscribe-season-group-rail-shell {
|
||||||
|
position: relative;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.subscribe-season-group-options {
|
.subscribe-season-group-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding-block: 0.125rem 0.375rem;
|
padding-block: 0.125rem 0.375rem;
|
||||||
scrollbar-width: thin;
|
scroll-behavior: smooth;
|
||||||
|
scroll-snap-type: inline proximity;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-season-group-options::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-season-group-option {
|
.subscribe-season-group-option {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 12rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
backdrop-filter: var(--app-grouped-list-backdrop-filter, blur(var(--transparent-blur-light, 6px)));
|
||||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(var(--v-theme-surface), 0.72);
|
background: rgba(var(--v-theme-surface), 0.82);
|
||||||
color: rgb(var(--v-theme-on-surface));
|
color: rgb(var(--v-theme-on-surface));
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
max-inline-size: 16rem;
|
min-inline-size: 0;
|
||||||
min-inline-size: 11rem;
|
|
||||||
padding-block: 0.5rem;
|
padding-block: 0.5rem;
|
||||||
padding-inline: 0.75rem;
|
padding-inline: 0.75rem;
|
||||||
|
scroll-snap-align: start;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.16s ease,
|
border-color 0.16s ease,
|
||||||
@@ -607,15 +685,26 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.subscribe-season-group-option:hover {
|
.subscribe-season-group-option:hover {
|
||||||
border-color: rgba(var(--v-theme-primary), 0.45);
|
border-color: rgba(var(--v-theme-primary), 0.45);
|
||||||
background: rgba(var(--v-theme-primary), 0.08);
|
background: rgba(var(--v-theme-surface), 0.9);
|
||||||
|
box-shadow: 0 6px 18px rgba(var(--v-theme-on-surface), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-season-group-option--active {
|
.subscribe-season-group-option--active {
|
||||||
border-color: rgb(var(--v-theme-primary));
|
border-color: rgb(var(--v-theme-primary));
|
||||||
background: rgba(var(--v-theme-primary), 0.14);
|
background: rgba(var(--v-theme-primary), 0.18);
|
||||||
color: rgb(var(--v-theme-primary));
|
color: rgb(var(--v-theme-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subscribe-season-group-option--active:hover {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-season-group-option:focus-visible,
|
||||||
|
.subscribe-season-group-nav:focus-visible {
|
||||||
|
outline: 2px solid rgba(var(--v-theme-primary), 0.45);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.subscribe-season-group-text {
|
.subscribe-season-group-text {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-inline-size: 0;
|
min-inline-size: 0;
|
||||||
@@ -644,6 +733,42 @@ onMounted(async () => {
|
|||||||
color: rgba(var(--v-theme-primary), 0.82);
|
color: rgba(var(--v-theme-primary), 0.82);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subscribe-season-group-nav {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: var(--app-grouped-list-backdrop-filter, blur(var(--transparent-blur-light, 6px)));
|
||||||
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: rgba(var(--v-theme-surface), 0.96);
|
||||||
|
block-size: 2.5rem;
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
inline-size: 2.5rem;
|
||||||
|
inset-block-start: 50%;
|
||||||
|
transform: translateY(-55%);
|
||||||
|
transition:
|
||||||
|
border-color 0.16s ease,
|
||||||
|
background-color 0.16s ease,
|
||||||
|
color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-season-group-nav:hover {
|
||||||
|
border-color: rgba(var(--v-theme-primary), 0.45);
|
||||||
|
background: rgba(var(--v-theme-surface), 0.96);
|
||||||
|
box-shadow: 0 6px 18px rgba(var(--v-theme-on-surface), 0.12);
|
||||||
|
color: rgb(var(--v-theme-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-season-group-nav--backward {
|
||||||
|
inset-inline-start: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-season-group-nav--forward {
|
||||||
|
inset-inline-end: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.subscribe-season-list {
|
.subscribe-season-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -706,8 +831,13 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-season-group-option {
|
.subscribe-season-group-option {
|
||||||
max-inline-size: 12rem;
|
flex-basis: 10rem;
|
||||||
min-inline-size: 9.5rem;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.subscribe-season-group-nav {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -848,7 +848,7 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
{{ getSubscribeText }}
|
{{ getSubscribeText }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn v-if="existsItemId" class="media-action-button" variant="tonal" @click="handlePlay()">
|
<VBtn v-if="existsItemId" class="media-action-button" variant="tonal" @click="handlePlay()" color="success">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-play" />
|
<VIcon icon="mdi-play" />
|
||||||
</template>
|
</template>
|
||||||
@@ -1561,9 +1561,10 @@ a.crew-name {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: 0 0 12rem;
|
flex: 0 0 12rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
backdrop-filter: var(--app-grouped-list-backdrop-filter, blur(var(--transparent-blur-light, 6px)));
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||||
border-radius: var(--app-control-radius);
|
border-radius: var(--app-control-radius);
|
||||||
background: rgba(var(--v-theme-surface), 0.72);
|
background: rgba(var(--v-theme-surface), 0.82);
|
||||||
color: rgb(var(--v-theme-on-surface));
|
color: rgb(var(--v-theme-on-surface));
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
min-inline-size: 0;
|
min-inline-size: 0;
|
||||||
@@ -1579,15 +1580,20 @@ a.crew-name {
|
|||||||
|
|
||||||
.episode-group-option:hover {
|
.episode-group-option:hover {
|
||||||
border-color: rgba(var(--v-theme-primary), 0.5);
|
border-color: rgba(var(--v-theme-primary), 0.5);
|
||||||
background: rgba(var(--v-theme-primary), 0.08);
|
background: rgba(var(--v-theme-surface), 0.9);
|
||||||
|
box-shadow: 0 6px 18px rgba(var(--v-theme-on-surface), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.episode-group-option--active {
|
.episode-group-option--active {
|
||||||
border-color: rgb(var(--v-theme-primary));
|
border-color: rgb(var(--v-theme-primary));
|
||||||
background: rgba(var(--v-theme-primary), 0.14);
|
background: rgba(var(--v-theme-primary), 0.18);
|
||||||
color: rgb(var(--v-theme-primary));
|
color: rgb(var(--v-theme-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.episode-group-option--active:hover {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
.episode-group-option:focus-visible,
|
.episode-group-option:focus-visible,
|
||||||
.episode-group-nav:focus-visible {
|
.episode-group-nav:focus-visible {
|
||||||
outline: 2px solid rgba(var(--v-theme-primary), 0.45);
|
outline: 2px solid rgba(var(--v-theme-primary), 0.45);
|
||||||
@@ -1632,9 +1638,10 @@ a.crew-name {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
backdrop-filter: var(--app-grouped-list-backdrop-filter, blur(var(--transparent-blur-light, 6px)));
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: rgba(var(--v-theme-surface), 0.92);
|
background: rgba(var(--v-theme-surface), 0.96);
|
||||||
block-size: 2.5rem;
|
block-size: 2.5rem;
|
||||||
color: rgb(var(--v-theme-on-surface));
|
color: rgb(var(--v-theme-on-surface));
|
||||||
inline-size: 2.5rem;
|
inline-size: 2.5rem;
|
||||||
@@ -1648,7 +1655,8 @@ a.crew-name {
|
|||||||
|
|
||||||
.episode-group-nav:hover {
|
.episode-group-nav:hover {
|
||||||
border-color: rgba(var(--v-theme-primary), 0.45);
|
border-color: rgba(var(--v-theme-primary), 0.45);
|
||||||
background: rgba(var(--v-theme-primary), 0.12);
|
background: rgba(var(--v-theme-surface), 0.96);
|
||||||
|
box-shadow: 0 6px 18px rgba(var(--v-theme-on-surface), 0.12);
|
||||||
color: rgb(var(--v-theme-primary));
|
color: rgb(var(--v-theme-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1667,16 +1675,20 @@ a.crew-name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.media-detail-transparent .episode-group-option {
|
.media-detail-transparent .episode-group-option {
|
||||||
backdrop-filter: blur(var(--transparent-blur-light, 6px));
|
backdrop-filter: blur(var(--transparent-blur, 10px));
|
||||||
background: rgba(var(--v-theme-surface), var(--transparent-opacity-light, 0.2));
|
background: rgba(var(--v-theme-surface), var(--transparent-opacity, 0.3));
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-detail-transparent .episode-group-option:hover {
|
.media-detail-transparent .episode-group-option:hover {
|
||||||
background: rgba(var(--v-theme-primary), 0.1);
|
background: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy, 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-detail-transparent .episode-group-option--active {
|
.media-detail-transparent .episode-group-option--active {
|
||||||
background: rgba(var(--v-theme-primary), 0.16);
|
background: rgba(var(--v-theme-primary), 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-detail-transparent .episode-group-option--active:hover {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-detail-transparent .episode-group-nav {
|
.media-detail-transparent .episode-group-nav {
|
||||||
|
|||||||
Reference in New Issue
Block a user