24 changed files with 519 additions and 390 deletions
@ -0,0 +1,84 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1 @@
|
||||
export function test() {} |
@ -0,0 +1,133 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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