feat: Detached pages and fix navigation actions

This commit is contained in:
Aleksi Lassila
2024-03-31 12:43:48 +03:00
parent 5b18c95766
commit b5b96bf3e5
14 changed files with 143 additions and 66 deletions

1
.idea/vcs.xml generated
View File

@@ -2,6 +2,5 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/backend" vcs="Git" />
</component>
</project>

View File

@@ -14,6 +14,8 @@
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';
import { appState } from './lib/stores/app-state.store';
import MoviePage from './lib/pages/MoviePage.svelte';
import DetatchedPage from './lib/components/DetatchedPage/DetatchedPage.svelte';
import Button from './lib/components/Button.svelte';
getReiverrApiClient()
.GET('/user', {})
@@ -23,7 +25,7 @@
</script>
<I18n />
<Container direction="horizontal" class="bg-stone-950 text-white flex flex-1 w-screen">
<Container class="bg-stone-950 text-white flex flex-1 w-screen">
{#if $appState.user === undefined}
<div class="h-screen w-screen flex flex-col items-center justify-center">
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
@@ -36,13 +38,12 @@
<LoginPage />
{:else}
<Router>
<Sidebar />
<Container class="flex-1 flex flex-col min-w-0">
<Container class="flex-1 flex flex-col min-w-0" direction="horizontal" trapFocus>
<Sidebar />
<Route path="/">
<BrowseSeriesPage />
</Route>
<Route path="movies">
<Route path="movies/*">
<MoviesPage />
</Route>
<Route path="library">
@@ -61,6 +62,16 @@
</Route>
</Container>
</Router>
<Router>
<Route path="movies/movie/:id">
<DetatchedPage>
<Button>Button 1</Button>
<Button>Button 2</Button>
<Button on:click={() => history.back()}>Back</Button>
</DetatchedPage>
</Route>
</Router>
{/if}
</Container>

View File

@@ -7,6 +7,7 @@
export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical';
export let gridCols: number = 0;
export let focusOnMount = false;
export let trapFocus = false;
export let debugOutline = false;
export let revealStrategy: RevealStrategy | undefined = undefined;
export let childrenRevealStrategy: RevealStrategy | undefined = undefined;
@@ -21,6 +22,7 @@
.setNavigationActions(navigationActions)
.setRevealStrategy(revealStrategy)
.setChildrenRevealStrategy(childrenRevealStrategy)
.setTrapFocus(trapFocus)
.getStores();
export const container = rest.container;
export const hasFocus = rest.hasFocus;

View File

@@ -63,6 +63,16 @@ export class TmdbApi implements Api<paths> {
}
}).then((res) => res.data as TmdbMovieFull2 | undefined);
}
getPopularMovies = () =>
TmdbApiOpen.GET('/3/movie/popular', {
params: {
query: {
language: get(settings)?.language,
region: get(settings)?.discover.region
}
}
}).then((res) => res.data?.results || []);
}
export const tmdbApi = new TmdbApi();
@@ -231,16 +241,6 @@ export const getTmdbMoviePoster = async (tmdbId: number) =>
/** Discover */
export const getTmdbPopularMovies = () =>
TmdbApiOpen.GET('/3/movie/popular', {
params: {
query: {
language: get(settings)?.language,
region: get(settings)?.discover.region
}
}
}).then((res) => res.data?.results || []);
export const getTmdbPopularSeries = () =>
TmdbApiOpen.GET('/3/tv/popular', {
params: {

View File

@@ -41,7 +41,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'fixed inset-0 justify-center items-center z-20 overflow-hidden flex transition-opacity reltaive',
'fixed inset-0 justify-center items-center z-20 overflow-hidden flex transition-opacity',
{
'opacity-0': hidden
}

View File

@@ -11,7 +11,6 @@
export let tmdbId: number | undefined = undefined;
export let tvdbId: number | undefined = undefined;
export let openInModal = true;
export let jellyfinId: string = '';
export let type: TitleType = 'movie';
export let backdropUrl: string;
@@ -32,14 +31,8 @@
<Container
active={focusable}
on:click={() => {
if (openInModal) {
if (tmdbId) {
//openTitleModal({ type, id: tmdbId, provider: 'tmdb' });
} else if (tvdbId) {
//openTitleModal({ type, id: tvdbId, provider: 'tvdb' });
}
} else if (tmdbId || tvdbId) {
navigate(`/${type}/${tmdbId || tvdbId}`);
if (tmdbId || tvdbId) {
navigate(`${type}/${tmdbId || tvdbId}`);
}
}}
class={classNames(

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import Card from './Card.svelte';
import type { TmdbMovie2 } from '../../apis/tmdb/tmdb-api';
import type { ComponentProps } from 'svelte';
import { TMDB_POSTER_SMALL } from '../../constants';
export let item: TmdbMovie2;
const props: ComponentProps<Card> = {
tmdbId: item.id,
title: item.title,
subtitle: item.release_date,
backdropUrl: TMDB_POSTER_SMALL + item.poster_path,
type: 'movie',
orientation: 'portrait',
rating: item.vote_average
};
</script>
<Card {...props} />

View File

@@ -0,0 +1,18 @@
<script>
import Container from '../../../Container.svelte';
</script>
<Container
navigationActions={{
left: () => {
console.log('Not called?');
history.back();
return false;
}
}}
focusOnMount
trapFocus
class="fixed inset-0"
>
<slot />
</Container>

View File

@@ -21,7 +21,7 @@
function onPrevious() {
if (index === 0) {
Selectable.focusLeft();
return false;
} else {
index = (index - 1 + length) % length;
}
@@ -41,7 +41,7 @@
navigationActions={{
right: onNext,
left: onPrevious,
up: () => Selectable.focusLeft() || true
up: () => Selectable.giveFocus('left') || true
}}
/>
<div class="flex flex-1 z-10 p-4">

View File

@@ -25,7 +25,7 @@
function onPrevious() {
if (showcaseIndex === 0) {
Selectable.focusLeft();
Selectable.giveFocus('left');
} else {
showcaseIndex = (showcaseIndex - 1 + showcaseLength) % showcaseLength;
}

View File

@@ -40,7 +40,7 @@
<Laptop class="w-8 h-8" slot="icon" />
</div>
</Container>
<Container on:click={() => navigate('/movie/359410')}>
<Container on:click={() => navigate('movies')}>
<div class={itemContainer(1, $focusIndex)}>
<CardStack class="w-8 h-8" slot="icon" />
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Container from '../../Container.svelte';
import { getPosterProps, getTmdbPopularMovies, TmdbApiOpen } from '../apis/tmdb/tmdb-api';
import { getPosterProps, tmdbApi, TmdbApiOpen } from '../apis/tmdb/tmdb-api';
import { formatDateToYearMonthDay } from '../utils';
import { settings } from '../stores/settings.store';
import type { TitleType } from '../types';
@@ -62,8 +62,6 @@
.then((res) => res.data?.results || [])
.then((i) => fetchCardProps(i, 'series'));
const fetchPopularMovies = () => getTmdbPopularMovies();
// const fetchLibraryItems = async () => {
// const items = await getJellyfinItems();
// const props = await fetchCardProps(items, 'series');
@@ -78,7 +76,7 @@
<Container focusOnMount>
<div class="h-screen flex flex-col">
<HeroShowcase items={getTmdbPopularMovies().then(getShowcasePropsFromTmdb)} />
<HeroShowcase items={tmdbApi.getPopularMovies().then(getShowcasePropsFromTmdb)} />
<div class="mt-8">
<Carousel scrollClass="">
<SidebarMargin slot="title" class="mx-4">

View File

@@ -1,5 +1,41 @@
<script lang="ts">
import Container from '../../Container.svelte';
import HeroShowcase from '../components/HeroShowcase/HeroShowcase.svelte';
import { tmdbApi } from '../apis/tmdb/tmdb-api';
import { getShowcasePropsFromTmdb } from '../components/HeroShowcase/HeroShowcase';
import Carousel from '../components/Carousel/Carousel.svelte';
import SidebarMargin from '../components/SidebarMargin.svelte';
import { _ } from 'svelte-i18n';
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
import TmdbCard from '../components/Card/TmdbCard.svelte';
import Button from '../components/Button.svelte';
import { useNavigate } from 'svelte-navigator';
const popularMovies = tmdbApi.getPopularMovies();
const navigate = useNavigate();
</script>
<Container focusOnMount>MoviesPage</Container>
<Container focusOnMount class="flex flex-col">
<div class="flex flex-col h-screen">
<HeroShowcase items={popularMovies.then(getShowcasePropsFromTmdb)} />
<div class="mt-8">
<Carousel>
<SidebarMargin slot="title" class="mx-4">
<div class="text-xl font-semibold text-zinc-300">
{$_('discover.streamingNow')}
</div>
</SidebarMargin>
{#await popularMovies}
<CarouselPlaceholderItems />
{:then items}
<div class="w-[4.5rem] h-1 shrink-0" />
{#each items as item (item.id)}
<div class="m-2">
<TmdbCard {item} />
</div>
{/each}
{/await}
</Carousel>
</div>
</div>
</Container>

View File

@@ -109,6 +109,7 @@ export class Selectable {
right: undefined
};
private focusByDefault: boolean = false;
private trapFocus: boolean = false;
private isInitialized: boolean = false;
private navigationActions: NavigationActions = {};
private isActive: boolean = true;
@@ -256,15 +257,33 @@ export class Selectable {
}
}
// if (this.navigationActions[direction]) {
// return this;
// } else
if (this.neighbors[direction]?.isFocusable()) {
return this.neighbors[direction];
} else {
} else if (!this.trapFocus) {
return this.parent?.getFocusableNeighbor(direction);
}
return undefined;
}
private giveFocus(direction: Direction) {
private giveFocus(direction: Direction): boolean {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let selectable: Selectable | undefined = this;
while (selectable) {
const action = selectable.navigationActions[direction];
if (action && action(this)) {
return true;
}
selectable = selectable.parent;
}
const neighbor = this.getFocusableNeighbor(direction);
// if (neighbor?.navigationActions?.[direction] && neighbor.navigationActions[direction]?.(this)) {
// return true;
// } else
if (neighbor) {
neighbor.focus();
return true;
@@ -273,24 +292,9 @@ export class Selectable {
}
}
static focusUp() {
static giveFocus(direction: Direction) {
const currentlyFocusedObject = get(Selectable.focusedObject);
return currentlyFocusedObject?.giveFocus('up');
}
static focusDown() {
const currentlyFocusedObject = get(Selectable.focusedObject);
return currentlyFocusedObject?.giveFocus('down');
}
static focusLeft() {
const currentlyFocusedObject = get(Selectable.focusedObject);
return currentlyFocusedObject?.giveFocus('left');
}
static focusRight() {
const currentlyFocusedObject = get(Selectable.focusedObject);
return currentlyFocusedObject?.giveFocus('right');
return currentlyFocusedObject?.giveFocus(direction);
}
_initializeSelectable() {
@@ -454,6 +458,11 @@ export class Selectable {
getScrollChildrenIntoView() {
return this.scrollChildrenIntoView;
}
setTrapFocus(trapFocus: boolean) {
this.trapFocus = trapFocus;
return this;
}
}
export function handleKeyboardNavigation(event: KeyboardEvent) {
@@ -472,21 +481,13 @@ export function handleKeyboardNavigation(event: KeyboardEvent) {
const navigationActions = currentlyFocusedObject.getNavigationActions();
if (event.key === 'ArrowUp') {
if (navigationActions.up && navigationActions.up(currentlyFocusedObject))
event.preventDefault();
else if (Selectable.focusUp()) event.preventDefault();
if (Selectable.giveFocus('up')) event.preventDefault();
} else if (event.key === 'ArrowDown') {
if (navigationActions.down && navigationActions.down(currentlyFocusedObject))
event.preventDefault();
else if (Selectable.focusDown()) event.preventDefault();
if (Selectable.giveFocus('down')) event.preventDefault();
} else if (event.key === 'ArrowLeft') {
if (navigationActions.left && navigationActions.left(currentlyFocusedObject))
event.preventDefault();
else if (Selectable.focusLeft()) event.preventDefault();
if (Selectable.giveFocus('left')) event.preventDefault();
} else if (event.key === 'ArrowRight') {
if (navigationActions.right && navigationActions.right(currentlyFocusedObject))
event.preventDefault();
else if (Selectable.focusRight()) event.preventDefault();
if (Selectable.giveFocus('right')) event.preventDefault();
} else if (event.key === 'Enter') {
if (navigationActions.enter && navigationActions.enter(currentlyFocusedObject))
event.preventDefault();