style: Improve series page user experience and focus + scroll logic

This commit is contained in:
Aleksi Lassila
2024-05-15 02:50:47 +03:00
parent 0a43a5c231
commit af0df71d2c
9 changed files with 66 additions and 31 deletions

View File

@@ -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
>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) {
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;
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 =
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) {

View File

@@ -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);
}