diff --git a/src/App.svelte b/src/App.svelte
index e0ee380..10ef247 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -15,6 +15,7 @@
import MoviePage from './lib/pages/MoviePage.svelte';
import ModalStack from './lib/components/Modal/ModalStack.svelte';
import PageNotFound from './lib/pages/PageNotFound.svelte';
+ import NavigationDebugger from './lib/components/NavigationDebugger.svelte';
appState.subscribe((s) => console.log('appState', s));
@@ -68,4 +69,6 @@
{/if}
+
+
diff --git a/src/Container.svelte b/src/Container.svelte
index 5f49a96..e79f935 100644
--- a/src/Container.svelte
+++ b/src/Container.svelte
@@ -45,15 +45,21 @@
dispatch('enter', { selectable, options, stopPropagation });
})
- .setOnNavigate((selectable, options) => {
+ .setOnNavigate((selectable, options, willLeaveContainer) => {
function preventNavigation() {
options.preventNavigation = true;
}
+ function stopPropagation() {
+ options.propagate = false;
+ }
+
dispatch('navigate', {
selectable,
options,
+ willLeaveContainer,
preventNavigation,
+ stopPropagation,
direction: options.direction
});
})
diff --git a/src/lib/components/DetachedPage/DetachedPage.svelte b/src/lib/components/DetachedPage/DetachedPage.svelte
index a8ff4c4..f83aadb 100644
--- a/src/lib/components/DetachedPage/DetachedPage.svelte
+++ b/src/lib/components/DetachedPage/DetachedPage.svelte
@@ -4,7 +4,11 @@
{
- if (detail.direction === 'left' && detail.options.willLeaveContainer) {
+ if (
+ detail.direction === 'left' &&
+ detail.options.willLeaveContainer &&
+ detail.selectable === detail.options.target
+ ) {
history.back();
detail.preventNavigation();
}
diff --git a/src/lib/components/HeroCarousel/HeroCarousel.svelte b/src/lib/components/HeroCarousel/HeroCarousel.svelte
index 3c2c6fe..ab5f0ec 100644
--- a/src/lib/components/HeroCarousel/HeroCarousel.svelte
+++ b/src/lib/components/HeroCarousel/HeroCarousel.svelte
@@ -48,7 +48,7 @@
} else if (detail.options.direction === 'left') {
if (onPrevious()) detail.preventNavigation();
} else if (detail.options.direction === 'up') {
- Selectable.giveFocus('left', true);
+ Selectable.giveFocus('left', false);
detail.preventNavigation();
}
}}
diff --git a/src/lib/components/Modal/FullScreenModal.svelte b/src/lib/components/Modal/FullScreenModal.svelte
index 2c4e6c1..9d33f53 100644
--- a/src/lib/components/Modal/FullScreenModal.svelte
+++ b/src/lib/components/Modal/FullScreenModal.svelte
@@ -9,7 +9,7 @@
{
- if (detail.direction === 'left' && detail.options.willLeaveContainer) {
+ if (detail.direction === 'left' && detail.willLeaveContainer) {
modalStack.close(modalId);
detail.preventNavigation();
}
diff --git a/src/lib/components/NavigationDebugger.svelte b/src/lib/components/NavigationDebugger.svelte
new file mode 100644
index 0000000..e114530
--- /dev/null
+++ b/src/lib/components/NavigationDebugger.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+{#if showOverlay}
+
+{/if}
diff --git a/src/lib/components/SeriesPage/EpisodeCarousel.svelte b/src/lib/components/SeriesPage/EpisodeCarousel.svelte
index 756f8ef..cd9dfd6 100644
--- a/src/lib/components/SeriesPage/EpisodeCarousel.svelte
+++ b/src/lib/components/SeriesPage/EpisodeCarousel.svelte
@@ -21,6 +21,8 @@
export let jellyfinEpisodes: Readable;
export let nextJellyfinEpisode: Readable;
+ export let selectedTmdbEpisode: TmdbEpisode | undefined;
+
const containers = new Map();
let scrollTop: number;
@@ -131,6 +133,7 @@
on:enter={(event) => {
scrollIntoView({ left: 64 + 16 })(event);
focusSeason(season);
+ selectedTmdbEpisode = episode;
}}
on:mount={(e) => handleEpisodeMount(e, episode)}
jellyfinEpisode={$jellyfinEpisodes?.find(
diff --git a/src/lib/components/SeriesPage/SeriesPage.svelte b/src/lib/components/SeriesPage/SeriesPage.svelte
index 43a2096..f1cc151 100644
--- a/src/lib/components/SeriesPage/SeriesPage.svelte
+++ b/src/lib/components/SeriesPage/SeriesPage.svelte
@@ -55,7 +55,7 @@
class="h-screen flex flex-col py-12 px-20 relative"
on:enter={scrollIntoView({ top: 0 })}
on:navigate={({ detail }) => {
- if (detail.direction === 'down') {
+ if (detail.direction === 'down' && detail.willLeaveContainer) {
if (episodesSelectable?.focusChild(1)) detail.preventNavigation();
}
}}
diff --git a/src/lib/components/VideoPlayer/ProgressBar.svelte b/src/lib/components/VideoPlayer/ProgressBar.svelte
index af54678..31e7a97 100644
--- a/src/lib/components/VideoPlayer/ProgressBar.svelte
+++ b/src/lib/components/VideoPlayer/ProgressBar.svelte
@@ -33,7 +33,7 @@
}
-
+
diff --git a/src/lib/components/VideoPlayer/VideoPlayer.svelte b/src/lib/components/VideoPlayer/VideoPlayer.svelte
index 30c3bc0..c3eebf1 100644
--- a/src/lib/components/VideoPlayer/VideoPlayer.svelte
+++ b/src/lib/components/VideoPlayer/VideoPlayer.svelte
@@ -5,7 +5,6 @@
import classNames from 'classnames';
import ProgressBar from './ProgressBar.svelte';
import { onDestroy } from 'svelte';
- import type { Readable } from 'svelte/store';
import type { Selectable } from '../../selectable';
export let playbackInfo: PlaybackInfo | undefined;
@@ -22,25 +21,44 @@
export let video: HTMLVideoElement;
let showInterface = true;
+ let showInterfaceTimeout: ReturnType;
let hideInterfaceTimeout: ReturnType;
let container: Selectable;
- function handleMouseMove() {
+ function handleShowInterface() {
showInterface = true;
- clearTimeout(hideInterfaceTimeout);
- hideInterfaceTimeout = setTimeout(() => {
- if (seeking) handleMouseMove();
- else {
- showInterface = false;
- container?.focusChild(1);
- }
- }, 2000);
+ clearTimeout(showInterfaceTimeout);
+ showInterfaceTimeout = setTimeout(() => {
+ if (seeking) handleShowInterface();
+ else handleHideInterface();
+ }, 5000);
}
- onDestroy(() => clearTimeout(hideInterfaceTimeout));
+ function handleHideInterface() {
+ showInterface = false;
+ clearTimeout(hideInterfaceTimeout);
+ hideInterfaceTimeout = setTimeout(() => {
+ container?.focusChild(1);
+ }, 200);
+ }
+
+ onDestroy(() => {
+ clearTimeout(showInterfaceTimeout);
+ clearTimeout(hideInterfaceTimeout);
+ });
-
+ {
+ if (!showInterface) {
+ detail.stopPropagation();
+ detail.preventNavigation();
+ }
+ handleShowInterface();
+ }}
+>
- Buttons
+ {
+ if (detail.direction === 'up') {
+ detail.stopPropagation();
+ detail.preventNavigation();
+ handleHideInterface();
+ }
+ }}
+ >
+ Buttons
+
{
- console.log(e.detail);
video.currentTime = e.detail;
}}
bind:totalTime
diff --git a/src/lib/selectable.ts b/src/lib/selectable.ts
index 22ee8b5..fd4ac04 100644
--- a/src/lib/selectable.ts
+++ b/src/lib/selectable.ts
@@ -33,26 +33,37 @@ const createFocusHandlerOptions = (): FocusEventOptions => ({
});
type NavigateEventOptions = {
- willLeaveContainer: boolean;
+ target?: Selectable;
preventNavigation: boolean;
+ propagate: boolean;
direction: Direction;
};
export type NavigateEvent = {
selectable: Selectable;
direction: Direction;
+ willLeaveContainer: boolean;
options: NavigateEventOptions;
preventNavigation: () => void;
+ stopPropagation: () => void;
};
-const createNavigateHandlerOptions = (direction: Direction): NavigateEventOptions => ({
- willLeaveContainer: false,
+const createNavigateHandlerOptions = (
+ target: Selectable | undefined,
+ direction: Direction
+): NavigateEventOptions => ({
+ target,
preventNavigation: false,
+ propagate: true,
direction
});
export type FocusHandler = (selectable: Selectable, options: FocusEventOptions) => void;
-export type NavigationHandler = (selectable: Selectable, options: NavigateEventOptions) => void;
+export type NavigationHandler = (
+ selectable: Selectable,
+ options: NavigateEventOptions,
+ willLeaveContainer: boolean
+) => void;
export class Selectable {
id: symbol;
@@ -207,61 +218,135 @@ export class Selectable {
return false;
}
- private giveFocus(direction: Direction, bypassActions: boolean = false): boolean {
- const focusIndex = get(this.focusIndex);
- const navigationEventOptions = createNavigateHandlerOptions(direction);
+ private giveFocus(direction: Direction, fireActions: boolean = true): boolean {
+ function getSelectable(selectable: Selectable): {
+ target?: Selectable;
+ cycledParent?: Selectable;
+ } {
+ const focusIndex = get(selectable.focusIndex);
- const indexAddition = {
- up: this.direction === 'vertical' ? -1 : -this.gridColumns,
- down: this.direction === 'vertical' ? 1 : this.gridColumns,
- left:
- this.direction === 'horizontal'
- ? (focusIndex % this.gridColumns) - 1 < 0
- ? 0
- : -1
- : -this.gridColumns,
- right:
- this.direction === 'horizontal'
- ? (focusIndex % this.gridColumns) + 1 >= this.gridColumns
- ? 0
- : 1
- : this.gridColumns
- }[direction];
+ const indexAddition = {
+ up: selectable.direction === 'vertical' ? -1 : -selectable.gridColumns,
+ down: selectable.direction === 'vertical' ? 1 : selectable.gridColumns,
+ left:
+ selectable.direction === 'horizontal'
+ ? (focusIndex % selectable.gridColumns) - 1 < 0
+ ? 0
+ : -1
+ : -selectable.gridColumns,
+ right:
+ selectable.direction === 'horizontal'
+ ? (focusIndex % selectable.gridColumns) + 1 >= selectable.gridColumns
+ ? 0
+ : 1
+ : selectable.gridColumns
+ }[direction];
- // Cycle siblings
- if (indexAddition !== 0) {
- let index = focusIndex + indexAddition;
- while (index >= 0 && index < this.children.length) {
- const children = this.children[index];
- if (children && children.isFocusable()) {
- this.onNavigate(this, navigationEventOptions);
- if (navigationEventOptions.preventNavigation) return true;
- children.focus();
- return true;
+ // Cycle siblings
+ if (indexAddition !== 0) {
+ let index = focusIndex + indexAddition;
+ while (index >= 0 && index < selectable.children.length) {
+ const child = selectable.children[index];
+ if (child && child.isFocusable()) {
+ return { target: child, cycledParent: selectable };
+ }
+ index += indexAddition;
}
- index += indexAddition;
+ }
+
+ if (selectable.neighbors[direction]?.isFocusable()) {
+ return { target: selectable.neighbors[direction] };
+ // return selectable.neighbors[direction];
+ } else if (!selectable.trapFocus) {
+ const parent = selectable.parent;
+ if (parent) return getSelectable(parent);
+ }
+
+ return {};
+ }
+
+ function propagateNavigationEvent(
+ selectable: Selectable,
+ options: NavigateEventOptions,
+ cycledParent?: Selectable
+ ) {
+ const willLeaveContainer = cycledParent ? cycledParent !== selectable.parent : false;
+
+ selectable.onNavigate(selectable, options, willLeaveContainer);
+
+ if (options.propagate && selectable.parent) {
+ propagateNavigationEvent(
+ selectable.parent,
+ options,
+ willLeaveContainer ? cycledParent : undefined
+ );
}
}
- // About to leave this container (=coulnd't cycle siblings)
- navigationEventOptions.willLeaveContainer = true;
- if (!bypassActions) {
- this.onNavigate(this, navigationEventOptions);
- if (navigationEventOptions.preventNavigation) return true;
- }
- if (this.neighbors[direction]?.isFocusable()) {
- this.neighbors[direction]?.focus();
+ const { target, cycledParent } = getSelectable(this);
+ const navigationEventOptions = createNavigateHandlerOptions(target, direction);
+ if (fireActions) propagateNavigationEvent(this, navigationEventOptions, cycledParent);
+
+ if (target && !navigationEventOptions.preventNavigation) {
+ target.focus();
return true;
- } else if (!this.trapFocus) {
- return this.parent?.giveFocus(direction, bypassActions) || false;
}
return false;
+
+ // const focusIndex = get(this.focusIndex);
+ //
+ // const indexAddition = {
+ // up: this.direction === 'vertical' ? -1 : -this.gridColumns,
+ // down: this.direction === 'vertical' ? 1 : this.gridColumns,
+ // left:
+ // this.direction === 'horizontal'
+ // ? (focusIndex % this.gridColumns) - 1 < 0
+ // ? 0
+ // : -1
+ // : -this.gridColumns,
+ // right:
+ // this.direction === 'horizontal'
+ // ? (focusIndex % this.gridColumns) + 1 >= this.gridColumns
+ // ? 0
+ // : 1
+ // : this.gridColumns
+ // }[direction];
+ //
+ // // Cycle siblings
+ // if (indexAddition !== 0) {
+ // let index = focusIndex + indexAddition;
+ // while (index >= 0 && index < this.children.length) {
+ // const children = this.children[index];
+ // if (children && children.isFocusable()) {
+ // propagateNavigationEvent(this, navigationEventOptions);
+ // if (navigationEventOptions.preventNavigation) return true;
+ // children.focus();
+ // return true;
+ // }
+ // index += indexAddition;
+ // }
+ // }
+ //
+ // // About to leave this container (=coulnd't cycle siblings)
+ // navigationEventOptions.willLeaveContainer = true;
+ // if (!bypassActions) {
+ // propagateNavigationEvent(this, navigationEventOptions);
+ // if (navigationEventOptions.preventNavigation) return true;
+ // }
+ // if (this.neighbors[direction]?.isFocusable()) {
+ // this.neighbors[direction]?.focus();
+ // return true;
+ // } else if (!this.trapFocus) {
+ // return this.parent?.giveFocus(direction, bypassActions) || false;
+ // }
+ //
+ // return false;
}
- static giveFocus(direction: Direction, bypassActions: boolean = false) {
+ static giveFocus(direction: Direction, fireActions?: boolean) {
const currentlyFocusedObject = get(Selectable.focusedObject);
- return currentlyFocusedObject?.giveFocus(direction, bypassActions);
+ return currentlyFocusedObject?.giveFocus(direction, fireActions);
}
private static initializeTreeStructure() {