24 changed files with 519 additions and 390 deletions
@ -0,0 +1,84 @@ |
|||||||
|
import classNames from "classnames"; |
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { Transition } from "@/components/Transition"; |
||||||
|
import { usePlayerStore } from "@/stores/player/store"; |
||||||
|
|
||||||
|
function shouldShowNextEpisodeButton( |
||||||
|
time: number, |
||||||
|
duration: number |
||||||
|
): "always" | "hover" | "none" { |
||||||
|
const percentage = time / duration; |
||||||
|
const secondsFromEnd = duration - time; |
||||||
|
if (secondsFromEnd <= 30) return "always"; |
||||||
|
if (percentage >= 0.9) return "hover"; |
||||||
|
return "none"; |
||||||
|
} |
||||||
|
|
||||||
|
function Button(props: { |
||||||
|
className: string; |
||||||
|
onClick?: () => void; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<button |
||||||
|
className={classNames( |
||||||
|
"font-bold rounded h-10 w-40 scale-95 hover:scale-100 transition-all duration-200", |
||||||
|
props.className |
||||||
|
)} |
||||||
|
type="button" |
||||||
|
onClick={props.onClick} |
||||||
|
> |
||||||
|
{props.children} |
||||||
|
</button> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO check if has next episode
|
||||||
|
export function NextEpisodeButton(props: { controlsShowing: boolean }) { |
||||||
|
const duration = usePlayerStore((s) => s.progress.duration); |
||||||
|
const isHidden = usePlayerStore((s) => s.interface.hideNextEpisodeBtn); |
||||||
|
const hideNextEpisodeButton = usePlayerStore((s) => s.hideNextEpisodeButton); |
||||||
|
const metaType = usePlayerStore((s) => s.meta?.type); |
||||||
|
const time = usePlayerStore((s) => s.progress.time); |
||||||
|
const showingState = shouldShowNextEpisodeButton(time, duration); |
||||||
|
const status = usePlayerStore((s) => s.status); |
||||||
|
|
||||||
|
let show = false; |
||||||
|
if (showingState === "always") show = true; |
||||||
|
else if (showingState === "hover" && props.controlsShowing) show = true; |
||||||
|
if (isHidden || status !== "playing" || duration === 0) show = false; |
||||||
|
|
||||||
|
const animation = showingState === "hover" ? "slide-up" : "fade"; |
||||||
|
let bottom = "bottom-24"; |
||||||
|
if (showingState === "always") |
||||||
|
bottom = props.controlsShowing ? "bottom-24" : "bottom-12"; |
||||||
|
|
||||||
|
if (metaType !== "show") return null; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Transition |
||||||
|
animation={animation} |
||||||
|
show={show} |
||||||
|
className="absolute right-12 bottom-0" |
||||||
|
> |
||||||
|
<div |
||||||
|
className={classNames([ |
||||||
|
"absolute bottom-0 right-0 transition-[bottom] duration-200 flex space-x-3", |
||||||
|
bottom, |
||||||
|
])} |
||||||
|
> |
||||||
|
<Button |
||||||
|
className="bg-video-buttons-secondary hover:bg-video-buttons-secondaryHover bg-opacity-90 text-video-buttons-secondaryText" |
||||||
|
onClick={hideNextEpisodeButton} |
||||||
|
> |
||||||
|
Cancel |
||||||
|
</Button> |
||||||
|
<Button className="bg-video-buttons-primary hover:bg-video-buttons-primaryHover text-video-buttons-primaryText flex justify-center items-center"> |
||||||
|
<Icon className="text-xl mr-1" icon={Icons.SKIP_EPISODE} /> |
||||||
|
Next episode |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</Transition> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
export function Card(props: { children: React.ReactNode }) { |
||||||
|
return ( |
||||||
|
<div className="h-full grid grid-rows-[1fr]"> |
||||||
|
<div className="px-6 h-full overflow-y-auto overflow-x-hidden"> |
||||||
|
{props.children} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function CardWithScrollable(props: { children: React.ReactNode }) { |
||||||
|
return ( |
||||||
|
<div className="[&>*]:px-6 h-full grid grid-rows-[auto,1fr] [&>*:nth-child(2)]:overflow-y-auto [&>*:nth-child(2)]:overflow-x-hidden"> |
||||||
|
{props.children} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export function test() {} |
@ -0,0 +1,133 @@ |
|||||||
|
import classNames from "classnames"; |
||||||
|
import { ReactNode } from "react"; |
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { Title } from "@/components/player/internals/ContextMenu/Misc"; |
||||||
|
|
||||||
|
export function Chevron(props: { children?: React.ReactNode }) { |
||||||
|
return ( |
||||||
|
<span className="text-white flex items-center font-medium"> |
||||||
|
{props.children} |
||||||
|
<Icon className="text-xl ml-1 -mr-1.5" icon={Icons.CHEVRON_RIGHT} /> |
||||||
|
</span> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function LinkTitle(props: { |
||||||
|
children: React.ReactNode; |
||||||
|
textClass?: string; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<span |
||||||
|
className={classNames([ |
||||||
|
"font-medium text-left", |
||||||
|
props.textClass || "text-video-context-type-main", |
||||||
|
])} |
||||||
|
> |
||||||
|
{props.children} |
||||||
|
</span> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function BackLink(props: { |
||||||
|
onClick?: () => void; |
||||||
|
children: React.ReactNode; |
||||||
|
rightSide?: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<Title rightSide={props.rightSide}> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
className="-ml-2 p-2 rounded hover:bg-video-context-light hover:bg-opacity-10" |
||||||
|
onClick={props.onClick} |
||||||
|
> |
||||||
|
<Icon className="text-xl" icon={Icons.ARROW_LEFT} /> |
||||||
|
</button> |
||||||
|
<span className="line-clamp-1 break-all">{props.children}</span> |
||||||
|
</Title> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function Link(props: { |
||||||
|
rightSide?: ReactNode; |
||||||
|
clickable?: boolean; |
||||||
|
active?: boolean; |
||||||
|
onClick?: () => void; |
||||||
|
children?: ReactNode; |
||||||
|
className?: string; |
||||||
|
}) { |
||||||
|
const classes = classNames( |
||||||
|
"flex py-2 px-3 rounded w-full -ml-3 w-[calc(100%+1.5rem)]", |
||||||
|
{ |
||||||
|
"cursor-default": !props.clickable, |
||||||
|
"hover:bg-video-context-border cursor-pointer": props.clickable, |
||||||
|
"bg-video-context-border": props.active, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
const content = ( |
||||||
|
<div className={classNames("flex items-center flex-1", props.className)}> |
||||||
|
<div className="flex-1 text-left">{props.children}</div> |
||||||
|
<div>{props.rightSide}</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
|
||||||
|
if (!props.onClick) { |
||||||
|
return <div className={classes}>{content}</div>; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<button type="button" className={classes} onClick={props.onClick}> |
||||||
|
{content} |
||||||
|
</button> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function ChevronLink(props: { |
||||||
|
rightText?: string; |
||||||
|
onClick?: () => void; |
||||||
|
children?: ReactNode; |
||||||
|
active?: boolean; |
||||||
|
}) { |
||||||
|
const rightContent = <Chevron>{props.rightText}</Chevron>; |
||||||
|
return ( |
||||||
|
<Link |
||||||
|
onClick={props.onClick} |
||||||
|
active={props.active} |
||||||
|
clickable |
||||||
|
rightSide={rightContent} |
||||||
|
> |
||||||
|
<LinkTitle>{props.children}</LinkTitle> |
||||||
|
</Link> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function SelectableLink(props: { |
||||||
|
selected?: boolean; |
||||||
|
onClick?: () => void; |
||||||
|
children?: ReactNode; |
||||||
|
disabled?: boolean; |
||||||
|
}) { |
||||||
|
const rightContent = ( |
||||||
|
<Icon |
||||||
|
icon={Icons.CIRCLE_CHECK} |
||||||
|
className="text-xl text-video-context-type-accent" |
||||||
|
/> |
||||||
|
); |
||||||
|
return ( |
||||||
|
<Link |
||||||
|
onClick={props.onClick} |
||||||
|
clickable={!props.disabled} |
||||||
|
rightSide={props.selected ? rightContent : null} |
||||||
|
> |
||||||
|
<LinkTitle |
||||||
|
textClass={classNames({ |
||||||
|
"text-white": props.selected, |
||||||
|
"text-video-context-type-main text-opacity-40": props.disabled, |
||||||
|
})} |
||||||
|
> |
||||||
|
{props.children} |
||||||
|
</LinkTitle> |
||||||
|
</Link> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
|
||||||
|
export function Title(props: { |
||||||
|
children: React.ReactNode; |
||||||
|
rightSide?: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<h3 className="font-bold text-video-context-type-main pb-3 pt-5 border-b border-video-context-border flex justify-between items-center"> |
||||||
|
<div className="flex items-center space-x-3">{props.children}</div> |
||||||
|
<div>{props.rightSide}</div> |
||||||
|
</h3> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function IconButton(props: { icon: Icons; onClick?: () => void }) { |
||||||
|
return ( |
||||||
|
<button type="button" onClick={props.onClick}> |
||||||
|
<Icon className="text-xl" icon={props.icon} /> |
||||||
|
</button> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function Divider() { |
||||||
|
return <hr className="!my-4 border-0 w-full h-px bg-video-context-border" />; |
||||||
|
} |
||||||
|
|
||||||
|
export function SmallText(props: { children: React.ReactNode }) { |
||||||
|
return <p className="text-sm mt-8 font-medium">{props.children}</p>; |
||||||
|
} |
||||||
|
|
||||||
|
export function Anchor(props: { |
||||||
|
children: React.ReactNode; |
||||||
|
onClick: () => void; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<a |
||||||
|
type="button" |
||||||
|
className="text-video-context-type-accent cursor-pointer" |
||||||
|
onClick={props.onClick} |
||||||
|
> |
||||||
|
{props.children} |
||||||
|
</a> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function FieldTitle(props: { children: React.ReactNode }) { |
||||||
|
return <p className="font-medium">{props.children}</p>; |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
import classNames from "classnames"; |
||||||
|
|
||||||
|
export function SectionTitle(props: { children: React.ReactNode }) { |
||||||
|
return ( |
||||||
|
<h3 className="uppercase font-bold text-video-context-type-secondary text-xs pt-8 pl-1 pb-2.5 border-b border-video-context-border"> |
||||||
|
{props.children} |
||||||
|
</h3> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function Section(props: { |
||||||
|
children: React.ReactNode; |
||||||
|
className?: string; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<div className={classNames("pt-4 space-y-1", props.className)}> |
||||||
|
{props.children} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
import * as Cards from "./Cards"; |
||||||
|
import * as Links from "./Links"; |
||||||
|
import * as Misc from "./Misc"; |
||||||
|
import * as Sections from "./Sections"; |
||||||
|
|
||||||
|
export const Menu = { |
||||||
|
...Cards, |
||||||
|
...Links, |
||||||
|
...Sections, |
||||||
|
...Misc, |
||||||
|
}; |
@ -1,176 +0,0 @@ |
|||||||
import classNames from "classnames"; |
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon"; |
|
||||||
|
|
||||||
function Card(props: { children: React.ReactNode }) { |
|
||||||
return ( |
|
||||||
<div className="h-full grid grid-rows-[1fr]"> |
|
||||||
<div className="px-6 h-full overflow-y-auto overflow-x-hidden"> |
|
||||||
{props.children} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function CardWithScrollable(props: { children: React.ReactNode }) { |
|
||||||
return ( |
|
||||||
<div className="[&>*]:px-6 h-full grid grid-rows-[auto,1fr] [&>*:nth-child(2)]:overflow-y-auto [&>*:nth-child(2)]:overflow-x-hidden"> |
|
||||||
{props.children} |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function SectionTitle(props: { children: React.ReactNode }) { |
|
||||||
return ( |
|
||||||
<h3 className="uppercase font-bold text-video-context-type-secondary text-xs pt-8 pl-1 pb-2.5 border-b border-video-context-border"> |
|
||||||
{props.children} |
|
||||||
</h3> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function LinkTitle(props: { children: React.ReactNode; textClass?: string }) { |
|
||||||
return ( |
|
||||||
<span |
|
||||||
className={classNames([ |
|
||||||
"font-medium text-left", |
|
||||||
props.textClass || "text-video-context-type-main", |
|
||||||
])} |
|
||||||
> |
|
||||||
<div>{props.children}</div> |
|
||||||
</span> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function Section(props: { children: React.ReactNode; className?: string }) { |
|
||||||
return ( |
|
||||||
<div className={classNames("pt-4 space-y-1", props.className)}> |
|
||||||
{props.children} |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function Link(props: { |
|
||||||
onClick?: () => void; |
|
||||||
children: React.ReactNode; |
|
||||||
active?: boolean; |
|
||||||
}) { |
|
||||||
const classes = classNames( |
|
||||||
"flex justify-between items-center py-2 pl-3 pr-3 -ml-3 rounded w-full", |
|
||||||
{ |
|
||||||
"cursor-default": !props.onClick, |
|
||||||
"hover:bg-video-context-border": !!props.onClick, |
|
||||||
"bg-video-context-border": props.active, |
|
||||||
} |
|
||||||
); |
|
||||||
const styles = { width: "calc(100% + 1.5rem)" }; |
|
||||||
|
|
||||||
if (!props.onClick) { |
|
||||||
return ( |
|
||||||
<div className={classes} style={styles}> |
|
||||||
{props.children} |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<button |
|
||||||
type="button" |
|
||||||
className={classes} |
|
||||||
style={styles} |
|
||||||
onClick={props.onClick} |
|
||||||
> |
|
||||||
{props.children} |
|
||||||
</button> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function Title(props: { |
|
||||||
children: React.ReactNode; |
|
||||||
rightSide?: React.ReactNode; |
|
||||||
}) { |
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<h3 className="font-bold text-video-context-type-main pb-3 pt-5 border-b border-video-context-border flex justify-between items-center"> |
|
||||||
<div className="flex items-center space-x-3">{props.children}</div> |
|
||||||
<div>{props.rightSide}</div> |
|
||||||
</h3> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function BackLink(props: { |
|
||||||
onClick?: () => void; |
|
||||||
children: React.ReactNode; |
|
||||||
rightSide?: React.ReactNode; |
|
||||||
}) { |
|
||||||
return ( |
|
||||||
<Title rightSide={props.rightSide}> |
|
||||||
<button |
|
||||||
type="button" |
|
||||||
className="-ml-2 p-2 rounded hover:bg-video-context-light hover:bg-opacity-10" |
|
||||||
onClick={props.onClick} |
|
||||||
> |
|
||||||
<Icon className="text-xl" icon={Icons.ARROW_LEFT} /> |
|
||||||
</button> |
|
||||||
<span className="line-clamp-1 break-all">{props.children}</span> |
|
||||||
</Title> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function LinkChevron(props: { children?: React.ReactNode }) { |
|
||||||
return ( |
|
||||||
<span className="text-white flex items-center font-medium"> |
|
||||||
{props.children} |
|
||||||
<Icon className="text-xl ml-1 -mr-1.5" icon={Icons.CHEVRON_RIGHT} /> |
|
||||||
</span> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function IconButton(props: { icon: Icons; onClick?: () => void }) { |
|
||||||
return ( |
|
||||||
<button type="button" onClick={props.onClick}> |
|
||||||
<Icon className="text-xl" icon={props.icon} /> |
|
||||||
</button> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function Divider() { |
|
||||||
return <hr className="!my-4 border-0 w-full h-px bg-video-context-border" />; |
|
||||||
} |
|
||||||
|
|
||||||
function SmallText(props: { children: React.ReactNode }) { |
|
||||||
return <p className="text-sm mt-8 font-medium">{props.children}</p>; |
|
||||||
} |
|
||||||
|
|
||||||
function Anchor(props: { children: React.ReactNode; onClick: () => void }) { |
|
||||||
return ( |
|
||||||
<a |
|
||||||
type="button" |
|
||||||
className="text-video-context-type-accent cursor-pointer" |
|
||||||
onClick={props.onClick} |
|
||||||
> |
|
||||||
{props.children} |
|
||||||
</a> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function FieldTitle(props: { children: React.ReactNode }) { |
|
||||||
return <p className="font-medium">{props.children}</p>; |
|
||||||
} |
|
||||||
|
|
||||||
export const Context = { |
|
||||||
CardWithScrollable, |
|
||||||
SectionTitle, |
|
||||||
LinkChevron, |
|
||||||
IconButton, |
|
||||||
FieldTitle, |
|
||||||
SmallText, |
|
||||||
BackLink, |
|
||||||
LinkTitle, |
|
||||||
Section, |
|
||||||
Divider, |
|
||||||
Anchor, |
|
||||||
Title, |
|
||||||
Link, |
|
||||||
Card, |
|
||||||
}; |
|
Loading…
Reference in new issue