mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-07 13:45:18 -05:00
Implement bottom controls
This commit is contained in:
parent
bcff909c21
commit
d93ce325e9
@ -1,10 +1,4 @@
|
|||||||
import type React from "react";
|
import type { ComponentProps, ComponentType } from "react";
|
||||||
import {
|
|
||||||
type ComponentProps,
|
|
||||||
type ComponentType,
|
|
||||||
type ForwardedRef,
|
|
||||||
forwardRef,
|
|
||||||
} from "react";
|
|
||||||
import { Platform, type PressableProps } from "react-native";
|
import { Platform, type PressableProps } from "react-native";
|
||||||
import type { SvgProps } from "react-native-svg";
|
import type { SvgProps } from "react-native-svg";
|
||||||
import type { YoshikiStyle } from "yoshiki";
|
import type { YoshikiStyle } from "yoshiki";
|
||||||
@ -13,12 +7,6 @@ import { PressableFeedback } from "./links";
|
|||||||
import { P } from "./text";
|
import { P } from "./text";
|
||||||
import { type Breakpoint, focusReset, ts } from "./utils";
|
import { type Breakpoint, focusReset, ts } from "./utils";
|
||||||
|
|
||||||
declare module "react" {
|
|
||||||
function forwardRef<T, P = {}>(
|
|
||||||
render: (props: P, ref: React.ForwardedRef<T>) => React.ReactElement | null,
|
|
||||||
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Icon = ComponentType<SvgProps>;
|
export type Icon = ComponentType<SvgProps>;
|
||||||
|
|
||||||
type IconProps = {
|
type IconProps = {
|
||||||
@ -54,27 +42,21 @@ export const Icon = ({ icon: Icon, color, size = 24, ...props }: IconProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IconButton = forwardRef(function IconButton<
|
export const IconButton = <AsProps = PressableProps>({
|
||||||
AsProps = PressableProps,
|
icon,
|
||||||
>(
|
size,
|
||||||
{
|
color,
|
||||||
icon,
|
as,
|
||||||
size,
|
...asProps
|
||||||
color,
|
}: IconProps & {
|
||||||
as,
|
as?: ComponentType<AsProps>;
|
||||||
...asProps
|
} & AsProps) => {
|
||||||
}: IconProps & {
|
|
||||||
as?: ComponentType<AsProps>;
|
|
||||||
} & AsProps,
|
|
||||||
ref: ForwardedRef<unknown>,
|
|
||||||
) {
|
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
|
|
||||||
const Container = as ?? PressableFeedback;
|
const Container = as ?? PressableFeedback;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
ref={ref as any}
|
|
||||||
focusRipple
|
focusRipple
|
||||||
{...(css(
|
{...(css(
|
||||||
{
|
{
|
||||||
@ -102,7 +84,7 @@ export const IconButton = forwardRef(function IconButton<
|
|||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export const IconFab = <AsProps = PressableProps>(
|
export const IconFab = <AsProps = PressableProps>(
|
||||||
props: ComponentProps<typeof IconButton<AsProps>>,
|
props: ComponentProps<typeof IconButton<AsProps>>,
|
||||||
|
|||||||
@ -20,11 +20,6 @@ export const Back = ({ name, ...props }: { name: string } & ViewProps) => {
|
|||||||
<View
|
<View
|
||||||
{...css(
|
{...css(
|
||||||
{
|
{
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bg: (theme) => theme.darkOverlay,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
140
front/src/ui/player/controls/bottom-controls.tsx
Normal file
140
front/src/ui/player/controls/bottom-controls.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
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 { 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 type { Chapter, KImage } from "~/models";
|
||||||
|
import {
|
||||||
|
H2,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
noTouch,
|
||||||
|
Poster,
|
||||||
|
tooltip,
|
||||||
|
ts,
|
||||||
|
} from "~/primitives";
|
||||||
|
import { FullscreenButton, PlayButton, VolumeSlider } from "./misc";
|
||||||
|
import { ProgressBar, ProgressText } from "./progress";
|
||||||
|
import { AudioMenu, QualityMenu, SubtitleMenu } from "./tracks-menu";
|
||||||
|
|
||||||
|
export const BottomControls = ({
|
||||||
|
player,
|
||||||
|
poster,
|
||||||
|
name,
|
||||||
|
chapters,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
player: VideoPlayer;
|
||||||
|
poster: KImage;
|
||||||
|
name: string;
|
||||||
|
chapters: Chapter[];
|
||||||
|
} & ViewProps) => {
|
||||||
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
{...css(
|
||||||
|
{
|
||||||
|
flexDirection: "row",
|
||||||
|
padding: ts(1),
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
{...css({
|
||||||
|
width: "15%",
|
||||||
|
display: { xs: "none", sm: "flex" },
|
||||||
|
position: "relative",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Poster
|
||||||
|
src={poster}
|
||||||
|
quality="low"
|
||||||
|
layout={{ width: percent(100) }}
|
||||||
|
{...(css({ position: "absolute", bottom: 0 }) as any)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
{...css({
|
||||||
|
marginLeft: { xs: ts(0.5), sm: ts(3) },
|
||||||
|
flexDirection: "column",
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
maxWidth: percent(100),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
||||||
|
name
|
||||||
|
</H2>
|
||||||
|
<ProgressBar player={player} chapters={chapters} />
|
||||||
|
<ControlButtons player={player} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ControlButtons = ({
|
||||||
|
player,
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
player: VideoPlayer;
|
||||||
|
previous: string;
|
||||||
|
next: string;
|
||||||
|
}) => {
|
||||||
|
const { css } = useYoshiki();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const spacing = css({ marginHorizontal: ts(1) });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
{...css(
|
||||||
|
{
|
||||||
|
flexDirection: "row",
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<View {...css({ flexDirection: "row" })}>
|
||||||
|
<View {...css({ flexDirection: "row" }, noTouch)}>
|
||||||
|
{previous && (
|
||||||
|
<IconButton
|
||||||
|
icon={SkipPrevious}
|
||||||
|
as={Link}
|
||||||
|
href={previous}
|
||||||
|
replace
|
||||||
|
{...tooltip(t("player.previous"), true)}
|
||||||
|
{...spacing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<PlayButton player={player} {...spacing} />
|
||||||
|
{next && (
|
||||||
|
<IconButton
|
||||||
|
icon={SkipNext}
|
||||||
|
as={Link}
|
||||||
|
href={next}
|
||||||
|
replace
|
||||||
|
{...tooltip(t("player.next"), true)}
|
||||||
|
{...spacing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{Platform.OS === "web" && <VolumeSlider player={player} />}
|
||||||
|
</View>
|
||||||
|
<ProgressText player={player} {...spacing} />
|
||||||
|
</View>
|
||||||
|
<View {...css({ flexDirection: "row" })}>
|
||||||
|
<SubtitleMenu {...(spacing as any)} />
|
||||||
|
<AudioMenu {...(spacing as any)} />
|
||||||
|
<QualityMenu {...(spacing as any)} />
|
||||||
|
{Platform.OS === "web" && <FullscreenButton {...spacing} />}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,45 +1,14 @@
|
|||||||
import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
|
import type { VideoPlayer } from "react-native-video";
|
||||||
import { useRouter } from "expo-router";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import {
|
import type { KImage } from "~/models";
|
||||||
type ReactNode,
|
import { Back } from "./back";
|
||||||
useCallback,
|
import { BottomControls } from "./bottom-controls";
|
||||||
useEffect,
|
import { TouchControls } from "./touch";
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
type ImageStyle,
|
|
||||||
Platform,
|
|
||||||
Pressable,
|
|
||||||
View,
|
|
||||||
type ViewProps,
|
|
||||||
} from "react-native";
|
|
||||||
import { useEvent, type VideoPlayer } from "react-native-video";
|
|
||||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
|
||||||
import type { AudioTrack, Chapter, KImage, Subtitle } from "~/models";
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
CircularProgress,
|
|
||||||
H1,
|
|
||||||
H2,
|
|
||||||
IconButton,
|
|
||||||
Poster,
|
|
||||||
PressableFeedback,
|
|
||||||
Skeleton,
|
|
||||||
Slider,
|
|
||||||
Tooltip,
|
|
||||||
tooltip,
|
|
||||||
ts,
|
|
||||||
useIsTouch,
|
|
||||||
} from "~/primitives";
|
|
||||||
import { LeftButtons } from "./components/left-buttons";
|
|
||||||
import { RightButtons } from "./components/right-buttons";
|
|
||||||
import { BottomScrubber, ScrubberTooltip } from "./scrubber";
|
|
||||||
|
|
||||||
export const Controls = ({
|
export const Controls = ({
|
||||||
player,
|
player,
|
||||||
title,
|
title,
|
||||||
|
poster,
|
||||||
}: {
|
}: {
|
||||||
player: VideoPlayer;
|
player: VideoPlayer;
|
||||||
title: string;
|
title: string;
|
||||||
@ -47,16 +16,11 @@ export const Controls = ({
|
|||||||
poster: KImage | null;
|
poster: KImage | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
// const show = useAtomValue(hoverAtom);
|
|
||||||
// const setHover = useSetAtom(hoverReasonAtom);
|
|
||||||
// const isSeeking = useAtomValue(seekingAtom);
|
|
||||||
// const isTouch = useIsTouch();
|
|
||||||
|
|
||||||
// const showBottomSeeker = isSeeking && isTouch;
|
|
||||||
|
|
||||||
// <TouchControls previousSlug={previousSlug} nextSlug={nextSlug} />
|
// <TouchControls previousSlug={previousSlug} nextSlug={nextSlug} />
|
||||||
return (
|
return (
|
||||||
<View
|
<TouchControls
|
||||||
|
player={player}
|
||||||
// onPointerEnter={(e) => {
|
// onPointerEnter={(e) => {
|
||||||
// if (e.nativeEvent.pointerType === "mouse")
|
// if (e.nativeEvent.pointerType === "mouse")
|
||||||
// setHover((x) => ({ ...x, mouseHover: true }));
|
// setHover((x) => ({ ...x, mouseHover: true }));
|
||||||
@ -65,118 +29,33 @@ export const Controls = ({
|
|||||||
// if (e.nativeEvent.pointerType === "mouse")
|
// if (e.nativeEvent.pointerType === "mouse")
|
||||||
// setHover((x) => ({ ...x, mouseHover: false }));
|
// setHover((x) => ({ ...x, mouseHover: false }));
|
||||||
// }}
|
// }}
|
||||||
{...css({
|
|
||||||
// TODO: animate show
|
|
||||||
//display: !show ? "none" : "flex",
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
// box-none does not work on the web while none does not work on android
|
|
||||||
pointerEvents: Platform.OS === "web" ? "none" : "box-none",
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Back
|
<Back
|
||||||
name={title}
|
name={title}
|
||||||
{...css({
|
{...css({
|
||||||
pointerEvents: "auto",
|
// pointerEvents: "auto",
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bg: (theme) => theme.darkOverlay,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<View
|
<BottomControls
|
||||||
|
player={player}
|
||||||
|
name={title}
|
||||||
|
poster={poster!}
|
||||||
|
chapters={[]}
|
||||||
{...css({
|
{...css({
|
||||||
// Fixed is used because firefox android make the hover disapear under the navigation bar in absolute
|
// Fixed is used because firefox android make the hover disapear under the navigation bar in absolute
|
||||||
position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
|
// position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
|
||||||
|
position: "absolute",
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bg: (theme) => theme.darkOverlay,
|
bg: (theme) => theme.darkOverlay,
|
||||||
flexDirection: "row",
|
|
||||||
pointerEvents: "auto",
|
|
||||||
padding: percent(1),
|
|
||||||
})}
|
})}
|
||||||
>
|
|
||||||
<VideoPoster poster={poster} alt={showName} isLoading={isLoading} />
|
|
||||||
<View
|
|
||||||
{...css({
|
|
||||||
marginLeft: { xs: ts(0.5), sm: ts(3) },
|
|
||||||
flexDirection: "column",
|
|
||||||
flexGrow: 1,
|
|
||||||
flexShrink: 1,
|
|
||||||
maxWidth: percent(100),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{!showBottomSeeker && (
|
|
||||||
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
|
||||||
{isLoading ? (
|
|
||||||
<Skeleton {...css({ width: rem(15), height: rem(2) })} />
|
|
||||||
) : (
|
|
||||||
name
|
|
||||||
)}
|
|
||||||
</H2>
|
|
||||||
)}
|
|
||||||
<ProgressBar chapters={chapters} url={url} />
|
|
||||||
{showBottomSeeker ? (
|
|
||||||
<BottomScrubber url={url} chapters={chapters} />
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
{...css({
|
|
||||||
flexDirection: "row",
|
|
||||||
flexGrow: 1,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<LeftButtons previousSlug={previousSlug} nextSlug={nextSlug} />
|
|
||||||
<RightButtons
|
|
||||||
subtitles={subtitles}
|
|
||||||
audios={audios}
|
|
||||||
fonts={fonts}
|
|
||||||
onMenuOpen={() => setHover((x) => ({ ...x, menuOpened: true }))}
|
|
||||||
onMenuClose={() => {
|
|
||||||
// Disable hover since the menu overlay makes the mouseout unreliable.
|
|
||||||
setHover((x) => ({
|
|
||||||
...x,
|
|
||||||
menuOpened: false,
|
|
||||||
mouseHover: false,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const VideoPoster = ({
|
|
||||||
poster,
|
|
||||||
alt,
|
|
||||||
isLoading,
|
|
||||||
}: {
|
|
||||||
poster?: KyooImage | null;
|
|
||||||
alt?: string;
|
|
||||||
isLoading: boolean;
|
|
||||||
}) => {
|
|
||||||
const { css } = useYoshiki();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
{...css({
|
|
||||||
width: "15%",
|
|
||||||
display: { xs: "none", sm: "flex" },
|
|
||||||
position: "relative",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Poster
|
|
||||||
src={poster}
|
|
||||||
quality="low"
|
|
||||||
alt={alt}
|
|
||||||
forcedLoading={isLoading}
|
|
||||||
layout={{ width: percent(100) }}
|
|
||||||
{...(css({ position: "absolute", bottom: 0 }) as { style: ImageStyle })}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</TouchControls>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
|
import FullscreenExit from "@material-symbols/svg-400/rounded/fullscreen_exit-fill.svg";
|
||||||
|
import Fullscreen from "@material-symbols/svg-400/rounded/fullscreen-fill.svg";
|
||||||
import Pause from "@material-symbols/svg-400/rounded/pause-fill.svg";
|
import Pause from "@material-symbols/svg-400/rounded/pause-fill.svg";
|
||||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||||
import VolumeDown from "@material-symbols/svg-400/rounded/volume_down-fill.svg";
|
import VolumeDown from "@material-symbols/svg-400/rounded/volume_down-fill.svg";
|
||||||
import VolumeMute from "@material-symbols/svg-400/rounded/volume_mute-fill.svg";
|
import VolumeMute from "@material-symbols/svg-400/rounded/volume_mute-fill.svg";
|
||||||
import VolumeOff from "@material-symbols/svg-400/rounded/volume_off-fill.svg";
|
import VolumeOff from "@material-symbols/svg-400/rounded/volume_off-fill.svg";
|
||||||
import VolumeUp from "@material-symbols/svg-400/rounded/volume_up-fill.svg";
|
import VolumeUp from "@material-symbols/svg-400/rounded/volume_up-fill.svg";
|
||||||
import { useState } from "react";
|
import { type ComponentProps, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { type PressableProps, View } from "react-native";
|
||||||
import { useEvent, type VideoPlayer } from "react-native-video";
|
import { useEvent, type VideoPlayer } from "react-native-video";
|
||||||
import { px, useYoshiki } from "yoshiki/native";
|
import { px, useYoshiki } from "yoshiki/native";
|
||||||
import {
|
import {
|
||||||
@ -18,7 +20,12 @@ import {
|
|||||||
ts,
|
ts,
|
||||||
} from "~/primitives";
|
} from "~/primitives";
|
||||||
|
|
||||||
export const PlayButton = ({ player, ...props }: { player: VideoPlayer }) => {
|
export const PlayButton = ({
|
||||||
|
player,
|
||||||
|
...props
|
||||||
|
}: { player: VideoPlayer } & Partial<
|
||||||
|
ComponentProps<typeof IconButton<PressableProps>>
|
||||||
|
>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [playing, setPlay] = useState(player.isPlaying);
|
const [playing, setPlay] = useState(player.isPlaying);
|
||||||
@ -39,6 +46,24 @@ export const PlayButton = ({ player, ...props }: { player: VideoPlayer }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FullscreenButton = (
|
||||||
|
props: Partial<ComponentProps<typeof IconButton<PressableProps>>>,
|
||||||
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// TODO: actually implement that
|
||||||
|
const [fullscreen, setFullscreen] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
icon={fullscreen ? FullscreenExit : Fullscreen}
|
||||||
|
onPress={() => console.log("lol")}
|
||||||
|
{...tooltip(t("player.fullscreen"), true)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => {
|
export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export const TouchControls = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DoublePressable
|
<DoublePressable
|
||||||
|
tabIndex={-1}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (isTouch) {
|
if (isTouch) {
|
||||||
show(!shouldShow);
|
show(!shouldShow);
|
||||||
|
|||||||
68
front/src/ui/player/controls/tracks-menu.tsx
Normal file
68
front/src/ui/player/controls/tracks-menu.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import ClosedCaption from "@material-symbols/svg-400/rounded/closed_caption-fill.svg";
|
||||||
|
import MusicNote from "@material-symbols/svg-400/rounded/music_note-fill.svg";
|
||||||
|
import SettingsIcon from "@material-symbols/svg-400/rounded/settings-fill.svg";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { IconButton, Menu, tooltip } from "~/primitives";
|
||||||
|
|
||||||
|
type MenuProps = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>;
|
||||||
|
|
||||||
|
export const SubtitleMenu = (props: Partial<MenuProps>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// {subtitles && subtitles.length > 0 && (
|
||||||
|
// )}
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
Trigger={IconButton}
|
||||||
|
icon={ClosedCaption}
|
||||||
|
{...tooltip(t("player.subtitles"), true)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* <Menu.Item */}
|
||||||
|
{/* label={t("player.subtitle-none")} */}
|
||||||
|
{/* selected={!selectedSubtitle} */}
|
||||||
|
{/* onSelect={() => setSubtitle(null)} */}
|
||||||
|
{/* /> */}
|
||||||
|
{/* {subtitles */}
|
||||||
|
{/* .filter((x) => !!x.link) */}
|
||||||
|
{/* .map((x, i) => ( */}
|
||||||
|
{/* <Menu.Item */}
|
||||||
|
{/* key={x.index ?? i} */}
|
||||||
|
{/* label={ */}
|
||||||
|
{/* x.link ? getSubtitleName(x) : `${getSubtitleName(x)} (${x.codec})` */}
|
||||||
|
{/* } */}
|
||||||
|
{/* selected={selectedSubtitle === x} */}
|
||||||
|
{/* disabled={!x.link} */}
|
||||||
|
{/* onSelect={() => setSubtitle(x)} */}
|
||||||
|
{/* /> */}
|
||||||
|
{/* ))} */}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AudioMenu = (props: Partial<MenuProps>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
Trigger={IconButton}
|
||||||
|
icon={MusicNote}
|
||||||
|
{...tooltip(t("player.audios"), true)}
|
||||||
|
{...props}
|
||||||
|
></Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QualityMenu = (props: Partial<MenuProps>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
Trigger={IconButton}
|
||||||
|
icon={SettingsIcon}
|
||||||
|
{...tooltip(t("player.quality"), true)}
|
||||||
|
{...props}
|
||||||
|
></Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -6,10 +6,6 @@ import {
|
|||||||
touchOnly,
|
touchOnly,
|
||||||
ts,
|
ts,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import Pause from "@material-symbols/svg-400/rounded/pause-fill.svg";
|
|
||||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
|
||||||
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 { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
@ -17,55 +13,6 @@ import { px, type Stylable, useYoshiki } from "yoshiki/native";
|
|||||||
import { HoverTouch, hoverAtom } from "../controls";
|
import { HoverTouch, hoverAtom } from "../controls";
|
||||||
import { playAtom } from "./state";
|
import { playAtom } from "./state";
|
||||||
|
|
||||||
export const LeftButtons = ({
|
|
||||||
previousSlug,
|
|
||||||
nextSlug,
|
|
||||||
}: {
|
|
||||||
previousSlug?: string | null;
|
|
||||||
nextSlug?: string | null;
|
|
||||||
}) => {
|
|
||||||
const { css } = useYoshiki();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
|
||||||
|
|
||||||
const spacing = css({ marginHorizontal: ts(1) });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View {...css({ flexDirection: "row" })}>
|
|
||||||
<View {...css({ flexDirection: "row" }, noTouch)}>
|
|
||||||
{previousSlug && (
|
|
||||||
<IconButton
|
|
||||||
icon={SkipPrevious}
|
|
||||||
as={Link}
|
|
||||||
href={previousSlug}
|
|
||||||
replace
|
|
||||||
{...tooltip(t("player.previous"), true)}
|
|
||||||
{...spacing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
icon={isPlaying ? Pause : PlayArrow}
|
|
||||||
onPress={() => setPlay(!isPlaying)}
|
|
||||||
{...tooltip(isPlaying ? t("player.pause") : t("player.play"), true)}
|
|
||||||
{...spacing}
|
|
||||||
/>
|
|
||||||
{nextSlug && (
|
|
||||||
<IconButton
|
|
||||||
icon={SkipNext}
|
|
||||||
as={Link}
|
|
||||||
href={nextSlug}
|
|
||||||
replace
|
|
||||||
{...tooltip(t("player.next"), true)}
|
|
||||||
{...spacing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{Platform.OS === "web" && <VolumeSlider />}
|
|
||||||
</View>
|
|
||||||
<ProgressText {...css({ marginLeft: ts(1) })} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TouchControls = ({
|
export const TouchControls = ({
|
||||||
previousSlug,
|
previousSlug,
|
||||||
nextSlug,
|
nextSlug,
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
* Kyoo - A portable and vast media library solution.
|
|
||||||
* Copyright (c) Kyoo.
|
|
||||||
*
|
|
||||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
|
||||||
*
|
|
||||||
* Kyoo is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* any later version.
|
|
||||||
*
|
|
||||||
* Kyoo is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Audio, Subtitle } from "@kyoo/models";
|
|
||||||
import { IconButton, Menu, tooltip, ts } from "@kyoo/primitives";
|
|
||||||
import ClosedCaption from "@material-symbols/svg-400/rounded/closed_caption-fill.svg";
|
|
||||||
import Fullscreen from "@material-symbols/svg-400/rounded/fullscreen-fill.svg";
|
|
||||||
import FullscreenExit from "@material-symbols/svg-400/rounded/fullscreen_exit-fill.svg";
|
|
||||||
import MusicNote from "@material-symbols/svg-400/rounded/music_note-fill.svg";
|
|
||||||
import SettingsIcon from "@material-symbols/svg-400/rounded/settings-fill.svg";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Platform, View } from "react-native";
|
|
||||||
import { type Stylable, useYoshiki } from "yoshiki/native";
|
|
||||||
import { useSubtitleName } from "../../../../packages/ui/src/utils";
|
|
||||||
import { fullscreenAtom, subtitleAtom } from "./state";
|
|
||||||
import { AudiosMenu, QualitiesMenu } from "./video";
|
|
||||||
|
|
||||||
export const RightButtons = ({
|
|
||||||
audios,
|
|
||||||
subtitles,
|
|
||||||
fonts,
|
|
||||||
onMenuOpen,
|
|
||||||
onMenuClose,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
audios?: Audio[];
|
|
||||||
subtitles?: Subtitle[];
|
|
||||||
fonts?: string[];
|
|
||||||
onMenuOpen: () => void;
|
|
||||||
onMenuClose: () => void;
|
|
||||||
} & Stylable) => {
|
|
||||||
const { css } = useYoshiki();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const getSubtitleName = useSubtitleName();
|
|
||||||
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
|
||||||
const [selectedSubtitle, setSubtitle] = useAtom(subtitleAtom);
|
|
||||||
|
|
||||||
const spacing = css({ marginHorizontal: ts(1) });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View {...css({ flexDirection: "row" }, props)}>
|
|
||||||
{subtitles && subtitles.length > 0 && (
|
|
||||||
<Menu
|
|
||||||
Trigger={IconButton}
|
|
||||||
icon={ClosedCaption}
|
|
||||||
onMenuOpen={onMenuOpen}
|
|
||||||
onMenuClose={onMenuClose}
|
|
||||||
{...tooltip(t("player.subtitles"), true)}
|
|
||||||
{...spacing}
|
|
||||||
>
|
|
||||||
<Menu.Item
|
|
||||||
label={t("player.subtitle-none")}
|
|
||||||
selected={!selectedSubtitle}
|
|
||||||
onSelect={() => setSubtitle(null)}
|
|
||||||
/>
|
|
||||||
{subtitles
|
|
||||||
.filter((x) => !!x.link)
|
|
||||||
.map((x, i) => (
|
|
||||||
<Menu.Item
|
|
||||||
key={x.index ?? i}
|
|
||||||
label={x.link ? getSubtitleName(x) : `${getSubtitleName(x)} (${x.codec})`}
|
|
||||||
selected={selectedSubtitle === x}
|
|
||||||
disabled={!x.link}
|
|
||||||
onSelect={() => setSubtitle(x)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
<AudiosMenu
|
|
||||||
Trigger={IconButton}
|
|
||||||
icon={MusicNote}
|
|
||||||
onMenuOpen={onMenuOpen}
|
|
||||||
onMenuClose={onMenuClose}
|
|
||||||
audios={audios}
|
|
||||||
{...tooltip(t("player.audios"), true)}
|
|
||||||
{...spacing}
|
|
||||||
/>
|
|
||||||
<QualitiesMenu
|
|
||||||
Trigger={IconButton}
|
|
||||||
icon={SettingsIcon}
|
|
||||||
onMenuOpen={onMenuOpen}
|
|
||||||
onMenuClose={onMenuClose}
|
|
||||||
{...tooltip(t("player.quality"), true)}
|
|
||||||
{...spacing}
|
|
||||||
/>
|
|
||||||
{Platform.OS === "web" && (
|
|
||||||
<IconButton
|
|
||||||
icon={isFullscreen ? FullscreenExit : Fullscreen}
|
|
||||||
onPress={() => setFullscreen(!isFullscreen)}
|
|
||||||
{...tooltip(t("player.fullscreen"), true)}
|
|
||||||
{...spacing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Loading…
x
Reference in New Issue
Block a user