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
+
);
};
+
+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 && (
+
+ )}
{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:^"