mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 01:05:08 +01:00
thumbnail fixes + next episode fixes + cursor now hides when controls are dismissed + back link can go back to search + hovering over controls no longer dismisses controls + improved colors for context menus + progress ring shown in episode selector + scrape progress ring shows progress again
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
46cb7793c2
commit
068b7071a4
@ -1,3 +1,4 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
import { ReactNode, useCallback, useEffect, useState } from "react";
|
import { ReactNode, useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
@ -5,6 +6,7 @@ import { useAsync } from "react-use";
|
|||||||
import { getMetaFromId } from "@/backend/metadata/getmeta";
|
import { getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
import { MWMediaType, MWSeasonMeta } from "@/backend/metadata/types/mw";
|
import { MWMediaType, MWSeasonMeta } from "@/backend/metadata/types/mw";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { ProgressRing } from "@/components/layout/ProgressRing";
|
||||||
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
|
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
|
||||||
import { Overlay } from "@/components/overlays/OverlayDisplay";
|
import { Overlay } from "@/components/overlays/OverlayDisplay";
|
||||||
import { OverlayPage } from "@/components/overlays/OverlayPage";
|
import { OverlayPage } from "@/components/overlays/OverlayPage";
|
||||||
@ -15,6 +17,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
|
|||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
import { useProgressStore } from "@/stores/progress";
|
||||||
|
|
||||||
function CenteredText(props: { children: React.ReactNode }) {
|
function CenteredText(props: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@ -98,6 +101,7 @@ function EpisodesView({
|
|||||||
const { setPlayerMeta } = usePlayerMeta();
|
const { setPlayerMeta } = usePlayerMeta();
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
const [loadingState] = useSeasonData(meta?.tmdbId ?? "", selectedSeason);
|
const [loadingState] = useSeasonData(meta?.tmdbId ?? "", selectedSeason);
|
||||||
|
const progress = useProgressStore();
|
||||||
|
|
||||||
const playEpisode = useCallback(
|
const playEpisode = useCallback(
|
||||||
(episodeId: string) => {
|
(episodeId: string) => {
|
||||||
@ -110,6 +114,8 @@ function EpisodesView({
|
|||||||
[setPlayerMeta, loadingState, router, onChange]
|
[setPlayerMeta, loadingState, router, onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!meta?.tmdbId) return null;
|
||||||
|
|
||||||
let content: ReactNode = null;
|
let content: ReactNode = null;
|
||||||
if (loadingState.error)
|
if (loadingState.error)
|
||||||
content = <CenteredText>Error loading season</CenteredText>;
|
content = <CenteredText>Error loading season</CenteredText>;
|
||||||
@ -124,21 +130,47 @@ function EpisodesView({
|
|||||||
</Menu.TextDisplay>
|
</Menu.TextDisplay>
|
||||||
) : null}
|
) : null}
|
||||||
{loadingState.value.season.episodes.map((ep) => {
|
{loadingState.value.season.episodes.map((ep) => {
|
||||||
|
const episodeProgress =
|
||||||
|
progress.items[meta?.tmdbId]?.episodes?.[ep.id];
|
||||||
|
|
||||||
|
let rightSide;
|
||||||
|
if (episodeProgress) {
|
||||||
|
const percentage =
|
||||||
|
(episodeProgress.progress.watched /
|
||||||
|
episodeProgress.progress.duration) *
|
||||||
|
100;
|
||||||
|
rightSide = (
|
||||||
|
<ProgressRing
|
||||||
|
className="h-[18px] w-[18px] text-white"
|
||||||
|
percentage={percentage > 90 ? 100 : percentage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.ChevronLink
|
<Menu.Link
|
||||||
key={ep.id}
|
key={ep.id}
|
||||||
onClick={() => playEpisode(ep.id)}
|
onClick={() => playEpisode(ep.id)}
|
||||||
active={ep.id === meta?.episode?.tmdbId}
|
active={ep.id === meta?.episode?.tmdbId}
|
||||||
|
clickable
|
||||||
|
rightSide={rightSide}
|
||||||
>
|
>
|
||||||
<Menu.LinkTitle>
|
<Menu.LinkTitle>
|
||||||
<div className="text-left flex items-center space-x-3">
|
<div className="text-left flex items-center space-x-3">
|
||||||
<span className="p-0.5 px-2 rounded inline bg-video-context-border">
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"p-0.5 px-2 rounded inline bg-video-context-hoverColor",
|
||||||
|
ep.id === meta?.episode?.tmdbId
|
||||||
|
? "text-white bg-opacity-100"
|
||||||
|
: "bg-opacity-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
E{ep.number}
|
E{ep.number}
|
||||||
</span>
|
</span>
|
||||||
<span className="line-clamp-1 break-all">{ep.title}</span>
|
<span className="line-clamp-1 break-all">{ep.title}</span>
|
||||||
</div>
|
</div>
|
||||||
</Menu.LinkTitle>
|
</Menu.LinkTitle>
|
||||||
</Menu.ChevronLink>
|
</Menu.Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Menu.Section>
|
</Menu.Section>
|
||||||
|
@ -4,7 +4,7 @@ import { useCallback } from "react";
|
|||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { PlayerMetaEpisode } from "@/stores/player/slices/source";
|
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
function shouldShowNextEpisodeButton(
|
function shouldShowNextEpisodeButton(
|
||||||
@ -37,7 +37,10 @@ function Button(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NextEpisodeButton(props: { controlsShowing: boolean }) {
|
export function NextEpisodeButton(props: {
|
||||||
|
controlsShowing: boolean;
|
||||||
|
onChange?: (meta: PlayerMeta) => void;
|
||||||
|
}) {
|
||||||
const duration = usePlayerStore((s) => s.progress.duration);
|
const duration = usePlayerStore((s) => s.progress.duration);
|
||||||
const isHidden = usePlayerStore((s) => s.interface.hideNextEpisodeBtn);
|
const isHidden = usePlayerStore((s) => s.interface.hideNextEpisodeBtn);
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
@ -67,7 +70,8 @@ export function NextEpisodeButton(props: { controlsShowing: boolean }) {
|
|||||||
const metaCopy = { ...meta };
|
const metaCopy = { ...meta };
|
||||||
metaCopy.episode = nextEp;
|
metaCopy.episode = nextEp;
|
||||||
setDirectMeta(metaCopy);
|
setDirectMeta(metaCopy);
|
||||||
}, [setDirectMeta, nextEp, meta]);
|
props.onChange?.(metaCopy);
|
||||||
|
}, [setDirectMeta, nextEp, meta, props]);
|
||||||
|
|
||||||
if (!meta?.episode || !nextEp) return null;
|
if (!meta?.episode || !nextEp) return null;
|
||||||
if (metaType !== "show") return null;
|
if (metaType !== "show") return null;
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
export function BottomControls(props: {
|
export function BottomControls(props: {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const setHoveringAnyControls = usePlayerStore(
|
||||||
|
(s) => s.setHoveringAnyControls
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setHoveringAnyControls(false);
|
||||||
|
};
|
||||||
|
}, [setHoveringAnyControls]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-white">
|
<div className="w-full text-white">
|
||||||
<Transition
|
<Transition
|
||||||
@ -11,13 +24,15 @@ export function BottomControls(props: {
|
|||||||
show={props.show}
|
show={props.show}
|
||||||
className="pointer-events-none flex justify-end pt-32 bg-gradient-to-t from-black to-transparent transition-opacity duration-200 absolute bottom-0 w-full"
|
className="pointer-events-none flex justify-end pt-32 bg-gradient-to-t from-black to-transparent transition-opacity duration-200 absolute bottom-0 w-full"
|
||||||
/>
|
/>
|
||||||
<Transition
|
<div
|
||||||
animation="slide-up"
|
onMouseOver={() => setHoveringAnyControls(true)}
|
||||||
show={props.show}
|
onMouseOut={() => setHoveringAnyControls(false)}
|
||||||
className="pointer-events-auto pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pb-3 mb-[env(safe-area-inset-bottom)] absolute bottom-0 w-full"
|
className="pointer-events-auto pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pb-3 mb-[env(safe-area-inset-bottom)] absolute bottom-0 w-full"
|
||||||
>
|
>
|
||||||
{props.children}
|
<Transition animation="slide-up" show={props.show}>
|
||||||
</Transition>
|
{props.children}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { usePlayerStore } from "@/stores/player/store";
|
|||||||
|
|
||||||
export interface PlayerProps {
|
export interface PlayerProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
showingControls: boolean;
|
||||||
onLoad?: () => void;
|
onLoad?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ export function Container(props: PlayerProps) {
|
|||||||
<ProgressSaver />
|
<ProgressSaver />
|
||||||
<KeyboardEvents />
|
<KeyboardEvents />
|
||||||
<div className="relative h-screen overflow-hidden">
|
<div className="relative h-screen overflow-hidden">
|
||||||
<VideoClickTarget />
|
<VideoClickTarget showingControls={props.showingControls} />
|
||||||
<HeadUpdater />
|
<HeadUpdater />
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
export function TopControls(props: {
|
export function TopControls(props: {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const setHoveringAnyControls = usePlayerStore(
|
||||||
|
(s) => s.setHoveringAnyControls
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setHoveringAnyControls(false);
|
||||||
|
};
|
||||||
|
}, [setHoveringAnyControls]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-white">
|
<div className="w-full text-white">
|
||||||
<Transition
|
<Transition
|
||||||
@ -11,13 +24,19 @@ export function TopControls(props: {
|
|||||||
show={props.show}
|
show={props.show}
|
||||||
className="pointer-events-none flex justify-end pb-32 bg-gradient-to-b from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)] transition-opacity duration-200 absolute top-0 w-full"
|
className="pointer-events-none flex justify-end pb-32 bg-gradient-to-b from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)] transition-opacity duration-200 absolute top-0 w-full"
|
||||||
/>
|
/>
|
||||||
<Transition
|
<div
|
||||||
animation="slide-down"
|
onMouseOver={() => setHoveringAnyControls(true)}
|
||||||
show={props.show}
|
onMouseOut={() => setHoveringAnyControls(false)}
|
||||||
className="pointer-events-auto pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pt-6 absolute top-0 w-full text-white"
|
className="pointer-events-auto pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pt-6 absolute top-0 w-full"
|
||||||
>
|
>
|
||||||
{props.children}
|
<Transition
|
||||||
</Transition>
|
animation="slide-down"
|
||||||
|
show={props.show}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
let startAt = 0;
|
let startAt = 0;
|
||||||
let automaticQuality = false;
|
let automaticQuality = false;
|
||||||
let preferenceQuality: SourceQuality | null = null;
|
let preferenceQuality: SourceQuality | null = null;
|
||||||
|
let lastVolume = 1;
|
||||||
|
|
||||||
function reportLevels() {
|
function reportLevels() {
|
||||||
if (!hls) return;
|
if (!hls) return;
|
||||||
@ -226,6 +227,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
destroyVideoElement();
|
destroyVideoElement();
|
||||||
videoElement = video;
|
videoElement = video;
|
||||||
setSource();
|
setSource();
|
||||||
|
this.setVolume(lastVolume);
|
||||||
},
|
},
|
||||||
processContainerElement(container) {
|
processContainerElement(container) {
|
||||||
containerElement = container;
|
containerElement = container;
|
||||||
@ -261,11 +263,13 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
videoElement.currentTime = time;
|
videoElement.currentTime = time;
|
||||||
},
|
},
|
||||||
async setVolume(v) {
|
async setVolume(v) {
|
||||||
if (!videoElement) return;
|
|
||||||
|
|
||||||
// clamp time between 0 and 1
|
// clamp time between 0 and 1
|
||||||
let volume = Math.min(v, 1);
|
let volume = Math.min(v, 1);
|
||||||
volume = Math.max(0, volume);
|
volume = Math.max(0, volume);
|
||||||
|
|
||||||
|
// actually set
|
||||||
|
lastVolume = v;
|
||||||
|
if (!videoElement) return;
|
||||||
videoElement.muted = volume === 0; // Muted attribute is always supported
|
videoElement.muted = volume === 0; // Muted attribute is always supported
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
|
@ -16,8 +16,8 @@ export function usePlayerMeta() {
|
|||||||
const setDirectMeta = useCallback(
|
const setDirectMeta = useCallback(
|
||||||
(m: PlayerMeta) => {
|
(m: PlayerMeta) => {
|
||||||
_setPlayerMeta(m);
|
_setPlayerMeta(m);
|
||||||
setMeta(m);
|
|
||||||
setScrapeStatus();
|
setScrapeStatus();
|
||||||
|
setMeta(m);
|
||||||
},
|
},
|
||||||
[_setPlayerMeta, setMeta, setScrapeStatus]
|
[_setPlayerMeta, setMeta, setScrapeStatus]
|
||||||
);
|
);
|
||||||
|
@ -8,12 +8,16 @@ export function useShouldShowControls() {
|
|||||||
);
|
);
|
||||||
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||||
const hasOpenOverlay = usePlayerStore((s) => s.interface.hasOpenOverlay);
|
const hasOpenOverlay = usePlayerStore((s) => s.interface.hasOpenOverlay);
|
||||||
|
const isHoveringControls = usePlayerStore(
|
||||||
|
(s) => s.interface.isHoveringControls
|
||||||
|
);
|
||||||
|
|
||||||
const isUsingTouch = lastHoveringState === PlayerHoverState.MOBILE_TAPPED;
|
const isUsingTouch = lastHoveringState === PlayerHoverState.MOBILE_TAPPED;
|
||||||
const isHovering = hovering !== PlayerHoverState.NOT_HOVERING;
|
const isHovering = hovering !== PlayerHoverState.NOT_HOVERING;
|
||||||
|
|
||||||
// when using touch, pause screens can be dismissed by tapping
|
// when using touch, pause screens can be dismissed by tapping
|
||||||
const showTargetsWithoutPause = isHovering || hasOpenOverlay;
|
const showTargetsWithoutPause =
|
||||||
|
isHovering || isHoveringControls || hasOpenOverlay;
|
||||||
const showTargetsIncludingPause = showTargetsWithoutPause || isPaused;
|
const showTargetsIncludingPause = showTargetsWithoutPause || isPaused;
|
||||||
const showTargets = isUsingTouch
|
const showTargets = isUsingTouch
|
||||||
? showTargetsWithoutPause
|
? showTargetsWithoutPause
|
||||||
|
@ -58,8 +58,9 @@ export function Link(props: {
|
|||||||
}) {
|
}) {
|
||||||
const classes = classNames("flex py-2 px-3 rounded w-full -ml-3", {
|
const classes = classNames("flex py-2 px-3 rounded w-full -ml-3", {
|
||||||
"cursor-default": !props.clickable,
|
"cursor-default": !props.clickable,
|
||||||
"hover:bg-video-context-border cursor-pointer": props.clickable,
|
"hover:bg-video-context-hoverColor hover:bg-opacity-50 cursor-pointer":
|
||||||
"bg-video-context-border": props.active,
|
props.clickable,
|
||||||
|
"bg-video-context-hoverColor bg-opacity-50": props.active,
|
||||||
});
|
});
|
||||||
const styles = { width: "calc(100% + 1.5rem)" };
|
const styles = { width: "calc(100% + 1.5rem)" };
|
||||||
|
|
||||||
|
@ -43,14 +43,14 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
|
|||||||
props.type === "noresult",
|
props.type === "noresult",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<svg
|
<Transition animation="fade" show={statusIsLoading(props)}>
|
||||||
width="100%"
|
<svg
|
||||||
height="100%"
|
width="100%"
|
||||||
viewBox="0 0 64 64"
|
height="100%"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 64 64"
|
||||||
className="rounded-full -rotate-90"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
className="rounded-full -rotate-90"
|
||||||
<Transition animation="fade" show={statusIsLoading(props)}>
|
>
|
||||||
<a.circle
|
<a.circle
|
||||||
strokeWidth="32"
|
strokeWidth="32"
|
||||||
strokeDasharray={to(spring.percentage, (val) => `${val} 100`)}
|
strokeDasharray={to(spring.percentage, (val) => `${val} 100`)}
|
||||||
@ -61,8 +61,8 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="transition-[strokeDasharray]"
|
className="transition-[strokeDasharray]"
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</svg>
|
||||||
</svg>
|
</Transition>
|
||||||
<Transition animation="fade" show={props.type === "error"}>
|
<Transition animation="fade" show={props.type === "error"}>
|
||||||
<Icon
|
<Icon
|
||||||
className="absolute inset-0 flex items-center justify-center text-white"
|
className="absolute inset-0 flex items-center justify-center text-white"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { playerStatus } from "@/stores/player/slices/source";
|
||||||
import { ThumbnailImage } from "@/stores/player/slices/thumbnails";
|
import { ThumbnailImage } from "@/stores/player/slices/thumbnails";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities";
|
import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities";
|
||||||
@ -54,12 +55,12 @@ class ThumnbnailWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.hls?.detachMedia();
|
|
||||||
this.hls?.destroy();
|
|
||||||
this.hls = null;
|
|
||||||
this.interrupted = true;
|
this.interrupted = true;
|
||||||
this.videoEl = null;
|
this.videoEl = null;
|
||||||
this.canvasEl = null;
|
this.canvasEl = null;
|
||||||
|
this.hls?.detachMedia();
|
||||||
|
this.hls?.destroy();
|
||||||
|
this.hls = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initVideo() {
|
private async initVideo() {
|
||||||
@ -91,6 +92,7 @@ class ThumnbnailWorker {
|
|||||||
);
|
);
|
||||||
const imgUrl = this.canvasEl.toDataURL();
|
const imgUrl = this.canvasEl.toDataURL();
|
||||||
if (this.interrupted) return;
|
if (this.interrupted) return;
|
||||||
|
|
||||||
this.cb({
|
this.cb({
|
||||||
at,
|
at,
|
||||||
data: imgUrl,
|
data: imgUrl,
|
||||||
@ -112,29 +114,42 @@ class ThumnbnailWorker {
|
|||||||
|
|
||||||
export function ThumbnailScraper() {
|
export function ThumbnailScraper() {
|
||||||
const addImage = usePlayerStore((s) => s.thumbnails.addImage);
|
const addImage = usePlayerStore((s) => s.thumbnails.addImage);
|
||||||
|
const status = usePlayerStore((s) => s.status);
|
||||||
|
const resetImages = usePlayerStore((s) => s.thumbnails.resetImages);
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
const source = usePlayerStore((s) => s.source);
|
const source = usePlayerStore((s) => s.source);
|
||||||
const workerRef = useRef<ThumnbnailWorker | null>(null);
|
const workerRef = useRef<ThumnbnailWorker | null>(null);
|
||||||
|
|
||||||
const inputStream = useMemo(() => {
|
// object references dont always trigger changes, so we serialize it to detect *any* change
|
||||||
if (!source) return null;
|
const sourceSeralized = JSON.stringify(source);
|
||||||
return selectQuality(source, {
|
|
||||||
automaticQuality: false,
|
|
||||||
lastChosenQuality: "360",
|
|
||||||
});
|
|
||||||
}, [source]);
|
|
||||||
|
|
||||||
// start worker with the stream
|
const start = useCallback(() => {
|
||||||
useEffect(() => {
|
let inputStream = null;
|
||||||
|
if (source)
|
||||||
|
inputStream = selectQuality(source, {
|
||||||
|
automaticQuality: false,
|
||||||
|
lastChosenQuality: "360",
|
||||||
|
});
|
||||||
// dont interrupt existing working
|
// dont interrupt existing working
|
||||||
if (workerRef.current) return;
|
if (workerRef.current) return;
|
||||||
|
if (status !== playerStatus.PLAYING) return;
|
||||||
if (!inputStream) return;
|
if (!inputStream) return;
|
||||||
|
resetImages();
|
||||||
const ins = new ThumnbnailWorker({
|
const ins = new ThumnbnailWorker({
|
||||||
addImage,
|
addImage,
|
||||||
});
|
});
|
||||||
workerRef.current = ins;
|
workerRef.current = ins;
|
||||||
ins.start(inputStream.stream);
|
ins.start(inputStream.stream);
|
||||||
}, [inputStream, addImage]);
|
}, [source, addImage, resetImages, status]);
|
||||||
|
const startRef = useRef(start);
|
||||||
|
useEffect(() => {
|
||||||
|
startRef.current = start;
|
||||||
|
}, [start, status]);
|
||||||
|
|
||||||
|
// start worker with the stream
|
||||||
|
useEffect(() => {
|
||||||
|
startRef.current();
|
||||||
|
}, [sourceSeralized]);
|
||||||
|
|
||||||
// destroy worker on unmount
|
// destroy worker on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -157,7 +172,8 @@ export function ThumbnailScraper() {
|
|||||||
workerRef.current.destroy();
|
workerRef.current.destroy();
|
||||||
workerRef.current = null;
|
workerRef.current = null;
|
||||||
}
|
}
|
||||||
}, [serializedMeta]);
|
startRef.current();
|
||||||
|
}, [serializedMeta, sourceSeralized, status]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
import { PointerEvent, useCallback } from "react";
|
import { PointerEvent, useCallback } from "react";
|
||||||
|
|
||||||
import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer";
|
import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer";
|
||||||
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
export function VideoClickTarget() {
|
export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||||
const show = useShouldShowVideoElement();
|
const show = useShouldShowVideoElement();
|
||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||||
@ -41,7 +42,10 @@ export function VideoClickTarget() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0"
|
className={classNames("absolute inset-0", {
|
||||||
|
"absolute inset-0": true,
|
||||||
|
"cursor-none": !props.showingControls,
|
||||||
|
})}
|
||||||
onDoubleClick={toggleFullscreen}
|
onDoubleClick={toggleFullscreen}
|
||||||
onPointerUp={togglePause}
|
onPointerUp={togglePause}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { RunOutput } from "@movie-web/providers";
|
import { RunOutput } from "@movie-web/providers";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
@ -8,6 +8,7 @@ import { convertRunoutputToSource } from "@/components/player/utils/convertRunou
|
|||||||
import { MetaPart } from "@/pages/parts/player/MetaPart";
|
import { MetaPart } from "@/pages/parts/player/MetaPart";
|
||||||
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
||||||
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
||||||
|
import { useLastNonPlayerLink } from "@/stores/history";
|
||||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||||
|
|
||||||
export function PlayerView() {
|
export function PlayerView() {
|
||||||
@ -19,7 +20,7 @@ export function PlayerView() {
|
|||||||
}>();
|
}>();
|
||||||
const { status, playMedia, reset } = usePlayer();
|
const { status, playMedia, reset } = usePlayer();
|
||||||
const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
|
const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
|
||||||
const [backUrl] = useState("/"); // TODO redirect to search when needed
|
const backUrl = useLastNonPlayerLink();
|
||||||
|
|
||||||
const paramsData = JSON.stringify({
|
const paramsData = JSON.stringify({
|
||||||
media: params.media,
|
media: params.media,
|
||||||
|
@ -20,7 +20,7 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Player.Container onLoad={props.onLoad}>
|
<Player.Container onLoad={props.onLoad} showingControls={showTargets}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<Player.BlackOverlay show={showTargets} />
|
<Player.BlackOverlay show={showTargets} />
|
||||||
<Player.EpisodesRouter onChange={props.onMetaChange} />
|
<Player.EpisodesRouter onChange={props.onMetaChange} />
|
||||||
@ -98,7 +98,10 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||||||
</Player.BottomControls>
|
</Player.BottomControls>
|
||||||
|
|
||||||
<Player.VolumeChangedPopout />
|
<Player.VolumeChangedPopout />
|
||||||
<Player.NextEpisodeButton controlsShowing={showTargets} />
|
<Player.NextEpisodeButton
|
||||||
|
controlsShowing={showTargets}
|
||||||
|
onChange={props.onMetaChange}
|
||||||
|
/>
|
||||||
</Player.Container>
|
</Player.Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,9 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
const currentProvider = sourceOrder.find(
|
const currentProvider = sourceOrder.find(
|
||||||
(s) => sources[s.id].status === "pending"
|
(s) => sources[s.id].status === "pending"
|
||||||
);
|
);
|
||||||
const currentProviderIndex = sourceOrder.findIndex(
|
const currentProviderIndex =
|
||||||
(provider) => currentProvider?.id === provider.id
|
sourceOrder.findIndex((provider) => currentProvider?.id === provider.id) ??
|
||||||
);
|
sourceOrder.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative" ref={containerRef}>
|
<div className="h-full w-full relative" ref={containerRef}>
|
||||||
|
@ -20,6 +20,7 @@ import { Layout } from "@/setup/Layout";
|
|||||||
import { BookmarkContextProvider } from "@/state/bookmark";
|
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||||
import { SettingsProvider } from "@/state/settings";
|
import { SettingsProvider } from "@/state/settings";
|
||||||
import { WatchedContextProvider } from "@/state/watched";
|
import { WatchedContextProvider } from "@/state/watched";
|
||||||
|
import { useHistoryListener } from "@/stores/history";
|
||||||
|
|
||||||
function LegacyUrlView({ children }: { children: ReactElement }) {
|
function LegacyUrlView({ children }: { children: ReactElement }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -55,6 +56,8 @@ function QuickSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
useHistoryListener();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<WatchedContextProvider>
|
<WatchedContextProvider>
|
||||||
|
53
src/stores/history/index.ts
Normal file
53
src/stores/history/index.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { useEffectOnce } from "react-use";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
interface HistoryRoute {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryStore {
|
||||||
|
routes: HistoryRoute[];
|
||||||
|
registerRoute(route: HistoryRoute): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHistoryStore = create(
|
||||||
|
immer<HistoryStore>((set) => ({
|
||||||
|
routes: [],
|
||||||
|
registerRoute(route) {
|
||||||
|
set((s) => {
|
||||||
|
s.routes.push(route);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useHistoryListener() {
|
||||||
|
const history = useHistory();
|
||||||
|
const loc = useLocation();
|
||||||
|
const registerRoute = useHistoryStore((s) => s.registerRoute);
|
||||||
|
useEffect(
|
||||||
|
() =>
|
||||||
|
history.listen((a) => {
|
||||||
|
registerRoute({ path: a.pathname });
|
||||||
|
}),
|
||||||
|
[history, registerRoute]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffectOnce(() => {
|
||||||
|
registerRoute({ path: loc.pathname });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLastNonPlayerLink() {
|
||||||
|
const routes = useHistoryStore((s) => s.routes);
|
||||||
|
const lastNonPlayerLink = useMemo(() => {
|
||||||
|
const reversedRoutes = [...routes];
|
||||||
|
reversedRoutes.reverse();
|
||||||
|
const route = reversedRoutes.find((v) => !v.path.startsWith("/media"));
|
||||||
|
return route?.path ?? "/";
|
||||||
|
}, [routes]);
|
||||||
|
return lastNonPlayerLink;
|
||||||
|
}
|
@ -27,12 +27,14 @@ export interface InterfaceSlice {
|
|||||||
volumeChangedWithKeybindDebounce: NodeJS.Timeout | null; // debounce for the duration of the "volume changed thingamajig"
|
volumeChangedWithKeybindDebounce: NodeJS.Timeout | null; // debounce for the duration of the "volume changed thingamajig"
|
||||||
|
|
||||||
leftControlHovering: boolean; // is the cursor hovered over the left side of player controls
|
leftControlHovering: boolean; // is the cursor hovered over the left side of player controls
|
||||||
|
isHoveringControls: boolean; // is the cursor hovered over any controls?
|
||||||
timeFormat: VideoPlayerTimeFormat; // Time format of the video player
|
timeFormat: VideoPlayerTimeFormat; // Time format of the video player
|
||||||
};
|
};
|
||||||
updateInterfaceHovering(newState: PlayerHoverState): void;
|
updateInterfaceHovering(newState: PlayerHoverState): void;
|
||||||
setSeeking(seeking: boolean): void;
|
setSeeking(seeking: boolean): void;
|
||||||
setTimeFormat(format: VideoPlayerTimeFormat): void;
|
setTimeFormat(format: VideoPlayerTimeFormat): void;
|
||||||
setHoveringLeftControls(state: boolean): void;
|
setHoveringLeftControls(state: boolean): void;
|
||||||
|
setHoveringAnyControls(state: boolean): void;
|
||||||
setHasOpenOverlay(state: boolean): void;
|
setHasOpenOverlay(state: boolean): void;
|
||||||
setLastVolume(state: number): void;
|
setLastVolume(state: number): void;
|
||||||
hideNextEpisodeButton(): void;
|
hideNextEpisodeButton(): void;
|
||||||
@ -46,6 +48,7 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
|||||||
isSeeking: false,
|
isSeeking: false,
|
||||||
lastVolume: 0,
|
lastVolume: 0,
|
||||||
leftControlHovering: false,
|
leftControlHovering: false,
|
||||||
|
isHoveringControls: false,
|
||||||
hovering: PlayerHoverState.NOT_HOVERING,
|
hovering: PlayerHoverState.NOT_HOVERING,
|
||||||
lastHoveringState: PlayerHoverState.NOT_HOVERING,
|
lastHoveringState: PlayerHoverState.NOT_HOVERING,
|
||||||
volumeChangedWithKeybind: false,
|
volumeChangedWithKeybind: false,
|
||||||
@ -89,6 +92,11 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
|||||||
s.interface.leftControlHovering = state;
|
s.interface.leftControlHovering = state;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setHoveringAnyControls(state) {
|
||||||
|
set((s) => {
|
||||||
|
s.interface.isHoveringControls = state;
|
||||||
|
});
|
||||||
|
},
|
||||||
hideNextEpisodeButton() {
|
hideNextEpisodeButton() {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.interface.hideNextEpisodeBtn = true;
|
s.interface.hideNextEpisodeBtn = true;
|
||||||
|
@ -9,6 +9,7 @@ export interface ThumbnailSlice {
|
|||||||
thumbnails: {
|
thumbnails: {
|
||||||
images: ThumbnailImage[];
|
images: ThumbnailImage[];
|
||||||
addImage(img: ThumbnailImage): void;
|
addImage(img: ThumbnailImage): void;
|
||||||
|
resetImages(): void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +74,11 @@ export function nearestImageAt(
|
|||||||
export const createThumbnailSlice: MakeSlice<ThumbnailSlice> = (set, get) => ({
|
export const createThumbnailSlice: MakeSlice<ThumbnailSlice> = (set, get) => ({
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
images: [],
|
images: [],
|
||||||
|
resetImages() {
|
||||||
|
set((s) => {
|
||||||
|
s.thumbnails.images = [];
|
||||||
|
});
|
||||||
|
},
|
||||||
addImage(img) {
|
addImage(img) {
|
||||||
const store = get();
|
const store = get();
|
||||||
const exactOrPastImageIndex = store.thumbnails.images.findIndex(
|
const exactOrPastImageIndex = store.thumbnails.images.findIndex(
|
||||||
|
@ -143,7 +143,8 @@ module.exports = {
|
|||||||
context: {
|
context: {
|
||||||
background: "#0C1216",
|
background: "#0C1216",
|
||||||
light: "#4D79A8",
|
light: "#4D79A8",
|
||||||
border: "#141D23",
|
border: "#1d252b",
|
||||||
|
hoverColor: "#1E2A32",
|
||||||
buttonFocus: "#202836",
|
buttonFocus: "#202836",
|
||||||
flagBg: "#202836",
|
flagBg: "#202836",
|
||||||
inputBg: "#202836",
|
inputBg: "#202836",
|
||||||
|
Loading…
Reference in New Issue
Block a user