mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
episode select popout styling, popout router sync & dragging to update time action
This commit is contained in:
parent
3b4e9ce2ca
commit
2a3c93c24f
14 changed files with 318 additions and 100 deletions
|
@ -35,7 +35,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
39
src/components/layout/ProgressRing.tsx
Normal file
39
src/components/layout/ProgressRing.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
radius?: number;
|
||||||
|
percentage: number;
|
||||||
|
backingRingClassname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProgressRing(props: Props) {
|
||||||
|
const radius = props.radius ?? 40;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={`${props.className ?? ""} -rotate-90`}
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className={`fill-transparent stroke-denim-700 stroke-[15] opacity-25 ${
|
||||||
|
props.backingRingClassname ?? ""
|
||||||
|
}`}
|
||||||
|
r={radius}
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
className="fill-transparent stroke-current stroke-[15] transition-[stroke-dashoffset] duration-150"
|
||||||
|
r={radius}
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
style={{
|
||||||
|
strokeDasharray: `${2 * Math.PI * radius} ${2 * Math.PI * radius}`,
|
||||||
|
strokeDashoffset: `${
|
||||||
|
2 * Math.PI * radius -
|
||||||
|
(props.percentage / 100) * (2 * Math.PI * radius)
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -36,3 +36,11 @@ body[data-no-select] {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-clamp {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
|
@ -26,13 +26,18 @@ export function ProgressAction() {
|
||||||
commitTime
|
commitTime
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO make dragging update timer
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dragRef.current === dragging) return;
|
if (dragRef.current === dragging) return;
|
||||||
dragRef.current = dragging;
|
dragRef.current = dragging;
|
||||||
controls.setSeeking(dragging);
|
controls.setSeeking(dragging);
|
||||||
}, [dragRef, dragging, controls]);
|
}, [dragRef, dragging, controls]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dragging) {
|
||||||
|
controls.setDraggingTime(videoTime.duration * (dragPercentage / 100));
|
||||||
|
}
|
||||||
|
}, [videoTime, dragging, dragPercentage, controls]);
|
||||||
|
|
||||||
let watchProgress = makePercentageString(
|
let watchProgress = makePercentageString(
|
||||||
makePercentage((videoTime.time / videoTime.duration) * 100)
|
makePercentage((videoTime.time / videoTime.duration) * 100)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useProgress } from "@/video/state/logic/progress";
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
|
|
||||||
function durationExceedsHour(secs: number): boolean {
|
function durationExceedsHour(secs: number): boolean {
|
||||||
|
@ -35,9 +36,13 @@ interface Props {
|
||||||
export function TimeAction(props: Props) {
|
export function TimeAction(props: Props) {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const videoTime = useProgress(descriptor);
|
const videoTime = useProgress(descriptor);
|
||||||
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
|
||||||
const hasHours = durationExceedsHour(videoTime.duration);
|
const hasHours = durationExceedsHour(videoTime.duration);
|
||||||
const time = formatSeconds(videoTime.time, hasHours);
|
const time = formatSeconds(
|
||||||
|
mediaPlaying.isSeeking ? videoTime.draggingTime : videoTime.time,
|
||||||
|
hasHours
|
||||||
|
);
|
||||||
const duration = formatSeconds(videoTime.duration, hasHours);
|
const duration = formatSeconds(videoTime.duration, hasHours);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
70
src/video/components/hooks/useSyncPopouts.ts
Normal file
70
src/video/components/hooks/useSyncPopouts.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { useInitialized } from "@/video/components/hooks/useInitialized";
|
||||||
|
import { ControlMethods, useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
function syncRouteToPopout(
|
||||||
|
location: ReturnType<typeof useLocation>,
|
||||||
|
controls: ControlMethods
|
||||||
|
) {
|
||||||
|
const parsed = new URLSearchParams(location.search);
|
||||||
|
const value = parsed.get("modal");
|
||||||
|
if (value) controls.openPopout(value);
|
||||||
|
else controls.closePopout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO make closing a popout go backwords in history
|
||||||
|
// TODO fix first event breaking (clicking on page somehow resolves it)
|
||||||
|
export function useSyncPopouts(descriptor: string) {
|
||||||
|
const history = useHistory();
|
||||||
|
const videoInterface = useInterface(descriptor);
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const intialized = useInitialized(descriptor);
|
||||||
|
const loc = useLocation();
|
||||||
|
|
||||||
|
const lastKnownValue = useRef<string | null>(null);
|
||||||
|
|
||||||
|
const controlsRef = useRef<typeof controls>(controls);
|
||||||
|
useEffect(() => {
|
||||||
|
controlsRef.current = controls;
|
||||||
|
}, [controls]);
|
||||||
|
|
||||||
|
// sync current popout to router
|
||||||
|
useEffect(() => {
|
||||||
|
const popoutId = videoInterface.popout;
|
||||||
|
if (lastKnownValue.current === popoutId) return;
|
||||||
|
lastKnownValue.current = popoutId;
|
||||||
|
// rest only triggers with changes
|
||||||
|
|
||||||
|
if (popoutId) {
|
||||||
|
const params = new URLSearchParams([["modal", popoutId]]).toString();
|
||||||
|
history.push({
|
||||||
|
search: params,
|
||||||
|
state: "popout",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
history.push({
|
||||||
|
search: "",
|
||||||
|
state: "popout",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [videoInterface, history]);
|
||||||
|
|
||||||
|
// sync router to popout state (but only if its not done by block of code above)
|
||||||
|
useEffect(() => {
|
||||||
|
if (loc.state === "popout") return;
|
||||||
|
|
||||||
|
// sync popout state
|
||||||
|
syncRouteToPopout(loc, controlsRef.current);
|
||||||
|
}, [loc]);
|
||||||
|
|
||||||
|
// mount hook
|
||||||
|
const routerInitialized = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (routerInitialized.current) return;
|
||||||
|
if (!intialized) return;
|
||||||
|
syncRouteToPopout(loc, controlsRef.current);
|
||||||
|
routerInitialized.current = true;
|
||||||
|
}, [loc, intialized]);
|
||||||
|
}
|
|
@ -10,18 +10,70 @@ import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useWatchedContext } from "@/state/watched";
|
||||||
|
import { ProgressRing } from "@/components/layout/ProgressRing";
|
||||||
|
|
||||||
function PopupSection(props: {
|
function PopupSection(props: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={["p-4", props.className || ""].join(" ")}>
|
<div className={["p-5", props.className || ""].join(" ")}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PopoutListEntryTypes {
|
||||||
|
active?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
isOnDarkBackground?: boolean;
|
||||||
|
percentageCompleted?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoutListEntry(props: PopoutListEntryTypes) {
|
||||||
|
const bg = props.isOnDarkBackground ? "bg-ash-200" : "bg-ash-400";
|
||||||
|
const hover = props.isOnDarkBackground
|
||||||
|
? "hover:bg-ash-200"
|
||||||
|
: "hover:bg-ash-400";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
"group -mx-2 flex items-center justify-between space-x-1 rounded p-2 font-semibold transition-[background-color,color] duration-150",
|
||||||
|
hover,
|
||||||
|
props.active
|
||||||
|
? `${bg} text-white outline-denim-700`
|
||||||
|
: "text-denim-700 hover:text-white",
|
||||||
|
].join(" ")}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
{props.active && (
|
||||||
|
<div className="absolute left-0 h-8 w-0.5 bg-bink-500" />
|
||||||
|
)}
|
||||||
|
<span className="truncate">{props.children}</span>
|
||||||
|
<div className="relative h-4 w-4">
|
||||||
|
<Icon
|
||||||
|
className="absolute inset-0 translate-x-2 text-white opacity-0 transition-[opacity,transform] duration-100 group-hover:translate-x-0 group-hover:opacity-100"
|
||||||
|
icon={Icons.CHEVRON_RIGHT}
|
||||||
|
/>
|
||||||
|
{props.percentageCompleted ? (
|
||||||
|
<ProgressRing
|
||||||
|
className="absolute inset-0 text-bink-600 opacity-100 transition-[opacity] group-hover:opacity-0"
|
||||||
|
backingRingClassname="stroke-ash-500"
|
||||||
|
percentage={
|
||||||
|
props.percentageCompleted > 90 ? 100 : props.percentageCompleted
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function EpisodeSelectionPopout() {
|
export function EpisodeSelectionPopout() {
|
||||||
const params = useParams<{
|
const params = useParams<{
|
||||||
media: string;
|
media: string;
|
||||||
|
@ -90,80 +142,113 @@ export function EpisodeSelectionPopout() {
|
||||||
setCurrentVisibleSeason({ seasonId: id });
|
setCurrentVisibleSeason({ seasonId: id });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isPickingSeason)
|
const { watched } = useWatchedContext();
|
||||||
return (
|
|
||||||
<>
|
const titlePositionClass = useMemo(() => {
|
||||||
<PopupSection className="flex items-center space-x-3 border-b border-denim-500 font-bold text-white">
|
const offset = isPickingSeason ? "left-0" : "left-10";
|
||||||
Pick a season
|
return [
|
||||||
</PopupSection>
|
"absolute w-full transition-[left,opacity] duration-200",
|
||||||
<PopupSection className="overflow-y-auto">
|
offset,
|
||||||
<div className="space-y-1">
|
].join(" ");
|
||||||
{currentSeasonInfo
|
}, [isPickingSeason]);
|
||||||
? meta?.seasons?.map?.((season) => (
|
|
||||||
<div
|
|
||||||
className="text-denim-800 -mx-2 flex items-center space-x-1 rounded p-2 text-white hover:bg-denim-600"
|
|
||||||
key={season.id}
|
|
||||||
onClick={() => setSeason(season.id)}
|
|
||||||
>
|
|
||||||
{season.title}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: "No season"}
|
|
||||||
</div>
|
|
||||||
</PopupSection>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PopupSection className="flex items-center space-x-3 border-b border-denim-500 font-bold text-white">
|
<PopupSection className="bg-ash-100 font-bold text-white">
|
||||||
<button
|
<div className="relative flex items-center">
|
||||||
className="-m-1.5 rounded p-1.5 hover:bg-denim-600"
|
<button
|
||||||
onClick={toggleIsPickingSeason}
|
className={[
|
||||||
type="button"
|
"-m-1.5 rounded-lg p-1.5 transition-opacity duration-100 hover:bg-ash-200",
|
||||||
|
isPickingSeason ? "pointer-events-none opacity-0" : "opacity-1",
|
||||||
|
].join(" ")}
|
||||||
|
onClick={toggleIsPickingSeason}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon icon={Icons.CHEVRON_LEFT} />
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
titlePositionClass,
|
||||||
|
!isPickingSeason ? "opacity-1" : "opacity-0",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{currentSeasonInfo?.title || ""}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
titlePositionClass,
|
||||||
|
isPickingSeason ? "opacity-1" : "opacity-0",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
Seasons
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</PopupSection>
|
||||||
|
<div className="relative grid h-full grid-rows-[minmax(1px,1fr)]">
|
||||||
|
<PopupSection
|
||||||
|
className={[
|
||||||
|
"absolute inset-0 z-30 overflow-y-auto border-ash-400 bg-ash-100 transition-[max-height,padding] duration-200",
|
||||||
|
isPickingSeason
|
||||||
|
? "max-h-full border-t"
|
||||||
|
: "max-h-0 overflow-hidden py-0",
|
||||||
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<Icon icon={Icons.CHEVRON_LEFT} />
|
{currentSeasonInfo
|
||||||
</button>
|
? meta?.seasons?.map?.((season) => (
|
||||||
<span>{currentSeasonInfo?.title || ""}</span>
|
<PopoutListEntry
|
||||||
</PopupSection>
|
key={season.id}
|
||||||
<PopupSection className="h-full overflow-y-auto">
|
active={meta?.episode?.seasonId === season.id}
|
||||||
{loading ? (
|
onClick={() => setSeason(season.id)}
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
isOnDarkBackground
|
||||||
<Loading />
|
>
|
||||||
</div>
|
{season.title}
|
||||||
) : error ? (
|
</PopoutListEntry>
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
))
|
||||||
<div className="flex flex-col flex-wrap items-center text-slate-400">
|
: "No season"}
|
||||||
<IconPatch
|
</PopupSection>
|
||||||
icon={Icons.EYE_SLASH}
|
<PopupSection className="relative h-full overflow-y-auto">
|
||||||
className="text-xl text-bink-600"
|
{loading ? (
|
||||||
/>
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<p className="mt-6 w-full text-center">
|
<Loading />
|
||||||
Something went wrong loading the episodes for{" "}
|
|
||||||
{currentSeasonInfo?.title?.toLowerCase()}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : error ? (
|
||||||
) : (
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="space-y-1">
|
<div className="flex flex-col flex-wrap items-center text-slate-400">
|
||||||
{currentSeasonEpisodes && currentSeasonInfo
|
<IconPatch
|
||||||
? currentSeasonEpisodes.map((e) => (
|
icon={Icons.EYE_SLASH}
|
||||||
<div
|
className="text-xl text-bink-600"
|
||||||
className={[
|
/>
|
||||||
"text-denim-800 -mx-2 flex items-center space-x-1 rounded p-2 text-white hover:bg-denim-600",
|
<p className="mt-6 w-full text-center">
|
||||||
meta?.episode?.episodeId === e.id &&
|
Something went wrong loading the episodes for{" "}
|
||||||
"outline outline-2 outline-denim-700",
|
{currentSeasonInfo?.title?.toLowerCase()}
|
||||||
].join(" ")}
|
</p>
|
||||||
onClick={() => setCurrent(currentSeasonInfo.id, e.id)}
|
</div>
|
||||||
key={e.id}
|
</div>
|
||||||
>
|
) : (
|
||||||
{e.number}. {e.title}
|
<div className="space-y-1">
|
||||||
</div>
|
{currentSeasonEpisodes && currentSeasonInfo
|
||||||
))
|
? currentSeasonEpisodes.map((e) => (
|
||||||
: "No episodes"}
|
<PopoutListEntry
|
||||||
</div>
|
key={e.id}
|
||||||
)}
|
active={e.id === meta?.episode?.episodeId}
|
||||||
</PopupSection>
|
onClick={() => setCurrent(currentSeasonInfo.id, e.id)}
|
||||||
|
percentageCompleted={
|
||||||
|
watched.items.find(
|
||||||
|
(item) =>
|
||||||
|
item.item?.series?.seasonId ===
|
||||||
|
currentSeasonInfo.id &&
|
||||||
|
item.item?.series?.episodeId === e.id
|
||||||
|
)?.percentage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
E{e.number} - {e.title}
|
||||||
|
</PopoutListEntry>
|
||||||
|
))
|
||||||
|
: "No episodes"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopupSection>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
|
import { useSyncPopouts } from "@/video/components/hooks/useSyncPopouts";
|
||||||
import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout";
|
import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
@ -19,13 +20,12 @@ function ShowPopout(props: { popoutId: string | null }) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO use new design for popouts
|
|
||||||
// TODO improve anti offscreen math
|
// TODO improve anti offscreen math
|
||||||
// TODO attach router history to popout state, so you can use back button to remove popout
|
|
||||||
export function PopoutProviderAction() {
|
export function PopoutProviderAction() {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const videoInterface = useInterface(descriptor);
|
const videoInterface = useInterface(descriptor);
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
|
useSyncPopouts(descriptor);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
controls.closePopout();
|
controls.closePopout();
|
||||||
|
@ -40,12 +40,12 @@ export function PopoutProviderAction() {
|
||||||
30
|
30
|
||||||
)}px`
|
)}px`
|
||||||
: "30px";
|
: "30px";
|
||||||
}, [videoInterface]);
|
}, [videoInterface.popoutBounds]);
|
||||||
const distanceFromBottom = useMemo(() => {
|
const distanceFromBottom = useMemo(() => {
|
||||||
return videoInterface.popoutBounds
|
return videoInterface.popoutBounds
|
||||||
? `${videoInterface.popoutBounds.height + 30}px`
|
? `${videoInterface.popoutBounds.height + 30}px`
|
||||||
: "30px";
|
: "30px";
|
||||||
}, [videoInterface]);
|
}, [videoInterface.popoutBounds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
|
@ -56,7 +56,7 @@ export function PopoutProviderAction() {
|
||||||
<div className="popout-wrapper pointer-events-auto absolute inset-0">
|
<div className="popout-wrapper pointer-events-auto absolute inset-0">
|
||||||
<div onClick={handleClick} className="absolute inset-0" />
|
<div onClick={handleClick} className="absolute inset-0" />
|
||||||
<div
|
<div
|
||||||
className="grid-template-rows-[auto,minmax(0px,1fr)] absolute z-10 grid h-[500px] w-72 rounded-lg bg-denim-300"
|
className="absolute z-10 grid h-[500px] w-80 grid-rows-[auto,minmax(0,1fr)] overflow-hidden rounded-lg bg-ash-200"
|
||||||
style={{
|
style={{
|
||||||
right: distanceFromRight,
|
right: distanceFromRight,
|
||||||
bottom: distanceFromBottom,
|
bottom: distanceFromBottom,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { updateInterface } from "@/video/state/logic/interface";
|
import { updateInterface } from "@/video/state/logic/interface";
|
||||||
import { updateMeta } from "@/video/state/logic/meta";
|
import { updateMeta } from "@/video/state/logic/meta";
|
||||||
|
import { updateProgress } from "@/video/state/logic/progress";
|
||||||
import { VideoPlayerMeta } from "@/video/state/types";
|
import { VideoPlayerMeta } from "@/video/state/types";
|
||||||
import { getPlayerState } from "../cache";
|
import { getPlayerState } from "../cache";
|
||||||
import { VideoPlayerStateController } from "../providers/providerTypes";
|
import { VideoPlayerStateController } from "../providers/providerTypes";
|
||||||
|
@ -11,6 +12,7 @@ export type ControlMethods = {
|
||||||
setFocused(focused: boolean): void;
|
setFocused(focused: boolean): void;
|
||||||
setMeta(data?: VideoPlayerMeta): void;
|
setMeta(data?: VideoPlayerMeta): void;
|
||||||
setCurrentEpisode(sId: string, eId: string): void;
|
setCurrentEpisode(sId: string, eId: string): void;
|
||||||
|
setDraggingTime(num: number): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useControls(
|
export function useControls(
|
||||||
|
@ -53,6 +55,13 @@ export function useControls(
|
||||||
state.interface.leftControlHovering = hovering;
|
state.interface.leftControlHovering = hovering;
|
||||||
updateInterface(descriptor, state);
|
updateInterface(descriptor, state);
|
||||||
},
|
},
|
||||||
|
setDraggingTime(num) {
|
||||||
|
state.progress.draggingTime = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(state.progress.duration, num)
|
||||||
|
);
|
||||||
|
updateProgress(descriptor, state);
|
||||||
|
},
|
||||||
openPopout(id: string) {
|
openPopout(id: string) {
|
||||||
state.interface.popout = id;
|
state.interface.popout = id;
|
||||||
updateInterface(descriptor, state);
|
updateInterface(descriptor, state);
|
||||||
|
|
|
@ -7,6 +7,7 @@ export type VideoProgressEvent = {
|
||||||
time: number;
|
time: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
buffered: number;
|
buffered: number;
|
||||||
|
draggingTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getProgressFromState(state: VideoPlayerState): VideoProgressEvent {
|
function getProgressFromState(state: VideoPlayerState): VideoProgressEvent {
|
||||||
|
@ -14,6 +15,7 @@ function getProgressFromState(state: VideoPlayerState): VideoProgressEvent {
|
||||||
time: state.progress.time,
|
time: state.progress.time,
|
||||||
duration: state.progress.duration,
|
duration: state.progress.duration,
|
||||||
buffered: state.progress.buffered,
|
buffered: state.progress.buffered,
|
||||||
|
draggingTime: state.progress.draggingTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,9 @@ export function createVideoStateProvider(
|
||||||
updateProgress(descriptor, state);
|
updateProgress(descriptor, state);
|
||||||
},
|
},
|
||||||
setSeeking(active) {
|
setSeeking(active) {
|
||||||
|
state.mediaPlaying.isSeeking = active;
|
||||||
|
updateInterface(descriptor, state);
|
||||||
|
|
||||||
// if it was playing when starting to seek, play again
|
// if it was playing when starting to seek, play again
|
||||||
if (!active) {
|
if (!active) {
|
||||||
if (!state.pausedWhenSeeking) this.play();
|
if (!state.pausedWhenSeeking) this.play();
|
||||||
|
|
|
@ -42,6 +42,7 @@ export type VideoPlayerState = {
|
||||||
time: number;
|
time: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
buffered: number;
|
buffered: number;
|
||||||
|
draggingTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// meta data of video
|
// meta data of video
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { ProgressRing } from "@/components/layout/ProgressRing";
|
||||||
import { ScrapeEventLog } from "@/hooks/useScrape";
|
import { ScrapeEventLog } from "@/hooks/useScrape";
|
||||||
|
|
||||||
interface MediaScrapeLogProps {
|
interface MediaScrapeLogProps {
|
||||||
|
@ -18,27 +19,11 @@ function MediaScrapePill({ event }: MediaScrapePillProps) {
|
||||||
<div className="flex h-9 w-[220px] items-center rounded-full bg-slate-800 p-3 text-denim-700">
|
<div className="flex h-9 w-[220px] items-center rounded-full bg-slate-800 p-3 text-denim-700">
|
||||||
<div className="mr-2 flex w-[18px] items-center justify-center">
|
<div className="mr-2 flex w-[18px] items-center justify-center">
|
||||||
{!event.errored ? (
|
{!event.errored ? (
|
||||||
<svg className="h-[18px] w-[18px] -rotate-90" viewBox="0 0 100 100">
|
<ProgressRing
|
||||||
<circle
|
className="h-[18px] w-[18px] text-bink-700"
|
||||||
className="fill-transparent stroke-denim-700 stroke-[15] opacity-25"
|
percentage={event.percentage}
|
||||||
r="40"
|
radius={40}
|
||||||
cx="50"
|
/>
|
||||||
cy="50"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
className="fill-transparent stroke-bink-700 stroke-[15] transition-[stroke-dashoffset] duration-150"
|
|
||||||
r="40"
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
style={{
|
|
||||||
strokeDasharray: `${2 * Math.PI * 40} ${2 * Math.PI * 40}`,
|
|
||||||
strokeDashoffset: `${
|
|
||||||
2 * Math.PI * 40 -
|
|
||||||
(event.percentage / 100) * (2 * Math.PI * 40)
|
|
||||||
}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
) : (
|
||||||
<Icon icon={Icons.X} className="text-[0.85em] text-rose-400" />
|
<Icon icon={Icons.X} className="text-[0.85em] text-rose-400" />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -18,7 +18,13 @@ module.exports = {
|
||||||
"denim-400": "#2B263D",
|
"denim-400": "#2B263D",
|
||||||
"denim-500": "#38334A",
|
"denim-500": "#38334A",
|
||||||
"denim-600": "#504B64",
|
"denim-600": "#504B64",
|
||||||
"denim-700": "#7A758F"
|
"denim-700": "#7A758F",
|
||||||
|
"ash-600": "#817998",
|
||||||
|
"ash-500": "#9C93B5",
|
||||||
|
"ash-400": "#3D394D",
|
||||||
|
"ash-300": "#2C293A",
|
||||||
|
"ash-200": "#2B2836",
|
||||||
|
"ash-100": "#1E1C26"
|
||||||
},
|
},
|
||||||
|
|
||||||
/* fonts */
|
/* fonts */
|
||||||
|
|
Loading…
Reference in a new issue