mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-08-07 09:01:29 -04:00
Fix player controls style
This commit is contained in:
parent
a52807c565
commit
8e060f0c55
@ -27,9 +27,10 @@ export const Video = z.object({
|
||||
|
||||
// Name of the tool that made the guess
|
||||
from: z.string(),
|
||||
get history() {
|
||||
return z.array(Video.shape.guess.omit({ history: true })).default([]);
|
||||
},
|
||||
// Adding that results in an infinite recursion
|
||||
// get history() {
|
||||
// return z.array(Video.shape.guess.omit({ history: true })).default([]);
|
||||
// },
|
||||
}),
|
||||
createdAt: zdate(),
|
||||
updatedAt: zdate(),
|
||||
|
@ -25,7 +25,7 @@ export const ImageBackground = ({
|
||||
layout: ImageLayout;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const { css, theme } = useYoshiki();
|
||||
const { apiUrl, authToken } = useToken();
|
||||
|
||||
return (
|
||||
@ -42,7 +42,10 @@ export const ImageBackground = ({
|
||||
}}
|
||||
placeholder={{ blurhash: src?.blurhash }}
|
||||
accessibilityLabel={alt}
|
||||
{...(css([layout, { overflow: "hidden" }], props) as any)}
|
||||
{...(css(
|
||||
[layout, { overflow: "hidden", backgroundColor: theme.overlay0 }],
|
||||
props,
|
||||
) as any)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -34,7 +34,7 @@ export const Image = ({
|
||||
style?: ImageStyle;
|
||||
layout: ImageLayout;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const { css, theme } = useYoshiki();
|
||||
const { apiUrl, authToken } = useToken();
|
||||
|
||||
return (
|
||||
@ -51,7 +51,10 @@ export const Image = ({
|
||||
}}
|
||||
placeholder={{ blurhash: src?.blurhash }}
|
||||
accessibilityLabel={alt}
|
||||
{...(css([layout, { borderRadius: 6 }], props) as any)}
|
||||
{...(css(
|
||||
[layout, { borderRadius: 6, backgroundColor: theme.overlay0 }],
|
||||
props,
|
||||
) as any)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -108,7 +108,6 @@ export const Skeleton = ({
|
||||
colors={["transparent", theme.overlay1, "transparent"]}
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
{ transform: [{ translateX: -width.value }] },
|
||||
animated,
|
||||
]}
|
||||
/>
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
tooltip,
|
||||
} from "~/primitives";
|
||||
|
||||
export const Back = ({ name, ...props }: { name: string } & ViewProps) => {
|
||||
export const Back = ({ name, ...props }: { name?: string } & ViewProps) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@ -35,6 +35,7 @@ export const Back = ({ name, ...props }: { name: string } & ViewProps) => {
|
||||
onPress={router.back}
|
||||
{...tooltip(t("player.back"))}
|
||||
/>
|
||||
{name ? (
|
||||
<H1
|
||||
{...css({
|
||||
alignSelf: "center",
|
||||
@ -44,40 +45,9 @@ export const Back = ({ name, ...props }: { name: string } & ViewProps) => {
|
||||
>
|
||||
{name}
|
||||
</H1>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Back.Loader = (props: ViewProps) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: percent(0.33),
|
||||
color: "white",
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
icon={ArrowBack}
|
||||
as={PressableFeedback}
|
||||
onPress={router.back}
|
||||
{...tooltip(t("player.back"))}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton {...css({ width: rem(5) })} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import type { ComponentProps } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View, type ViewProps } from "react-native";
|
||||
import type { VideoPlayer } from "react-native-video";
|
||||
import { percent, useYoshiki } from "yoshiki/native";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import type { Chapter, KImage } from "~/models";
|
||||
import {
|
||||
H2,
|
||||
@ -12,6 +12,7 @@ import {
|
||||
Link,
|
||||
type Menu,
|
||||
Poster,
|
||||
Skeleton,
|
||||
tooltip,
|
||||
ts,
|
||||
useIsTouch,
|
||||
@ -31,11 +32,11 @@ export const BottomControls = ({
|
||||
...props
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
poster: KImage;
|
||||
name: string;
|
||||
poster?: KImage | null;
|
||||
name?: string;
|
||||
chapters: Chapter[];
|
||||
previous: string | null;
|
||||
next: string | null;
|
||||
previous?: string | null;
|
||||
next?: string | null;
|
||||
setMenu: (isOpen: boolean) => void;
|
||||
} & ViewProps) => {
|
||||
const { css } = useYoshiki();
|
||||
@ -57,25 +58,34 @@ export const BottomControls = ({
|
||||
position: "relative",
|
||||
})}
|
||||
>
|
||||
{poster !== undefined ? (
|
||||
<Poster
|
||||
src={poster}
|
||||
quality="low"
|
||||
layout={{ width: percent(100) }}
|
||||
{...(css({ position: "absolute", bottom: 0 }) as any)}
|
||||
/>
|
||||
) : (
|
||||
<Poster.Loader
|
||||
layout={{ width: percent(100) }}
|
||||
{...(css({ position: "absolute", bottom: 0 }) as any)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
{...css({
|
||||
marginLeft: { xs: ts(0.5), sm: ts(3) },
|
||||
marginHorizontal: { xs: ts(0.5), sm: ts(3) },
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
maxWidth: percent(100),
|
||||
flex: 1,
|
||||
})}
|
||||
>
|
||||
{name ? (
|
||||
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
||||
name
|
||||
{name}
|
||||
</H2>
|
||||
) : (
|
||||
<Skeleton {...css({ width: rem(15), height: rem(2) })} />
|
||||
)}
|
||||
<ProgressBar player={player} chapters={chapters} />
|
||||
<ControlButtons
|
||||
player={player}
|
||||
@ -96,8 +106,8 @@ const ControlButtons = ({
|
||||
...props
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
previous: string | null;
|
||||
next: string | null;
|
||||
previous?: string | null;
|
||||
next?: string | null;
|
||||
setMenu: (isOpen: boolean) => void;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
@ -116,7 +126,7 @@ const ControlButtons = ({
|
||||
{...css(
|
||||
{
|
||||
flexDirection: "row",
|
||||
flexGrow: 1,
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
@ -124,7 +134,7 @@ const ControlButtons = ({
|
||||
)}
|
||||
>
|
||||
<View {...css({ flexDirection: "row" })}>
|
||||
{isTouch && (
|
||||
{!isTouch && (
|
||||
<View {...css({ flexDirection: "row" })}>
|
||||
{previous && (
|
||||
<IconButton
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { ViewProps } from "react-native";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { VideoPlayer } from "react-native-video";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import type { Chapter, KImage } from "~/models";
|
||||
@ -11,22 +13,23 @@ import { TouchControls } from "./touch";
|
||||
|
||||
export const Controls = ({
|
||||
player,
|
||||
title,
|
||||
subTitle,
|
||||
name,
|
||||
poster,
|
||||
subName,
|
||||
chapters,
|
||||
previous,
|
||||
next,
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
title: string;
|
||||
subTitle: string;
|
||||
poster: KImage;
|
||||
name?: string;
|
||||
poster?: KImage | null;
|
||||
subName?: string;
|
||||
chapters: Chapter[];
|
||||
previous: string | null;
|
||||
next: string | null;
|
||||
previous?: string | null;
|
||||
next?: string | null;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTouch = useIsTouch();
|
||||
|
||||
const [hover, setHover] = useState(false);
|
||||
@ -44,9 +47,13 @@ export const Controls = ({
|
||||
} satisfies ViewProps;
|
||||
|
||||
return (
|
||||
<TouchControls player={player} forceShow={hover || menuOpenned}>
|
||||
<TouchControls
|
||||
player={player}
|
||||
forceShow={hover || menuOpenned}
|
||||
{...css(StyleSheet.absoluteFillObject)}
|
||||
>
|
||||
<Back
|
||||
name={title}
|
||||
name={name}
|
||||
{...css(
|
||||
{
|
||||
// pointerEvents: "auto",
|
||||
@ -55,6 +62,9 @@ export const Controls = ({
|
||||
left: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
paddingTop: insets.top,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
},
|
||||
hoverControls,
|
||||
)}
|
||||
@ -64,7 +74,7 @@ export const Controls = ({
|
||||
)}
|
||||
<BottomControls
|
||||
player={player}
|
||||
name={subTitle}
|
||||
name={subName}
|
||||
poster={poster}
|
||||
chapters={chapters}
|
||||
previous={previous}
|
||||
@ -79,6 +89,9 @@ export const Controls = ({
|
||||
left: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
hoverControls,
|
||||
)}
|
||||
|
@ -39,9 +39,9 @@ export const ProgressBar = ({
|
||||
}}
|
||||
setProgress={setSeek}
|
||||
endSeek={() => {
|
||||
setProgress(seek!);
|
||||
setSeek(null);
|
||||
player.seekTo(seek!);
|
||||
setTimeout(player.play, 10);
|
||||
setSeek(null);
|
||||
}}
|
||||
// onHover={(progress, layout) => {
|
||||
// setHoverProgress(progress);
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
Pressable,
|
||||
type PressableProps,
|
||||
} from "react-native";
|
||||
import type { VideoPlayer } from "react-native-video";
|
||||
import { useEvent, type VideoPlayer } from "react-native-video";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { useIsTouch } from "~/primitives";
|
||||
|
||||
@ -18,9 +18,14 @@ export const TouchControls = ({
|
||||
const { css } = useYoshiki();
|
||||
const isTouch = useIsTouch();
|
||||
|
||||
const [_show, setShow] = useState(true);
|
||||
const [playing, setPlay] = useState(player.isPlaying);
|
||||
useEvent(player, "onPlaybackStateChange", (status) => {
|
||||
setPlay(status.isPlaying);
|
||||
});
|
||||
|
||||
const [_show, setShow] = useState(false);
|
||||
const hideTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const shouldShow = forceShow || _show;
|
||||
const shouldShow = forceShow || _show || !playing;
|
||||
const show = useCallback((val: boolean = true) => {
|
||||
setShow(val);
|
||||
if (hideTimeout.current) clearTimeout(hideTimeout.current);
|
||||
|
@ -10,31 +10,15 @@ import { type QueryIdentifier, useFetch } from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { Controls, LoadingIndicator } from "./controls";
|
||||
|
||||
const mapMetadata = (item: FullVideo | undefined) => {
|
||||
if (!item) return null;
|
||||
|
||||
// TODO: map current entry using entries' duration & the current playtime
|
||||
const currentEntry = 0;
|
||||
const entry = item.entries[currentEntry] ?? item.entries[0];
|
||||
if (!entry) return null;
|
||||
|
||||
return {
|
||||
currentEntry,
|
||||
title: `${entry.name} (${entryDisplayNumber(entry)})`,
|
||||
description: entry.description,
|
||||
subtitle: item.show!.kind !== "movie" ? item.show!.name : null,
|
||||
poster: item.show!.poster,
|
||||
thumbnail: item.show!.thumbnail,
|
||||
};
|
||||
};
|
||||
|
||||
export const Player = () => {
|
||||
const [slug, setSlug] = useQueryState<string>("slug", undefined!);
|
||||
const [start, setStart] = useQueryState<number | undefined>("t", undefined);
|
||||
|
||||
const { data, error } = useFetch(Player.query(slug));
|
||||
const { data: info, error: infoError } = useFetch(Player.infoQuery(slug));
|
||||
const metadata = mapMetadata(data);
|
||||
// TODO: map current entry using entries' duration & the current playtime
|
||||
const currentEntry = 0;
|
||||
const entry = data?.entries[currentEntry] ?? data?.entries[0];
|
||||
|
||||
const { apiUrl, authToken } = useToken();
|
||||
const [playMode] = useLocalSetting<"direct" | "hls">("playMode", "direct");
|
||||
@ -44,15 +28,15 @@ export const Player = () => {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
externalSubtitles: info?.subtitles
|
||||
.filter((x) => x.link)
|
||||
.map((x) => ({
|
||||
uri: x.link!,
|
||||
// TODO: translate this `Unknown`
|
||||
label: x.title ?? "Unknown",
|
||||
language: x.language ?? "und",
|
||||
type: x.codec,
|
||||
})),
|
||||
// externalSubtitles: info?.subtitles
|
||||
// .filter((x) => x.link)
|
||||
// .map((x) => ({
|
||||
// uri: x.link!,
|
||||
// // TODO: translate this `Unknown`
|
||||
// label: x.title ?? "Unknown",
|
||||
// language: x.language ?? "und",
|
||||
// type: x.codec,
|
||||
// })),
|
||||
},
|
||||
(p) => {
|
||||
p.playWhenInactive = true;
|
||||
@ -111,9 +95,9 @@ export const Player = () => {
|
||||
}}
|
||||
>
|
||||
<Head
|
||||
title={metadata?.title}
|
||||
description={metadata?.description}
|
||||
image={metadata?.thumbnail?.high}
|
||||
title={entry ? `${entry.name} (${entryDisplayNumber(entry)})` : null}
|
||||
description={entry?.description}
|
||||
image={data?.show?.thumbnail?.high}
|
||||
/>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
@ -129,15 +113,24 @@ export const Player = () => {
|
||||
pictureInPicture
|
||||
autoEnterPictureInPicture
|
||||
resizeMode={"contain"}
|
||||
controls
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
/>
|
||||
<ContrastArea mode="dark">
|
||||
<LoadingIndicator player={player} />
|
||||
<Controls
|
||||
player={player}
|
||||
title={metadata?.title}
|
||||
subTitle={metadata?.subtitle}
|
||||
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}
|
||||
/>
|
||||
</ContrastArea>
|
||||
</View>
|
||||
@ -147,7 +140,7 @@ export const Player = () => {
|
||||
Player.query = (slug: string): QueryIdentifier<FullVideo> => ({
|
||||
path: ["api", "videos", slug],
|
||||
params: {
|
||||
fields: ["next", "previous", "show"],
|
||||
with: ["next", "previous", "show"],
|
||||
},
|
||||
parser: FullVideo,
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user