diff --git a/src/api/context/VideoPlayerContext.tsx b/src/api/context/VideoPlayerContext.tsx index 3b72020..983104e 100644 --- a/src/api/context/VideoPlayerContext.tsx +++ b/src/api/context/VideoPlayerContext.tsx @@ -33,6 +33,7 @@ export interface VideoPlayerAPI { setMuted: (m: boolean) => void; seekTo: (ts: number) => void; + seekToChapterOffset: (offset: number) => void; }; // @ts-ignore diff --git a/src/api/context/VideoPlayerProvider.tsx b/src/api/context/VideoPlayerProvider.tsx index ef3d60e..24c7a77 100644 --- a/src/api/context/VideoPlayerProvider.tsx +++ b/src/api/context/VideoPlayerProvider.tsx @@ -52,6 +52,10 @@ export const VideoPlayerProvider = ({ setAvailableFormats([]); setActiveFormat(null); setVideoInfo(null); + setActiveChapters({ + chapters: [], + type: "video", + }); if(!videoID) return; try { @@ -81,7 +85,11 @@ export const VideoPlayerProvider = ({ if(!videoInfo) return; setAvailableFormats(videoInfo.formats); - setActiveFormat(videoInfo.formats[0]); + setActiveFormat(videoInfo.formats + .filter(f => f.type == "basic") + .filter(f => currentInstance.type != "invidious" || f.isProxied) + .findLast(x=>x) + ); }, [videoInfo]); useEffect(() => { @@ -113,6 +121,27 @@ export const VideoPlayerProvider = ({ setPlayState("playing"); }); + const seekTo = (ts: number) => { + videoElement.currentTime = clamp(0, ts, videoElement.duration); + }; + + const seekToChapterOffset = (offset: number = 0) => { + let currentChapterIndex = activeChapters.chapters.findIndex(c => c.time > videoElement.currentTime) - 1; + let currentChapter = activeChapters.chapters[currentChapterIndex]; + if(!currentChapter) { + if(offset == 1) videoElement.fastSeek(videoElement.duration); + return; + } + let chapterProgress = videoElement.currentTime - currentChapter.time; + let targetIndex = currentChapterIndex + offset; + if(offset == -1) { + if(chapterProgress > 3) targetIndex++; + } + let target = activeChapters.chapters[targetIndex]; + + videoElement.fastSeek(clamp(0, target?.time || 0, videoElement.duration)); + } + return ( { - videoElement.currentTime = clamp(0, ts, videoElement.duration); - }, + seekTo, + seekToChapterOffset, }}> {children} diff --git a/src/components/cards/FormatCard.tsx b/src/components/cards/FormatCard.tsx new file mode 100644 index 0000000..3aac717 --- /dev/null +++ b/src/components/cards/FormatCard.tsx @@ -0,0 +1,45 @@ +import { Code, Grid, Group, Paper, Stack, Text } from "@mantine/core"; +import { VideoFormat } from "../../api/types/format"; +import { IconCheck } from "@tabler/icons-react"; + +export const FormatCard = ({ + format, + isSelected, + onClick, +}: { + format: VideoFormat, + isSelected?: boolean, + onClick?: () => void, +}) => { + return ( + + + + + {isSelected && ( + + )} + + + + + + + {format.id} + + + {format.itag} + + + + {format.mimeType} + + + + + + ); +}; + diff --git a/src/components/options/OptionsContext.tsx b/src/components/options/OptionsContext.tsx index bd3e64b..6d9a200 100644 --- a/src/components/options/OptionsContext.tsx +++ b/src/components/options/OptionsContext.tsx @@ -1,8 +1,9 @@ import { useDisclosure, useHotkeys } from "@mantine/hooks"; -import { createContext, useState } from "react"; +import { createContext, useCallback, useState } from "react"; import { useSoundEffect } from "../../hooks/useSoundEffect"; -import { Drawer, ScrollArea } from "@mantine/core"; +import { Button, Drawer, Group, ScrollArea, Text } from "@mantine/core"; import { OptionsRouter } from "./OptionsRouter"; +import { IconArrowLeft } from "@tabler/icons-react"; export type OptionsView = "main" | "instanceSelect" | "openWith" | "formatSelect"; @@ -17,11 +18,11 @@ export interface OptionsContextAPI { export const OptionsContext = createContext({ opened: false, - open: () => {}, - close: () => {}, - toggle: () => {}, + open: () => { }, + close: () => { }, + toggle: () => { }, view: "main", - setView: () => {}, + setView: () => { }, }); export const OptionsProvider = ({ children }: React.PropsWithChildren) => { @@ -30,24 +31,24 @@ export const OptionsProvider = ({ children }: React.PropsWithChildren) => { const openOptionsSfx = useSoundEffect(["openSettings"]); const closeOptionsSfx = useSoundEffect(["closeSettings"]); - const open = () => { - if(!opened) openOptionsSfx(); + const open = useCallback(() => { + if (!opened) openOptionsSfx(); handlers.open(); - }; + }, [opened]); - const close = () => { - if(opened) closeOptionsSfx(); + const close = useCallback(() => { + if (opened) closeOptionsSfx(); handlers.close(); setView("main"); - }; + }, [opened]); - const toggle = () => { - if(opened) { + const toggle = useCallback(() => { + if (opened) { close(); } else { open(); } - }; + }, [opened]); useHotkeys([ ["o", () => toggle()], @@ -56,7 +57,7 @@ export const OptionsProvider = ({ children }: React.PropsWithChildren) => { useHotkeys([ ["Ctrl + o", () => toggle()], ], [], true); - + return ( { > close()} position="right" - title="Options" scrollAreaComponent={ScrollArea.Autosize} + styles={{ + title: { + width: "100%", + } + }} + title={( + + {view !== "main" && ( + + )} + + {({ + main: "Options", + formatSelect: "Select Format", + instanceSelect: "Select Instance", + openWith: "Open with...", + } as Record)[view]} + + + )} > diff --git a/src/components/options/OptionsRouter.tsx b/src/components/options/OptionsRouter.tsx index 0e9e02d..cf2a261 100644 --- a/src/components/options/OptionsRouter.tsx +++ b/src/components/options/OptionsRouter.tsx @@ -4,6 +4,7 @@ import { OptionsContext } from "./OptionsContext"; import { OptionsMainView } from "./views/MainView"; import { OptionsInstanceView } from "./views/InstanceView"; import { OptionsOpenWithView } from "./views/OpenWith"; +import { OptionsFormatView } from "./views/FormatView"; export const OptionsRouter = () => { const { view } = useContext(OptionsContext); @@ -13,6 +14,7 @@ export const OptionsRouter = () => { {view == "main" && } {view == "instanceSelect" && } {view == "openWith" && } + {view == "formatSelect" && } ); }; diff --git a/src/components/options/comps/FormatSelect.tsx b/src/components/options/comps/FormatSelect.tsx index 506f7bd..8a772c5 100644 --- a/src/components/options/comps/FormatSelect.tsx +++ b/src/components/options/comps/FormatSelect.tsx @@ -1,37 +1,32 @@ -import { Group, Select, Stack } from "@mantine/core"; +import { Grid, Select, Stack, Text, UnstyledButton } from "@mantine/core"; import { useContext } from "react"; import { VideoPlayerContext } from "../../../api/context/VideoPlayerContext"; -import { IconCheck } from "@tabler/icons-react"; +import { FormatCard } from "../../cards/FormatCard"; +import { OptionsContext } from "../OptionsContext"; +import { IconArrowRight } from "@tabler/icons-react"; export const FormatSelect = () => { - const { - availableFormats, - activeFormat, - setFormat, - } = useContext(VideoPlayerContext); + const { setView } = useContext(OptionsContext); + const { activeFormat } = useContext(VideoPlayerContext); return ( -