mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-21 08:15:12 +02:00
feat: Implement back button and selectable registrars
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
? item?.UserData?.PlaybackPositionTicks / 10_000_000
|
||||
: undefined
|
||||
};
|
||||
console.log('startTime', playbackInfo.startTime);
|
||||
|
||||
if (mediaSourceId) reportPlaybackStarted(id, sessionId, mediaSourceId);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -305,7 +305,6 @@
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
console.log('Video destroyed');
|
||||
clearInterval(progressInterval);
|
||||
if (fullscreen) exitFullscreen?.();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user