diff --git a/front/apps/mobile/app/_layout.tsx b/front/apps/mobile/app/_layout.tsx index 4297dfdb..de7aea67 100644 --- a/front/apps/mobile/app/_layout.tsx +++ b/front/apps/mobile/app/_layout.tsx @@ -28,6 +28,7 @@ import { createQueryClient } from "@kyoo/models"; import i18next from "i18next"; import { initReactI18next } from "react-i18next"; import { getLocales } from "expo-localization"; +import { PortalProvider } from "@gorhom/portal"; import "intl-pluralrules"; // TODO: use a backend to load jsons. @@ -72,7 +73,9 @@ export default function Root() { return ( - + + + ); diff --git a/front/apps/mobile/package.json b/front/apps/mobile/package.json index e3daf666..97d5dd9c 100644 --- a/front/apps/mobile/package.json +++ b/front/apps/mobile/package.json @@ -9,6 +9,7 @@ "web": "expo start --web" }, "dependencies": { + "@gorhom/portal": "^1.0.14", "@kyoo/ui": "workspace:^", "@material-symbols/svg-400": "^0.4.2", "@shopify/flash-list": "1.3.1", diff --git a/front/apps/web/package.json b/front/apps/web/package.json index 7e801e89..fc7cbac4 100644 --- a/front/apps/web/package.json +++ b/front/apps/web/package.json @@ -13,6 +13,7 @@ "dependencies": { "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", + "@gorhom/portal": "^1.0.14", "@kyoo/models": "workspace:^", "@kyoo/primitives": "workspace:^", "@kyoo/ui": "workspace:^", diff --git a/front/apps/web/src/pages/_app.tsx b/front/apps/web/src/pages/_app.tsx index cdabab4d..f8c6b18f 100755 --- a/front/apps/web/src/pages/_app.tsx +++ b/front/apps/web/src/pages/_app.tsx @@ -20,10 +20,11 @@ import "../polyfill"; -import { ReactNode, useState } from "react"; -import NextApp, { AppContext, type AppProps } from "next/app"; +import { PortalProvider } from "@gorhom/portal"; import { createTheme, ThemeProvider as MTheme } from "@mui/material"; import { Hydrate, QueryClientProvider } from "@tanstack/react-query"; +import { ReactNode, useState } from "react"; +import NextApp, { AppContext, type AppProps } from "next/app"; import { HiddenIfNoJs, SkeletonCss, @@ -32,7 +33,6 @@ import { } from "@kyoo/primitives"; import { createQueryClient, fetchQuery, QueryIdentifier, QueryPage } from "@kyoo/models"; import { useTheme, useMobileHover } from "yoshiki/web"; -import { useYoshiki } from "yoshiki/native"; import superjson from "superjson"; import Head from "next/head"; import { withTranslations } from "../i18n"; @@ -105,7 +105,9 @@ const App = ({ Component, pageProps }: AppProps) => { - } /> + + } /> + diff --git a/front/packages/primitives/package.json b/front/packages/primitives/package.json index 4f6e0249..b2bc3101 100644 --- a/front/packages/primitives/package.json +++ b/front/packages/primitives/package.json @@ -8,6 +8,7 @@ "typescript": "^4.9.3" }, "peerDependencies": { + "@gorhom/portal": "*", "@material-symbols/svg-400": "*", "expo-linear-gradient": "*", "moti": "*", diff --git a/front/packages/primitives/src/index.ts b/front/packages/primitives/src/index.ts index ce833c42..8f97aef1 100644 --- a/front/packages/primitives/src/index.ts +++ b/front/packages/primitives/src/index.ts @@ -31,6 +31,7 @@ export * from "./container"; export * from "./divider"; export * from "./progress"; export * from "./slider"; +export * from "./menu"; export * from "./animated"; export * from "./utils"; diff --git a/front/packages/primitives/src/links.tsx b/front/packages/primitives/src/links.tsx index e8fc29bc..0a8c1e2f 100644 --- a/front/packages/primitives/src/links.tsx +++ b/front/packages/primitives/src/links.tsx @@ -18,18 +18,20 @@ * along with Kyoo. If not, see . */ -import { ComponentType, Fragment, ReactNode } from "react"; +import { ComponentProps, ComponentType, Fragment, ReactNode } from "react"; import { Platform, + Pressable, TextProps, TouchableOpacity, TouchableNativeFeedback, View, ViewProps, StyleSheet, + PressableProps, } from "react-native"; import { LinkCore, TextLink } from "solito/link"; -import { useYoshiki, Pressable } from "yoshiki/native"; +import { useYoshiki } from "yoshiki/native"; export const A = ({ href, @@ -58,19 +60,20 @@ export const A = ({ ); }; -export const Link = ({ - href, +export const PressableFeedback = ({ children, + WebComponent, ...props }: ViewProps & { - href: string; - onFocus?: () => void; - onBlur?: () => void; - onPressIn?: () => void; - onPressOut?: () => void; + onFocus?: PressableProps["onFocus"]; + onBlur?: PressableProps["onBlur"]; + onPressIn?: PressableProps["onPressIn"]; + onPressOut?: PressableProps["onPressOut"]; + onPress?: PressableProps["onPress"]; + WebComponent?: ComponentType; }) => { - const { onBlur, onFocus, onPressIn, onPressOut, ...noFocusProps } = props; - const focusProps = { onBlur, onFocus, onPressIn, onPressOut }; + const { onBlur, onFocus, onPressIn, onPressOut, onPress, ...noPressProps } = props; + const pressProps = { onBlur, onFocus, onPressIn, onPressOut, onPress }; const radiusStyle = Platform.select({ android: { style: { borderRadius: StyleSheet.flatten(props?.style)?.borderRadius, overflow: "hidden" }, @@ -78,28 +81,43 @@ export const Link = ({ default: {}, }); const Wrapper = radiusStyle.style ? View : Fragment; + const InnerPressable = Platform.select>({ + web: WebComponent ?? Pressable, + android: TouchableNativeFeedback, + ios: TouchableOpacity, + default: Pressable, + }); return ( - ({ - web: View, - android: TouchableNativeFeedback, - ios: TouchableOpacity, - default: Pressable, - })} - componentProps={Platform.select({ - android: { useForeground: true, ...focusProps }, + ({ + android: { useForeground: true, ...pressProps }, default: props, })} > {Platform.select({ - android: {children}, - ios: {children}, + android: {children}, + ios: {children}, default: children, })} - + ); }; + +export const Link = ({ + href, + children, + ...props +}: { href: string } & Omit, "WebComponent">) => { + return ( + + {children} + + ); +}; diff --git a/front/packages/primitives/src/menu.tsx b/front/packages/primitives/src/menu.tsx new file mode 100644 index 00000000..d975848a --- /dev/null +++ b/front/packages/primitives/src/menu.tsx @@ -0,0 +1,145 @@ +/* + * 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 { Portal } from "@gorhom/portal"; +import { ScrollView } from "moti"; +import { ComponentType, createContext, ReactNode, useContext, useEffect, useState } from "react"; +import { PressableProps, StyleSheet, Pressable } from "react-native"; +import { percent, px, sm, useYoshiki, xl } from "yoshiki/native"; +import Close from "@material-symbols/svg-400/rounded/close-fill.svg"; +import { IconButton } from "./icons"; +import { PressableFeedback } from "./links"; +import { P } from "./text"; +import { ContrastArea } from "./themes"; +import { ts } from "./utils"; + +const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined); + +const Menu = ({ + Triger, + onMenuOpen, + onMenuClose, + children, + ...props +}: { + Triger: ComponentType; + children: ReactNode | ReactNode[] | null; + onMenuOpen: () => void; + onMenuClose: () => void; +} & Omit) => { + const [isOpen, setOpen] = useState(false); + + useEffect(() => { + if (isOpen) onMenuOpen?.call(null); + else onMenuClose?.call(null); + }, [isOpen, onMenuClose, onMenuOpen]); + + return ( + <> + {/* @ts-ignore */} + setOpen(true)} {...props} /> + {isOpen && ( + + + {({ css, theme }) => ( + + setOpen(false)} + focusable={false} + {...css({ ...StyleSheet.absoluteFillObject, flexGrow: 1, bg: "transparent" })} + /> + theme.background, + position: "absolute", + bottom: 0, + width: percent(100), + alignSelf: "center", + borderTopLeftRadius: px(26), + borderTopRightRadius: { xs: px(26), xl: 0 }, + paddingTop: { xs: px(26), xl: 0 }, + marginTop: { xs: px(72), xl: 0 }, + }, + sm({ + maxWidth: px(640), + marginHorizontal: px(56), + }), + xl({ + top: 0, + right: 0, + marginRight: 0, + borderBottomLeftRadius: px(26), + }), + ])} + > + setOpen(false)} + {...css({ alignSelf: "flex-end", display: { xs: "none", xl: "flex" } })} + /> + {children} + + + )} + + + )} + + ); +}; + +const MenuItem = ({ + label, + icon, + selected, + as, + onPress, + ...props +}: { + label: string; + icon?: JSX.Element; + selected?: boolean; + as?: ComponentType; +} & AsProps) => { + const { css } = useYoshiki(); + const setOpen = useContext(MenuContext); + + const As: ComponentType = as ?? PressableFeedback; + return ( + { + setOpen?.call(null, false); + onPress?.call(null, e); + }} + {...css( + { paddingHorizontal: ts(2), width: percent(100), height: ts(5), justifyContent: "center" }, + props as any, + )} + > + {icon ?? null} +

{label}

+
+ ); +}; +Menu.Item = MenuItem; + +export { Menu }; diff --git a/front/packages/primitives/src/themes/theme.tsx b/front/packages/primitives/src/themes/theme.tsx index f0b5fd49..34972b0f 100644 --- a/front/packages/primitives/src/themes/theme.tsx +++ b/front/packages/primitives/src/themes/theme.tsx @@ -135,7 +135,7 @@ export const ContrastArea = ({ contrastText, }: { children: ReactNode | YoshikiFunc; - mode?: "light" | "dark"; + mode?: "light" | "dark" | "user"; contrastText?: boolean; }) => { const oldTheme = useTheme(); diff --git a/front/packages/ui/package.json b/front/packages/ui/package.json index d583ea20..4942fa22 100644 --- a/front/packages/ui/package.json +++ b/front/packages/ui/package.json @@ -25,10 +25,5 @@ "react-native-reanimated": "*", "react-native-svg": "*", "yoshiki": "*" - }, - "peerDependenciesMeta": { - "react-native": { - "optional": true - } } } diff --git a/front/packages/ui/src/player/components/right-buttons.tsx b/front/packages/ui/src/player/components/right-buttons.tsx index 086b768c..2f51000e 100644 --- a/front/packages/ui/src/player/components/right-buttons.tsx +++ b/front/packages/ui/src/player/components/right-buttons.tsx @@ -19,64 +19,75 @@ */ import { Font, Track } from "@kyoo/models"; -import { IconButton, tooltip } from "@kyoo/primitives"; +import { IconButton, tooltip, Menu, ts, A } from "@kyoo/primitives"; import { useAtom } from "jotai"; import { useRouter } from "solito/router"; -import { useState } from "react"; import { Platform, View } from "react-native"; import { useTranslation } from "react-i18next"; +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 { Stylable, useYoshiki } from "yoshiki/native"; +import { createParam } from "solito"; import { fullscreenAtom, subtitleAtom } from "../state"; +const { useParam } = createParam<{ subtitle?: string }>(); + export const RightButtons = ({ subtitles, fonts, onMenuOpen, onMenuClose, + ...props }: { subtitles?: Track[]; fonts?: Font[]; onMenuOpen: () => void; onMenuClose: () => void; -}) => { +} & Stylable) => { + const { css } = useYoshiki(); const { t } = useTranslation(); - const [subtitleAnchor, setSubtitleAnchor] = useState(null); const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom); + const [selectedSubtitle, setSubtitle] = useParam("subtitle"); + + const spacing = css({ marginHorizontal: ts(1) }); + + subtitles = [{ id: 1, slug: "oto", displayName: "toto" }]; return ( - *": { - // m: { xs: "4px !important", sm: "8px !important" }, - // p: { xs: "4px !important", sm: "8px !important" }, - // }, - // }} - > - {/* {subtitles && ( */} - {/* */} - {/* { */} - {/* setSubtitleAnchor(event.currentTarget); */} - {/* onMenuOpen(); */} - {/* }} */} - {/* sx={{ color: "white" }} */} - {/* > */} - {/* */} - {/* */} - {/* */} - {/* )} */} + + {subtitles && ( + + setSubtitle(undefined)} + /> + {subtitles.map((x) => ( + setSubtitle(x.slug)} + /> + ))} + + )} {Platform.OS === "web" && ( setFullscreen(!isFullscreen)} {...tooltip(t("player.fullscreen"), true)} + {...spacing} /> )} {/* {subtitleAnchor && ( */} diff --git a/front/packages/ui/src/player/keyboard.tsx b/front/packages/ui/src/player/keyboard.tsx index 6d3019d8..1bde4252 100644 --- a/front/packages/ui/src/player/keyboard.tsx +++ b/front/packages/ui/src/player/keyboard.tsx @@ -31,6 +31,7 @@ import { subtitleAtom, volumeAtom, } from "./state"; +import { Platform } from "react-native"; type Action = | { type: "play" } @@ -93,6 +94,7 @@ export const useVideoKeyboard = ( const router = useRouter(); useEffect(() => { + if (Platform.OS !== "web") return; const handler = (event: KeyboardEvent) => { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return; diff --git a/front/packages/ui/src/player/state.tsx b/front/packages/ui/src/player/state.tsx index 3848e25d..8969111b 100644 --- a/front/packages/ui/src/player/state.tsx +++ b/front/packages/ui/src/player/state.tsx @@ -26,6 +26,7 @@ import { ResizeMode, Video as NativeVideo, VideoProps } from "expo-av"; import SubtitleOctopus from "libass-wasm"; import Hls from "hls.js"; import { bakedAtom } from "../jotai-utils"; +import { Platform } from "react-native"; enum PlayMode { Direct, @@ -100,6 +101,7 @@ export const Video = ({ const setFullscreen = useSetAtom(privateFullscreen); useEffect(() => { + if (Platform.OS !== "web") return; const handler = () => { setFullscreen(document.fullscreenElement != null); }; diff --git a/front/yarn.lock b/front/yarn.lock index 469a631b..ade47e26 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2142,6 +2142,18 @@ __metadata: languageName: node linkType: hard +"@gorhom/portal@npm:^1.0.14": + version: 1.0.14 + resolution: "@gorhom/portal@npm:1.0.14" + dependencies: + nanoid: ^3.3.1 + peerDependencies: + react: "*" + react-native: "*" + checksum: 227bb96a2db854ab29bb9da8d4f3823c7f7448358de459709dd1b78522110da564c9a8734c6bc7d7153ed7c99320e0fb5d60b420c2ebb75ecaf2f0d757f410f9 + languageName: node + linkType: hard + "@graphql-typed-document-node/core@npm:^3.1.0": version: 3.1.1 resolution: "@graphql-typed-document-node/core@npm:3.1.1" @@ -2339,6 +2351,7 @@ __metadata: solito: ^2.0.5 typescript: ^4.9.3 peerDependencies: + "@gorhom/portal": "*" "@material-symbols/svg-400": "*" expo-linear-gradient: "*" moti: "*" @@ -2375,9 +2388,6 @@ __metadata: react-native-reanimated: "*" react-native-svg: "*" yoshiki: "*" - peerDependenciesMeta: - react-native: - optional: true languageName: unknown linkType: soft @@ -6545,19 +6555,19 @@ __metadata: "expo-av@file:///home/anonymus-raccoon/projects/expo/packages/expo-av/::locator=mobile%40workspace%3Aapps%2Fmobile": version: 13.0.1 - resolution: "expo-av@file:///home/anonymus-raccoon/projects/expo/packages/expo-av/#///home/anonymus-raccoon/projects/expo/packages/expo-av/::hash=5981bc&locator=mobile%40workspace%3Aapps%2Fmobile" + resolution: "expo-av@file:///home/anonymus-raccoon/projects/expo/packages/expo-av/#///home/anonymus-raccoon/projects/expo/packages/expo-av/::hash=6acf81&locator=mobile%40workspace%3Aapps%2Fmobile" peerDependencies: expo: "*" - checksum: 3313e891f708f423f29c417372701fa6ddd5bd30028a1ebcd9634d61e5ed0475dbbcc51384fc88e82b4c0dd482dc6af38f9b1d4d103fb1a81ea05d2a4c967586 + checksum: 89c23346fcf8cc3ed50b523969dad76c14e9bb3f526c7cc0032783fca9aeacc1c14a1aca86b84cb5ebd51d4ee2142f7fb507ed47d7430e12b71733858a70acc9 languageName: node linkType: hard "expo-av@file:///home/anonymus-raccoon/projects/expo/packages/expo-av/::locator=web%40workspace%3Aapps%2Fweb": version: 13.0.1 - resolution: "expo-av@file:///home/anonymus-raccoon/projects/expo/packages/expo-av/#///home/anonymus-raccoon/projects/expo/packages/expo-av/::hash=5981bc&locator=web%40workspace%3Aapps%2Fweb" + resolution: "expo-av@file:///home/anonymus-raccoon/projects/expo/packages/expo-av/#///home/anonymus-raccoon/projects/expo/packages/expo-av/::hash=6acf81&locator=web%40workspace%3Aapps%2Fweb" peerDependencies: expo: "*" - checksum: 3313e891f708f423f29c417372701fa6ddd5bd30028a1ebcd9634d61e5ed0475dbbcc51384fc88e82b4c0dd482dc6af38f9b1d4d103fb1a81ea05d2a4c967586 + checksum: 89c23346fcf8cc3ed50b523969dad76c14e9bb3f526c7cc0032783fca9aeacc1c14a1aca86b84cb5ebd51d4ee2142f7fb507ed47d7430e12b71733858a70acc9 languageName: node linkType: hard @@ -9962,6 +9972,7 @@ __metadata: resolution: "mobile@workspace:apps/mobile" dependencies: "@babel/core": ^7.19.3 + "@gorhom/portal": ^1.0.14 "@kyoo/ui": "workspace:^" "@material-symbols/svg-400": ^0.4.2 "@shopify/flash-list": 1.3.1 @@ -10057,7 +10068,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.1.23, nanoid@npm:^3.3.4": +"nanoid@npm:^3.1.23, nanoid@npm:^3.3.1, nanoid@npm:^3.3.4": version: 3.3.4 resolution: "nanoid@npm:3.3.4" bin: @@ -13639,6 +13650,7 @@ __metadata: dependencies: "@emotion/react": ^11.9.3 "@emotion/styled": ^11.9.3 + "@gorhom/portal": ^1.0.14 "@kyoo/models": "workspace:^" "@kyoo/primitives": "workspace:^" "@kyoo/ui": "workspace:^"