mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -04:00
Finish movie's header rework
This commit is contained in:
parent
e5b236f51c
commit
1b76bbf6c2
@ -31,7 +31,7 @@
|
|||||||
"react-native-safe-area-context": "4.4.1",
|
"react-native-safe-area-context": "4.4.1",
|
||||||
"react-native-screens": "~3.18.0",
|
"react-native-screens": "~3.18.0",
|
||||||
"react-native-svg": "13.4.0",
|
"react-native-svg": "13.4.0",
|
||||||
"yoshiki": "0.2.11"
|
"yoshiki": "0.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.19.3",
|
"@babel/core": "^7.19.3",
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
"react-native-web": "^0.18.10",
|
"react-native-web": "^0.18.10",
|
||||||
"solito": "^2.0.5",
|
"solito": "^2.0.5",
|
||||||
"superjson": "^1.11.0",
|
"superjson": "^1.11.0",
|
||||||
"yoshiki": "0.2.11",
|
"yoshiki": "0.3.1",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { View, ViewProps } from "react-native";
|
import { View, ViewProps } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||||
|
|
||||||
export const Container = (props: ViewProps) => {
|
export const Container = (props: ViewProps) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
@ -29,12 +29,13 @@ export const Container = (props: ViewProps) => {
|
|||||||
{...css(
|
{...css(
|
||||||
{
|
{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
paddingHorizontal: "15px",
|
paddingHorizontal: px(15),
|
||||||
marginHorizontal: "auto",
|
marginHorizontal: "auto",
|
||||||
width: {
|
width: {
|
||||||
sm: "540px",
|
xs: percent(100),
|
||||||
md: "880px",
|
sm: px(540),
|
||||||
lg: "1170px",
|
md: px(880),
|
||||||
|
lg: px(1170),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props,
|
props,
|
||||||
|
56
front/packages/primitives/src/divider.tsx
Normal file
56
front/packages/primitives/src/divider.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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 { HR as EHR } from "@expo/html-elements";
|
||||||
|
import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
|
||||||
|
import { alpha, ts } from ".";
|
||||||
|
|
||||||
|
export const HR = ({
|
||||||
|
orientation,
|
||||||
|
...props
|
||||||
|
}: { orientation: "vertical" | "horizontal" } & Stylable) => {
|
||||||
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EHR
|
||||||
|
{...css(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
bg: (theme) => alpha(theme.overlay0, 0.7),
|
||||||
|
borderWidth: 0,
|
||||||
|
},
|
||||||
|
orientation === "vertical" && {
|
||||||
|
width: px(1),
|
||||||
|
height: "auto",
|
||||||
|
marginVertical: ts(1),
|
||||||
|
marginHorizontal: ts(2),
|
||||||
|
},
|
||||||
|
orientation === "horizontal" && {
|
||||||
|
height: px(1),
|
||||||
|
width: "auto",
|
||||||
|
marginHorizontal: ts(1),
|
||||||
|
marginVertical: ts(2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
props,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Header, Main, Nav, Footer } from "@expo/html-elements";
|
export { Header, Main, Nav, Footer, UL } from "@expo/html-elements";
|
||||||
export * from "./text";
|
export * from "./text";
|
||||||
export * from "./themes";
|
export * from "./themes";
|
||||||
export * from "./icons";
|
export * from "./icons";
|
||||||
@ -28,11 +28,13 @@ export * from "./image";
|
|||||||
export * from "./skeleton";
|
export * from "./skeleton";
|
||||||
export * from "./tooltip";
|
export * from "./tooltip";
|
||||||
export * from "./container";
|
export * from "./container";
|
||||||
|
export * from "./divider";
|
||||||
|
|
||||||
export * from "./animated";
|
export * from "./animated";
|
||||||
|
|
||||||
export * from "./utils/breakpoints";
|
export * from "./utils/breakpoints";
|
||||||
export * from "./utils/nojs";
|
export * from "./utils/nojs";
|
||||||
|
export * from "./utils/head";
|
||||||
|
|
||||||
import { px } from "yoshiki/native";
|
import { px } from "yoshiki/native";
|
||||||
|
|
||||||
|
@ -43,10 +43,13 @@ export const A = ({
|
|||||||
textProps={css(
|
textProps={css(
|
||||||
{
|
{
|
||||||
// TODO: use a real font here.
|
// TODO: use a real font here.
|
||||||
fontFamily: Platform.OS === "web" ? theme.fonts.paragraph : undefined,
|
// fontFamily: theme.fonts.paragraph,
|
||||||
color: theme.paragraph,
|
color: theme.link,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selectable: true,
|
||||||
|
...props,
|
||||||
},
|
},
|
||||||
props,
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -83,8 +86,8 @@ export const Link = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{Platform.select<ReactNode>({
|
{Platform.select<ReactNode>({
|
||||||
android: <View {...props}>{children}</View>,
|
android: <View {...noFocusProps}>{children}</View>,
|
||||||
ios: <View {...props}>{children}</View>,
|
ios: <View {...noFocusProps}>{children}</View>,
|
||||||
default: children,
|
default: children,
|
||||||
})}
|
})}
|
||||||
</LinkCore>
|
</LinkCore>
|
||||||
|
@ -46,11 +46,13 @@ export const SkeletonCss = () => (
|
|||||||
export const Skeleton = ({
|
export const Skeleton = ({
|
||||||
children,
|
children,
|
||||||
show: forcedShow,
|
show: forcedShow,
|
||||||
|
lines = 1,
|
||||||
variant = "text",
|
variant = "text",
|
||||||
...props
|
...props
|
||||||
}: Omit<ViewProps, "children"> & {
|
}: Omit<ViewProps, "children"> & {
|
||||||
children?: JSX.Element | JSX.Element[] | boolean | null;
|
children?: JSX.Element | JSX.Element[] | boolean | null;
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
|
lines?: number;
|
||||||
variant?: "text" | "round" | "custom";
|
variant?: "text" | "round" | "custom";
|
||||||
}) => {
|
}) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
@ -65,13 +67,14 @@ export const Skeleton = ({
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: px(6),
|
|
||||||
},
|
|
||||||
variant === "text" && {
|
|
||||||
width: percent(75),
|
|
||||||
height: rem(1.2),
|
|
||||||
},
|
},
|
||||||
|
lines === 1 && { overflow: "hidden", borderRadius: px(6) },
|
||||||
|
variant === "text" &&
|
||||||
|
lines === 1 && {
|
||||||
|
width: percent(75),
|
||||||
|
height: rem(1.2),
|
||||||
|
marginBottom: rem(0.5),
|
||||||
|
},
|
||||||
variant === "round" && {
|
variant === "round" && {
|
||||||
borderRadius: 9999999,
|
borderRadius: 9999999,
|
||||||
},
|
},
|
||||||
@ -81,56 +84,68 @@ export const Skeleton = ({
|
|||||||
>
|
>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{children}
|
{children}
|
||||||
{(forcedShow || !children || children === true) && (
|
{(forcedShow || !children || children === true) &&
|
||||||
<MotiView
|
[...Array(lines)].map((_, i) => (
|
||||||
key="skeleton"
|
<MotiView
|
||||||
// No clue why it is a number on mobile and a string on web but /shrug
|
key={`skeleton_${i}`}
|
||||||
animate={{ opacity: Platform.OS === "web" ? "1" : 1 }}
|
// No clue why it is a number on mobile and a string on web but /shrug
|
||||||
exit={{ opacity: 0 }}
|
animate={{ opacity: Platform.OS === "web" ? "1" : 1 }}
|
||||||
transition={{ type: "timing" }}
|
exit={{ opacity: 0 }}
|
||||||
onLayout={(e) => setWidth(e.nativeEvent.layout.width)}
|
transition={{ type: "timing" }}
|
||||||
{...css(
|
onLayout={(e) => setWidth(e.nativeEvent.layout.width)}
|
||||||
{
|
{...css(
|
||||||
bg: (theme) => theme.overlay0,
|
[
|
||||||
position: "absolute",
|
{
|
||||||
top: 0,
|
bg: (theme) => theme.overlay0,
|
||||||
bottom: 0,
|
},
|
||||||
left: 0,
|
lines === 1 && {
|
||||||
right: 0,
|
position: "absolute",
|
||||||
},
|
top: 0,
|
||||||
hiddenIfNoJs,
|
bottom: 0,
|
||||||
)}
|
left: 0,
|
||||||
>
|
right: 0,
|
||||||
<LinearGradient
|
},
|
||||||
start={{ x: 0, y: 0.5 }}
|
lines !== 1 && {
|
||||||
end={{ x: 1, y: 0.5 }}
|
width: i === lines - 1 ? percent(40) : percent(100),
|
||||||
colors={["transparent", theme.overlay1, "transparent"]}
|
height: rem(1.2),
|
||||||
transition={{
|
marginBottom: rem(0.5),
|
||||||
loop: true,
|
overflow: "hidden",
|
||||||
repeatReverse: false,
|
borderRadius: px(6),
|
||||||
}}
|
},
|
||||||
animate={{
|
],
|
||||||
translateX: width
|
hiddenIfNoJs,
|
||||||
? [perc(-100), { value: perc(100), type: "timing", duration: 800, delay: 800 }]
|
)}
|
||||||
: undefined,
|
>
|
||||||
}}
|
<LinearGradient
|
||||||
{...css([
|
start={{ x: 0, y: 0.5 }}
|
||||||
{
|
end={{ x: 1, y: 0.5 }}
|
||||||
position: "absolute",
|
colors={["transparent", theme.overlay1, "transparent"]}
|
||||||
top: 0,
|
transition={{
|
||||||
bottom: 0,
|
loop: true,
|
||||||
left: 0,
|
repeatReverse: false,
|
||||||
right: 0,
|
}}
|
||||||
},
|
animate={{
|
||||||
Platform.OS === "web" && {
|
translateX: width
|
||||||
// @ts-ignore Web only properties
|
? [perc(-100), { value: perc(100), type: "timing", duration: 800, delay: 800 }]
|
||||||
animation: "skeleton 1.6s linear 0.5s infinite",
|
: undefined,
|
||||||
transform: "translateX(-100%)",
|
}}
|
||||||
},
|
{...css([
|
||||||
])}
|
{
|
||||||
/>
|
position: "absolute",
|
||||||
</MotiView>
|
top: 0,
|
||||||
)}
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
Platform.OS === "web" && {
|
||||||
|
// @ts-ignore Web only properties
|
||||||
|
animation: "skeleton 1.6s linear 0.5s infinite",
|
||||||
|
transform: "translateX(-100%)",
|
||||||
|
},
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
</MotiView>
|
||||||
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -19,8 +19,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ComponentType, ComponentProps } from "react";
|
import { ComponentType, ComponentProps } from "react";
|
||||||
import { Platform, TextProps } from "react-native";
|
import { Platform, Text, TextProps, TextStyle } from "react-native";
|
||||||
import { rem, useYoshiki } from "yoshiki/native";
|
import { percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||||
import {
|
import {
|
||||||
H1 as EH1,
|
H1 as EH1,
|
||||||
H2 as EH2,
|
H2 as EH2,
|
||||||
@ -29,11 +29,14 @@ import {
|
|||||||
H5 as EH5,
|
H5 as EH5,
|
||||||
H6 as EH6,
|
H6 as EH6,
|
||||||
P as EP,
|
P as EP,
|
||||||
|
LI as ELI,
|
||||||
} from "@expo/html-elements";
|
} from "@expo/html-elements";
|
||||||
|
import { ts } from ".";
|
||||||
|
|
||||||
const styleText = (
|
const styleText = (
|
||||||
Component: ComponentType<ComponentProps<typeof EP>>,
|
Component: ComponentType<ComponentProps<typeof EP>>,
|
||||||
type?: "header" | "sub",
|
type?: "header" | "sub",
|
||||||
|
custom?: TextStyle,
|
||||||
) => {
|
) => {
|
||||||
const Text = (props: ComponentProps<typeof EP>) => {
|
const Text = (props: ComponentProps<typeof EP>) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
@ -43,16 +46,13 @@ const styleText = (
|
|||||||
{...css(
|
{...css(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
// TODO: use custom fonts on mobile also.
|
marginTop: 0,
|
||||||
fontFamily:
|
marginBottom: rem(0.5),
|
||||||
Platform.OS === "web"
|
// fontFamily: type === "header" ? theme.fonts.heading : theme.fonts.paragraph,
|
||||||
? type === "header"
|
|
||||||
? theme.fonts.heading
|
|
||||||
: theme.fonts.paragraph
|
|
||||||
: undefined,
|
|
||||||
color: type === "header" ? theme.heading : theme.paragraph,
|
color: type === "header" ? theme.heading : theme.paragraph,
|
||||||
},
|
},
|
||||||
type === "sub" && { fontWeight: "300", opacity: 0.8, fontSize: rem(0.8) },
|
type === "sub" && { fontWeight: "300", opacity: 0.8, fontSize: rem(0.8) },
|
||||||
|
custom,
|
||||||
],
|
],
|
||||||
props as TextProps,
|
props as TextProps,
|
||||||
)}
|
)}
|
||||||
@ -62,12 +62,31 @@ const styleText = (
|
|||||||
return Text;
|
return Text;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const H1 = styleText(EH1, "header");
|
export const H1 = styleText(EH1, "header", { fontSize: rem(3) });
|
||||||
export const H2 = styleText(EH2, "header");
|
export const H2 = styleText(EH2, "header", { fontSize: rem(2) });
|
||||||
export const H3 = styleText(EH3, "header");
|
export const H3 = styleText(EH3, "header");
|
||||||
export const H4 = styleText(EH4, "header");
|
export const H4 = styleText(EH4, "header");
|
||||||
export const H5 = styleText(EH5, "header");
|
export const H5 = styleText(EH5, "header");
|
||||||
export const H6 = styleText(EH6, "header");
|
export const H6 = styleText(EH6, "header");
|
||||||
export const Heading = styleText(EP, "header");
|
export const Heading = styleText(EP, "header");
|
||||||
export const P = styleText(EP);
|
export const P = styleText(EP, undefined, { fontSize: rem(1) });
|
||||||
export const SubP = styleText(EP, "sub");
|
export const SubP = styleText(EP, "sub");
|
||||||
|
|
||||||
|
export const LI = ({ children, ...props }: TextProps) => {
|
||||||
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<P accessibilityRole="listitem" {...props}>
|
||||||
|
<Text
|
||||||
|
{...css({
|
||||||
|
height: percent(100),
|
||||||
|
marginBottom: rem(0.5),
|
||||||
|
paddingRight: ts(1),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{String.fromCharCode(0x2022)}
|
||||||
|
</Text>
|
||||||
|
{children}
|
||||||
|
</P>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -31,6 +31,7 @@ export const catppuccin: ThemeBuilder = {
|
|||||||
appbar: "#e64553",
|
appbar: "#e64553",
|
||||||
overlay0: "#9ca0b0",
|
overlay0: "#9ca0b0",
|
||||||
overlay1: "#7c7f93",
|
overlay1: "#7c7f93",
|
||||||
|
link: "#1e66f5",
|
||||||
default: {
|
default: {
|
||||||
background: "#eff1f5",
|
background: "#eff1f5",
|
||||||
accent: "#ea76cb",
|
accent: "#ea76cb",
|
||||||
@ -61,6 +62,7 @@ export const catppuccin: ThemeBuilder = {
|
|||||||
appbar: "#94e2d5",
|
appbar: "#94e2d5",
|
||||||
overlay0: "#6c7086",
|
overlay0: "#6c7086",
|
||||||
overlay1: "#9399b2",
|
overlay1: "#9399b2",
|
||||||
|
link: "#89b4fa",
|
||||||
default: {
|
default: {
|
||||||
background: "#1e1e2e",
|
background: "#1e1e2e",
|
||||||
accent: "#f5c2e7",
|
accent: "#f5c2e7",
|
||||||
|
@ -37,6 +37,7 @@ type Mode = {
|
|||||||
appbar: Property.Color;
|
appbar: Property.Color;
|
||||||
overlay0: Property.Color;
|
overlay0: Property.Color;
|
||||||
overlay1: Property.Color;
|
overlay1: Property.Color;
|
||||||
|
link: Property.Color;
|
||||||
variant: Variant;
|
variant: Variant;
|
||||||
colors: {
|
colors: {
|
||||||
red: Property.Color;
|
red: Property.Color;
|
||||||
@ -64,13 +65,13 @@ declare module "yoshiki" {
|
|||||||
user: Mode & Variant;
|
user: Mode & Variant;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// declare module "yoshiki/native" {
|
declare module "yoshiki/native" {
|
||||||
// export interface Theme extends ThemeSettings, Mode, Variant {
|
export interface Theme extends ThemeSettings, Mode, Variant {
|
||||||
// light: Mode & Variant;
|
light: Mode & Variant;
|
||||||
// dark: Mode & Variant;
|
dark: Mode & Variant;
|
||||||
// user: Mode & Variant;
|
user: Mode & Variant;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
export type { Theme } from "yoshiki";
|
export type { Theme } from "yoshiki";
|
||||||
export type ThemeBuilder = ThemeSettings & {
|
export type ThemeBuilder = ThemeSettings & {
|
||||||
@ -158,5 +159,5 @@ export const ContrastArea = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const alpha = (color: Property.Color, alpha: number) => {
|
export const alpha = (color: Property.Color, alpha: number) => {
|
||||||
return color + (alpha * 255).toString(16);
|
return color + Math.round(alpha * 255).toString(16);
|
||||||
};
|
};
|
||||||
|
23
front/packages/primitives/src/utils/head.tsx
Normal file
23
front/packages/primitives/src/utils/head.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const Head = ({ title, description }: { title?: string | null; description?: string | null }) => {
|
||||||
|
return null;
|
||||||
|
};
|
30
front/packages/primitives/src/utils/head.web.tsx
Normal file
30
front/packages/primitives/src/utils/head.web.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 NextHead from "next/head";
|
||||||
|
|
||||||
|
export const Head = ({ title, description }: { title?: string | null; description?: string | null }) => {
|
||||||
|
return (
|
||||||
|
<NextHead>
|
||||||
|
{title && <title>{title + "- Kyoo"}</title>}
|
||||||
|
{description && <meta name="description" content={description} />}
|
||||||
|
</NextHead>
|
||||||
|
);
|
||||||
|
};
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Movie, QueryIdentifier, Show, getDisplayDate } from "@kyoo/models";
|
import { Movie, QueryIdentifier, Show, getDisplayDate, Genre, Studio } from "@kyoo/models";
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
H1,
|
H1,
|
||||||
@ -31,43 +31,40 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
IconButton,
|
IconButton,
|
||||||
IconFab,
|
IconFab,
|
||||||
|
Head,
|
||||||
|
HR,
|
||||||
|
H2,
|
||||||
|
UL,
|
||||||
|
LI,
|
||||||
|
A,
|
||||||
|
ts,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, StyleSheet, View } from "react-native";
|
import { StyleSheet, View } from "react-native";
|
||||||
import { em, percent, rem, vh, useYoshiki, Stylable } from "yoshiki/native";
|
import {
|
||||||
import { Fetch, WithLoading } from "../fetch";
|
Theme,
|
||||||
|
sm,
|
||||||
|
md,
|
||||||
|
px,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
em,
|
||||||
|
percent,
|
||||||
|
rem,
|
||||||
|
vh,
|
||||||
|
useYoshiki,
|
||||||
|
Stylable,
|
||||||
|
} from "yoshiki/native";
|
||||||
|
import { Fetch } from "../fetch";
|
||||||
import { Navbar } from "../navbar";
|
import { Navbar } from "../navbar";
|
||||||
|
|
||||||
// const StudioText = ({
|
|
||||||
// studio,
|
|
||||||
// loading = false,
|
|
||||||
// sx,
|
|
||||||
// }: {
|
|
||||||
// studio?: Studio | null;
|
|
||||||
// loading?: boolean;
|
|
||||||
// sx?: SxProps;
|
|
||||||
// }) => {
|
|
||||||
// const { t } = useTranslation("browse");
|
|
||||||
|
|
||||||
// if (!loading && !studio) return null;
|
|
||||||
// return (
|
|
||||||
// <Typography sx={sx}>
|
|
||||||
// {t("show.studio")}:{" "}
|
|
||||||
// {loading ? (
|
|
||||||
// <Skeleton width="5rem" sx={{ display: "inline-flex" }} />
|
|
||||||
// ) : (
|
|
||||||
// <Link href={`/studio/${studio!.slug}`}>{studio!.name}</Link>
|
|
||||||
// )}
|
|
||||||
// </Typography>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
const TitleLine = ({
|
const TitleLine = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
slug,
|
slug,
|
||||||
name,
|
name,
|
||||||
date,
|
date,
|
||||||
poster,
|
poster,
|
||||||
|
studio,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@ -75,6 +72,7 @@ const TitleLine = ({
|
|||||||
name?: string;
|
name?: string;
|
||||||
date?: string;
|
date?: string;
|
||||||
poster?: string | null;
|
poster?: string | null;
|
||||||
|
studio?: Studio | null;
|
||||||
} & Stylable) => {
|
} & Stylable) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -83,162 +81,197 @@ const TitleLine = ({
|
|||||||
<Container
|
<Container
|
||||||
{...css(
|
{...css(
|
||||||
{
|
{
|
||||||
flexDirection: { xs: "column", sm: "row" },
|
flexDirection: { xs: "column", md: "row" },
|
||||||
alignItems: { xs: "center", sm: "flex-start" },
|
|
||||||
},
|
},
|
||||||
props,
|
props,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Poster
|
|
||||||
src={poster}
|
|
||||||
alt={name}
|
|
||||||
isLoading={isLoading}
|
|
||||||
layout={{
|
|
||||||
width: { xs: percent(50), md: percent(25) },
|
|
||||||
}}
|
|
||||||
{...css({ maxWidth: { xs: px(175), sm: "unset" }, flexShrink: 0 })}
|
|
||||||
/>
|
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
alignSelf: { xs: "center", sm: "flex-end", md: "center" },
|
flexDirection: { xs: "column", sm: "row" },
|
||||||
alignItems: { xs: "center", sm: "flex-start" },
|
alignItems: { xs: "center", sm: "flex-start" },
|
||||||
paddingLeft: { sm: em(2.5) },
|
flexGrow: 1,
|
||||||
flexShrink: 1,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Skeleton {...css({ width: rem(15), height: rem(3), marginBottom: rem(0.5) })}>
|
<Poster
|
||||||
{isLoading || (
|
src={poster}
|
||||||
<H1
|
alt={name}
|
||||||
{...css({
|
isLoading={isLoading}
|
||||||
fontWeight: { md: "900" },
|
layout={{
|
||||||
fontSize: rem(3),
|
width: { xs: percent(50), md: percent(25) },
|
||||||
marginTop: 0,
|
}}
|
||||||
marginBottom: rem(0.5),
|
{...css({ maxWidth: { xs: px(175), sm: "unset" }, flexShrink: 0 })}
|
||||||
textAlign: { xs: "center", sm: "flex-start" },
|
/>
|
||||||
color: (theme) => ({ xs: theme.user.heading, md: theme.heading }),
|
<View
|
||||||
})}
|
{...css({
|
||||||
>
|
alignSelf: { xs: "center", sm: "flex-end", md: "center" },
|
||||||
{name}
|
alignItems: { xs: "center", sm: "flex-start" },
|
||||||
</H1>
|
paddingLeft: { sm: em(2.5) },
|
||||||
)}
|
flexShrink: 1,
|
||||||
</Skeleton>
|
flexGrow: 1,
|
||||||
{(isLoading || date) && (
|
})}
|
||||||
<Skeleton
|
>
|
||||||
{...css({
|
<Skeleton {...css({ width: rem(15), height: rem(3) })}>
|
||||||
width: rem(5),
|
|
||||||
height: rem(1.5),
|
|
||||||
marginBottom: rem(0.5),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{isLoading || (
|
{isLoading || (
|
||||||
<P
|
<H1
|
||||||
{...css({
|
{...css({
|
||||||
fontWeight: "300",
|
fontWeight: { md: "900" },
|
||||||
fontSize: rem(1.5),
|
textAlign: { xs: "center", sm: "left" },
|
||||||
letterSpacing: 0,
|
color: (theme: Theme) => ({ xs: theme.user.heading, md: theme.heading }),
|
||||||
marginTop: 0,
|
|
||||||
marginBottom: rem(0.5),
|
|
||||||
textAlign: { xs: "center", sm: "flex-start" },
|
|
||||||
color: (theme) => ({ xs: theme.user.heading, md: theme.heading }),
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{date}
|
{name}
|
||||||
</P>
|
</H1>
|
||||||
)}
|
)}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
)}
|
{(isLoading || date) && (
|
||||||
<View {...css({ flexDirection: "row" })} /*sx={{ "& > *": { m: ".3rem !important" } }} */>
|
<Skeleton
|
||||||
<IconFab
|
{...css({
|
||||||
icon="play-arrow"
|
width: rem(5),
|
||||||
as={Link}
|
height: rem(1.5),
|
||||||
href={`/watch/${slug}`}
|
})}
|
||||||
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
|
>
|
||||||
{...css({ bg: { xs: theme.user.accent, md: theme.accent } })}
|
{isLoading || (
|
||||||
{...tooltip(t("show.play"))}
|
<P
|
||||||
/>
|
{...css({
|
||||||
<IconButton
|
fontWeight: "300",
|
||||||
icon="local-movies"
|
fontSize: rem(1.5),
|
||||||
color={{ xs: theme.user.colors.black, md: theme.colors.white }}
|
letterSpacing: 0,
|
||||||
{...tooltip(t("show.trailer"))}
|
textAlign: { xs: "center", sm: "left" },
|
||||||
/>
|
color: (theme: Theme) => ({ xs: theme.user.heading, md: theme.heading }),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{date}
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
</Skeleton>
|
||||||
|
)}
|
||||||
|
<View {...css({ flexDirection: "row" })}>
|
||||||
|
<IconFab
|
||||||
|
icon="play-arrow"
|
||||||
|
as={Link}
|
||||||
|
href={`/watch/${slug}`}
|
||||||
|
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
|
||||||
|
{...css({ bg: { xs: theme.user.accent, md: theme.accent } })}
|
||||||
|
{...tooltip(t("show.play"))}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="local-movies"
|
||||||
|
color={{ xs: theme.user.colors.black, md: theme.colors.white }}
|
||||||
|
{...tooltip(t("show.trailer"))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{/* <View */}
|
<View
|
||||||
{/* {...css({ */}
|
{...css([
|
||||||
{/* display: { xs: "none", md: "flex" }, */}
|
{
|
||||||
{/* flexDirection: "column", */}
|
paddingTop: { xs: ts(3), sm: ts(8) },
|
||||||
{/* alignSelf: "flex-end", */}
|
alignSelf: { xs: "flex-start", md: "flex-end" },
|
||||||
{/* paddingRight: px(15), */}
|
justifyContent: "flex-end",
|
||||||
{/* })} */}
|
flexDirection: "column",
|
||||||
{/* > */}
|
},
|
||||||
{/* {(isLoading || logo || true) && ( */}
|
md({
|
||||||
{/* <Image */}
|
position: "absolute",
|
||||||
{/* src={logo} */}
|
top: 0,
|
||||||
{/* alt="" */}
|
bottom: 0,
|
||||||
{/* layout={{ */}
|
right: 0,
|
||||||
{/* width: "100%", */}
|
width: percent(25),
|
||||||
{/* height: px(100), */}
|
height: percent(100),
|
||||||
{/* }} */}
|
paddingRight: ts(3),
|
||||||
{/* // sx={{ display: { xs: "none", lg: "unset" } }} */}
|
}),
|
||||||
{/* /> */}
|
])}
|
||||||
{/* )} */}
|
>
|
||||||
{/* {/1* <StudioText loading={!data} studio={data?.studio} sx={{ mt: "auto", mb: 3 }} /> *1/} */}
|
<P
|
||||||
{/* </View> */}
|
{...css({
|
||||||
|
color: (theme) => theme.user.paragraph,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t("show.studio")}:{" "}
|
||||||
|
{isLoading ? (
|
||||||
|
<Skeleton />
|
||||||
|
) : (
|
||||||
|
<A href={`/studio/${studio!.slug}`} {...css({ color: (theme) => theme.user.link })}>
|
||||||
|
{studio!.name}
|
||||||
|
</A>
|
||||||
|
)}
|
||||||
|
</P>
|
||||||
|
</View>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const Tata = () => {
|
const Description = ({
|
||||||
// return (
|
isLoading,
|
||||||
// <Container sx={{ pt: 2 }}>
|
overview,
|
||||||
// <Typography align="justify" sx={{ flexBasis: 0, flexGrow: 1, pt: { sm: 2 } }}>
|
genres,
|
||||||
// {data
|
...props
|
||||||
// ? data.overview ?? t("show.noOverview")
|
}: {
|
||||||
// : [...Array(4)].map((_, i) => <Skeleton key={i} />)}
|
isLoading: boolean;
|
||||||
// </Typography>
|
overview?: string | null;
|
||||||
// <Divider
|
genres?: Genre[];
|
||||||
// orientation="vertical"
|
} & Stylable) => {
|
||||||
// variant="middle"
|
const { t } = useTranslation();
|
||||||
// flexItem
|
const { css } = useYoshiki();
|
||||||
// sx={{ mx: 2, display: { xs: "none", sm: "block" } }}
|
|
||||||
// />
|
|
||||||
// <Box sx={{ flexBasis: "25%", display: { xs: "none", sm: "block" } }}>
|
|
||||||
// <StudioText
|
|
||||||
// loading={!data}
|
|
||||||
// studio={data?.studio}
|
|
||||||
// sx={{ display: { xs: "none", sm: "block", md: "none" }, pb: 2 }}
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <Typography variant="h4" component="h2">
|
return (
|
||||||
// {t("show.genre")}
|
<Container {...css({ flexDirection: { xs: "column", sm: "row" } }, props)}>
|
||||||
// </Typography>
|
<P
|
||||||
// {!data || data.genres?.length ? (
|
{...css({
|
||||||
// <ul>
|
display: { xs: "flex", sm: "none" },
|
||||||
// {(data ? data.genres! : [...Array(3)]).map((genre, i) => (
|
color: (theme: Theme) => theme.user.paragraph,
|
||||||
// <li key={genre?.id ?? i}>
|
})}
|
||||||
// <Typography>
|
>
|
||||||
// {genre ? <Link href={`/genres/${genre.slug}`}>{genre.name}</Link> : <Skeleton />}
|
{t("show.genre")}:{" "}
|
||||||
// </Typography>
|
{(isLoading ? [...Array(3)] : genres!).map((genre, i) => (
|
||||||
// </li>
|
<>
|
||||||
// ))}
|
{i !== 0 && ", "}
|
||||||
// </ul>
|
{isLoading ? (
|
||||||
// ) : (
|
<Skeleton key={i} />
|
||||||
// <Typography>{t("show.genre-none")}</Typography>
|
) : (
|
||||||
// )}
|
<A key={genre.slug} href={`/genres/${genre.slug}`}>
|
||||||
// </Box>
|
{genre.name}
|
||||||
// </Container>
|
</A>
|
||||||
// );
|
)}
|
||||||
// };
|
</>
|
||||||
|
))}
|
||||||
|
</P>
|
||||||
|
|
||||||
const min = Platform.OS === "web"
|
<Skeleton
|
||||||
? (...values: number[]): number => `min(${values.join(", ")})` as unknown as number
|
lines={4}
|
||||||
: (...values: number[]): number => Math.min(...values);
|
{...css({ width: percent(100), flexBasis: 0, flexGrow: 1, paddingTop: ts(4) })}
|
||||||
const max = Platform.OS === "web"
|
>
|
||||||
? (...values: number[]): number => `max(${values.join(", ")})` as unknown as number
|
{isLoading || (
|
||||||
: (...values: number[]): number => Math.max(...values);
|
<P {...css({ flexBasis: 0, flexGrow: 1, textAlign: "justify", paddingTop: ts(4) })}>
|
||||||
const px = Platform.OS === "web"
|
{overview ?? t("show.noOverview")}
|
||||||
? (value: number): number => `${value}px` as unknown as number
|
</P>
|
||||||
: (value: number): number => value;
|
)}
|
||||||
|
</Skeleton>
|
||||||
|
<HR
|
||||||
|
orientation="vertical"
|
||||||
|
{...css({ marginX: ts(2), display: { xs: "none", sm: "flex" } })}
|
||||||
|
/>
|
||||||
|
<View {...css({ flexBasis: percent(25), display: { xs: "none", sm: "flex" } })}>
|
||||||
|
<H2>{t("show.genre")}</H2>
|
||||||
|
{isLoading || genres?.length ? (
|
||||||
|
<UL>
|
||||||
|
{(isLoading ? [...Array(3)] : genres!).map((genre, i) => (
|
||||||
|
<LI key={genre?.id ?? i}>
|
||||||
|
{isLoading ? (
|
||||||
|
<Skeleton {...css({ marginBottom: 0 })} />
|
||||||
|
) : (
|
||||||
|
<A href={`/genres/${genre.slug}`}>{genre.name}</A>
|
||||||
|
)}
|
||||||
|
</LI>
|
||||||
|
))}
|
||||||
|
</UL>
|
||||||
|
) : (
|
||||||
|
<P>{t("show.genre-none")}</P>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ShowHeader = ({
|
export const ShowHeader = ({
|
||||||
query,
|
query,
|
||||||
@ -256,19 +289,22 @@ export const ShowHeader = ({
|
|||||||
<Navbar {...css({ bg: "transparent" })} />
|
<Navbar {...css({ bg: "transparent" })} />
|
||||||
<Fetch query={query}>
|
<Fetch query={query}>
|
||||||
{({ isLoading, ...data }) => (
|
{({ isLoading, ...data }) => (
|
||||||
<>
|
<Main {...css(StyleSheet.absoluteFillObject)}>
|
||||||
{/* TODO: HEAD element for SEO*/}
|
<Head title={data?.name} description={data?.overview} />
|
||||||
{/* TODO: Add a shadow on navbar items */}
|
{/* TODO: Add a shadow on navbar items */}
|
||||||
{/* TODO: Put the navbar outside of the scrollbox */}
|
{/* TODO: Put the navbar outside of the scrollbox */}
|
||||||
<ImageBackground
|
<ImageBackground
|
||||||
src={data?.thumbnail}
|
src={data?.thumbnail}
|
||||||
alt=""
|
alt=""
|
||||||
as={Main}
|
|
||||||
containerStyle={{
|
containerStyle={{
|
||||||
height: { xs: vh(40), sm: min(vh(60), px(750)), lg: vh(70) },
|
height: {
|
||||||
minHeight: { xs: px(350), sm: px(500), lg: px(600) },
|
xs: vh(40),
|
||||||
|
sm: min(vh(60), px(750)),
|
||||||
|
md: min(vh(60), px(680)),
|
||||||
|
lg: vh(70),
|
||||||
|
},
|
||||||
|
minHeight: { xs: px(350), sm: px(300), md: px(400), lg: px(600) },
|
||||||
}}
|
}}
|
||||||
{...css(StyleSheet.absoluteFillObject)}
|
|
||||||
>
|
>
|
||||||
<TitleLine
|
<TitleLine
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@ -276,31 +312,24 @@ export const ShowHeader = ({
|
|||||||
name={data?.name}
|
name={data?.name}
|
||||||
date={data ? getDisplayDate(data as any) : undefined}
|
date={data ? getDisplayDate(data as any) : undefined}
|
||||||
poster={data?.poster}
|
poster={data?.poster}
|
||||||
|
studio={data?.studio}
|
||||||
{...css({
|
{...css({
|
||||||
marginTop: { xs: max(vh(20), px(200)), sm: vh(45), md: vh(35) }
|
marginTop: {
|
||||||
|
xs: max(vh(20), px(200)),
|
||||||
|
sm: vh(45),
|
||||||
|
md: max(vh(30), px(150)),
|
||||||
|
lg: max(vh(35), px(200)),
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{/* <Container sx={{ display: { xs: "block", sm: "none" }, pt: 3 }}> */}
|
|
||||||
{/* <StudioText loading={!data} studio={data?.studio} sx={{ mb: 1 }} /> */}
|
|
||||||
{/* <Typography sx={{ mb: 1 }}> */}
|
|
||||||
{/* {t("show.genre")} */}
|
|
||||||
{/* {": "} */}
|
|
||||||
{/* {!data ? ( */}
|
|
||||||
{/* <Skeleton width="10rem" sx={{ display: "inline-flex" }} /> */}
|
|
||||||
{/* ) : data?.genres && data.genres.length ? ( */}
|
|
||||||
{/* data.genres.map((genre, i) => [ */}
|
|
||||||
{/* i > 0 && ", ", */}
|
|
||||||
{/* <Link key={genre.id} href={`/genres/${genre.slug}`}> */}
|
|
||||||
{/* {genre.name} */}
|
|
||||||
{/* </Link>, */}
|
|
||||||
{/* ]) */}
|
|
||||||
{/* ) : ( */}
|
|
||||||
{/* t("show.genre-none") */}
|
|
||||||
{/* )} */}
|
|
||||||
{/* </Typography> */}
|
|
||||||
{/* </Container> */}
|
|
||||||
</ImageBackground>
|
</ImageBackground>
|
||||||
</>
|
<Description
|
||||||
|
isLoading={isLoading}
|
||||||
|
overview={data?.overview}
|
||||||
|
genres={data?.genres}
|
||||||
|
{...css({ paddingTop: { xs: 0, md: ts(2) } })}
|
||||||
|
/>
|
||||||
|
</Main>
|
||||||
)}
|
)}
|
||||||
</Fetch>
|
</Fetch>
|
||||||
</>
|
</>
|
||||||
|
@ -9682,7 +9682,7 @@ __metadata:
|
|||||||
react-native-screens: ~3.18.0
|
react-native-screens: ~3.18.0
|
||||||
react-native-svg: 13.4.0
|
react-native-svg: 13.4.0
|
||||||
typescript: ^4.6.3
|
typescript: ^4.6.3
|
||||||
yoshiki: 0.2.11
|
yoshiki: 0.3.1
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@ -13319,7 +13319,7 @@ __metadata:
|
|||||||
superjson: ^1.11.0
|
superjson: ^1.11.0
|
||||||
typescript: ^4.9.3
|
typescript: ^4.9.3
|
||||||
webpack: ^5.75.0
|
webpack: ^5.75.0
|
||||||
yoshiki: 0.2.11
|
yoshiki: 0.3.1
|
||||||
zod: ^3.19.1
|
zod: ^3.19.1
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@ -13644,9 +13644,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"yoshiki@npm:0.2.11":
|
"yoshiki@npm:0.3.1":
|
||||||
version: 0.2.11
|
version: 0.3.1
|
||||||
resolution: "yoshiki@npm:0.2.11"
|
resolution: "yoshiki@npm:0.3.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node": 18.x.x
|
"@types/node": 18.x.x
|
||||||
"@types/react": 18.x.x
|
"@types/react": 18.x.x
|
||||||
@ -13661,7 +13661,7 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
react-native-web:
|
react-native-web:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 5a2bbb62b2270d3456f114cfbb24a84ad6b8a94b147687929ccffe2d179560ef40b46df1d4054eda91310d295a9a674bbb201765deb86dc96a2133bfd702235a
|
checksum: 9448b628b61bbcc4485af7aed667a1c0f8490a2066fa35953b4a02126f1d31d94f90e27a592797f8ecedc0ce2220976a7651ba989f4ff3c68513496b2f9fdd0b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user