diff --git a/front/src/primitives/icons.tsx b/front/src/primitives/icons.tsx index e178d110..70113a9b 100644 --- a/front/src/primitives/icons.tsx +++ b/front/src/primitives/icons.tsx @@ -1,10 +1,4 @@ -import type React from "react"; -import { - type ComponentProps, - type ComponentType, - type ForwardedRef, - forwardRef, -} from "react"; +import type { ComponentProps, ComponentType } from "react"; import { Platform, type PressableProps } from "react-native"; import type { SvgProps } from "react-native-svg"; import type { YoshikiStyle } from "yoshiki"; @@ -13,12 +7,6 @@ import { PressableFeedback } from "./links"; import { P } from "./text"; import { type Breakpoint, focusReset, ts } from "./utils"; -declare module "react" { - function forwardRef( - render: (props: P, ref: React.ForwardedRef) => React.ReactElement | null, - ): (props: P & React.RefAttributes) => React.ReactElement | null; -} - export type Icon = ComponentType; type IconProps = { @@ -54,27 +42,21 @@ export const Icon = ({ icon: Icon, color, size = 24, ...props }: IconProps) => { ); }; -export const IconButton = forwardRef(function IconButton< - AsProps = PressableProps, ->( - { - icon, - size, - color, - as, - ...asProps - }: IconProps & { - as?: ComponentType; - } & AsProps, - ref: ForwardedRef, -) { +export const IconButton = ({ + icon, + size, + color, + as, + ...asProps +}: IconProps & { + as?: ComponentType; +} & AsProps) => { const { css, theme } = useYoshiki(); const Container = as ?? PressableFeedback; return ( ); -}); +}; export const IconFab = ( props: ComponentProps>, diff --git a/front/src/ui/player/controls/back.tsx b/front/src/ui/player/controls/back.tsx index 72e9fc3c..43ab4cea 100644 --- a/front/src/ui/player/controls/back.tsx +++ b/front/src/ui/player/controls/back.tsx @@ -20,11 +20,6 @@ export const Back = ({ name, ...props }: { name: string } & ViewProps) => { theme.darkOverlay, display: "flex", flexDirection: "row", alignItems: "center", diff --git a/front/src/ui/player/controls/bottom-controls.tsx b/front/src/ui/player/controls/bottom-controls.tsx new file mode 100644 index 00000000..343c8f1c --- /dev/null +++ b/front/src/ui/player/controls/bottom-controls.tsx @@ -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 ( + + + + + +

+ name +

+ + +
+
+ ); +}; + +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 ( + + + + {previous && ( + + )} + + {next && ( + + )} + {Platform.OS === "web" && } + + + + + + + + {Platform.OS === "web" && } + + + ); +}; diff --git a/front/src/ui/player/controls/index.tsx b/front/src/ui/player/controls/index.tsx index 2d3a3d5f..9ab306cd 100644 --- a/front/src/ui/player/controls/index.tsx +++ b/front/src/ui/player/controls/index.tsx @@ -1,45 +1,14 @@ -import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg"; -import { useRouter } from "expo-router"; -import { - type ReactNode, - useCallback, - useEffect, - 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"; +import type { VideoPlayer } from "react-native-video"; +import { useYoshiki } from "yoshiki/native"; +import type { KImage } from "~/models"; +import { Back } from "./back"; +import { BottomControls } from "./bottom-controls"; +import { TouchControls } from "./touch"; export const Controls = ({ player, title, + poster, }: { player: VideoPlayer; title: string; @@ -47,16 +16,11 @@ export const Controls = ({ poster: KImage | null; }) => { const { css } = useYoshiki(); - // const show = useAtomValue(hoverAtom); - // const setHover = useSetAtom(hoverReasonAtom); - // const isSeeking = useAtomValue(seekingAtom); - // const isTouch = useIsTouch(); - - // const showBottomSeeker = isSeeking && isTouch; // return ( - { // if (e.nativeEvent.pointerType === "mouse") // setHover((x) => ({ ...x, mouseHover: true })); @@ -65,118 +29,33 @@ export const Controls = ({ // if (e.nativeEvent.pointerType === "mouse") // 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", - })} > theme.darkOverlay, })} /> - theme.darkOverlay, - flexDirection: "row", - pointerEvents: "auto", - padding: percent(1), })} - > - - - {!showBottomSeeker && ( -

- {isLoading ? ( - - ) : ( - name - )} -

- )} - - {showBottomSeeker ? ( - - ) : ( - - - setHover((x) => ({ ...x, menuOpened: true }))} - onMenuClose={() => { - // Disable hover since the menu overlay makes the mouseout unreliable. - setHover((x) => ({ - ...x, - menuOpened: false, - mouseHover: false, - })); - }} - /> - - )} -
-
-
- ); -}; - -const VideoPoster = ({ - poster, - alt, - isLoading, -}: { - poster?: KyooImage | null; - alt?: string; - isLoading: boolean; -}) => { - const { css } = useYoshiki(); - - return ( - - - +
); }; diff --git a/front/src/ui/player/controls/misc.tsx b/front/src/ui/player/controls/misc.tsx index 3a6744f0..e551823d 100644 --- a/front/src/ui/player/controls/misc.tsx +++ b/front/src/ui/player/controls/misc.tsx @@ -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 PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-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 VolumeOff from "@material-symbols/svg-400/rounded/volume_off-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 { View } from "react-native"; +import { type PressableProps, View } from "react-native"; import { useEvent, type VideoPlayer } from "react-native-video"; import { px, useYoshiki } from "yoshiki/native"; import { @@ -18,7 +20,12 @@ import { ts, } from "~/primitives"; -export const PlayButton = ({ player, ...props }: { player: VideoPlayer }) => { +export const PlayButton = ({ + player, + ...props +}: { player: VideoPlayer } & Partial< + ComponentProps> +>) => { const { t } = useTranslation(); const [playing, setPlay] = useState(player.isPlaying); @@ -39,6 +46,24 @@ export const PlayButton = ({ player, ...props }: { player: VideoPlayer }) => { ); }; +export const FullscreenButton = ( + props: Partial>>, +) => { + const { t } = useTranslation(); + + // TODO: actually implement that + const [fullscreen, setFullscreen] = useState(true); + + return ( + console.log("lol")} + {...tooltip(t("player.fullscreen"), true)} + {...props} + /> + ); +}; + export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => { const { css } = useYoshiki(); const { t } = useTranslation(); diff --git a/front/src/ui/player/controls/touch.tsx b/front/src/ui/player/controls/touch.tsx index 924884e6..54d46969 100644 --- a/front/src/ui/player/controls/touch.tsx +++ b/front/src/ui/player/controls/touch.tsx @@ -45,6 +45,7 @@ export const TouchControls = ({ return ( { if (isTouch) { show(!shouldShow); diff --git a/front/src/ui/player/controls/tracks-menu.tsx b/front/src/ui/player/controls/tracks-menu.tsx new file mode 100644 index 00000000..d3af1d22 --- /dev/null +++ b/front/src/ui/player/controls/tracks-menu.tsx @@ -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>>; + +export const SubtitleMenu = (props: Partial) => { + const { t } = useTranslation(); + + // {subtitles && subtitles.length > 0 && ( + // )} + return ( + + {/* setSubtitle(null)} */} + {/* /> */} + {/* {subtitles */} + {/* .filter((x) => !!x.link) */} + {/* .map((x, i) => ( */} + {/* setSubtitle(x)} */} + {/* /> */} + {/* ))} */} + + ); +}; + +export const AudioMenu = (props: Partial) => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export const QualityMenu = (props: Partial) => { + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/front/src/ui/player/old/left-buttons.tsx b/front/src/ui/player/old/left-buttons.tsx index cd97c70f..28216dfb 100644 --- a/front/src/ui/player/old/left-buttons.tsx +++ b/front/src/ui/player/old/left-buttons.tsx @@ -6,10 +6,6 @@ import { touchOnly, ts, } 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 { useTranslation } from "react-i18next"; import { Platform, View } from "react-native"; @@ -17,55 +13,6 @@ import { px, type Stylable, useYoshiki } from "yoshiki/native"; import { HoverTouch, hoverAtom } from "../controls"; 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 ( - - - {previousSlug && ( - - )} - setPlay(!isPlaying)} - {...tooltip(isPlaying ? t("player.pause") : t("player.play"), true)} - {...spacing} - /> - {nextSlug && ( - - )} - {Platform.OS === "web" && } - - - - ); -}; - export const TouchControls = ({ previousSlug, nextSlug, diff --git a/front/src/ui/player/old/right-buttons.tsx b/front/src/ui/player/old/right-buttons.tsx deleted file mode 100644 index 3f095ed9..00000000 --- a/front/src/ui/player/old/right-buttons.tsx +++ /dev/null @@ -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 . - */ - -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 ( - - {subtitles && subtitles.length > 0 && ( - - setSubtitle(null)} - /> - {subtitles - .filter((x) => !!x.link) - .map((x, i) => ( - setSubtitle(x)} - /> - ))} - - )} - - - {Platform.OS === "web" && ( - setFullscreen(!isFullscreen)} - {...tooltip(t("player.fullscreen"), true)} - {...spacing} - /> - )} - - ); -};