diff --git a/front/public/translations/en.json b/front/public/translations/en.json
index e7992e29..ce6de080 100644
--- a/front/public/translations/en.json
+++ b/front/public/translations/en.json
@@ -255,7 +255,8 @@
"notInPristine": "Unavailable in pristine",
"unsupportedError": "Video codec not supported, transcoding in progress...",
"not-available": "{{entry}} is not available on kyoo yet, ask your server admins about it",
- "fatal": "Fatal playback error"
+ "fatal": "Fatal playback error",
+ "entry-list": "Entry list"
},
"search": {
"empty": "No result found. Try a different query."
diff --git a/front/src/primitives/index.ts b/front/src/primitives/index.ts
index a81c6b9c..cd5d6e0e 100644
--- a/front/src/primitives/index.ts
+++ b/front/src/primitives/index.ts
@@ -17,6 +17,7 @@ export * from "./modal";
export * from "./popup";
export * from "./progress";
export * from "./select";
+export * from "./side-menu";
export * from "./skeleton";
export * from "./slider";
export * from "./tabs";
diff --git a/front/src/primitives/side-menu.tsx b/front/src/primitives/side-menu.tsx
new file mode 100644
index 00000000..ad6afc85
--- /dev/null
+++ b/front/src/primitives/side-menu.tsx
@@ -0,0 +1,50 @@
+import { Portal } from "@gorhom/portal";
+import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
+import type { ReactNode } from "react";
+import { Pressable, View } from "react-native";
+import { cn } from "~/utils";
+import { IconButton } from "./icons";
+import { Heading } from "./text";
+
+export const SideMenu = ({
+ isOpen,
+ title,
+ onClose,
+ children,
+ className,
+ containerClassName,
+}: {
+ isOpen: boolean;
+ title?: string;
+ onClose: () => void;
+ children: ReactNode;
+ className?: string;
+ containerClassName?: string;
+}) => {
+ if (!isOpen) return null;
+
+ return (
+
+
+
+ {title && (
+
+ {title}
+
+
+ )}
+ {children}
+
+
+ );
+};
diff --git a/front/src/ui/details/season.tsx b/front/src/ui/details/season.tsx
index d18726ce..c3d043ba 100644
--- a/front/src/ui/details/season.tsx
+++ b/front/src/ui/details/season.tsx
@@ -156,33 +156,40 @@ SeasonHeader.query = (slug: string): QueryIdentifier => ({
export const EntryList = ({
slug,
season,
+ currentEntrySlug,
onSelectVideos,
search,
+ withContainer,
+ stickyHeaderConfig,
...props
}: {
slug: string;
season: string | number;
+ currentEntrySlug?: string;
onSelectVideos?: (entry: {
displayNumber: string;
name: string | null;
videos: Entry["videos"];
}) => void;
search?: string;
+ withContainer?: boolean;
} & Partial>>) => {
const { t } = useTranslation();
const { items: seasons, error } = useInfiniteFetch(SeasonHeader.query(slug));
if (error) console.error("Could not fetch seasons", error);
+ const C = withContainer ? Container : View;
+
return (
}
Divider={() => (
-
+
-
+
)}
getItemType={(item, idx) =>
item ? item.kind : idx === 0 ? "season" : "episode"
@@ -194,7 +201,7 @@ export const EntryList = ({
}
placeholderCount={5}
Render={({ item }) => (
-
+
{item.kind === "season" ? (
)}
-
+
)}
Loader={({ index }) => (
-
- {index === 0 ? : }
-
+ {index === 0 ? : }
)}
+ stickyHeaderConfig={{
+ ...stickyHeaderConfig,
+ backdropComponent: () => (
+ // hr bottom margin is m-4 and layout gap is 2 but it's only applied on the web and idk why
+
+ ),
+ }}
{...props}
/>
);
diff --git a/front/src/ui/details/serie.tsx b/front/src/ui/details/serie.tsx
index ff36ea06..b2a97162 100644
--- a/front/src/ui/details/serie.tsx
+++ b/front/src/ui/details/serie.tsx
@@ -165,15 +165,10 @@ export const SerieDetails = () => {
/>
)}
contentContainerStyle={{ paddingBottom: insets.bottom }}
+ withContainer
onScroll={scrollHandler}
scrollEventThrottle={16}
- stickyHeaderConfig={{
- offset: headerHeight,
- backdropComponent: () => (
- // hr bottom margin is m-4 and layout gap is 2 but it's only applied on the web and idk why
-
- ),
- }}
+ stickyHeaderConfig={{ offset: headerHeight }}
/>
);
diff --git a/front/src/ui/player/controls/bottom-controls.tsx b/front/src/ui/player/controls/bottom-controls.tsx
index 500cd78e..593824cb 100644
--- a/front/src/ui/player/controls/bottom-controls.tsx
+++ b/front/src/ui/player/controls/bottom-controls.tsx
@@ -1,3 +1,4 @@
+import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg";
import SkipNext from "@material-symbols/svg-400/rounded/skip_next-fill.svg";
import SkipPrevious from "@material-symbols/svg-400/rounded/skip_previous-fill.svg";
import { type ComponentProps, useState } from "react";
@@ -33,6 +34,7 @@ export const BottomControls = ({
playPrev,
playNext,
setMenu,
+ onOpenEntriesMenu,
className,
...props
}: {
@@ -43,6 +45,7 @@ export const BottomControls = ({
playPrev: (() => boolean) | null;
playNext: (() => boolean) | null;
setMenu: (isOpen: boolean) => void;
+ onOpenEntriesMenu?: () => void;
} & ViewProps) => {
const [seek, setSeek] = useState(null);
const bottomSeek = Platform.OS !== "web" && seek !== null;
@@ -90,6 +93,7 @@ export const BottomControls = ({
playPrev={playPrev}
playNext={playNext}
setMenu={setMenu}
+ onOpenEntriesMenu={onOpenEntriesMenu}
/>
)}
@@ -102,6 +106,7 @@ const ControlButtons = ({
playPrev,
playNext,
setMenu,
+ onOpenEntriesMenu,
className,
...props
}: {
@@ -109,6 +114,7 @@ const ControlButtons = ({
playPrev: (() => boolean) | null;
playNext: (() => boolean) | null;
setMenu: (isOpen: boolean) => void;
+ onOpenEntriesMenu?: () => void;
className?: string;
}) => {
const { t } = useTranslation();
@@ -170,6 +176,15 @@ const ControlButtons = ({
/>
+ {onOpenEntriesMenu && (
+
+ )}
diff --git a/front/src/ui/player/controls/index.tsx b/front/src/ui/player/controls/index.tsx
index cc13e608..e9cda2b7 100644
--- a/front/src/ui/player/controls/index.tsx
+++ b/front/src/ui/player/controls/index.tsx
@@ -20,6 +20,7 @@ export const Controls = ({
chapters,
playPrev,
playNext,
+ onOpenEntriesMenu,
forceShow,
}: {
player: VideoPlayer;
@@ -32,6 +33,7 @@ export const Controls = ({
chapters: Chapter[];
playPrev: (() => boolean) | null;
playNext: (() => boolean) | null;
+ onOpenEntriesMenu?: () => void;
forceShow?: boolean;
}) => {
const isTouch = useIsTouch();
@@ -83,6 +85,7 @@ export const Controls = ({
chapters={chapters}
playPrev={playPrev}
playNext={playNext}
+ onOpenEntriesMenu={onOpenEntriesMenu}
setMenu={setMenu}
className="absolute bottom-0 w-full bg-slate-900/50 px-safe pt-safe"
{...hoverControls}
diff --git a/front/src/ui/player/entries-menu.tsx b/front/src/ui/player/entries-menu.tsx
new file mode 100644
index 00000000..c68feabd
--- /dev/null
+++ b/front/src/ui/player/entries-menu.tsx
@@ -0,0 +1,27 @@
+import { useTranslation } from "react-i18next";
+import { SideMenu } from "~/primitives";
+import { EntryList } from "~/ui/details/season";
+
+export const EntriesMenu = ({
+ isOpen,
+ onClose,
+ showSlug,
+ season,
+ currentEntrySlug,
+}: {
+ isOpen: boolean;
+ onClose: () => void;
+ showSlug: string;
+ season: string | number;
+ currentEntrySlug?: string;
+}) => {
+ return (
+
+
+
+ );
+};
diff --git a/front/src/ui/player/index.tsx b/front/src/ui/player/index.tsx
index e2c8f0f1..ba72f2ae 100644
--- a/front/src/ui/player/index.tsx
+++ b/front/src/ui/player/index.tsx
@@ -18,6 +18,7 @@ import { Controls, LoadingIndicator } from "./controls";
import { ErrorPopup } from "./controls/error-popup";
import { toggleFullscreen } from "./controls/misc";
import { PlayModeContext } from "./controls/tracks-menu";
+import { EntriesMenu } from "./entries-menu";
import { useKeyboard } from "./keyboard";
import { useLanguagePreference } from "./language-preference";
import { useProgressObserver } from "./progress-observer";
@@ -48,6 +49,7 @@ export const Player = () => {
const playModeState = useState(defaultPlayMode);
const [playMode, setPlayMode] = playModeState;
const [playbackError, setPlaybackError] = useState();
+ const [entriesMenuOpen, setEntriesMenuOpen] = useState(false);
const player = useVideoPlayer(
{
uri: `${apiUrl}/api/videos/${slug}/${playMode === "direct" ? "direct" : "master.m3u8"}?clientId=${clientId}`,
@@ -223,9 +225,23 @@ export const Player = () => {
chapters={info?.chapters ?? []}
playPrev={data?.previous ? playPrev : null}
playNext={data?.next ? playNext : null}
+ onOpenEntriesMenu={
+ data?.show?.kind === "serie"
+ ? () => setEntriesMenuOpen(true)
+ : undefined
+ }
forceShow={!!playbackError}
/>
+ {data?.show?.kind === "serie" && (
+ setEntriesMenuOpen(false)}
+ showSlug={data.show.slug}
+ season={entry?.kind === "episode" ? entry.seasonNumber : 0}
+ currentEntrySlug={entry?.slug}
+ />
+ )}
{playbackError && (