feat: Implement back button and selectable registrars

This commit is contained in:
Aleksi Lassila
2024-04-16 01:50:13 +03:00
parent f519fb7447
commit 32bde1ff9e
16 changed files with 197 additions and 89 deletions

View File

@@ -45,12 +45,7 @@
</div>
<div class="relative">
<Container
direction="horizontal"
handleNavigateOut={{ left: () => true }}
let:focusIndex
on:enter
>
<Container direction="horizontal" let:focusIndex on:enter>
<div
class={classNames(
'flex overflow-x-scroll items-center overflow-y-visible relative scrollbar-hide',

View File

@@ -35,7 +35,6 @@
}black 5%, black 95%, ${fadeRight ? '' : 'black 100%, '}transparent 100%);`}
on:scroll={updateScrollPosition}
bind:this={element}
handleNavigateOut={{ left: () => true }}
let:focusIndex
>
<slot {focusIndex} />

View File

@@ -1,21 +1,36 @@
<script>
<script lang="ts">
import Container from '../../../Container.svelte';
</script>
import { type KeyEvent, type NavigateEvent, useRegistrar } from '../../selectable.js';
import { get } from 'svelte/store';
<Container
on:navigate={({ detail }) => {
if (
detail.direction === 'left' &&
detail.willLeaveContainer &&
detail.selectable === detail.options.target
) {
history.back();
const selectable = useRegistrar();
function handleGoBack({ detail }: CustomEvent<KeyEvent> | CustomEvent<NavigateEvent>) {
if ('willLeaveContainer' in detail) {
if (detail.direction !== 'left' || !detail.willLeaveContainer) return;
detail.preventNavigation();
}
}}
focusOnMount
trapFocus
class="fixed inset-0 z-20 bg-stone-950 overflow-y-auto"
>
<slot />
history.back();
}
function handleGoToTop({ detail }: CustomEvent<KeyEvent> | CustomEvent<NavigateEvent>) {
if ('willLeaveContainer' in detail) {
// Navigate event
if (detail.direction === 'left' && detail.willLeaveContainer) {
detail.preventNavigation();
get(selectable)?.focus();
}
} else {
// Back event
get(selectable)?.focus();
}
}
</script>
<Container class="fixed inset-0 z-20 bg-stone-950 overflow-y-auto" trapFocus direction="horizontal">
<Container />
<Container on:navigate={handleGoToTop} on:back={handleGoToTop} focusOnMount>
<slot {handleGoBack} registrar={selectable.registrar} />
</Container>
</Container>

View File

@@ -7,6 +7,9 @@
import PageDots from '../HeroShowcase/PageDots.svelte';
import SidebarMargin from '../SidebarMargin.svelte';
import type { Readable, Writable } from 'svelte/store';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let urls: Promise<string[]>;
@@ -47,15 +50,21 @@
<Container
class="flex-1 flex"
on:enter
on:navigate={({ detail }) => {
on:navigate={(event) => {
const detail = event.detail;
if (!backgroundHasFocus) return;
if (detail.options.direction === 'right') {
if (onNext()) detail.preventNavigation();
} else if (detail.options.direction === 'left') {
if (onPrevious()) detail.preventNavigation();
} else if (detail.options.direction === 'up') {
Selectable.giveFocus('left', false);
detail.preventNavigation();
if (detail.direction === 'right') {
if (onNext()) {
detail.preventNavigation();
detail.stopPropagation();
}
} else if (detail.direction === 'left') {
if (onPrevious()) {
detail.preventNavigation();
detail.stopPropagation();
}
} else {
dispatch('navigate', detail);
}
}}
bind:hasFocusWithin

View File

@@ -7,6 +7,8 @@
import { TMDB_IMAGES_ORIGINAL, TMDB_POSTER_SMALL } from '../../constants';
import HeroCarousel from '../HeroCarousel/HeroCarousel.svelte';
import SidebarMargin from '../SidebarMargin.svelte';
import { get } from 'svelte/store';
import { sidebarSelectable } from '../../selectable';
export let items: Promise<ShowcaseItemProps[]> = Promise.resolve([]);
@@ -17,6 +19,11 @@
urls={items.then((items) => items.map((i) => `${TMDB_IMAGES_ORIGINAL}${i.backdropUrl}`))}
bind:index={showcaseIndex}
on:enter
on:navigate={({ detail }) => {
if (detail.direction === 'up') {
get(sidebarSelectable)?.focus();
}
}}
>
<div class="h-full flex-1 flex overflow-hidden z-10 relative">
{#await items}

View File

@@ -63,8 +63,6 @@
// Handle focus next episode
nextJellyfinEpisode.subscribe(($jellyfinEpisode) => {
console.log('got next jellyfin episode', $jellyfinEpisode, tmdbEpisode, selectable);
const isNextEpisode =
$jellyfinEpisode?.IndexNumber === tmdbEpisode.episode_number &&
$jellyfinEpisode?.ParentIndexNumber === tmdbEpisode.season_number;

View File

@@ -49,7 +49,7 @@
$: showEpisodeInfo = scrollTop > 140;
</script>
<DetachedPage>
<DetachedPage let:handleGoBack let:registrar>
<ScrollHelper bind:scrollTop />
<div class="relative">
<Container
@@ -141,10 +141,17 @@
{/if}
{/await}
{#await Promise.all([$jellyfinItem, $sonarrItem]) then [jellyfinItem, sonarrItem]}
<Container direction="horizontal" class="flex mt-8" focusOnMount>
<Container
direction="horizontal"
class="flex mt-8"
focusOnMount
on:navigate={handleGoBack}
on:back={handleGoBack}
{registrar}
>
{#if $nextJellyfinEpisode}
<Button
class="mr-2"
class="mr-4"
on:clickOrSelect={() =>
$nextJellyfinEpisode?.Id &&
playerState.streamJellyfinId($nextJellyfinEpisode.Id)}
@@ -156,7 +163,7 @@
{/if}
{#if sonarrItem}
<Button
class="mr-2"
class="mr-4"
on:clickOrSelect={() =>
modalStack.create(SonarrMediaMangerModal, { id: sonarrItem.id || -1 })}
>
@@ -169,7 +176,7 @@
</Button>
{:else}
<Button
class="mr-2"
class="mr-4"
on:clickOrSelect={() => addSeriesToSonarr(Number(id))}
inactive={$addSeriesToSonarrFetching}
>
@@ -178,11 +185,11 @@
</Button>
{/if}
{#if PLATFORM_WEB}
<Button class="mr-2">
<Button class="mr-4">
Open In TMDB
<ExternalLink size={19} slot="icon-after" />
</Button>
<Button class="mr-2">
<Button class="mr-4">
Open In Jellyfin
<ExternalLink size={19} slot="icon-after" />
</Button>
@@ -192,7 +199,7 @@
</div>
</HeroCarousel>
</Container>
<Container on:enter={scrollIntoView({ vertical: 64 })} bind:container={episodesSelectable}>
<Container on:enter={scrollIntoView({ vertical: 64 })} bind:selectable={episodesSelectable}>
<EpisodeCarousel
id={Number(id)}
tmdbSeries={tmdbSeriesData}

View File

@@ -4,7 +4,7 @@
import { type Readable, writable, type Writable } from 'svelte/store';
import Container from '../../../Container.svelte';
import { useNavigate } from 'svelte-navigator';
import type { Selectable } from '../../selectable';
import { type Selectable, sidebarSelectable } from '../../selectable';
const navigate = useNavigate();
let selectedIndex = 0;
@@ -47,7 +47,8 @@
)}
bind:hasFocusWithin={isNavBarOpen}
bind:focusIndex
bind:container={selectable}
bind:selectable
registrar={sidebarSelectable.registrar}
>
<!-- Background -->
<div

View File

@@ -73,7 +73,6 @@
? item?.UserData?.PlaybackPositionTicks / 10_000_000
: undefined
};
console.log('startTime', playbackInfo.startTime);
if (mediaSourceId) reportPlaybackStarted(id, sessionId, mediaSourceId);

View File

@@ -82,7 +82,6 @@
on:timeupdate={() => (progressTime = !seeking && videoDidLoad ? video.currentTime : progressTime)}
on:progress={handleProgress}
on:loadeddata={() => {
console.log('loadedData');
video.currentTime = progressTime;
videoDidLoad = true;
}}

View File

@@ -82,7 +82,7 @@
class={classNames('absolute inset-x-12 bottom-8 transition-opacity flex flex-col', {
'opacity-0': !showInterface
})}
bind:container
bind:selectable={container}
>
<Container
direction="horizontal"

View File

@@ -305,7 +305,6 @@
});
onDestroy(() => {
console.log('Video destroyed');
clearInterval(progressInterval);
if (fullscreen) exitFullscreen?.();
});