mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-10 22:25:06 +01:00
refactored context menu links, + next episode button styling + mobile UI
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
2c38e8281c
commit
75109ce45c
@ -43,6 +43,7 @@ export enum Icons {
|
||||
TACHOMETER = "tachometer",
|
||||
MAIL = "mail",
|
||||
CIRCLE_CHECK = "circle_check",
|
||||
SKIP_EPISODE = "skip_episode",
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
@ -93,6 +94,7 @@ const iconList: Record<Icons, string> = {
|
||||
tachometer: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 576 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M128 288c-17.67 0-32 14.33-32 32s14.33 32 32 32 32-14.33 32-32-14.33-32-32-32zm154.65-97.08l16.24-48.71c1.16-3.45 3.18-6.35 4.92-9.43-4.73-2.76-9.94-4.78-15.81-4.78-17.67 0-32 14.33-32 32 0 15.78 11.63 28.29 26.65 30.92zM176 176c-17.67 0-32 14.33-32 32s14.33 32 32 32 32-14.33 32-32-14.33-32-32-32zM288 32C128.94 32 0 160.94 0 320c0 52.8 14.25 102.26 39.06 144.8 5.61 9.62 16.3 15.2 27.44 15.2h443c11.14 0 21.83-5.58 27.44-15.2C561.75 422.26 576 372.8 576 320c0-159.06-128.94-288-288-288zm212.27 400H75.73C57.56 397.63 48 359.12 48 320 48 187.66 155.66 80 288 80s240 107.66 240 240c0 39.12-9.56 77.63-27.73 112zM416 320c0 17.67 14.33 32 32 32s32-14.33 32-32-14.33-32-32-32-32 14.33-32 32zm-56.41-182.77c-12.72-4.23-26.16 2.62-30.38 15.17l-45.34 136.01C250.49 290.58 224 318.06 224 352c0 11.72 3.38 22.55 8.88 32h110.25c5.5-9.45 8.88-20.28 8.88-32 0-19.45-8.86-36.66-22.55-48.4l45.34-136.01c4.17-12.57-2.64-26.17-15.21-30.36zM432 208c0-15.8-11.66-28.33-26.72-30.93-.07.21-.07.43-.14.65l-19.5 58.49c4.37 2.24 9.11 3.8 14.36 3.8 17.67-.01 32-14.34 32-32.01z"/></svg>`,
|
||||
mail: `<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19.25 4.125H2.75C2.56766 4.125 2.3928 4.19743 2.26386 4.32636C2.13493 4.4553 2.0625 4.63016 2.0625 4.8125V16.5C2.0625 16.8647 2.20737 17.2144 2.46523 17.4723C2.72309 17.7301 3.07283 17.875 3.4375 17.875H18.5625C18.9272 17.875 19.2769 17.7301 19.5348 17.4723C19.7926 17.2144 19.9375 16.8647 19.9375 16.5V4.8125C19.9375 4.63016 19.8651 4.4553 19.7361 4.32636C19.6072 4.19743 19.4323 4.125 19.25 4.125ZM8.48289 11L3.4375 15.6243V6.3757L8.48289 11ZM9.50039 11.9324L10.5316 12.882C10.6585 12.9985 10.8244 13.0631 10.9966 13.0631C11.1687 13.0631 11.3346 12.9985 11.4615 12.882L12.4927 11.9324L17.4771 16.5H4.51773L9.50039 11.9324ZM13.5171 11L18.5625 6.37484V15.6252L13.5171 11Z" fill="currentColor" /></svg>`,
|
||||
circle_check: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path fill="currentColor" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>`,
|
||||
skip_episode: `<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.625 2.8125V15.1875C14.625 15.3367 14.5657 15.4798 14.4602 15.5852C14.3548 15.6907 14.2117 15.75 14.0625 15.75C13.9133 15.75 13.7702 15.6907 13.6648 15.5852C13.5593 15.4798 13.5 15.3367 13.5 15.1875V10.3198L5.09273 15.5777C4.92342 15.684 4.72878 15.7431 4.52895 15.7489C4.32913 15.7547 4.13139 15.707 3.95621 15.6107C3.78102 15.5144 3.63477 15.373 3.53258 15.2012C3.43039 15.0294 3.37599 14.8333 3.375 14.6334V3.36656C3.37599 3.16666 3.43039 2.97065 3.53258 2.79883C3.63477 2.62702 3.78102 2.48564 3.95621 2.38933C4.13139 2.29303 4.32913 2.2453 4.52895 2.25109C4.72878 2.25688 4.92342 2.31598 5.09273 2.42227L13.5 7.68023V2.8125C13.5 2.66332 13.5593 2.52024 13.6648 2.41475C13.7702 2.30926 13.9133 2.25 14.0625 2.25C14.2117 2.25 14.3548 2.30926 14.4602 2.41475C14.5657 2.52024 14.625 2.66332 14.625 2.8125Z" fill="currentColor"/></svg>`,
|
||||
};
|
||||
|
||||
function ChromeCastButton() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ReactNode, useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsync } from "react-use";
|
||||
|
||||
@ -11,7 +11,7 @@ import { OverlayPage } from "@/components/overlays/OverlayPage";
|
||||
import { OverlayRouter } from "@/components/overlays/OverlayRouter";
|
||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
||||
import { Context } from "@/components/player/internals/ContextUtils";
|
||||
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";
|
||||
@ -56,16 +56,18 @@ function SeasonsView({
|
||||
let content: ReactNode = null;
|
||||
if (seasons) {
|
||||
content = (
|
||||
<Context.Section className="pb-6">
|
||||
<Menu.Section className="pb-6">
|
||||
{seasons?.map((season) => {
|
||||
return (
|
||||
<Context.Link key={season.id} onClick={() => setSeason(season.id)}>
|
||||
<Context.LinkTitle>{season.title}</Context.LinkTitle>
|
||||
<Context.LinkChevron />
|
||||
</Context.Link>
|
||||
<Menu.ChevronLink
|
||||
key={season.id}
|
||||
onClick={() => setSeason(season.id)}
|
||||
>
|
||||
{season.title}
|
||||
</Menu.ChevronLink>
|
||||
);
|
||||
})}
|
||||
</Context.Section>
|
||||
</Menu.Section>
|
||||
);
|
||||
} else if (loadingState.error)
|
||||
content = <CenteredText>Error loading season</CenteredText>;
|
||||
@ -73,10 +75,10 @@ function SeasonsView({
|
||||
content = <CenteredText>Loading...</CenteredText>;
|
||||
|
||||
return (
|
||||
<Context.CardWithScrollable>
|
||||
<Context.Title>{meta?.title}</Context.Title>
|
||||
<Menu.CardWithScrollable>
|
||||
<Menu.Title>{meta?.title}</Menu.Title>
|
||||
{content}
|
||||
</Context.CardWithScrollable>
|
||||
</Menu.CardWithScrollable>
|
||||
);
|
||||
}
|
||||
|
||||
@ -115,37 +117,36 @@ function EpisodesView({
|
||||
content = <CenteredText>Loading...</CenteredText>;
|
||||
else if (loadingState.value) {
|
||||
content = (
|
||||
<Context.Section className="pb-6">
|
||||
<Menu.Section className="pb-6">
|
||||
{loadingState.value.season.episodes.map((ep) => {
|
||||
return (
|
||||
<Context.Link
|
||||
<Menu.ChevronLink
|
||||
key={ep.id}
|
||||
onClick={() => playEpisode(ep.id)}
|
||||
active={ep.id === meta?.episode?.tmdbId}
|
||||
>
|
||||
<Context.LinkTitle>
|
||||
<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">
|
||||
E{ep.number}
|
||||
</span>
|
||||
<span className="line-clamp-1 break-all">{ep.title}</span>
|
||||
</div>
|
||||
</Context.LinkTitle>
|
||||
<Context.LinkChevron />
|
||||
</Context.Link>
|
||||
</Menu.LinkTitle>
|
||||
</Menu.ChevronLink>
|
||||
);
|
||||
})}
|
||||
</Context.Section>
|
||||
</Menu.Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Context.CardWithScrollable>
|
||||
<Context.BackLink onClick={goBack}>
|
||||
<Menu.CardWithScrollable>
|
||||
<Menu.BackLink onClick={goBack}>
|
||||
{loadingState?.value?.season.title || t("videoPlayer.loading")}
|
||||
</Context.BackLink>
|
||||
</Menu.BackLink>
|
||||
{content}
|
||||
</Context.CardWithScrollable>
|
||||
</Menu.CardWithScrollable>
|
||||
);
|
||||
}
|
||||
|
||||
|
84
src/components/player/atoms/NextEpisodeButton.tsx
Normal file
84
src/components/player/atoms/NextEpisodeButton.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
@ -32,7 +32,7 @@ export function ProgressBar() {
|
||||
}, [setDraggingTime, duration, dragPercentage]);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div className="w-full" ref={ref}>
|
||||
<div
|
||||
className="group w-full h-8 flex items-center cursor-pointer"
|
||||
onMouseDown={dragMouseDown}
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
SourceSelectionView,
|
||||
} from "@/components/player/atoms/settings/SourceSelectingView";
|
||||
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
||||
import { Context } from "@/components/player/internals/ContextUtils";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
@ -41,34 +41,34 @@ function SettingsOverlay({ id }: { id: string }) {
|
||||
<SettingsMenu id={id} />
|
||||
</OverlayPage>
|
||||
<OverlayPage id={id} path="/quality" width={343} height={400}>
|
||||
<Context.Card>
|
||||
<Menu.Card>
|
||||
<QualityView id={id} />
|
||||
</Context.Card>
|
||||
</Menu.Card>
|
||||
</OverlayPage>
|
||||
<OverlayPage id={id} path="/captions" width={343} height={431}>
|
||||
<Context.Card>
|
||||
<Menu.Card>
|
||||
<CaptionsView id={id} />
|
||||
</Context.Card>
|
||||
</Menu.Card>
|
||||
</OverlayPage>
|
||||
<OverlayPage id={id} path="/captions/settings" width={343} height={310}>
|
||||
<Context.Card>
|
||||
<Menu.Card>
|
||||
<CaptionSettingsView id={id} />
|
||||
</Context.Card>
|
||||
</Menu.Card>
|
||||
</OverlayPage>
|
||||
<OverlayPage id={id} path="/source" width={343} height={431}>
|
||||
<Context.Card>
|
||||
<Menu.Card>
|
||||
<SourceSelectionView id={id} onChoose={setChosenSourceId} />
|
||||
</Context.Card>
|
||||
</Menu.Card>
|
||||
</OverlayPage>
|
||||
<OverlayPage id={id} path="/source/embeds" width={343} height={431}>
|
||||
<Context.Card>
|
||||
<Menu.Card>
|
||||
<EmbedSelectionView id={id} sourceId={chosenSourceId} />
|
||||
</Context.Card>
|
||||
</Menu.Card>
|
||||
</OverlayPage>
|
||||
<OverlayPage id={id} path="/playback" width={343} height={215}>
|
||||
<Context.Card>
|
||||
<Menu.Card>
|
||||
<PlaybackSettingsView id={id} />
|
||||
</Context.Card>
|
||||
</Menu.Card>
|
||||
</OverlayPage>
|
||||
</OverlayRouter>
|
||||
</Overlay>
|
||||
|
@ -9,7 +9,7 @@ function durationExceedsHour(secs: number): boolean {
|
||||
return secs > 60 * 60;
|
||||
}
|
||||
|
||||
export function Time() {
|
||||
export function Time(props: { short?: boolean }) {
|
||||
const timeFormat = usePlayerStore((s) => s.interface.timeFormat);
|
||||
const setTimeFormat = usePlayerStore((s) => s.setTimeFormat);
|
||||
|
||||
@ -40,16 +40,26 @@ export function Time() {
|
||||
},
|
||||
});
|
||||
|
||||
const timeString = `${formatSeconds(currentTime, hasHours)} / ${formatSeconds(
|
||||
duration,
|
||||
hasHours
|
||||
)}`;
|
||||
const timeFinishedString = `${t("videoPlayer.timeLeft", {
|
||||
timeLeft: formatSeconds(
|
||||
let timeString;
|
||||
let timeFinishedString;
|
||||
if (props.short) {
|
||||
timeString = formatSeconds(currentTime, hasHours);
|
||||
timeFinishedString = `-${formatSeconds(
|
||||
secondsRemaining,
|
||||
durationExceedsHour(secondsRemaining)
|
||||
),
|
||||
})} • ${formattedTimeFinished}`;
|
||||
)}`;
|
||||
} else {
|
||||
timeString = `${formatSeconds(currentTime, hasHours)} / ${formatSeconds(
|
||||
duration,
|
||||
hasHours
|
||||
)}`;
|
||||
timeFinishedString = `${t("videoPlayer.timeLeft", {
|
||||
timeLeft: formatSeconds(
|
||||
secondsRemaining,
|
||||
durationExceedsHour(secondsRemaining)
|
||||
),
|
||||
})} • ${formattedTimeFinished}`;
|
||||
}
|
||||
|
||||
const child =
|
||||
timeFormat === VideoPlayerTimeFormat.REGULAR ? (
|
||||
|
@ -12,3 +12,4 @@ export * from "./Settings";
|
||||
export * from "./Episodes";
|
||||
export * from "./Airplay";
|
||||
export * from "./VolumeChangedPopout";
|
||||
export * from "./NextEpisodeButton";
|
||||
|
@ -2,7 +2,7 @@ import classNames from "classnames";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Context } from "@/components/player/internals/ContextUtils";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { useProgressBar } from "@/hooks/useProgressBar";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
@ -80,7 +80,7 @@ function CaptionSetting(props: {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Context.FieldTitle>{props.label}</Context.FieldTitle>
|
||||
<Menu.FieldTitle>{props.label}</Menu.FieldTitle>
|
||||
<div className="grid items-center grid-cols-[1fr,auto] gap-4">
|
||||
<div ref={ref}>
|
||||
<div
|
||||
@ -165,10 +165,10 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Context.BackLink onClick={() => router.navigate("/captions")}>
|
||||
<Menu.BackLink onClick={() => router.navigate("/captions")}>
|
||||
Custom captions
|
||||
</Context.BackLink>
|
||||
<Context.Section className="space-y-6">
|
||||
</Menu.BackLink>
|
||||
<Menu.Section className="space-y-6">
|
||||
<CaptionSetting
|
||||
label="Text size"
|
||||
max={200}
|
||||
@ -186,7 +186,7 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
||||
textTransformer={(s) => `${s}%`}
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<Context.FieldTitle>Color</Context.FieldTitle>
|
||||
<Menu.FieldTitle>Color</Menu.FieldTitle>
|
||||
<div className="flex justify-center items-center">
|
||||
{colors.map((v) => (
|
||||
<ColorOption
|
||||
@ -197,7 +197,7 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Context.Section>
|
||||
</Menu.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import { FlagIcon } from "@/components/FlagIcon";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Context } from "@/components/player/internals/ContextUtils";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { Caption } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
@ -26,25 +24,14 @@ export function CaptionOption(props: {
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-[auto,1fr,auto] items-center gap-3 rounded -ml-3 -mr-3 px-3 py-2 cursor-pointer hover:bg-video-context-border"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div>
|
||||
<FlagIcon countryCode={props.countryCode} />
|
||||
</div>
|
||||
<span
|
||||
className={classNames(props.selected && "text-white", "font-medium")}
|
||||
>
|
||||
{props.children}
|
||||
<SelectableLink selected={props.selected} onClick={props.onClick}>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-3">
|
||||
<FlagIcon countryCode={props.countryCode} />
|
||||
</span>
|
||||
<span>{props.children}</span>
|
||||
</span>
|
||||
{props.selected ? (
|
||||
<Icon
|
||||
icon={Icons.CIRCLE_CHECK}
|
||||
className="text-xl text-video-context-type-accent"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</SelectableLink>
|
||||
);
|
||||
}
|
||||
|
||||
@ -75,7 +62,7 @@ export function CaptionsView({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Context.BackLink
|
||||
<Menu.BackLink
|
||||
onClick={() => router.navigate("/")}
|
||||
rightSide={
|
||||
<button
|
||||
@ -87,8 +74,8 @@ export function CaptionsView({ id }: { id: string }) {
|
||||
}
|
||||
>
|
||||
Captions
|
||||
</Context.BackLink>
|
||||
<Context.Section>
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<CaptionOption onClick={() => disableCaption()} selected={!lang}>
|
||||
Off
|
||||
</CaptionOption>
|
||||
@ -102,7 +89,7 @@ export function CaptionsView({ id }: { id: string }) {
|
||||
{v.title}
|
||||
</CaptionOption>
|
||||
))}
|
||||
</Context.Section>
|
||||
</Menu.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import classNames from "classnames";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { Context } from "@/components/player/internals/ContextUtils";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
@ -49,19 +49,19 @@ export function PlaybackSettingsView({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Context.BackLink onClick={() => router.navigate("/")}>
|
||||
<Menu.BackLink onClick={() => router.navigate("/")}>
|
||||
Playback settings
|
||||
</Context.BackLink>
|
||||
<Context.Section>
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<div className="space-y-4 mt-3">
|
||||
<Context.FieldTitle>Playback speed</Context.FieldTitle>
|
||||
<Menu.FieldTitle>Playback speed</Menu.FieldTitle>
|
||||
<ButtonList
|
||||
options={options}
|
||||
selected={playbackRate}
|
||||
onClick={setPlaybackRate}
|
||||
/>
|
||||
</div>
|
||||
</Context.Section>
|
||||
</Menu.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { Toggle } from "@/components/buttons/Toggle";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Context } from "@/components/player/internals/ContextUtils";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import {
|
||||
@ -12,32 +12,6 @@ import {
|
||||
} from "@/stores/player/utils/qualities";
|
||||
import { useQualityStore } from "@/stores/quality";
|
||||
|
||||
export function QualityOption(props: {
|
||||
children: React.ReactNode;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
let textClasses;
|
||||
if (props.selected) textClasses = "text-white";
|
||||
if (props.disabled)
|
||||
textClasses = "text-video-context-type-main text-opacity-40";
|
||||
|
||||
return (
|
||||
<Context.Link onClick={props.disabled ? undefined : props.onClick}>
|
||||
<Context.LinkTitle textClass={textClasses}>
|
||||
{props.children}
|
||||
</Context.LinkTitle>
|
||||
{props.selected ? (
|
||||
<Icon
|
||||
icon={Icons.CIRCLE_CHECK}
|
||||
className="text-xl text-video-context-type-accent"
|
||||
/>
|
||||
) : null}
|
||||
</Context.Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function QualityView({ id }: { id: string }) {
|
||||
const router = useOverlayRouter(id);
|
||||
const availableQualities = usePlayerStore((s) => s.qualities);
|
||||
@ -70,12 +44,12 @@ export function QualityView({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Context.BackLink onClick={() => router.navigate("/")}>
|
||||
<Menu.BackLink onClick={() => router.navigate("/")}>
|
||||
Quality
|
||||
</Context.BackLink>
|
||||
<Context.Section>
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
{allVisibleQualities.map((v) => (
|
||||
<QualityOption
|
||||
<SelectableLink
|
||||
key={v}
|
||||
selected={v === currentQuality}
|
||||
onClick={
|
||||
@ -84,21 +58,22 @@ export function QualityView({ id }: { id: string }) {
|
||||
disabled={!availableQualities.includes(v)}
|
||||
>
|
||||
{qualityToString(v)}
|
||||
</QualityOption>
|
||||
</SelectableLink>
|
||||
))}
|
||||
<Context.Divider />
|
||||
<Context.Link>
|
||||
<Context.LinkTitle>Automatic quality</Context.LinkTitle>
|
||||
<Toggle onClick={changeAutomatic} enabled={autoQuality} />
|
||||
</Context.Link>
|
||||
<Context.SmallText>
|
||||
<Menu.Divider />
|
||||
<Menu.Link
|
||||
rightSide={<Toggle onClick={changeAutomatic} enabled={autoQuality} />}
|
||||
>
|
||||
Automatic quality
|
||||
</Menu.Link>
|
||||
<Menu.SmallText>
|
||||
You can try{" "}
|
||||
<Context.Anchor onClick={() => router.navigate("/source")}>
|
||||
<Menu.Anchor onClick={() => router.navigate("/source")}>
|
||||
switching source
|
||||
</Context.Anchor>{" "}
|
||||
</Menu.Anchor>{" "}
|
||||
to get different quality options.
|
||||
</Context.SmallText>
|
||||
</Context.Section>
|
||||
</Menu.SmallText>
|
||||
</Menu.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Toggle } from "@/components/buttons/Toggle";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { Context } from "@/components/player/internals/ContextUtils";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { qualityToString } from "@/stores/player/utils/qualities";
|
||||
@ -35,45 +35,51 @@ export function SettingsMenu({ id }: { id: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Context.Card>
|
||||
<Context.SectionTitle>Video settings</Context.SectionTitle>
|
||||
<Context.Section>
|
||||
<Context.Link onClick={() => router.navigate("/quality")}>
|
||||
<Context.LinkTitle>Quality</Context.LinkTitle>
|
||||
<Context.LinkChevron>
|
||||
{currentQuality ? qualityToString(currentQuality) : ""}
|
||||
</Context.LinkChevron>
|
||||
</Context.Link>
|
||||
<Context.Link onClick={() => router.navigate("/source")}>
|
||||
<Context.LinkTitle>Video source</Context.LinkTitle>
|
||||
<Context.LinkChevron>{sourceName}</Context.LinkChevron>
|
||||
</Context.Link>
|
||||
<Context.Link>
|
||||
<Context.LinkTitle>Download</Context.LinkTitle>
|
||||
<Context.IconButton icon={Icons.DOWNLOAD} />
|
||||
</Context.Link>
|
||||
</Context.Section>
|
||||
<Menu.Card>
|
||||
<Menu.SectionTitle>Video settings</Menu.SectionTitle>
|
||||
<Menu.Section>
|
||||
<Menu.ChevronLink
|
||||
onClick={() => router.navigate("/quality")}
|
||||
rightText={currentQuality ? qualityToString(currentQuality) : ""}
|
||||
>
|
||||
Quality
|
||||
</Menu.ChevronLink>
|
||||
<Menu.ChevronLink
|
||||
onClick={() => router.navigate("/source")}
|
||||
rightText={sourceName}
|
||||
>
|
||||
Video source
|
||||
</Menu.ChevronLink>
|
||||
<Menu.Link
|
||||
clickable
|
||||
rightSide={<Icon className="text-xl" icon={Icons.DOWNLOAD} />}
|
||||
>
|
||||
Download
|
||||
</Menu.Link>
|
||||
</Menu.Section>
|
||||
|
||||
<Context.SectionTitle>Viewing Experience</Context.SectionTitle>
|
||||
<Context.Section>
|
||||
<Context.Link>
|
||||
<Context.LinkTitle>Enable Captions</Context.LinkTitle>
|
||||
<Toggle
|
||||
enabled={subtitlesEnabled}
|
||||
onClick={() => toggleSubtitles()}
|
||||
/>
|
||||
</Context.Link>
|
||||
<Context.Link onClick={() => router.navigate("/captions")}>
|
||||
<Context.LinkTitle>Caption settings</Context.LinkTitle>
|
||||
<Context.LinkChevron>
|
||||
{selectedCaptionLanguage ?? ""}
|
||||
</Context.LinkChevron>
|
||||
</Context.Link>
|
||||
<Context.Link onClick={() => router.navigate("/playback")}>
|
||||
<Context.LinkTitle>Playback settings</Context.LinkTitle>
|
||||
<Context.LinkChevron />
|
||||
</Context.Link>
|
||||
</Context.Section>
|
||||
</Context.Card>
|
||||
<Menu.SectionTitle>Viewing Experience</Menu.SectionTitle>
|
||||
<Menu.Section>
|
||||
<Menu.Link
|
||||
rightSide={
|
||||
<Toggle
|
||||
enabled={subtitlesEnabled}
|
||||
onClick={() => toggleSubtitles()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Enable Captions
|
||||
</Menu.Link>
|
||||
<Menu.ChevronLink
|
||||
onClick={() => router.navigate("/captions")}
|
||||
rightText={selectedCaptionLanguage}
|
||||
>
|
||||
Caption settings
|
||||
</Menu.ChevronLink>
|
||||
<Menu.ChevronLink onClick={() => router.navigate("/playback")}>
|
||||
Playback settings
|
||||
</Menu.ChevronLink>
|
||||
</Menu.Section>
|
||||
</Menu.Card>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import classNames from "classnames";
|
||||
import { ReactNode, useEffect, useMemo, useRef } from "react";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Context } from "@/components/player/internals/ContextUtils";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
||||
import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { metaToScrapeMedia } from "@/stores/player/slices/source";
|
||||
@ -20,31 +19,6 @@ export interface EmbedSelectionViewProps {
|
||||
sourceId: string | null;
|
||||
}
|
||||
|
||||
export function SourceOption(props: {
|
||||
children: React.ReactNode;
|
||||
selected?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onClick={props.onClick}
|
||||
className="grid grid-cols-[1fr,auto] items-center gap-3 rounded -ml-3 -mr-3 px-3 py-2 cursor-pointer hover:bg-video-context-border"
|
||||
>
|
||||
<span
|
||||
className={classNames(props.selected && "text-white", "font-medium")}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
{props.selected ? (
|
||||
<Icon
|
||||
icon={Icons.CIRCLE_CHECK}
|
||||
className="text-xl text-video-context-type-accent"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmbedOption(props: {
|
||||
embedId: string;
|
||||
url: string;
|
||||
@ -76,12 +50,12 @@ export function EmbedOption(props: {
|
||||
else if (request.error) content = <span>Failed to scrape</span>;
|
||||
|
||||
return (
|
||||
<SourceOption onClick={run}>
|
||||
<SelectableLink onClick={run}>
|
||||
<span className="flex flex-col">
|
||||
<span>{embedName}</span>
|
||||
{content}
|
||||
</span>
|
||||
</SourceOption>
|
||||
</SelectableLink>
|
||||
);
|
||||
}
|
||||
|
||||
@ -150,10 +124,10 @@ export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Context.BackLink onClick={() => router.navigate("/source")}>
|
||||
<Menu.BackLink onClick={() => router.navigate("/source")}>
|
||||
{sourceName}
|
||||
</Context.BackLink>
|
||||
<Context.Section>{content}</Context.Section>
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>{content}</Menu.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -174,12 +148,12 @@ export function SourceSelectionView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Context.BackLink onClick={() => router.navigate("/")}>
|
||||
<Menu.BackLink onClick={() => router.navigate("/")}>
|
||||
Sources
|
||||
</Context.BackLink>
|
||||
<Context.Section>
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
{sources.map((v) => (
|
||||
<SourceOption
|
||||
<SelectableLink
|
||||
key={v.id}
|
||||
onClick={() => {
|
||||
onChoose?.(v.id);
|
||||
@ -188,9 +162,9 @@ export function SourceSelectionView({
|
||||
selected={v.id === currentSourceId}
|
||||
>
|
||||
{v.name}
|
||||
</SourceOption>
|
||||
</SelectableLink>
|
||||
))}
|
||||
</Context.Section>
|
||||
</Menu.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
17
src/components/player/internals/ContextMenu/Cards.tsx
Normal file
17
src/components/player/internals/ContextMenu/Cards.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
1
src/components/player/internals/ContextMenu/Items.tsx
Normal file
1
src/components/player/internals/ContextMenu/Items.tsx
Normal file
@ -0,0 +1 @@
|
||||
export function test() {}
|
133
src/components/player/internals/ContextMenu/Links.tsx
Normal file
133
src/components/player/internals/ContextMenu/Links.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
50
src/components/player/internals/ContextMenu/Misc.tsx
Normal file
50
src/components/player/internals/ContextMenu/Misc.tsx
Normal file
@ -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>;
|
||||
}
|
20
src/components/player/internals/ContextMenu/Sections.tsx
Normal file
20
src/components/player/internals/ContextMenu/Sections.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
11
src/components/player/internals/ContextMenu/index.ts
Normal file
11
src/components/player/internals/ContextMenu/index.ts
Normal file
@ -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,
|
||||
};
|
@ -3,6 +3,7 @@ import { ReactNode } from "react";
|
||||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
import { Player } from "@/components/player";
|
||||
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
@ -16,6 +17,7 @@ export interface PlayerPartProps {
|
||||
export function PlayerPart(props: PlayerPartProps) {
|
||||
const { showTargets, showTouchTargets } = useShouldShowControls();
|
||||
const status = usePlayerStore((s) => s.status);
|
||||
const { isMobile } = useIsMobile();
|
||||
|
||||
return (
|
||||
<Player.Container onLoad={props.onLoad}>
|
||||
@ -53,23 +55,25 @@ export function PlayerPart(props: PlayerPartProps) {
|
||||
<div className="hidden sm:flex items-center justify-end">
|
||||
<BrandPill />
|
||||
</div>
|
||||
<div className="flex sm:hidden items-center justify-end">
|
||||
<Player.Airplay />
|
||||
</div>
|
||||
</div>
|
||||
</Player.TopControls>
|
||||
|
||||
<Player.BottomControls show={showTargets}>
|
||||
<Player.ProgressBar />
|
||||
<div className="flex justify-between">
|
||||
<Player.LeftSideControls className="hidden lg:flex">
|
||||
<div className="flex items-center space-x-3">
|
||||
{isMobile ? <Player.Time short /> : null}
|
||||
<Player.ProgressBar />
|
||||
</div>
|
||||
<div className="hidden lg:flex justify-between">
|
||||
<Player.LeftSideControls>
|
||||
<Player.Pause />
|
||||
<Player.SkipBackward />
|
||||
<Player.SkipForward />
|
||||
<Player.Volume />
|
||||
<Player.Time />
|
||||
</Player.LeftSideControls>
|
||||
<Player.LeftSideControls className="flex lg:hidden">
|
||||
{/* Do mobile controls here :) */}
|
||||
<Player.Time />
|
||||
</Player.LeftSideControls>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Player.Episodes onChange={props.onMetaChange} />
|
||||
<Player.Airplay />
|
||||
@ -77,9 +81,20 @@ export function PlayerPart(props: PlayerPartProps) {
|
||||
<Player.Fullscreen />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[2.5rem,1fr,2.5rem] gap-3 lg:hidden">
|
||||
<div />
|
||||
<div className="flex justify-center space-x-3">
|
||||
<Player.Episodes />
|
||||
<Player.Settings />
|
||||
</div>
|
||||
<div>
|
||||
<Player.Fullscreen />
|
||||
</div>
|
||||
</div>
|
||||
</Player.BottomControls>
|
||||
|
||||
<Player.VolumeChangedPopout />
|
||||
<Player.NextEpisodeButton controlsShowing={showTargets} />
|
||||
</Player.Container>
|
||||
);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ export interface InterfaceSlice {
|
||||
hovering: PlayerHoverState;
|
||||
lastHoveringState: PlayerHoverState;
|
||||
canAirplay: boolean;
|
||||
hideNextEpisodeBtn: boolean;
|
||||
|
||||
volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently?
|
||||
volumeChangedWithKeybindDebounce: NodeJS.Timeout | null; // debounce for the duration of the "volume changed thingamajig"
|
||||
@ -33,6 +34,7 @@ export interface InterfaceSlice {
|
||||
setHoveringLeftControls(state: boolean): void;
|
||||
setHasOpenOverlay(state: boolean): void;
|
||||
setLastVolume(state: number): void;
|
||||
hideNextEpisodeButton(): void;
|
||||
}
|
||||
|
||||
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||
@ -48,6 +50,7 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||
volumeChangedWithKeybindDebounce: null,
|
||||
timeFormat: VideoPlayerTimeFormat.REGULAR,
|
||||
canAirplay: false,
|
||||
hideNextEpisodeBtn: false,
|
||||
},
|
||||
|
||||
setLastVolume(state) {
|
||||
@ -84,4 +87,9 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||
s.interface.leftControlHovering = state;
|
||||
});
|
||||
},
|
||||
hideNextEpisodeButton() {
|
||||
set((s) => {
|
||||
s.interface.hideNextEpisodeBtn = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -109,6 +109,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
|
||||
setMeta(meta) {
|
||||
set((s) => {
|
||||
s.meta = meta;
|
||||
s.interface.hideNextEpisodeBtn = false;
|
||||
});
|
||||
},
|
||||
setCaption(caption) {
|
||||
|
@ -131,6 +131,15 @@ module.exports = {
|
||||
set: "#A75FC9"
|
||||
},
|
||||
|
||||
buttons: {
|
||||
secondary: "#161F25",
|
||||
secondaryText: "#8EA3B0",
|
||||
secondaryHover: "#1B262E",
|
||||
primary: "#fff",
|
||||
primaryText: "#000",
|
||||
primaryHover: "#dedede"
|
||||
},
|
||||
|
||||
context: {
|
||||
background: "#0C1216",
|
||||
light: "#4D79A8",
|
||||
|
Loading…
Reference in New Issue
Block a user