style: Improve series page user experience and focus + scroll logic
This commit is contained in:
@@ -28,12 +28,13 @@
|
||||
direction="horizontal"
|
||||
class={classNames(
|
||||
$$restProps.class,
|
||||
'overflow-x-auto scrollbar-hide relative p-1 overflow-y-visible'
|
||||
'overflow-x-auto scrollbar-hide relative overflow-y-visible'
|
||||
)}
|
||||
style={`mask-image: linear-gradient(to right, transparent 0%, ${
|
||||
fadeLeft ? '' : 'black 0%, '
|
||||
}black 5%, black 95%, ${fadeRight ? '' : 'black 100%, '}transparent 100%);`}
|
||||
on:scroll={updateScrollPosition}
|
||||
on:enter
|
||||
bind:this={element}
|
||||
let:focusIndex
|
||||
>
|
||||
|
||||
@@ -36,9 +36,10 @@
|
||||
</script>
|
||||
|
||||
<Container
|
||||
class="fixed inset-0 z-20 bg-secondary-800 overflow-y-auto"
|
||||
class="fixed inset-0 z-20 bg-secondary-800 overflow-y-auto scrollbar-hide"
|
||||
trapFocus
|
||||
direction="horizontal"
|
||||
on:mount
|
||||
>
|
||||
<Container />
|
||||
<Container on:navigate={handleGoToTop} on:back={handleGoToTop} focusOnMount>
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
const verticalScrollParent = getScrollParent(div, 'vertical');
|
||||
const horizontalScrollParent = getScrollParent(div, 'horizontal');
|
||||
|
||||
console.log('verticalScrollParent', verticalScrollParent);
|
||||
|
||||
function handler() {
|
||||
scrollTop = verticalScrollParent ? verticalScrollParent.scrollTop : scrollTop;
|
||||
scrollLeft = horizontalScrollParent ? horizontalScrollParent.scrollLeft : scrollLeft;
|
||||
|
||||
@@ -75,14 +75,15 @@
|
||||
|
||||
<Container
|
||||
on:enter
|
||||
class={classNames('transition-transform mx-20', {
|
||||
'-translate-y-24': translateUp
|
||||
class={classNames('transition-transform mx-32', {
|
||||
'-translate-y-16': translateUp
|
||||
})}
|
||||
>
|
||||
<UICarousel
|
||||
class={classNames('flex -mx-2 transition-opacity mb-8', {
|
||||
class={classNames('flex transition-opacity mb-8', {
|
||||
'opacity-0': translateUp
|
||||
})}
|
||||
on:enter={scrollIntoView({ horizontal: 64 })}
|
||||
>
|
||||
{#each $tmdbSeasons || [] as season, i}
|
||||
<Container
|
||||
@@ -97,7 +98,7 @@
|
||||
<div
|
||||
class={classNames(
|
||||
'px-3 py-1 cursor-pointer whitespace-nowrap text-xl tracking-wide font-medium rounded-lg',
|
||||
'hover:font-semibold hover:tracking-wide hover:text-white',
|
||||
'hover:tracking-wide hover:text-white',
|
||||
{
|
||||
'bg-primary-500 text-black': hasFocus,
|
||||
//'bg-stone-800/50': hasFocus,
|
||||
@@ -123,7 +124,7 @@
|
||||
{#key episode.id}
|
||||
<TmdbEpisodeCard
|
||||
on:mount={(e) => handleMountCard(e.detail, episode)}
|
||||
on:enter={scrollIntoView({ vertical: 64 })}
|
||||
on:enter={scrollIntoView({ top: 92, bottom: 128 })}
|
||||
{episode}
|
||||
handlePlay={jellyfinEpisodeId
|
||||
? () => playerState.streamJellyfinId(jellyfinEpisodeId)
|
||||
@@ -137,7 +138,7 @@
|
||||
<ManageSeasonCard
|
||||
backdropUrl={TMDB_BACKDROP_SMALL + $tmdbSeries?.backdrop_path}
|
||||
on:clickOrSelect={() => openSeasonMediaManager(id, seasonIndex + 1)}
|
||||
on:enter={scrollIntoView({ vertical: 64 })}
|
||||
on:enter={scrollIntoView({ top: 92, bottom: 128 })}
|
||||
/>
|
||||
{/if}
|
||||
</CardGrid>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<ScrollHelper bind:scrollTop />
|
||||
<div class="relative">
|
||||
<Container
|
||||
class="h-screen flex flex-col py-12 px-20"
|
||||
class="h-[calc(100vh-4rem)] flex flex-col py-16 px-32"
|
||||
on:enter={scrollIntoView({ top: 0 })}
|
||||
on:navigate={({ detail }) => {
|
||||
if (detail.direction === 'down' && detail.willLeaveContainer) {
|
||||
@@ -112,7 +112,9 @@
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-4xl mt-4">
|
||||
<div
|
||||
class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-4xl mt-4 text-lg"
|
||||
>
|
||||
{series.overview}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -170,7 +172,7 @@
|
||||
})}
|
||||
>
|
||||
<EpisodeGrid
|
||||
on:enter={scrollIntoView({ vertical: 32 })}
|
||||
on:enter={scrollIntoView({ top: -32, bottom: 128 })}
|
||||
id={Number(id)}
|
||||
tmdbSeries={tmdbSeriesData}
|
||||
{jellyfinEpisodes}
|
||||
@@ -179,7 +181,7 @@
|
||||
/>
|
||||
<Container on:enter={scrollIntoView({ top: 0 })} class="pt-8">
|
||||
{#await $tmdbSeries then series}
|
||||
<Carousel scrollClass="px-20" class="mb-8">
|
||||
<Carousel scrollClass="px-32" class="mb-8">
|
||||
<div slot="header">Show Cast</div>
|
||||
{#each series?.aggregate_credits?.cast?.slice(0, 15) || [] as credit}
|
||||
<TmdbPersonCard
|
||||
@@ -190,7 +192,7 @@
|
||||
</Carousel>
|
||||
{/await}
|
||||
{#await $recommendations then recommendations}
|
||||
<Carousel scrollClass="px-20" class="mb-8">
|
||||
<Carousel scrollClass="px-32" class="mb-8">
|
||||
<div slot="header">Recommendations</div>
|
||||
{#each recommendations || [] as recommendation}
|
||||
<TmdbCard item={recommendation} on:enter={scrollIntoView({ horizontal: 64 + 30 })} />
|
||||
@@ -199,7 +201,7 @@
|
||||
{/await}
|
||||
</Container>
|
||||
{#await $tmdbSeries then series}
|
||||
<Container class="flex-1 bg-secondary-950 pt-8 px-20" on:enter={scrollIntoView({ top: 0 })}>
|
||||
<Container class="flex-1 bg-secondary-950 pt-8 px-32" on:enter={scrollIntoView({ top: 0 })}>
|
||||
<h1 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">More Information</h1>
|
||||
<div class="text-zinc-300 font-medium text-lg flex flex-wrap">
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
on:touchend={handleStopSeeking}
|
||||
/>
|
||||
</Container>
|
||||
<div class="flex justify-between px-2 pt-4">
|
||||
<div class="flex justify-between px-2 pt-4 text-lg">
|
||||
<span>{formatSecondsToTime(progressTime)}</span>
|
||||
<span>-{formatSecondsToTime(totalTime - progressTime)}</span>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,9 @@
|
||||
class="flex justify-between px-2 py-4 items-end"
|
||||
>
|
||||
<div>
|
||||
<div class="text-secondary-300 font-medium text-wider text-lg">{subtitle}</div>
|
||||
<div class="text-secondary-300 font-medium text-wider text-xl mb-1 tracking-wide">
|
||||
{subtitle}
|
||||
</div>
|
||||
<h1 class="header4">{title}</h1>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
@@ -176,7 +178,7 @@
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TextAlignLeft size={19} />
|
||||
<TextAlignLeft size={24} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
on:clickOrSelect={() => {
|
||||
@@ -189,7 +191,7 @@
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ChatBubble size={19} />
|
||||
<ChatBubble size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -250,7 +250,10 @@ export class Selectable {
|
||||
|
||||
// Cycle siblings
|
||||
if (indexAddition !== 0) {
|
||||
let index = focusIndex + indexAddition;
|
||||
let index =
|
||||
focusIndex === selectable.children.length - 1
|
||||
? focusIndex + indexAddition
|
||||
: Math.min(focusIndex + indexAddition, selectable.children.length - 1);
|
||||
while (index >= 0 && index < selectable.children.length) {
|
||||
const child = selectable.children[index];
|
||||
if (child && child.isFocusable()) {
|
||||
@@ -890,18 +893,36 @@ export const scrollElementIntoView = (htmlElement: HTMLElement, offsets: Offsets
|
||||
let top = -1;
|
||||
|
||||
if (offsets.top !== undefined && offsets.bottom !== undefined) {
|
||||
const topClipsAbove = boundingRect.y - parentBoundingRect.y < offsets.top;
|
||||
const bottomClipsBelow =
|
||||
boundingRect.y + boundingRect.height >
|
||||
parentBoundingRect.y + parentBoundingRect.height - offsets.bottom;
|
||||
|
||||
const distanceToParentTop = verticalParent.scrollTop + boundingRect.y - parentBoundingRect.y;
|
||||
const distanceToParentBottom =
|
||||
verticalParent.scrollHeight -
|
||||
verticalParent.scrollTop -
|
||||
(boundingRect.y - parentBoundingRect.y) -
|
||||
boundingRect.height;
|
||||
|
||||
const reverse =
|
||||
boundingRect.height > verticalParent.clientHeight - offsets.top - offsets.bottom;
|
||||
|
||||
if (
|
||||
(topClipsAbove && !bottomClipsBelow && !reverse) ||
|
||||
(!topClipsAbove && bottomClipsBelow && reverse)
|
||||
) {
|
||||
top = distanceToParentTop - offsets.top;
|
||||
} else if (
|
||||
(!topClipsAbove && bottomClipsBelow && !reverse) ||
|
||||
(topClipsAbove && !bottomClipsBelow && reverse)
|
||||
) {
|
||||
top =
|
||||
boundingRect.y - parentBoundingRect.y < offsets.top
|
||||
? boundingRect.y - parentBoundingRect.y + verticalParent.scrollTop - offsets.top
|
||||
: boundingRect.y - parentBoundingRect.y + htmlElement.clientHeight >
|
||||
verticalParent.clientHeight - offsets.bottom
|
||||
? boundingRect.y -
|
||||
parentBoundingRect.y +
|
||||
htmlElement.clientHeight +
|
||||
verticalParent.scrollTop +
|
||||
offsets.bottom -
|
||||
verticalParent.clientHeight
|
||||
: -1;
|
||||
verticalParent.scrollHeight -
|
||||
verticalParent.clientHeight -
|
||||
distanceToParentBottom +
|
||||
offsets.bottom;
|
||||
}
|
||||
} else if (offsets.top !== undefined) {
|
||||
top = boundingRect.y - parentBoundingRect.y + verticalParent.scrollTop - offsets.top;
|
||||
} else if (offsets.bottom !== undefined) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type Readable, writable } from 'svelte/store';
|
||||
import type { Selectable } from './selectable';
|
||||
|
||||
export function formatSecondsToTime(seconds: number) {
|
||||
const days = Math.floor(seconds / 60 / 60 / 24);
|
||||
@@ -110,11 +111,15 @@ export function getScrollParent(
|
||||
const parent = node.parentElement;
|
||||
|
||||
if (parent) {
|
||||
const { overflow } = window.getComputedStyle(parent);
|
||||
|
||||
if (
|
||||
(direction === 'vertical' && parent.scrollHeight > parent.clientHeight) ||
|
||||
(direction === 'horizontal' && parent.scrollWidth > parent.clientWidth)
|
||||
) {
|
||||
return parent;
|
||||
} else if (overflow.split(' ').every((o) => o === 'auto' || o === 'scroll')) {
|
||||
return parent;
|
||||
} else {
|
||||
return getScrollParent(parent, direction);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user