diff --git a/src/App.svelte b/src/App.svelte index 257cc67..ab215f8 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,20 +1,14 @@ @@ -37,7 +35,8 @@ {disabled} on:clickOrSelect={() => { if (tmdbId || tvdbId) { - navigate(navigateWithType ? `${type}/${tmdbId || tvdbId}` : `${tmdbId || tvdbId}`); + // navigate(navigateWithType ? `${type}/${tmdbId || tvdbId}` : `${tmdbId || tvdbId}`); + navigate(`/${type}/${tmdbId || tvdbId}`); } }} on:enter diff --git a/src/lib/components/DetachedPage/DetachedPage.svelte b/src/lib/components/DetachedPage/DetachedPage.svelte index 14e5d37..02984cb 100644 --- a/src/lib/components/DetachedPage/DetachedPage.svelte +++ b/src/lib/components/DetachedPage/DetachedPage.svelte @@ -2,15 +2,19 @@ import Container from '../../../Container.svelte'; import { type KeyEvent, type NavigateEvent, useRegistrar } from '../../selectable.js'; import { get } from 'svelte/store'; + import Sidebar from '../Sidebar/Sidebar.svelte'; + import classNames from 'classnames'; + + export let topmost = true; // Top element, that when focused and back is pressed, will exit the modal const topSelectable = useRegistrar(); function handleGoBack({ detail }: CustomEvent | CustomEvent) { - if ('willLeaveContainer' in detail) { - if (detail.direction !== 'left' || !detail.willLeaveContainer) return; - detail.preventNavigation(); - } + // if ('willLeaveContainer' in detail) { + // if (detail.direction !== 'left' || !detail.willLeaveContainer) return; + // detail.preventNavigation(); + // } const selectable = get(topSelectable); if (selectable && get(selectable.focusIndex) === 0) { @@ -23,10 +27,10 @@ function handleGoToTop({ detail }: CustomEvent | CustomEvent) { if ('willLeaveContainer' in detail) { // Navigate event - if (detail.direction === 'left' && detail.willLeaveContainer) { - detail.preventNavigation(); - get(topSelectable)?.focus(); - } + // if (detail.direction === 'left' && detail.willLeaveContainer) { + // detail.preventNavigation(); + // get(topSelectable)?.focus(); + // } } else { // Back event const selectable = get(topSelectable); @@ -36,13 +40,15 @@ - - + + diff --git a/src/lib/components/SeriesPage/EpisodeGrid.svelte b/src/lib/components/SeriesPage/EpisodeGrid.svelte index 1167b3e..5ad89cb 100644 --- a/src/lib/components/SeriesPage/EpisodeGrid.svelte +++ b/src/lib/components/SeriesPage/EpisodeGrid.svelte @@ -16,18 +16,18 @@ import UICarousel from '../Carousel/UICarousel.svelte'; import classNames from 'classnames'; import ScrollHelper from '../ScrollHelper.svelte'; - import { useNavigate } from 'svelte-navigator'; import ManageSeasonCard from './ManageSeasonCard.svelte'; import { TMDB_BACKDROP_SMALL } from '../../constants'; import { modalStack, openSeasonMediaManager } from '../Modal/modal.store'; - - const navigate = useNavigate(); + import { navigate } from '../StackRouter/StackRouter'; export let id: number; export let tmdbSeries: Readable; export let jellyfinEpisodes: Promise; export let currentJellyfinEpisode: Promise; + console.log('ID IS: ', id); + let awaitedJellyfinEpisodes: JellyfinItem[] = []; jellyfinEpisodes.then((episodes) => { awaitedJellyfinEpisodes = episodes; @@ -44,7 +44,7 @@ ); function handleOpenEpisodePage(episode: TmdbSeasonEpisode) { - navigate(`season/${episode.season_number}/episode/${episode.episode_number}`); + navigate(`/series/${id}/season/${episode.season_number}/episode/${episode.episode_number}`); } function handleMountSeasonButton(s: Selectable, seasonNumber: number) { diff --git a/src/lib/components/SeriesPage/SeriesPage.svelte b/src/lib/components/SeriesPage/SeriesPage.svelte index f199857..422c25d 100644 --- a/src/lib/components/SeriesPage/SeriesPage.svelte +++ b/src/lib/components/SeriesPage/SeriesPage.svelte @@ -19,7 +19,6 @@ import TmdbPersonCard from '../PersonCard/TmdbPersonCard.svelte'; import TmdbCard from '../Card/TmdbCard.svelte'; import EpisodeGrid from './EpisodeGrid.svelte'; - import { Route } from 'svelte-navigator'; import EpisodePage from '../../pages/EpisodePage.svelte'; import SeriesMediaManagerModal from '../MediaManagerModal/SeasonMediaManagerModal.svelte'; @@ -124,7 +123,6 @@ direction="horizontal" class="flex mt-8" focusOnMount - on:navigate={handleGoBack} on:back={handleGoBack} on:mount={registrar} > @@ -229,5 +227,3 @@ - - diff --git a/src/lib/components/Sidebar/Sidebar.svelte b/src/lib/components/Sidebar/Sidebar.svelte index dcd723c..4c4fa34 100644 --- a/src/lib/components/Sidebar/Sidebar.svelte +++ b/src/lib/components/Sidebar/Sidebar.svelte @@ -10,26 +10,28 @@ import classNames from 'classnames'; import { get, type Readable, writable, type Writable } from 'svelte/store'; import Container from '../../../Container.svelte'; - import { useLocation, useNavigate } from 'svelte-navigator'; import { registrars, Selectable } from '../../selectable'; + import { defaultStackRouter, navigate } from '../StackRouter/StackRouter'; + import { onMount } from 'svelte'; - const location = useLocation(); - const navigate = useNavigate(); let selectedIndex = 0; - $: activeIndex = { - '': 0, - series: 0, - movies: 1, - library: 2, - search: 3, - manage: 4 - }[$location.pathname.split('/')[1] || '/']; + let activeIndex = -1; + + // '': 0, + // series: 0, + // movies: 1, + // library: 2, + // search: 3, + // manage: 4 + // }[$location.pathname.split('/')[1] || '/']; let isNavBarOpen: Readable; let focusIndex: Writable = writable(0); let selectable: Selectable; - focusIndex.subscribe((v) => (selectedIndex = v)); + focusIndex.subscribe((v) => { + selectedIndex = v; + }); const selectIndex = (index: number) => () => { if (index === activeIndex) { @@ -40,14 +42,34 @@ const path = { 0: '/', - 1: 'movies', - 2: 'library', - 3: 'search', - 4: 'manage' + 1: '/movies', + 2: '/library', + 3: '/search', + 4: '/manage' }[index] || '/'; navigate(path); selectedIndex = index; }; + + onMount(() => { + defaultStackRouter.subscribe((r) => { + const bottomPage = r[0]; + console.log('bottomPage', bottomPage); + if (bottomPage) { + activeIndex = + { + '/': 0, + '/series': 0, + '/movies': 1, + '/library': 2, + '/search': 3, + '/manage': 4 + }[bottomPage.route.path] ?? -1; + selectable.focusIndex.set(activeIndex); + selectedIndex = activeIndex; + } + }); + }); + import { type StackRouterStore } from './StackRouter'; + + export let stack: StackRouterStore; + + + + +{#each $stack as page, index (page.id)} + +{/each} diff --git a/src/lib/components/StackRouter/StackRouter.ts b/src/lib/components/StackRouter/StackRouter.ts new file mode 100644 index 0000000..7cf43cc --- /dev/null +++ b/src/lib/components/StackRouter/StackRouter.ts @@ -0,0 +1,261 @@ +import { derived, get, writable } from 'svelte/store'; +import { type ComponentType } from 'svelte'; +import SeriesHomePage from '../../pages/SeriesHomePage.svelte'; +import SeriesPage from '../SeriesPage/SeriesPage.svelte'; +import EpisodePage from '../../pages/EpisodePage.svelte'; +import { modalStack } from '../Modal/modal.store'; +import MoviesHomePage from '../../pages/MoviesHomePage.svelte'; +import MoviePage from '../../pages/MoviePage.svelte'; +import LibraryPage from '../../pages/LibraryPage.svelte'; +import SearchPage from '../../pages/SearchPage.svelte'; +import PageNotFound from '../../pages/PageNotFound.svelte'; +import ManagePage from '../../pages/ManagePage.svelte'; + +interface Page { + id: symbol; + route: Route; + props?: Record; +} + +interface Route { + path: string; + component: ComponentType; + default?: boolean; + root?: boolean; + parent?: Route; +} + +export type StackRouterStore = ReturnType; + +type Indexes = { id: string; bottom: number; top: number }; + +export function useStackRouter({ routes, notFound }: { routes: Route[]; notFound: Route }) { + const { initialPages, initialIndexes } = getInitialValues(); + const indexes = writable(initialIndexes); + const pageStack = writable(initialPages); + const visibleStack = derived([indexes, pageStack], ([$indexes, $stack]) => { + return $stack.slice($indexes.bottom, $indexes.top + 1); + }); + + visibleStack.subscribe((v) => console.log('visibleStack', v)); + + function getInitialValues() { + const initialUrl = window.location.pathname; + const initialPages = [routeStringToRoute(initialUrl)]; + + let initialRouteParent = initialPages[0]?.route.parent; + const initialProps = initialPages[0]?.props; + while (initialRouteParent) { + const newPage: Page = { + id: Symbol(), + route: initialRouteParent, + props: initialProps + }; + initialPages.unshift(newPage); + initialRouteParent = initialRouteParent.parent; + } + const initialIndexes: Indexes = { + id: Math.random().toString(36).slice(2), + bottom: 0, + top: initialPages.length - 1 + }; + + history.replaceState(initialIndexes, '', initialUrl); + return { + initialPages, + initialIndexes + }; + } + + function routeStringToRoute(routeString: string): Page { + for (const route of routes) { + const targetParts = routeString.split('/'); + const routeParts = route.path.split('/'); + const params: Record = {}; + + if (targetParts.length !== routeParts.length) continue; + let i = 0; + while (i < targetParts.length) { + if (routeParts[i]?.startsWith(':')) { + const paramName = routeParts[i]?.slice(1); + if (paramName) { + // @ts-ignore + params[paramName] = targetParts[i]; + } + } else if (routeParts[i] !== targetParts[i]) break; + i++; + } + if (i === targetParts.length) { + return { props: params, id: Symbol(), route }; + } + } + + return { + id: Symbol(), + route: notFound + }; + } + + const navigate = (routeString: string, options: { replaceStack?: boolean } = {}) => { + const page: Page = routeStringToRoute(routeString); + const replaceStack = page.route.root || options.replaceStack || false; + + pageStack.update((prev) => { + const idxs = get(indexes); + if (replaceStack) return [page]; + else { + prev.splice(idxs.top + 1, Infinity, page); + return prev; + } + }); + + if (replaceStack) { + const stack = get(pageStack); + indexes.update((prev) => { + const indexes: Indexes = { + id: Math.random().toString(36).slice(2), + bottom: stack.length - 1, + top: stack.length - 1 + }; + history.pushState(indexes, '', routeString); + return indexes; + }); + } else { + indexes.update((prev) => { + const indexes: Indexes = { id: prev.id, bottom: prev.bottom, top: prev.top + 1 }; + history.pushState(indexes, '', routeString); + return indexes; + }); + } + }; + + const handlePopState = (e: PopStateEvent) => { + const newIndexes: Indexes = e.state; + const prevIndexes = get(indexes); + + modalStack.reset(); + + if (prevIndexes.id === newIndexes.id) { + indexes.set(newIndexes); + } else { + const initialValues = getInitialValues(); + indexes.set(initialValues.initialIndexes); + pageStack.set(initialValues.initialPages); + } + }; + + const back = () => { + // pageStack.update((prev) => { + // if (prev.length > 1) { + // prev.pop(); + // } + // return prev; + // }); + + history.back(); + }; + + return { + subscribe: visibleStack.subscribe, + navigate, + back, + handlePopState + }; +} + +const seriesHomeRoute: Route = { + path: '/series', + default: true, + root: true, + component: SeriesHomePage +}; + +const seriesRoute: Route = { + path: '/series/:id', + component: SeriesPage, + parent: seriesHomeRoute +}; + +const episodeRoute: Route = { + path: '/series/:id/season/:season/episode/:episode', + component: EpisodePage, + parent: seriesRoute +}; + +const moviesHomeRoute: Route = { + path: '/movies', + component: MoviesHomePage, + root: true +}; + +const movieRoute: Route = { + path: '/movie/:id', + component: MoviePage, + parent: moviesHomeRoute +}; + +const libraryRoute: Route = { + path: '/library', + component: LibraryPage, + root: true +}; + +const searchRoute: Route = { + path: '/search', + component: SearchPage, + root: true +}; + +const manageRoute: Route = { + path: '/manage', + component: ManagePage, + root: true +}; + +const notFoundRoute: Route = { + path: '/404', + component: PageNotFound, + root: true +}; + +export const defaultStackRouter = useStackRouter({ + routes: [ + seriesHomeRoute, + seriesRoute, + episodeRoute, + moviesHomeRoute, + movieRoute, + libraryRoute, + searchRoute, + manageRoute + ], + notFound: notFoundRoute +}); +// export const defaultStackRouter = useStackRouter({ +// '/': { +// component: SeriesHomePage, +// default: true +// }, +// '/series/:id': { +// component: SeriesPage +// }, +// '/series/:id/season/:season/episode/:episode': { +// component: EpisodePage +// } +// // '/movies': { +// // component: MoviesHomePage +// // }, +// // '/movies/:id': { +// // component: MoviePage +// // }, +// // '/library': { +// // component: LibraryPage +// // }, +// // '/settings': { +// // component: ManagePage +// // } +// } as const); + +export const navigate = defaultStackRouter.navigate; +export const back = defaultStackRouter.back; +defaultStackRouter.subscribe(console.log); diff --git a/src/lib/pages/LibraryPage.svelte b/src/lib/pages/LibraryPage.svelte index cd0839b..055defe 100644 --- a/src/lib/pages/LibraryPage.svelte +++ b/src/lib/pages/LibraryPage.svelte @@ -7,9 +7,6 @@ import CardGrid from '../components/CardGrid.svelte'; import JellyfinCard from '../components/Card/JellyfinCard.svelte'; import { scrollIntoView } from '../selectable'; - import { Route } from 'svelte-navigator'; - import MoviePage from './MoviePage.svelte'; - import SeriesPage from '../components/SeriesPage/SeriesPage.svelte'; const libraryItemsP = jellyfinApi.getLibraryItems(); @@ -44,6 +41,3 @@ {/await} - - - diff --git a/src/lib/pages/MoviePage.svelte b/src/lib/pages/MoviePage.svelte index 768e4b1..dbddc7a 100644 --- a/src/lib/pages/MoviePage.svelte +++ b/src/lib/pages/MoviePage.svelte @@ -10,9 +10,8 @@ import { radarrApi } from '../apis/radarr/radarr-api'; import { useActionRequests, useRequest } from '../stores/data.store'; import DetachedPage from '../components/DetachedPage/DetachedPage.svelte'; - import { modalStack, openMovieMediaManager } from '../components/Modal/modal.store'; + import { openMovieMediaManager } from '../components/Modal/modal.store'; import { playerState } from '../components/VideoPlayer/VideoPlayer'; - import ManageMediaModal from '../components/MediaManager/radarr/RadarrMediaMangerModal.svelte'; export let id: string; diff --git a/src/lib/pages/MoviesHomePage.svelte b/src/lib/pages/MoviesHomePage.svelte index efb14a7..e7bc2d7 100644 --- a/src/lib/pages/MoviesHomePage.svelte +++ b/src/lib/pages/MoviesHomePage.svelte @@ -9,12 +9,10 @@ import { jellyfinApi } from '../apis/jellyfin/jellyfin-api'; import { useRequest } from '../stores/data.store'; import JellyfinCard from '../components/Card/JellyfinCard.svelte'; - import { Route, useNavigate } from 'svelte-navigator'; import MoviePage from './MoviePage.svelte'; import { formatDateToYearMonthDay } from '../utils'; import TmdbCard from '../components/Card/TmdbCard.svelte'; - - const navigate = useNavigate(); + import { navigate } from '../components/StackRouter/StackRouter'; const continueWatching = jellyfinApi.getContinueWatching('movie'); const recentlyAdded = jellyfinApi.getRecentlyAdded('movie'); @@ -64,7 +62,7 @@ items={popularMovies.then(getShowcasePropsFromTmdbMovie)} on:enter={scrollIntoView({ top: 0 })} on:select={({ detail }) => { - navigate(`${detail?.id}`); + navigate(`/movie/${detail?.id}`); }} /> @@ -110,5 +108,3 @@ {/await} - - diff --git a/src/lib/pages/PageNotFound.svelte b/src/lib/pages/PageNotFound.svelte index 022c1d2..44ccb79 100644 --- a/src/lib/pages/PageNotFound.svelte +++ b/src/lib/pages/PageNotFound.svelte @@ -1,12 +1,11 @@ -
404 {$location.pathname}
+
404 {window.location.pathname}
diff --git a/src/lib/pages/SeriesHomePage.svelte b/src/lib/pages/SeriesHomePage.svelte index 753d458..d706336 100644 --- a/src/lib/pages/SeriesHomePage.svelte +++ b/src/lib/pages/SeriesHomePage.svelte @@ -3,19 +3,14 @@ import { TmdbApi, tmdbApi } from '../apis/tmdb/tmdb-api'; import { jellyfinApi } from '../apis/jellyfin/jellyfin-api'; - import { useRequest } from '../stores/data.store'; import Carousel from '../components/Carousel/Carousel.svelte'; - import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte'; import HeroShowcase from '../components/HeroShowcase/HeroShowcase.svelte'; import { getShowcasePropsFromTmdbSeries } from '../components/HeroShowcase/HeroShowcase'; import { scrollIntoView } from '../selectable'; import JellyfinCard from '../components/Card/JellyfinCard.svelte'; - import { Route, useNavigate } from 'svelte-navigator'; - import SeriesPage from '../components/SeriesPage/SeriesPage.svelte'; import { formatDateToYearMonthDay } from '../utils'; import TmdbCard from '../components/Card/TmdbCard.svelte'; - - const navigate = useNavigate(); + import { navigate } from '../components/StackRouter/StackRouter'; const continueWatching = jellyfinApi.getContinueWatchingSeries(); const recentlyAdded = jellyfinApi.getRecentlyAdded('series'); @@ -61,7 +56,7 @@ items={tmdbApi.getPopularSeries().then(getShowcasePropsFromTmdbSeries)} on:enter={scrollIntoView({ top: 0 })} on:select={({ detail }) => { - navigate(`${detail?.id}`); + if (detail) navigate(`/series/:id`, { params: { id: detail?.id.toString() } }); }} /> @@ -107,7 +102,3 @@ {/await}
- - - - diff --git a/src/lib/selectable.ts b/src/lib/selectable.ts index b47f30c..609cdb7 100644 --- a/src/lib/selectable.ts +++ b/src/lib/selectable.ts @@ -252,8 +252,10 @@ export class Selectable { // Cycle siblings if (indexAddition !== 0) { + const totalRows = Math.ceil(selectable.children.length / indexAddition); + const currentRow = Math.floor(focusIndex / indexAddition); let index = - focusIndex === selectable.children.length - 1 + currentRow === totalRows - 1 ? focusIndex + indexAddition : Math.min(focusIndex + indexAddition, selectable.children.length - 1); while (index >= 0 && index < selectable.children.length) {