feat: Detached pages and fix navigation actions
This commit is contained in:
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -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>
|
||||
@@ -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>
|
||||
<Container class="flex-1 flex flex-col min-w-0" direction="horizontal" trapFocus>
|
||||
<Sidebar />
|
||||
|
||||
<Container class="flex-1 flex flex-col min-w-0">
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
19
src/lib/components/Card/TmdbCard.svelte
Normal file
19
src/lib/components/Card/TmdbCard.svelte
Normal 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} />
|
||||
18
src/lib/components/DetatchedPage/DetatchedPage.svelte
Normal file
18
src/lib/components/DetatchedPage/DetatchedPage.svelte
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
function onPrevious() {
|
||||
if (showcaseIndex === 0) {
|
||||
Selectable.focusLeft();
|
||||
Selectable.giveFocus('left');
|
||||
} else {
|
||||
showcaseIndex = (showcaseIndex - 1 + showcaseLength) % showcaseLength;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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): 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;
|
||||
}
|
||||
|
||||
private giveFocus(direction: Direction) {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user