diff --git a/src/components/player/atoms/Settings.tsx b/src/components/player/atoms/Settings.tsx index 7c0cbfa3..c2e7a6ee 100644 --- a/src/components/player/atoms/Settings.tsx +++ b/src/components/player/atoms/Settings.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Icons } from "@/components/Icon"; import { OverlayAnchor } from "@/components/overlays/OverlayAnchor"; @@ -6,7 +6,10 @@ import { Overlay } from "@/components/overlays/OverlayDisplay"; import { OverlayPage } from "@/components/overlays/OverlayPage"; import { OverlayRouter } from "@/components/overlays/OverlayRouter"; import { SettingsMenu } from "@/components/player/atoms/settings/SettingsMenu"; -import { SourceSelectionView } from "@/components/player/atoms/settings/SourceSelectingView"; +import { + EmbedSelectionView, + SourceSelectionView, +} from "@/components/player/atoms/settings/SourceSelectingView"; import { VideoPlayerButton } from "@/components/player/internals/Button"; import { Context } from "@/components/player/internals/ContextUtils"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; @@ -17,6 +20,19 @@ import { CaptionsView } from "./settings/CaptionsView"; import { QualityView } from "./settings/QualityView"; function SettingsOverlay({ id }: { id: string }) { + const [chosenSourceId, setChosenSourceId] = useState(null); + const router = useOverlayRouter(id); + + // reset source id when going to home or closing overlay + useEffect(() => { + if (!router.isRouterActive) { + setChosenSourceId(null); + } + if (router.route === "/") { + setChosenSourceId(null); + } + }, [router.isRouterActive, router.route]); + return ( @@ -40,7 +56,12 @@ function SettingsOverlay({ id }: { id: string }) { - + + + + + + diff --git a/src/components/player/atoms/settings/SourceSelectingView.tsx b/src/components/player/atoms/settings/SourceSelectingView.tsx index ba439387..483afa0d 100644 --- a/src/components/player/atoms/settings/SourceSelectingView.tsx +++ b/src/components/player/atoms/settings/SourceSelectingView.tsx @@ -1,12 +1,26 @@ import classNames from "classnames"; -import { useMemo } from "react"; +import { ReactNode, useCallback, useEffect, useMemo, useRef } from "react"; +import { useAsyncFn } from "react-use"; import { Icon, Icons } from "@/components/Icon"; +import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; import { Context } from "@/components/player/internals/ContextUtils"; +import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; +import { metaToScrapeMedia } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; import { providers } from "@/utils/providers"; +export interface SourceSelectionViewProps { + id: string; + onChoose?: (id: string) => void; +} + +export interface EmbedSelectionViewProps { + id: string; + sourceId: string | null; +} + export function SourceOption(props: { children: React.ReactNode; selected?: boolean; @@ -32,7 +46,105 @@ export function SourceOption(props: { ); } -export function SourceSelectionView({ id }: { id: string }) { +export function EmbedOption(props: { + embedId: string; + url: string; + routerId: string; +}) { + const router = useOverlayRouter(props.routerId); + const meta = usePlayerStore((s) => s.meta); + const setSource = usePlayerStore((s) => s.setSource); + const progress = usePlayerStore((s) => s.progress.time); + const embedName = useMemo(() => { + if (!props.embedId) return "..."; + const sourceMeta = providers.getMetadata(props.embedId); + return sourceMeta?.name ?? "..."; + }, [props.embedId]); + const [request, run] = useAsyncFn(async () => { + const result = await providers.runEmbedScraper({ + id: props.embedId, + url: props.url, + }); + setSource(convertRunoutputToSource({ stream: result.stream }), progress); + router.close(); + }, [props.embedId, meta, router]); + + let content: ReactNode = null; + if (request.loading) content = loading...; + else if (request.error) content = Failed to scrape; + + return ( + + + {embedName} + {content} + + + ); +} + +export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) { + const router = useOverlayRouter(id); + const meta = usePlayerStore((s) => s.meta); + const setSource = usePlayerStore((s) => s.setSource); + const progress = usePlayerStore((s) => s.progress.time); + const sourceName = useMemo(() => { + if (!sourceId) return "..."; + const sourceMeta = providers.getMetadata(sourceId); + return sourceMeta?.name ?? "..."; + }, [sourceId]); + const [request, run] = useAsyncFn(async () => { + if (!sourceId || !meta) return null; + const scrapeMedia = metaToScrapeMedia(meta); + const result = await providers.runSourceScraper({ + id: sourceId, + media: scrapeMedia, + }); + if (result.stream) { + setSource(convertRunoutputToSource({ stream: result.stream }), progress); + router.close(); + return null; + } + return result.embeds; + }, [sourceId, meta, router]); + + const lastSourceId = useRef(null); + useEffect(() => { + if (lastSourceId.current === sourceId) return; + lastSourceId.current = sourceId; + if (!sourceId) return; + run(); + }, [run, sourceId]); + + let content: ReactNode = null; + if (request.loading) content =

loading...

; + else if (request.error) content =

Failed to scrape

; + else if (request.value && request.value.length === 0) + content =

No embeds found

; + else if (request.value) + content = request.value.map((v) => ( + + )); + + return ( + <> + router.navigate("/source")}> + {sourceName} + + {content} + + ); +} + +export function SourceSelectionView({ + id, + onChoose, +}: SourceSelectionViewProps) { const router = useOverlayRouter(id); const metaType = usePlayerStore((s) => s.meta?.type); const sources = useMemo(() => { @@ -49,7 +161,15 @@ export function SourceSelectionView({ id }: { id: string }) { {sources.map((v) => ( - {v.name} + { + onChoose?.(v.id); + router.navigate("/source/embeds"); + }} + > + {v.name} + ))} diff --git a/src/components/player/utils/convertRunoutputToSource.ts b/src/components/player/utils/convertRunoutputToSource.ts index 773496a8..efc59f20 100644 --- a/src/components/player/utils/convertRunoutputToSource.ts +++ b/src/components/player/utils/convertRunoutputToSource.ts @@ -20,7 +20,9 @@ function isAllowedQuality(inp: string): inp is SourceQuality { return allowedQualities.includes(inp); } -export function convertRunoutputToSource(out: RunOutput): SourceSliceSource { +export function convertRunoutputToSource(out: { + stream: RunOutput["stream"]; +}): SourceSliceSource { if (out.stream.type === "hls") { return { type: "hls", diff --git a/src/hooks/useOverlayRouter.ts b/src/hooks/useOverlayRouter.ts index e504d084..4ca89fca 100644 --- a/src/hooks/useOverlayRouter.ts +++ b/src/hooks/useOverlayRouter.ts @@ -81,7 +81,12 @@ export function useInternalOverlayRouter(id: string) { [id, setRoute, setTransition, setAnchorPoint] ); + const activeRoute = routerActive + ? joinPath(splitPath(route.slice(`/${id}`.length))) + : "/"; + return { + activeRoute, showBackwardsTransition, isCurrentPage, isOverlayActive, @@ -97,6 +102,7 @@ export function useOverlayRouter(id: string) { const router = useInternalOverlayRouter(id); return { id, + route: router.activeRoute, isRouterActive: router.isOverlayActive(), open: router.open, close: router.close,