mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-02-19 17:50:08 -05:00
Use tailwind in player
This commit is contained in:
parent
d587d119fd
commit
546935d988
@ -1,15 +1,18 @@
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { type Stylable, useYoshiki } from "yoshiki/native";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const CircularProgress = ({
|
||||
size = 48,
|
||||
tickness = 5,
|
||||
color,
|
||||
...props
|
||||
}: { size?: number; tickness?: number; color?: string } & Stylable) => {
|
||||
const { theme } = useYoshiki();
|
||||
|
||||
}: {
|
||||
tickness?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<ActivityIndicator size={size} color={color ?? theme.accent} {...props} />
|
||||
<ActivityIndicator
|
||||
colorClassName={cn("accent-accent")}
|
||||
size="large"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,8 +5,7 @@ import {
|
||||
View,
|
||||
type ViewProps,
|
||||
} from "react-native";
|
||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
import { focusReset } from "./utils";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const Slider = ({
|
||||
progress,
|
||||
@ -17,7 +16,7 @@ export const Slider = ({
|
||||
startSeek,
|
||||
endSeek,
|
||||
onHover,
|
||||
size = 6,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
progress: number;
|
||||
@ -31,9 +30,7 @@ export const Slider = ({
|
||||
position: number | null,
|
||||
layout: { x: number; y: number; width: number; height: number },
|
||||
) => void;
|
||||
size?: number;
|
||||
} & Partial<ViewProps>) => {
|
||||
const { css } = useYoshiki();
|
||||
const ref = useRef<View>(null);
|
||||
const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
||||
const [isSeeking, setSeek] = useState(false);
|
||||
@ -41,8 +38,6 @@ export const Slider = ({
|
||||
const [isFocus, setFocus] = useState(false);
|
||||
const smallBar = !(isSeeking || isHover || isFocus);
|
||||
|
||||
const ts = (value: number) => px(value * size);
|
||||
|
||||
const change = (event: GestureResponderEvent) => {
|
||||
event.preventDefault();
|
||||
const locationX = Platform.select({
|
||||
@ -57,7 +52,6 @@ export const Slider = ({
|
||||
ref={ref}
|
||||
// @ts-expect-error Web only
|
||||
onMouseEnter={() => setHover(true)}
|
||||
// @ts-expect-error Web only
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
onHover?.(null, layout);
|
||||
@ -104,98 +98,39 @@ export const Slider = ({
|
||||
break;
|
||||
}
|
||||
}}
|
||||
{...css(
|
||||
{
|
||||
paddingVertical: ts(1),
|
||||
// @ts-expect-error Web only
|
||||
cursor: "pointer",
|
||||
...focusReset,
|
||||
},
|
||||
props,
|
||||
)}
|
||||
className={cn("cursor-pointer justify-center py-2 outline-0", className)}
|
||||
{...props}
|
||||
>
|
||||
<View
|
||||
{...css([
|
||||
{
|
||||
width: percent(100),
|
||||
height: ts(1),
|
||||
bg: (theme) => theme.overlay0,
|
||||
},
|
||||
smallBar && { transform: "scaleY(0.4)" as any },
|
||||
])}
|
||||
className={cn(
|
||||
"h-2 w-full overflow-hidden rounded bg-slate-400",
|
||||
smallBar && "scale-y-50",
|
||||
)}
|
||||
>
|
||||
{subtleProgress !== undefined && (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
bg: (theme) => theme.overlay1,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
{
|
||||
style: {
|
||||
width: percent((subtleProgress / max) * 100),
|
||||
},
|
||||
},
|
||||
)}
|
||||
className={cn("absolute left-0 h-full bg-slate-300")}
|
||||
style={{ width: `${(subtleProgress / max) * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
bg: (theme) => theme.accent,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
{
|
||||
// In an inline style because yoshiki's insertion can not catch up with the constant redraw
|
||||
style: {
|
||||
width: percent((progress / max) * 100),
|
||||
},
|
||||
},
|
||||
)}
|
||||
className="absolute left-0 h-full bg-accent"
|
||||
style={{ width: `${(progress / max) * 100}%` }}
|
||||
/>
|
||||
{markers?.map((x) => (
|
||||
<View
|
||||
key={x}
|
||||
{...css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: percent(Math.min(100, (x / max) * 100)),
|
||||
bg: (theme) => theme.accent,
|
||||
width: ts(0.5),
|
||||
height: ts(1),
|
||||
})}
|
||||
className="absolute h-full w-1 bg-accent"
|
||||
style={{ left: `${Math.min(100, (x / max) * 100)}%` }}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<View
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
marginY: ts(0.5),
|
||||
bg: (theme) => theme.accent,
|
||||
width: ts(2),
|
||||
height: ts(2),
|
||||
borderRadius: ts(1),
|
||||
marginLeft: ts(-1),
|
||||
},
|
||||
smallBar && { opacity: 0 },
|
||||
],
|
||||
{
|
||||
style: {
|
||||
left: percent((progress / max) * 100),
|
||||
},
|
||||
},
|
||||
className={cn(
|
||||
"absolute my-1 ml-[-6px] h-3 w-3 rounded-full bg-accent",
|
||||
smallBar && "opacity-0",
|
||||
)}
|
||||
style={{ left: `${(progress / max) * 100}%` }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -2,7 +2,6 @@ import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View, type ViewProps } from "react-native";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import {
|
||||
H1,
|
||||
IconButton,
|
||||
@ -10,29 +9,19 @@ import {
|
||||
Skeleton,
|
||||
tooltip,
|
||||
} from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const Back = ({
|
||||
name,
|
||||
showHref,
|
||||
className,
|
||||
...props
|
||||
}: { showHref?: string; name?: string } & ViewProps) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: percent(0.33),
|
||||
color: "white",
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<View className={cn("flex-row items-center", className)} {...props}>
|
||||
<IconButton
|
||||
icon={ArrowBack}
|
||||
as={PressableFeedback}
|
||||
@ -40,20 +29,14 @@ export const Back = ({
|
||||
if (router.canGoBack()) router.back();
|
||||
else if (showHref) router.navigate(showHref);
|
||||
}}
|
||||
className="my-4 ml-4"
|
||||
iconClassName="fill-slate-200"
|
||||
{...tooltip(t("player.back"))}
|
||||
/>
|
||||
{name ? (
|
||||
<H1
|
||||
{...css({
|
||||
alignSelf: "center",
|
||||
fontSize: rem(1.5),
|
||||
marginLeft: rem(1),
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</H1>
|
||||
<H1 className="my-4 ml-4 text-2xl text-slate-200">{name}</H1>
|
||||
) : (
|
||||
<Skeleton {...css({ width: rem(5) })} />
|
||||
<Skeleton className="my-4 w-20" />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -2,9 +2,13 @@ import SkipNext from "@material-symbols/svg-400/rounded/skip_next-fill.svg";
|
||||
import SkipPrevious from "@material-symbols/svg-400/rounded/skip_previous-fill.svg";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View, type ViewProps } from "react-native";
|
||||
import {
|
||||
Platform,
|
||||
type PressableProps,
|
||||
View,
|
||||
type ViewProps,
|
||||
} from "react-native";
|
||||
import type { VideoPlayer } from "react-native-video";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import type { Chapter, KImage } from "~/models";
|
||||
import {
|
||||
H2,
|
||||
@ -14,9 +18,9 @@ import {
|
||||
Poster,
|
||||
Skeleton,
|
||||
tooltip,
|
||||
ts,
|
||||
useIsTouch,
|
||||
} from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
import { FullscreenButton, PlayButton, VolumeSlider } from "./misc";
|
||||
import { ProgressBar, ProgressText } from "./progress";
|
||||
import { AudioMenu, QualityMenu, SubtitleMenu, VideoMenu } from "./tracks-menu";
|
||||
@ -29,6 +33,7 @@ export const BottomControls = ({
|
||||
previous,
|
||||
next,
|
||||
setMenu,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
@ -39,52 +44,26 @@ export const BottomControls = ({
|
||||
next?: string | null;
|
||||
setMenu: (isOpen: boolean) => void;
|
||||
} & ViewProps) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
flexDirection: "row",
|
||||
padding: ts(1),
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
width: "15%",
|
||||
display: { xs: "none", sm: "flex" },
|
||||
position: "relative",
|
||||
})}
|
||||
>
|
||||
<View className={cn("flex-row p-2", className)} {...props}>
|
||||
<View className="m-4 w-1/5 max-w-50 max-sm:hidden">
|
||||
{poster !== undefined ? (
|
||||
<Poster
|
||||
src={poster}
|
||||
quality="low"
|
||||
layout={{ width: percent(100) }}
|
||||
{...(css({ position: "absolute", bottom: 0 }) as any)}
|
||||
className="absolute bottom-0 w-full"
|
||||
/>
|
||||
) : (
|
||||
<Poster.Loader
|
||||
layout={{ width: percent(100) }}
|
||||
{...(css({ position: "absolute", bottom: 0 }) as any)}
|
||||
/>
|
||||
<Poster.Loader className="absolute bottom-0 w-full" />
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
{...css({
|
||||
marginHorizontal: { xs: ts(0.5), sm: ts(3) },
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
})}
|
||||
>
|
||||
<View className="my-1 mr-4 flex-1 max-sm:ml-4 sm:my-6">
|
||||
{name ? (
|
||||
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
||||
<H2 numberOfLines={1} className="pb-2 text-slate-200">
|
||||
{name}
|
||||
</H2>
|
||||
) : (
|
||||
<Skeleton {...css({ width: rem(15), height: rem(2) })} />
|
||||
<Skeleton className="h-8 w-1/5" />
|
||||
)}
|
||||
<ProgressBar player={player} chapters={chapters} />
|
||||
<ControlButtons
|
||||
@ -103,71 +82,79 @@ const ControlButtons = ({
|
||||
previous,
|
||||
next,
|
||||
setMenu,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
previous?: string | null;
|
||||
next?: string | null;
|
||||
setMenu: (isOpen: boolean) => void;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const isTouch = useIsTouch();
|
||||
|
||||
const spacing = css({ marginHorizontal: ts(1) });
|
||||
const menuProps = {
|
||||
onMenuOpen: () => setMenu(true),
|
||||
onMenuClose: () => setMenu(false),
|
||||
...spacing,
|
||||
} satisfies Partial<ComponentProps<typeof Menu>>;
|
||||
className: "mr-4",
|
||||
iconClassName: "fill-slate-200",
|
||||
} satisfies Partial<
|
||||
ComponentProps<
|
||||
typeof Menu<ComponentProps<typeof IconButton<PressableProps>>>
|
||||
>
|
||||
>;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
flexDirection: "row",
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
props,
|
||||
)}
|
||||
className={cn("flex-1 flex-row flex-wrap justify-between", className)}
|
||||
{...props}
|
||||
>
|
||||
<View {...css({ flexDirection: "row" })}>
|
||||
<View className="flex-row items-center">
|
||||
{!isTouch && (
|
||||
<View {...css({ flexDirection: "row" })}>
|
||||
<View className="flex-row">
|
||||
{previous && (
|
||||
<IconButton
|
||||
icon={SkipPrevious}
|
||||
as={Link}
|
||||
href={`/watch/${previous}`}
|
||||
replace
|
||||
className="mr-4"
|
||||
iconClassName="fill-slate-200"
|
||||
{...tooltip(t("player.previous"), true)}
|
||||
{...spacing}
|
||||
/>
|
||||
)}
|
||||
<PlayButton player={player} {...spacing} />
|
||||
<PlayButton
|
||||
player={player}
|
||||
className="mr-4"
|
||||
iconClassName="fill-slate-200"
|
||||
/>
|
||||
{next && (
|
||||
<IconButton
|
||||
icon={SkipNext}
|
||||
as={Link}
|
||||
href={`/watch/${next}`}
|
||||
replace
|
||||
className="mr-4"
|
||||
iconClassName="fill-slate-200"
|
||||
{...tooltip(t("player.next"), true)}
|
||||
{...spacing}
|
||||
/>
|
||||
)}
|
||||
{Platform.OS === "web" && <VolumeSlider player={player} />}
|
||||
{Platform.OS === "web" && (
|
||||
<VolumeSlider player={player} iconClassName="fill-slate-200" />
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<ProgressText player={player} {...spacing} />
|
||||
<ProgressText player={player} className="mx-2 text-slate-300" />
|
||||
</View>
|
||||
<View {...css({ flexDirection: "row" })}>
|
||||
<View className="flex-row">
|
||||
<SubtitleMenu player={player} {...menuProps} />
|
||||
<AudioMenu player={player} {...menuProps} />
|
||||
<VideoMenu player={player} {...menuProps} />
|
||||
<QualityMenu player={player} {...menuProps} />
|
||||
{Platform.OS === "web" && <FullscreenButton {...spacing} />}
|
||||
{Platform.OS === "web" && (
|
||||
<FullscreenButton className="mr-4" iconClassName="fill-slate-200" />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ViewProps } from "react-native";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { View } from "react-native";
|
||||
import type { VideoPlayer } from "react-native-video";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import type { Chapter, KImage } from "~/models";
|
||||
import { useIsTouch } from "~/primitives";
|
||||
import { Back } from "./back";
|
||||
@ -30,8 +28,6 @@ export const Controls = ({
|
||||
previous?: string | null;
|
||||
next?: string | null;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTouch = useIsTouch();
|
||||
|
||||
const [hover, setHover] = useState(false);
|
||||
@ -53,28 +49,17 @@ export const Controls = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View {...css(StyleSheet.absoluteFillObject)}>
|
||||
<View className="absolute inset-0">
|
||||
<TouchControls
|
||||
player={player}
|
||||
forceShow={hover || menuOpened}
|
||||
{...css(StyleSheet.absoluteFillObject)}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<Back
|
||||
showHref={showHref}
|
||||
name={name}
|
||||
{...css(
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
paddingTop: insets.top,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
},
|
||||
hoverControls,
|
||||
)}
|
||||
className="absolute top-0 w-full bg-slate-900/50 px-safe pt-safe"
|
||||
{...hoverControls}
|
||||
/>
|
||||
{isTouch && (
|
||||
<MiddleControls player={player} previous={previous} next={next} />
|
||||
@ -87,21 +72,10 @@ export const Controls = ({
|
||||
previous={previous}
|
||||
next={next}
|
||||
setMenu={setMenu}
|
||||
{...css(
|
||||
{
|
||||
// Fixed is used because firefox android make the hover disappear under the navigation bar in absolute
|
||||
// position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
hoverControls,
|
||||
)}
|
||||
// Fixed is used because firefox android make the hover disappear under the navigation bar in absolute
|
||||
// position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
|
||||
className="absolute bottom-0 w-full bg-slate-900/50 px-safe pt-safe"
|
||||
{...hoverControls}
|
||||
/>
|
||||
</TouchControls>
|
||||
</View>
|
||||
|
||||
@ -2,59 +2,53 @@ import SkipNext from "@material-symbols/svg-400/rounded/skip_next-fill.svg";
|
||||
import SkipPrevious from "@material-symbols/svg-400/rounded/skip_previous-fill.svg";
|
||||
import { View } from "react-native";
|
||||
import type { VideoPlayer } from "react-native-video";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { IconButton, Link, ts } from "~/primitives";
|
||||
import { IconButton, Link } from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
import { PlayButton } from "./misc";
|
||||
|
||||
export const MiddleControls = ({
|
||||
player,
|
||||
previous,
|
||||
next,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
previous?: string | null;
|
||||
next?: string | null;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
const common = css({
|
||||
backgroundColor: (theme) => theme.darkOverlay,
|
||||
marginHorizontal: ts(3),
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
props,
|
||||
className={cn(
|
||||
"absolute inset-0 flex-row items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<IconButton
|
||||
icon={SkipPrevious}
|
||||
as={Link}
|
||||
href={previous ?? ""}
|
||||
href={previous}
|
||||
replace
|
||||
size={ts(4)}
|
||||
{...css([!previous && { opacity: 0, pointerEvents: "none" }], common)}
|
||||
className={cn(
|
||||
"mx-12 h-16 w-16 bg-gray-800/70",
|
||||
!previous && "pointer-events-none opacity-0",
|
||||
)}
|
||||
/>
|
||||
<PlayButton
|
||||
player={player}
|
||||
className={cn("mx-12 h-32 w-32 bg-gray-800/70")}
|
||||
/>
|
||||
<PlayButton player={player} size={ts(8)} {...common} />
|
||||
<IconButton
|
||||
icon={SkipNext}
|
||||
as={Link}
|
||||
href={next ?? ""}
|
||||
href={next}
|
||||
replace
|
||||
size={ts(4)}
|
||||
{...css([!next && { opacity: 0, pointerEvents: "none" }], common)}
|
||||
className={cn(
|
||||
"mx-12 h-16 w-16 bg-gray-800/70",
|
||||
!next && "pointer-events-none opacity-0",
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -10,15 +10,8 @@ import { type ComponentProps, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type PressableProps, View } from "react-native";
|
||||
import { useEvent, type VideoPlayer } from "react-native-video";
|
||||
import { px, useYoshiki } from "yoshiki/native";
|
||||
import {
|
||||
alpha,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Slider,
|
||||
tooltip,
|
||||
ts,
|
||||
} from "~/primitives";
|
||||
import { CircularProgress, IconButton, Slider, tooltip } from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const PlayButton = ({
|
||||
player,
|
||||
@ -85,8 +78,16 @@ export const FullscreenButton = (
|
||||
);
|
||||
};
|
||||
|
||||
export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => {
|
||||
const { css } = useYoshiki();
|
||||
export const VolumeSlider = ({
|
||||
player,
|
||||
className,
|
||||
iconClassName,
|
||||
...props
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [volume, setVolume] = useState(player.volume);
|
||||
@ -98,15 +99,8 @@ export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => {
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
display: { xs: "none", sm: "flex" },
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingRight: ts(1),
|
||||
},
|
||||
props,
|
||||
)}
|
||||
className={cn("flex-row items-center pr-2 max-sm:hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<IconButton
|
||||
icon={
|
||||
@ -121,6 +115,7 @@ export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => {
|
||||
onPress={() => {
|
||||
player.muted = !muted;
|
||||
}}
|
||||
iconClassName={iconClassName}
|
||||
{...tooltip(t("player.mute"), true)}
|
||||
/>
|
||||
<Slider
|
||||
@ -128,8 +123,7 @@ export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => {
|
||||
setProgress={(vol) => {
|
||||
player.volume = vol / 100;
|
||||
}}
|
||||
size={4}
|
||||
{...css({ width: px(100) })}
|
||||
className="h-1 w-24"
|
||||
{...tooltip(t("player.volume"), true)}
|
||||
/>
|
||||
</View>
|
||||
@ -137,7 +131,6 @@ export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => {
|
||||
};
|
||||
|
||||
export const LoadingIndicator = ({ player }: { player: VideoPlayer }) => {
|
||||
const { css } = useYoshiki();
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
useEvent(player, "onStatusChange", (status) => {
|
||||
@ -147,19 +140,8 @@ export const LoadingIndicator = ({ player }: { player: VideoPlayer }) => {
|
||||
if (!isLoading) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bg: (theme) => alpha(theme.colors.black, 0.3),
|
||||
justifyContent: "center",
|
||||
})}
|
||||
>
|
||||
<CircularProgress {...css({ alignSelf: "center" })} />
|
||||
<View className="pointer-events-none absolute inset-0 justify-center bg-slate-900/30">
|
||||
<CircularProgress className="self-center" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import type { TextProps } from "react-native";
|
||||
import { useEvent, type VideoPlayer } from "react-native-video";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import type { Chapter } from "~/models";
|
||||
import { P, Slider } from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const ProgressBar = ({
|
||||
player,
|
||||
@ -76,10 +76,9 @@ export const ProgressBar = ({
|
||||
|
||||
export const ProgressText = ({
|
||||
player,
|
||||
className,
|
||||
...props
|
||||
}: { player: VideoPlayer } & TextProps) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
const [progress, setProgress] = useState(player.currentTime);
|
||||
useEvent(player, "onProgress", (progress) => {
|
||||
setProgress(progress.currentTime);
|
||||
@ -90,7 +89,7 @@ export const ProgressText = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<P {...css({ alignSelf: "center" }, props)}>
|
||||
<P className={cn("text-center", className)} {...props}>
|
||||
{toTimerString(progress, duration)} : {toTimerString(duration)}
|
||||
</P>
|
||||
);
|
||||
|
||||
@ -4,13 +4,12 @@ import {
|
||||
Platform,
|
||||
Pressable,
|
||||
type PressableProps,
|
||||
StyleSheet,
|
||||
View,
|
||||
type ViewProps,
|
||||
} from "react-native";
|
||||
import { useEvent, type VideoPlayer } from "react-native-video";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { useIsTouch } from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
import { toggleFullscreen } from "./misc";
|
||||
|
||||
export const TouchControls = ({
|
||||
@ -22,7 +21,6 @@ export const TouchControls = ({
|
||||
player: VideoPlayer;
|
||||
forceShow?: boolean;
|
||||
} & ViewProps) => {
|
||||
const { css } = useYoshiki();
|
||||
const isTouch = useIsTouch();
|
||||
|
||||
const [playing, setPlay] = useState(player.isPlaying);
|
||||
@ -31,7 +29,7 @@ export const TouchControls = ({
|
||||
});
|
||||
|
||||
const [_show, setShow] = useState(false);
|
||||
const hideTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const hideTimeout = useRef<NodeJS.Timeout | number | null>(null);
|
||||
const shouldShow = forceShow || _show || !playing;
|
||||
const show = useCallback((val: boolean = true) => {
|
||||
setShow(val);
|
||||
@ -90,10 +88,7 @@ export const TouchControls = ({
|
||||
// instantly hide the controls when mouse leaves the view
|
||||
if (e.nativeEvent.pointerType === "mouse") show(false);
|
||||
}}
|
||||
{...css({
|
||||
cursor: (shouldShow ? "unset" : "none") as any,
|
||||
...StyleSheet.absoluteFillObject,
|
||||
})}
|
||||
className={cn("absolute inset-0", !shouldShow && "cursor-none")}
|
||||
/>
|
||||
{shouldShow && children}
|
||||
</View>
|
||||
@ -107,7 +102,7 @@ const DoublePressable = ({
|
||||
}: {
|
||||
onDoublePress: (e: GestureResponderEvent) => boolean | undefined;
|
||||
} & PressableProps) => {
|
||||
const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({
|
||||
const touch = useRef<{ count: number; timeout?: NodeJS.Timeout | number }>({
|
||||
count: 0,
|
||||
});
|
||||
|
||||
|
||||
@ -5,12 +5,11 @@ import VideoSettings from "@material-symbols/svg-400/rounded/video_settings-fill
|
||||
import { type ComponentProps, createContext, useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEvent, type VideoPlayer } from "react-native-video";
|
||||
import { useForceRerender } from "yoshiki";
|
||||
import { IconButton, Menu, tooltip } from "~/primitives";
|
||||
import { useFetch } from "~/query";
|
||||
import { useDisplayName, useSubtitleName } from "~/track-utils";
|
||||
import { Info } from "~/ui/info";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { useForceRerender, useQueryState } from "~/utils";
|
||||
|
||||
type MenuProps = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>;
|
||||
|
||||
|
||||
@ -5,10 +5,9 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { useEvent, useVideoPlayer, VideoView } from "react-native-video";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { entryDisplayNumber } from "~/components/entries";
|
||||
import { FullVideo, type KyooError } from "~/models";
|
||||
import { ContrastArea, Head } from "~/primitives";
|
||||
import { Head } from "~/primitives";
|
||||
import { useToken } from "~/providers/account-context";
|
||||
import { useLocalSetting } from "~/providers/settings";
|
||||
import { type QueryIdentifier, useFetch } from "~/query";
|
||||
@ -158,14 +157,13 @@ export const Player = () => {
|
||||
setPlayMode("hls");
|
||||
else setPlaybackError({ status: error.code, message: error.message });
|
||||
});
|
||||
const { css } = useYoshiki();
|
||||
if (error || infoError || playbackError) {
|
||||
return (
|
||||
<>
|
||||
<Back
|
||||
showHref={data?.show?.href}
|
||||
name={data?.show?.name ?? "Error"}
|
||||
{...css({ position: "relative", bg: (theme) => theme.accent })}
|
||||
className="bg-accent"
|
||||
/>
|
||||
<ErrorView error={error ?? infoError ?? playbackError!} />
|
||||
</>
|
||||
@ -173,12 +171,7 @@ export const Player = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "black",
|
||||
}}
|
||||
>
|
||||
<View className="flex-1 bg-black">
|
||||
<Head
|
||||
title={title}
|
||||
description={entry?.description}
|
||||
@ -200,27 +193,25 @@ export const Player = () => {
|
||||
resizeMode={"contain"}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
/>
|
||||
<ContrastArea mode="dark">
|
||||
<LoadingIndicator player={player} />
|
||||
<PlayModeContext.Provider value={playModeState}>
|
||||
<Controls
|
||||
player={player}
|
||||
showHref={data?.show?.href}
|
||||
name={data?.show?.name}
|
||||
poster={data?.show?.poster}
|
||||
subName={
|
||||
entry
|
||||
? [entryDisplayNumber(entry), entry.name]
|
||||
.filter((x) => x)
|
||||
.join(" - ")
|
||||
: undefined
|
||||
}
|
||||
chapters={info?.chapters ?? []}
|
||||
previous={data?.previous?.video}
|
||||
next={data?.next?.video}
|
||||
/>
|
||||
</PlayModeContext.Provider>
|
||||
</ContrastArea>
|
||||
<LoadingIndicator player={player} />
|
||||
<PlayModeContext.Provider value={playModeState}>
|
||||
<Controls
|
||||
player={player}
|
||||
showHref={data?.show?.href}
|
||||
name={data?.show?.name}
|
||||
poster={data?.show?.poster}
|
||||
subName={
|
||||
entry
|
||||
? [entryDisplayNumber(entry), entry.name]
|
||||
.filter((x) => x)
|
||||
.join(" - ")
|
||||
: undefined
|
||||
}
|
||||
chapters={info?.chapters ?? []}
|
||||
previous={data?.previous?.video}
|
||||
next={data?.next?.video}
|
||||
/>
|
||||
</PlayModeContext.Provider>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useReducer } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { Movie, Show } from "~/models";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const useForceRerender = () => {
|
||||
return useReducer((x) => x + 1, 0)[1];
|
||||
};
|
||||
|
||||
export function setServerData(_key: string, _val: any) {}
|
||||
export function getServerData(key: string) {
|
||||
return key;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user