diff --git a/package.json b/package.json index fe430fa3..0e9b69df 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,12 @@ { "name": "movie-web", - "version": "3.0.5", + "version": "3.0.6", "private": true, "homepage": "https://movie.squeezebox.dev", "dependencies": { "@formkit/auto-animate": "^1.0.0-beta.5", "@headlessui/react": "^1.5.0", "@react-spring/web": "^9.7.1", - "@types/react-helmet": "^6.1.6", "@use-gesture/react": "^10.2.24", "crypto-js": "^4.1.1", "fscreen": "^1.2.0", @@ -92,6 +91,7 @@ "vite-plugin-package-version": "^1.0.2", "vite-plugin-pwa": "^0.14.4", "vitest": "^0.28.5", - "workbox-window": "^6.5.4" + "workbox-window": "^6.5.4", + "@types/react-helmet": "^6.1.6" } } diff --git a/src/backend/embeds/playm4u.ts b/src/backend/embeds/playm4u.ts index 8328d337..1e5c3ca4 100644 --- a/src/backend/embeds/playm4u.ts +++ b/src/backend/embeds/playm4u.ts @@ -10,6 +10,7 @@ registerEmbedScraper({ async getStream() { // throw new Error("Oh well 2") return { + embedId: "", streamUrl: "", quality: MWStreamQuality.Q1080P, captions: [], diff --git a/src/backend/embeds/streamm4u.ts b/src/backend/embeds/streamm4u.ts index d0eba66a..b28b7ab8 100644 --- a/src/backend/embeds/streamm4u.ts +++ b/src/backend/embeds/streamm4u.ts @@ -3,7 +3,7 @@ import { registerEmbedScraper } from "@/backend/helpers/register"; import { MWStreamQuality, MWStreamType, - MWStream, + MWEmbedStream, } from "@/backend/helpers/streams"; import { proxiedFetch } from "@/backend/helpers/fetch"; @@ -13,7 +13,7 @@ const URL_API = `${URL_BASE}/api`; const URL_API_SOURCE = `${URL_API}/source`; async function scrape(embed: string) { - const sources: MWStream[] = []; + const sources: MWEmbedStream[] = []; const embedID = embed.split("/").pop(); @@ -28,6 +28,7 @@ async function scrape(embed: string) { for (const stream of streams) { sources.push({ + embedId: "", streamUrl: stream.file as string, quality: stream.label as MWStreamQuality, type: stream.type as MWStreamType, diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 64d039b7..0dec6422 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -1,4 +1,4 @@ -import { MWStream } from "./streams"; +import { MWEmbedStream } from "./streams"; export enum MWEmbedType { M4UFREE = "m4ufree", @@ -23,5 +23,5 @@ export type MWEmbedScraper = { rank: number; disabled?: boolean; - getStream(ctx: MWEmbedContext): Promise; + getStream(ctx: MWEmbedContext): Promise; }; diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index cb160305..70e20348 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -43,7 +43,13 @@ async function findBestEmbedStream( providerId: string, ctx: MWProviderRunContext ): Promise { - if (result.stream) return result.stream; + if (result.stream) { + return { + ...result.stream, + providerId, + embedId: providerId, + }; + } let embedNum = 0; for (const embed of result.embeds) { @@ -89,6 +95,7 @@ async function findBestEmbedStream( type: "embed", }); + stream.providerId = providerId; return stream; } diff --git a/src/backend/helpers/streams.ts b/src/backend/helpers/streams.ts index d5985cfa..27e9fddb 100644 --- a/src/backend/helpers/streams.ts +++ b/src/backend/helpers/streams.ts @@ -28,5 +28,11 @@ export type MWStream = { streamUrl: string; type: MWStreamType; quality: MWStreamQuality; + providerId?: string; + embedId?: string; captions: MWCaption[]; }; + +export type MWEmbedStream = MWStream & { + embedId: string; +}; diff --git a/src/components/popout/FloatingContainer.tsx b/src/components/popout/FloatingContainer.tsx index 48e4f5cc..0735d912 100644 --- a/src/components/popout/FloatingContainer.tsx +++ b/src/components/popout/FloatingContainer.tsx @@ -1,5 +1,11 @@ import { Transition } from "@/components/Transition"; -import React, { ReactNode, useCallback, useEffect, useRef } from "react"; +import React, { + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { createPortal } from "react-dom"; interface Props { @@ -10,6 +16,8 @@ interface Props { } export function FloatingContainer(props: Props) { + const [portalElement, setPortalElement] = useState(null); + const ref = useRef(null); const target = useRef(null); useEffect(() => { @@ -34,23 +42,34 @@ export function FloatingContainer(props: Props) { [props] ); - return createPortal( - -
- -
- - - {props.children} - -
-
, - document.body + useEffect(() => { + const element = ref.current?.closest(".popout-location"); + setPortalElement(element ?? document.body); + }, []); + + return ( +
+ {portalElement + ? createPortal( + +
+ +
+ + + {props.children} + +
+
, + portalElement + ) + : null} +
); } diff --git a/src/setup/constants.ts b/src/setup/constants.ts index 8350efdd..aa10ca29 100644 --- a/src/setup/constants.ts +++ b/src/setup/constants.ts @@ -1,4 +1,4 @@ export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb"; export const GITHUB_LINK = "https://github.com/movie-web/movie-web"; -export const APP_VERSION = "3.0.5"; +export const APP_VERSION = "3.0.6"; export const GA_ID = "G-44YVXRL61C"; diff --git a/src/video/components/VideoPlayerBase.tsx b/src/video/components/VideoPlayerBase.tsx index 57549729..18f34ab0 100644 --- a/src/video/components/VideoPlayerBase.tsx +++ b/src/video/components/VideoPlayerBase.tsx @@ -8,6 +8,7 @@ import { useVideoPlayerDescriptor, VideoPlayerContextProvider, } from "../state/hooks"; +import { MetaAction } from "./actions/MetaAction"; import { VideoElementInternal } from "./internal/VideoElementInternal"; export interface VideoPlayerBaseProps { @@ -38,12 +39,13 @@ function VideoPlayerBaseWithState(props: VideoPlayerBaseProps) {
+ diff --git a/src/video/components/actions/MetaAction.tsx b/src/video/components/actions/MetaAction.tsx new file mode 100644 index 00000000..35b2543d --- /dev/null +++ b/src/video/components/actions/MetaAction.tsx @@ -0,0 +1,59 @@ +import { MWCaption } from "@/backend/helpers/streams"; +import { DetailedMeta } from "@/backend/metadata/getmeta"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useMeta } from "@/video/state/logic/meta"; +import { useProgress } from "@/video/state/logic/progress"; +import { useEffect } from "react"; + +export type WindowMeta = { + meta: DetailedMeta; + captions: MWCaption[]; + episode?: { + episodeId: string; + seasonId: string; + }; + seasons?: { + id: string; + number: number; + title: string; + episodes?: { id: string; number: number; title: string }[]; + }[]; + progress: { + time: number; + duration: number; + }; +} | null; + +declare global { + interface Window { + meta?: Record; + } +} + +export function MetaAction() { + const descriptor = useVideoPlayerDescriptor(); + const meta = useMeta(descriptor); + const progress = useProgress(descriptor); + + useEffect(() => { + if (!window.meta) window.meta = {}; + if (meta) { + window.meta[descriptor] = { + meta: meta.meta, + captions: meta.captions, + seasons: meta.seasons, + episode: meta.episode, + progress: { + time: progress.time, + duration: progress.duration, + }, + }; + } + + return () => { + if (window.meta) delete window.meta[descriptor]; + }; + }, [meta, descriptor, progress]); + + return null; +} diff --git a/src/video/components/controllers/SourceController.tsx b/src/video/components/controllers/SourceController.tsx index 64c6403c..14d5ad93 100644 --- a/src/video/components/controllers/SourceController.tsx +++ b/src/video/components/controllers/SourceController.tsx @@ -8,6 +8,8 @@ interface SourceControllerProps { source: string; type: MWStreamType; quality: MWStreamQuality; + providerId?: string; + embedId?: string; } export function SourceController(props: SourceControllerProps) { diff --git a/src/video/components/popouts/SourceSelectionPopout.tsx b/src/video/components/popouts/SourceSelectionPopout.tsx index 9651b306..62bec772 100644 --- a/src/video/components/popouts/SourceSelectionPopout.tsx +++ b/src/video/components/popouts/SourceSelectionPopout.tsx @@ -18,12 +18,14 @@ import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; import { FloatingCardView } from "@/components/popout/FloatingCard"; import { FloatingView } from "@/components/popout/FloatingView"; import { useFloatingRouter } from "@/hooks/useFloatingRouter"; +import { useSource } from "@/video/state/logic/source"; import { PopoutListEntry } from "./PopoutUtils"; interface EmbedEntryProps { name: string; type: MWEmbedType; url: string; + active: boolean; onSelect: (stream: MWStream) => void; } @@ -43,6 +45,7 @@ export function EmbedEntry(props: EmbedEntryProps) { isOnDarkBackground loading={loading} errored={!!error} + active={props.active} onClick={() => { scrapeEmbed(); }} @@ -61,6 +64,9 @@ export function SourceSelectionPopout(props: { const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); const meta = useMeta(descriptor); + const { source } = useSource(descriptor); + const providerRef = useRef(null); + const providers = useMemo( () => meta @@ -96,6 +102,8 @@ export function SourceSelectionPopout(props: { quality: stream.quality, source: stream.streamUrl, type: stream.type, + embedId: stream.embedId, + providerId: providerRef.current ?? undefined, }); if (meta) { controls.setMeta({ @@ -106,7 +114,6 @@ export function SourceSelectionPopout(props: { controls.closePopout(); } - const providerRef = useRef(null); const selectProvider = (providerId?: string) => { if (!providerId) { providerRef.current = null; @@ -188,6 +195,7 @@ export function SourceSelectionPopout(props: { {providers.map((v) => ( { selectProvider(v.id); }} @@ -234,6 +242,10 @@ export function SourceSelectionPopout(props: { onClick={() => { if (scrapeResult.stream) selectSource(scrapeResult.stream); }} + active={ + selectedProviderPopulated?.id === source?.providerId && + selectedProviderPopulated?.id === source?.embedId + } > Native source @@ -245,6 +257,7 @@ export function SourceSelectionPopout(props: { name={v.displayName ?? ""} key={v.url} url={v.url} + active={false} // TODO add embed id extractor onSelect={(stream) => { selectSource(stream); }} diff --git a/src/video/state/logic/source.ts b/src/video/state/logic/source.ts index a940a96e..6f2d0afb 100644 --- a/src/video/state/logic/source.ts +++ b/src/video/state/logic/source.ts @@ -9,6 +9,8 @@ export type VideoSourceEvent = { quality: MWStreamQuality; url: string; type: MWStreamType; + providerId?: string; + embedId?: string; caption: null | { id: string; url: string; diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts index ab111f37..5f9490ce 100644 --- a/src/video/state/providers/castingStateProvider.ts +++ b/src/video/state/providers/castingStateProvider.ts @@ -133,6 +133,8 @@ export function createCastingStateProvider( type: source.type, url: source.source, caption: null, + embedId: source.embedId, + providerId: source.providerId, }; resetStateForSource(descriptor, state); updateSource(descriptor, state); @@ -224,6 +226,8 @@ export function createCastingStateProvider( quality: state.source.quality, source: state.source.url, type: state.source.type, + embedId: state.source.embedId, + providerId: state.source.providerId, }); return { diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts index 1643a980..3a01b145 100644 --- a/src/video/state/providers/providerTypes.ts +++ b/src/video/state/providers/providerTypes.ts @@ -4,6 +4,8 @@ type VideoPlayerSource = { source: string; type: MWStreamType; quality: MWStreamQuality; + providerId?: string; + embedId?: string; } | null; export type VideoPlayerStateController = { diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts index 0f0971b5..4b085133 100644 --- a/src/video/state/providers/videoStateProvider.ts +++ b/src/video/state/providers/videoStateProvider.ts @@ -189,6 +189,8 @@ export function createVideoStateProvider( type: source.type, url: source.source, caption: null, + embedId: source.embedId, + providerId: source.providerId, }; updateSource(descriptor, state); }, @@ -334,6 +336,8 @@ export function createVideoStateProvider( quality: state.source.quality, source: state.source.url, type: state.source.type, + embedId: state.source.embedId, + providerId: state.source.providerId, }); return { diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 6b04a55a..8b27c25e 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -58,6 +58,8 @@ export type VideoPlayerState = { quality: MWStreamQuality; url: string; type: MWStreamType; + providerId?: string; + embedId?: string; caption: null | { url: string; id: string; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index e4a09f7e..b674fb9f 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -146,6 +146,8 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { source={props.stream.streamUrl} type={props.stream.type} quality={props.stream.quality} + embedId={props.stream.embedId} + providerId={props.stream.providerId} /> (null); const lastSearchValue = useRef<(string | undefined)[] | null>(null);