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 && (