mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 01:35: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 { useTranslation } from "react-i18next";
|
||||
import { useAsync } from "react-use";
|
||||
@ -5,6 +6,7 @@ import { useAsync } from "react-use";
|
||||
import { getMetaFromId } from "@/backend/metadata/getmeta";
|
||||
import { MWMediaType, MWSeasonMeta } from "@/backend/metadata/types/mw";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { ProgressRing } from "@/components/layout/ProgressRing";
|
||||
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
|
||||
import { Overlay } from "@/components/overlays/OverlayDisplay";
|
||||
import { OverlayPage } from "@/components/overlays/OverlayPage";
|
||||
@ -15,6 +17,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
|
||||
function CenteredText(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
@ -98,6 +101,7 @@ function EpisodesView({
|
||||
const { setPlayerMeta } = usePlayerMeta();
|
||||
const meta = usePlayerStore((s) => s.meta);
|
||||
const [loadingState] = useSeasonData(meta?.tmdbId ?? "", selectedSeason);
|
||||
const progress = useProgressStore();
|
||||
|
||||
const playEpisode = useCallback(
|
||||
(episodeId: string) => {
|
||||
@ -110,6 +114,8 @@ function EpisodesView({
|
||||
[setPlayerMeta, loadingState, router, onChange]
|
||||
);
|
||||
|
||||
if (!meta?.tmdbId) return null;
|
||||
|
||||
let content: ReactNode = null;
|
||||
if (loadingState.error)
|
||||
content = <CenteredText>Error loading season</CenteredText>;
|
||||
@ -124,21 +130,47 @@ function EpisodesView({
|
||||
</Menu.TextDisplay>
|
||||
) : null}
|
||||
{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 (
|
||||
<Menu.ChevronLink
|
||||
<Menu.Link
|
||||
key={ep.id}
|
||||
onClick={() => playEpisode(ep.id)}
|
||||
active={ep.id === meta?.episode?.tmdbId}
|
||||
clickable
|
||||
rightSide={rightSide}
|
||||
>
|
||||
<Menu.LinkTitle>
|
||||
<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}
|
||||
</span>
|
||||
<span className="line-clamp-1 break-all">{ep.title}</span>
|
||||
</div>
|
||||
</Menu.LinkTitle>
|
||||
</Menu.ChevronLink>
|
||||
</Menu.Link>
|
||||
);
|
||||
})}
|
||||
</Menu.Section>
|
||||
|
@ -4,7 +4,7 @@ import { useCallback } from "react";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||
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";
|
||||
|
||||
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 isHidden = usePlayerStore((s) => s.interface.hideNextEpisodeBtn);
|
||||
const meta = usePlayerStore((s) => s.meta);
|
||||
@ -67,7 +70,8 @@ export function NextEpisodeButton(props: { controlsShowing: boolean }) {
|
||||
const metaCopy = { ...meta };
|
||||
metaCopy.episode = nextEp;
|
||||
setDirectMeta(metaCopy);
|
||||
}, [setDirectMeta, nextEp, meta]);
|
||||
props.onChange?.(metaCopy);
|
||||
}, [setDirectMeta, nextEp, meta, props]);
|
||||
|
||||
if (!meta?.episode || !nextEp) return null;
|
||||
if (metaType !== "show") return null;
|
||||
|
@ -1,9 +1,22 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export function BottomControls(props: {
|
||||
show?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const setHoveringAnyControls = usePlayerStore(
|
||||
(s) => s.setHoveringAnyControls
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setHoveringAnyControls(false);
|
||||
};
|
||||
}, [setHoveringAnyControls]);
|
||||
|
||||
return (
|
||||
<div className="w-full text-white">
|
||||
<Transition
|
||||
@ -11,13 +24,15 @@ export function BottomControls(props: {
|
||||
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"
|
||||
/>
|
||||
<Transition
|
||||
animation="slide-up"
|
||||
show={props.show}
|
||||
<div
|
||||
onMouseOver={() => setHoveringAnyControls(true)}
|
||||
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"
|
||||
>
|
||||
{props.children}
|
||||
</Transition>
|
||||
<Transition animation="slide-up" show={props.show}>
|
||||
{props.children}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export interface PlayerProps {
|
||||
children?: ReactNode;
|
||||
showingControls: boolean;
|
||||
onLoad?: () => void;
|
||||
}
|
||||
|
||||
@ -89,7 +90,7 @@ export function Container(props: PlayerProps) {
|
||||
<ProgressSaver />
|
||||
<KeyboardEvents />
|
||||
<div className="relative h-screen overflow-hidden">
|
||||
<VideoClickTarget />
|
||||
<VideoClickTarget showingControls={props.showingControls} />
|
||||
<HeadUpdater />
|
||||
{props.children}
|
||||
</div>
|
||||
|
@ -1,9 +1,22 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export function TopControls(props: {
|
||||
show?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const setHoveringAnyControls = usePlayerStore(
|
||||
(s) => s.setHoveringAnyControls
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setHoveringAnyControls(false);
|
||||
};
|
||||
}, [setHoveringAnyControls]);
|
||||
|
||||
return (
|
||||
<div className="w-full text-white">
|
||||
<Transition
|
||||
@ -11,13 +24,19 @@ export function TopControls(props: {
|
||||
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"
|
||||
/>
|
||||
<Transition
|
||||
animation="slide-down"
|
||||
show={props.show}
|
||||
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"
|
||||
<div
|
||||
onMouseOver={() => setHoveringAnyControls(true)}
|
||||
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"
|
||||
>
|
||||
{props.children}
|
||||
</Transition>
|
||||
<Transition
|
||||
animation="slide-down"
|
||||
show={props.show}
|
||||
className="text-white"
|
||||
>
|
||||
{props.children}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
let startAt = 0;
|
||||
let automaticQuality = false;
|
||||
let preferenceQuality: SourceQuality | null = null;
|
||||
let lastVolume = 1;
|
||||
|
||||
function reportLevels() {
|
||||
if (!hls) return;
|
||||
@ -226,6 +227,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
destroyVideoElement();
|
||||
videoElement = video;
|
||||
setSource();
|
||||
this.setVolume(lastVolume);
|
||||
},
|
||||
processContainerElement(container) {
|
||||
containerElement = container;
|
||||
@ -261,11 +263,13 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
videoElement.currentTime = time;
|
||||
},
|
||||
async setVolume(v) {
|
||||
if (!videoElement) return;
|
||||
|
||||
// clamp time between 0 and 1
|
||||
let volume = Math.min(v, 1);
|
||||
volume = Math.max(0, volume);
|
||||
|
||||
// actually set
|
||||
lastVolume = v;
|
||||
if (!videoElement) return;
|
||||
videoElement.muted = volume === 0; // Muted attribute is always supported
|
||||
|
||||
// update state
|
||||
|
@ -16,8 +16,8 @@ export function usePlayerMeta() {
|
||||
const setDirectMeta = useCallback(
|
||||
(m: PlayerMeta) => {
|
||||
_setPlayerMeta(m);
|
||||
setMeta(m);
|
||||
setScrapeStatus();
|
||||
setMeta(m);
|
||||
},
|
||||
[_setPlayerMeta, setMeta, setScrapeStatus]
|
||||
);
|
||||
|
@ -8,12 +8,16 @@ export function useShouldShowControls() {
|
||||
);
|
||||
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||
const hasOpenOverlay = usePlayerStore((s) => s.interface.hasOpenOverlay);
|
||||
const isHoveringControls = usePlayerStore(
|
||||
(s) => s.interface.isHoveringControls
|
||||
);
|
||||
|
||||
const isUsingTouch = lastHoveringState === PlayerHoverState.MOBILE_TAPPED;
|
||||
const isHovering = hovering !== PlayerHoverState.NOT_HOVERING;
|
||||
|
||||
// when using touch, pause screens can be dismissed by tapping
|
||||
const showTargetsWithoutPause = isHovering || hasOpenOverlay;
|
||||
const showTargetsWithoutPause =
|
||||
isHovering || isHoveringControls || hasOpenOverlay;
|
||||
const showTargetsIncludingPause = showTargetsWithoutPause || isPaused;
|
||||
const showTargets = isUsingTouch
|
||||
? showTargetsWithoutPause
|
||||
|
@ -58,8 +58,9 @@ export function Link(props: {
|
||||
}) {
|
||||
const classes = classNames("flex py-2 px-3 rounded w-full -ml-3", {
|
||||
"cursor-default": !props.clickable,
|
||||
"hover:bg-video-context-border cursor-pointer": props.clickable,
|
||||
"bg-video-context-border": props.active,
|
||||
"hover:bg-video-context-hoverColor hover:bg-opacity-50 cursor-pointer":
|
||||
props.clickable,
|
||||
"bg-video-context-hoverColor bg-opacity-50": props.active,
|
||||
});
|
||||
const styles = { width: "calc(100% + 1.5rem)" };
|
||||
|
||||
|
@ -43,14 +43,14 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
|
||||
props.type === "noresult",
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 64 64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="rounded-full -rotate-90"
|
||||
>
|
||||
<Transition animation="fade" show={statusIsLoading(props)}>
|
||||
<Transition animation="fade" show={statusIsLoading(props)}>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 64 64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="rounded-full -rotate-90"
|
||||
>
|
||||
<a.circle
|
||||
strokeWidth="32"
|
||||
strokeDasharray={to(spring.percentage, (val) => `${val} 100`)}
|
||||
@ -61,8 +61,8 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
|
||||
stroke="currentColor"
|
||||
className="transition-[strokeDasharray]"
|
||||
/>
|
||||
</Transition>
|
||||
</svg>
|
||||
</svg>
|
||||
</Transition>
|
||||
<Transition animation="fade" show={props.type === "error"}>
|
||||
<Icon
|
||||
className="absolute inset-0 flex items-center justify-center text-white"
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 { usePlayerStore } from "@/stores/player/store";
|
||||
import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities";
|
||||
@ -54,12 +55,12 @@ class ThumnbnailWorker {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.hls?.detachMedia();
|
||||
this.hls?.destroy();
|
||||
this.hls = null;
|
||||
this.interrupted = true;
|
||||
this.videoEl = null;
|
||||
this.canvasEl = null;
|
||||
this.hls?.detachMedia();
|
||||
this.hls?.destroy();
|
||||
this.hls = null;
|
||||
}
|
||||
|
||||
private async initVideo() {
|
||||
@ -91,6 +92,7 @@ class ThumnbnailWorker {
|
||||
);
|
||||
const imgUrl = this.canvasEl.toDataURL();
|
||||
if (this.interrupted) return;
|
||||
|
||||
this.cb({
|
||||
at,
|
||||
data: imgUrl,
|
||||
@ -112,29 +114,42 @@ class ThumnbnailWorker {
|
||||
|
||||
export function ThumbnailScraper() {
|
||||
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 source = usePlayerStore((s) => s.source);
|
||||
const workerRef = useRef<ThumnbnailWorker | null>(null);
|
||||
|
||||
const inputStream = useMemo(() => {
|
||||
if (!source) return null;
|
||||
return selectQuality(source, {
|
||||
automaticQuality: false,
|
||||
lastChosenQuality: "360",
|
||||
});
|
||||
}, [source]);
|
||||
// object references dont always trigger changes, so we serialize it to detect *any* change
|
||||
const sourceSeralized = JSON.stringify(source);
|
||||
|
||||
// start worker with the stream
|
||||
useEffect(() => {
|
||||
const start = useCallback(() => {
|
||||
let inputStream = null;
|
||||
if (source)
|
||||
inputStream = selectQuality(source, {
|
||||
automaticQuality: false,
|
||||
lastChosenQuality: "360",
|
||||
});
|
||||
// dont interrupt existing working
|
||||
if (workerRef.current) return;
|
||||
if (status !== playerStatus.PLAYING) return;
|
||||
if (!inputStream) return;
|
||||
resetImages();
|
||||
const ins = new ThumnbnailWorker({
|
||||
addImage,
|
||||
});
|
||||
workerRef.current = ins;
|
||||
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
|
||||
useEffect(() => {
|
||||
@ -157,7 +172,8 @@ export function ThumbnailScraper() {
|
||||
workerRef.current.destroy();
|
||||
workerRef.current = null;
|
||||
}
|
||||
}, [serializedMeta]);
|
||||
startRef.current();
|
||||
}, [serializedMeta, sourceSeralized, status]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import classNames from "classnames";
|
||||
import { PointerEvent, useCallback } from "react";
|
||||
|
||||
import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer";
|
||||
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export function VideoClickTarget() {
|
||||
export function VideoClickTarget(props: { showingControls: boolean }) {
|
||||
const show = useShouldShowVideoElement();
|
||||
const display = usePlayerStore((s) => s.display);
|
||||
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||
@ -41,7 +42,10 @@ export function VideoClickTarget() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
className={classNames("absolute inset-0", {
|
||||
"absolute inset-0": true,
|
||||
"cursor-none": !props.showingControls,
|
||||
})}
|
||||
onDoubleClick={toggleFullscreen}
|
||||
onPointerUp={togglePause}
|
||||
/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { 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 { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
||||
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
||||
import { useLastNonPlayerLink } from "@/stores/history";
|
||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||
|
||||
export function PlayerView() {
|
||||
@ -19,7 +20,7 @@ export function PlayerView() {
|
||||
}>();
|
||||
const { status, playMedia, reset } = usePlayer();
|
||||
const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
|
||||
const [backUrl] = useState("/"); // TODO redirect to search when needed
|
||||
const backUrl = useLastNonPlayerLink();
|
||||
|
||||
const paramsData = JSON.stringify({
|
||||
media: params.media,
|
||||
|
@ -20,7 +20,7 @@ export function PlayerPart(props: PlayerPartProps) {
|
||||
const { isMobile } = useIsMobile();
|
||||
|
||||
return (
|
||||
<Player.Container onLoad={props.onLoad}>
|
||||
<Player.Container onLoad={props.onLoad} showingControls={showTargets}>
|
||||
{props.children}
|
||||
<Player.BlackOverlay show={showTargets} />
|
||||
<Player.EpisodesRouter onChange={props.onMetaChange} />
|
||||
@ -98,7 +98,10 @@ export function PlayerPart(props: PlayerPartProps) {
|
||||
</Player.BottomControls>
|
||||
|
||||
<Player.VolumeChangedPopout />
|
||||
<Player.NextEpisodeButton controlsShowing={showTargets} />
|
||||
<Player.NextEpisodeButton
|
||||
controlsShowing={showTargets}
|
||||
onChange={props.onMetaChange}
|
||||
/>
|
||||
</Player.Container>
|
||||
);
|
||||
}
|
||||
|
@ -41,9 +41,9 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
const currentProvider = sourceOrder.find(
|
||||
(s) => sources[s.id].status === "pending"
|
||||
);
|
||||
const currentProviderIndex = sourceOrder.findIndex(
|
||||
(provider) => currentProvider?.id === provider.id
|
||||
);
|
||||
const currentProviderIndex =
|
||||
sourceOrder.findIndex((provider) => currentProvider?.id === provider.id) ??
|
||||
sourceOrder.length - 1;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative" ref={containerRef}>
|
||||
|
@ -20,6 +20,7 @@ import { Layout } from "@/setup/Layout";
|
||||
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||
import { SettingsProvider } from "@/state/settings";
|
||||
import { WatchedContextProvider } from "@/state/watched";
|
||||
import { useHistoryListener } from "@/stores/history";
|
||||
|
||||
function LegacyUrlView({ children }: { children: ReactElement }) {
|
||||
const location = useLocation();
|
||||
@ -55,6 +56,8 @@ function QuickSearch() {
|
||||
}
|
||||
|
||||
function App() {
|
||||
useHistoryListener();
|
||||
|
||||
return (
|
||||
<SettingsProvider>
|
||||
<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"
|
||||
|
||||
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
|
||||
};
|
||||
updateInterfaceHovering(newState: PlayerHoverState): void;
|
||||
setSeeking(seeking: boolean): void;
|
||||
setTimeFormat(format: VideoPlayerTimeFormat): void;
|
||||
setHoveringLeftControls(state: boolean): void;
|
||||
setHoveringAnyControls(state: boolean): void;
|
||||
setHasOpenOverlay(state: boolean): void;
|
||||
setLastVolume(state: number): void;
|
||||
hideNextEpisodeButton(): void;
|
||||
@ -46,6 +48,7 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||
isSeeking: false,
|
||||
lastVolume: 0,
|
||||
leftControlHovering: false,
|
||||
isHoveringControls: false,
|
||||
hovering: PlayerHoverState.NOT_HOVERING,
|
||||
lastHoveringState: PlayerHoverState.NOT_HOVERING,
|
||||
volumeChangedWithKeybind: false,
|
||||
@ -89,6 +92,11 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||
s.interface.leftControlHovering = state;
|
||||
});
|
||||
},
|
||||
setHoveringAnyControls(state) {
|
||||
set((s) => {
|
||||
s.interface.isHoveringControls = state;
|
||||
});
|
||||
},
|
||||
hideNextEpisodeButton() {
|
||||
set((s) => {
|
||||
s.interface.hideNextEpisodeBtn = true;
|
||||
|
@ -9,6 +9,7 @@ export interface ThumbnailSlice {
|
||||
thumbnails: {
|
||||
images: ThumbnailImage[];
|
||||
addImage(img: ThumbnailImage): void;
|
||||
resetImages(): void;
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,6 +74,11 @@ export function nearestImageAt(
|
||||
export const createThumbnailSlice: MakeSlice<ThumbnailSlice> = (set, get) => ({
|
||||
thumbnails: {
|
||||
images: [],
|
||||
resetImages() {
|
||||
set((s) => {
|
||||
s.thumbnails.images = [];
|
||||
});
|
||||
},
|
||||
addImage(img) {
|
||||
const store = get();
|
||||
const exactOrPastImageIndex = store.thumbnails.images.findIndex(
|
||||
|
@ -143,7 +143,8 @@ module.exports = {
|
||||
context: {
|
||||
background: "#0C1216",
|
||||
light: "#4D79A8",
|
||||
border: "#141D23",
|
||||
border: "#1d252b",
|
||||
hoverColor: "#1E2A32",
|
||||
buttonFocus: "#202836",
|
||||
flagBg: "#202836",
|
||||
inputBg: "#202836",
|
||||
|
Loading…
Reference in New Issue
Block a user