mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-01 04:34:50 -04:00
Add list view on browse
This commit is contained in:
parent
3b29e1a87a
commit
be6551888e
@ -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.7"
|
"yoshiki": "0.2.9"
|
||||||
},
|
},
|
||||||
"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.7",
|
"yoshiki": "0.2.9",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -70,6 +70,11 @@ const GlobalCssTheme = () => {
|
|||||||
#__next {
|
#__next {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.infinite-scroll-component__outerdiv {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<WebTooltip theme={theme} />
|
<WebTooltip theme={theme} />
|
||||||
<SkeletonCss />
|
<SkeletonCss />
|
||||||
|
47
front/packages/primitives/src/animated.tsx
Normal file
47
front/packages/primitives/src/animated.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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 { motify } from "moti";
|
||||||
|
import { Component, ComponentType, FunctionComponent } from "react";
|
||||||
|
import { Poster } from "./image";
|
||||||
|
|
||||||
|
const getDisplayName = (Cmp: ComponentType<any>) => {
|
||||||
|
return Cmp.displayName || Cmp.name || "Component";
|
||||||
|
};
|
||||||
|
|
||||||
|
const asClass = <Props,>(Cmp: FunctionComponent<Props>) => {
|
||||||
|
// TODO: ensure that every props is given at least once.
|
||||||
|
return class AsClass extends Component<Partial<Props> & { forward?: Partial<Props> }> {
|
||||||
|
static displayName = `WithClass(${getDisplayName(Cmp)})`;
|
||||||
|
|
||||||
|
constructor(props: Partial<Props> & { forward?: Partial<Props> }) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// @ts-ignore See todo above
|
||||||
|
return <Cmp {...this.props} {...this.props.forward} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Animated = {
|
||||||
|
Poster: motify(asClass(Poster))(),
|
||||||
|
};
|
@ -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 { useState } from "react";
|
import { ComponentType, ReactNode, useState } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Image as Img,
|
Image as Img,
|
||||||
@ -26,10 +26,14 @@ import {
|
|||||||
ImageStyle,
|
ImageStyle,
|
||||||
Platform,
|
Platform,
|
||||||
ImageProps,
|
ImageProps,
|
||||||
|
ViewProps,
|
||||||
|
ViewStyle,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { percent, useYoshiki } from "yoshiki/native";
|
||||||
import { YoshikiStyle } from "yoshiki/dist/type";
|
import { StyleList, YoshikiStyle } from "yoshiki/dist/type";
|
||||||
import { Skeleton } from "./skeleton";
|
import { Skeleton } from "./skeleton";
|
||||||
|
import { LinearGradient, LinearGradientProps } from "expo-linear-gradient";
|
||||||
|
import { alpha, ContrastArea } from "./themes";
|
||||||
|
|
||||||
type YoshikiEnhanced<Style> = Style extends any
|
type YoshikiEnhanced<Style> = Style extends any
|
||||||
? {
|
? {
|
||||||
@ -42,22 +46,21 @@ type WithLoading<T> = (T & { isLoading?: boolean }) | (Partial<T> & { isLoading:
|
|||||||
type Props = WithLoading<{
|
type Props = WithLoading<{
|
||||||
src?: string | ImageSourcePropType | null;
|
src?: string | ImageSourcePropType | null;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
fallback?: string | ImageSourcePropType;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type ImageLayout = YoshikiEnhanced<
|
||||||
|
| { width: ViewStyle["width"]; height: ViewStyle["height"] }
|
||||||
|
| { width: ViewStyle["width"]; aspectRatio: ViewStyle["aspectRatio"] }
|
||||||
|
| { height: ViewStyle["height"]; aspectRatio: ViewStyle["aspectRatio"] }
|
||||||
|
>;
|
||||||
|
|
||||||
export const Image = ({
|
export const Image = ({
|
||||||
src,
|
src,
|
||||||
alt,
|
alt,
|
||||||
isLoading: forcedLoading = false,
|
isLoading: forcedLoading = false,
|
||||||
layout,
|
layout,
|
||||||
...props
|
...props
|
||||||
}: Props & { style?: ImageStyle } & {
|
}: Props & { style?: ViewStyle } & { layout: ImageLayout }) => {
|
||||||
layout: YoshikiEnhanced<
|
|
||||||
| { width: ImageStyle["width"]; height: ImageStyle["height"] }
|
|
||||||
| { width: ImageStyle["width"]; aspectRatio: ImageStyle["aspectRatio"] }
|
|
||||||
| { height: ImageStyle["height"]; aspectRatio: ImageStyle["aspectRatio"] }
|
|
||||||
>;
|
|
||||||
}) => {
|
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const [state, setState] = useState<"loading" | "errored" | "finished">(
|
const [state, setState] = useState<"loading" | "errored" | "finished">(
|
||||||
src ? "loading" : "errored",
|
src ? "loading" : "errored",
|
||||||
@ -71,11 +74,11 @@ export const Image = ({
|
|||||||
setOldSource(src);
|
setOldSource(src);
|
||||||
}
|
}
|
||||||
|
|
||||||
const border = { borderRadius: 6 } satisfies ImageStyle;
|
const border = { borderRadius: 6 } satisfies ViewStyle;
|
||||||
|
|
||||||
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border])} />;
|
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||||
if (!src || state === "errored")
|
if (!src || state === "errored")
|
||||||
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border])} />;
|
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />;
|
||||||
|
|
||||||
const nativeProps = Platform.select<ImageProps>({
|
const nativeProps = Platform.select<ImageProps>({
|
||||||
web: {
|
web: {
|
||||||
@ -85,22 +88,20 @@ export const Image = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton variant="custom" show={state === "loading"} {...css([layout, border])}>
|
<Skeleton variant="custom" show={state === "loading"} {...css([layout, border], props)}>
|
||||||
<Img
|
<Img
|
||||||
source={typeof src === "string" ? { uri: src } : src}
|
source={typeof src === "string" ? { uri: src } : src}
|
||||||
accessibilityLabel={alt}
|
accessibilityLabel={alt}
|
||||||
onLoad={() => setState("finished")}
|
onLoad={() => setState("finished")}
|
||||||
onError={() => setState("errored")}
|
onError={() => setState("errored")}
|
||||||
{...nativeProps}
|
{...nativeProps}
|
||||||
{...css(
|
{...css([
|
||||||
[
|
{
|
||||||
{
|
width: percent(100),
|
||||||
resizeMode: "cover",
|
height: percent(100),
|
||||||
},
|
resizeMode: "cover",
|
||||||
layout,
|
},
|
||||||
],
|
])}
|
||||||
props,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
);
|
);
|
||||||
@ -111,8 +112,90 @@ export const Poster = ({
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
layout,
|
layout,
|
||||||
...props
|
...props
|
||||||
}: Props & { style?: ImageStyle } & {
|
}: Props & { style?: ViewStyle } & {
|
||||||
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
|
layout: YoshikiEnhanced<{ width: ViewStyle["width"] } | { height: ViewStyle["height"] }>;
|
||||||
}) => (
|
}) => (
|
||||||
<Image isLoading={isLoading} alt={alt} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />
|
<Image isLoading={isLoading} alt={alt} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ImageBackground = <AsProps = ViewProps,>({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
gradient = true,
|
||||||
|
as,
|
||||||
|
children,
|
||||||
|
containerStyle,
|
||||||
|
imageStyle,
|
||||||
|
isLoading,
|
||||||
|
...asProps
|
||||||
|
}: {
|
||||||
|
as?: ComponentType<AsProps>;
|
||||||
|
gradient?: Partial<LinearGradientProps> | boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
containerStyle?: StyleList<ViewStyle>;
|
||||||
|
imageStyle?: StyleList<ImageStyle>;
|
||||||
|
} & AsProps &
|
||||||
|
Props) => {
|
||||||
|
const [isErrored, setErrored] = useState(false);
|
||||||
|
|
||||||
|
const nativeProps = Platform.select<ImageProps>({
|
||||||
|
web: {
|
||||||
|
defaultSource: typeof src === "string" ? { uri: src! } : Array.isArray(src) ? src[0] : src!,
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
const Container = as ?? View;
|
||||||
|
return (
|
||||||
|
<ContrastArea contrastText>
|
||||||
|
{({ css, theme }) => (
|
||||||
|
<Container {...(asProps as AsProps)}>
|
||||||
|
<View
|
||||||
|
{...css([
|
||||||
|
{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
bg: (theme) => theme.background,
|
||||||
|
},
|
||||||
|
containerStyle,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{src && !isErrored && (
|
||||||
|
<Img
|
||||||
|
source={typeof src === "string" ? { uri: src } : src}
|
||||||
|
accessibilityLabel={alt}
|
||||||
|
onError={() => setErrored(true)}
|
||||||
|
{...nativeProps}
|
||||||
|
{...css([
|
||||||
|
{ width: percent(100), height: percent(100), resizeMode: "cover" },
|
||||||
|
imageStyle,
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{gradient && (
|
||||||
|
<LinearGradient
|
||||||
|
start={{ x: 0, y: 0.25 }}
|
||||||
|
end={{ x: 0, y: 1 }}
|
||||||
|
colors={["transparent", alpha(theme.colors.black, 0.6)]}
|
||||||
|
{...css(
|
||||||
|
{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
typeof gradient === "object" ? gradient : undefined,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</ContrastArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -28,6 +28,8 @@ export * from "./image";
|
|||||||
export * from "./skeleton";
|
export * from "./skeleton";
|
||||||
export * from "./tooltip";
|
export * from "./tooltip";
|
||||||
|
|
||||||
|
export * from "./animated";
|
||||||
|
|
||||||
export * from "./utils/breakpoints";
|
export * from "./utils/breakpoints";
|
||||||
export * from "./utils/nojs";
|
export * from "./utils/nojs";
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
import { ComponentType, ReactNode } from "react";
|
import { ComponentType, ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
Platform,
|
Platform,
|
||||||
Pressable,
|
|
||||||
TextProps,
|
TextProps,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableNativeFeedback,
|
TouchableNativeFeedback,
|
||||||
@ -29,7 +28,7 @@ import {
|
|||||||
ViewProps,
|
ViewProps,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { LinkCore, TextLink } from "solito/link";
|
import { LinkCore, TextLink } from "solito/link";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki, Pressable } from "yoshiki/native";
|
||||||
|
|
||||||
export const A = ({
|
export const A = ({
|
||||||
href,
|
href,
|
||||||
@ -55,7 +54,20 @@ export const A = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Link = ({ href, children, ...props }: ViewProps & { href: string }) => {
|
export const Link = ({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ViewProps & {
|
||||||
|
href: string;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
onPressIn?: () => void;
|
||||||
|
onPressOut?: () => void;
|
||||||
|
}) => {
|
||||||
|
const { onBlur, onFocus, onPressIn, onPressOut, ...noFocusProps } = props;
|
||||||
|
const focusProps = { onBlur, onFocus, onPressIn, onPressOut };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkCore
|
<LinkCore
|
||||||
href={href}
|
href={href}
|
||||||
@ -66,7 +78,7 @@ export const Link = ({ href, children, ...props }: ViewProps & { href: string })
|
|||||||
default: Pressable,
|
default: Pressable,
|
||||||
})}
|
})}
|
||||||
componentProps={Platform.select<object>({
|
componentProps={Platform.select<object>({
|
||||||
android: { useForeground: true },
|
android: { useForeground: true, ...focusProps },
|
||||||
default: props,
|
default: props,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -69,7 +69,7 @@ export const Skeleton = ({
|
|||||||
borderRadius: px(6),
|
borderRadius: px(6),
|
||||||
},
|
},
|
||||||
variant === "text" && {
|
variant === "text" && {
|
||||||
margin: px(2),
|
margin: rem(1),
|
||||||
width: percent(75),
|
width: percent(75),
|
||||||
height: rem(1.2),
|
height: rem(1.2),
|
||||||
},
|
},
|
||||||
|
@ -68,5 +68,6 @@ 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 P = styleText(EP);
|
export const P = styleText(EP);
|
||||||
export const SubP = styleText(EP, "sub");
|
export const SubP = styleText(EP, "sub");
|
||||||
|
@ -83,7 +83,7 @@ export const catppuccin: ThemeBuilder = {
|
|||||||
blue: "#89b4fa",
|
blue: "#89b4fa",
|
||||||
yellow: "#f9e2af",
|
yellow: "#f9e2af",
|
||||||
black: "#11111b",
|
black: "#11111b",
|
||||||
white: "#cdd6f4",
|
white: "#f5f0f8",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { Property } from "csstype";
|
import { Property } from "csstype";
|
||||||
import { Theme, ThemeProvider, useTheme } from "yoshiki";
|
import { Theme, ThemeProvider } from "yoshiki";
|
||||||
|
import { useTheme, useYoshiki } from "yoshiki/native";
|
||||||
import "yoshiki";
|
import "yoshiki";
|
||||||
import { catppuccin } from "./catppuccin";
|
import { catppuccin } from "./catppuccin";
|
||||||
|
|
||||||
@ -57,7 +58,9 @@ type Variant = {
|
|||||||
|
|
||||||
declare module "yoshiki" {
|
declare module "yoshiki" {
|
||||||
// TODO: Add specifics colors
|
// TODO: Add specifics colors
|
||||||
export interface Theme extends ThemeSettings, Mode, Variant {}
|
export interface Theme extends ThemeSettings, Mode, Variant {
|
||||||
|
builder: ThemeBuilder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { Theme } from "yoshiki";
|
export type { Theme } from "yoshiki";
|
||||||
@ -70,7 +73,7 @@ export const selectMode = (theme: ThemeBuilder, mode: "light" | "dark"): Theme =
|
|||||||
const { light, dark, ...options } = theme;
|
const { light, dark, ...options } = theme;
|
||||||
const value = mode === "light" ? light : dark;
|
const value = mode === "light" ? light : dark;
|
||||||
const { default: def, ...modeOpt } = value;
|
const { default: def, ...modeOpt } = value;
|
||||||
return { ...options, ...modeOpt, ...def, variant: value.variant };
|
return { ...options, ...modeOpt, ...def, variant: value.variant, builder: theme };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const switchVariant = (theme: Theme) => {
|
export const switchVariant = (theme: Theme) => {
|
||||||
@ -88,12 +91,56 @@ export const switchVariant = (theme: Theme) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SwitchVariant = ({ children }: { children?: JSX.Element | JSX.Element[] }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return <ThemeProvider theme={switchVariant(theme)}>{children}</ThemeProvider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ThemeSelector = ({ children }: { children: ReactNode }) => {
|
export const ThemeSelector = ({ children }: { children: ReactNode }) => {
|
||||||
return <ThemeProvider theme={selectMode(catppuccin, "light")}>{children}</ThemeProvider>;
|
return <ThemeProvider theme={selectMode(catppuccin, "light")}>{children}</ThemeProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T;
|
||||||
|
|
||||||
|
const YoshikiProvider = ({ children }: { children: YoshikiFunc<ReactNode> }) => {
|
||||||
|
const yoshiki = useYoshiki();
|
||||||
|
return <>{children(yoshiki)}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SwitchVariant = ({ children }: { children: ReactNode | YoshikiFunc<ReactNode> }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={switchVariant(theme)}>
|
||||||
|
{typeof children === "function" ? <YoshikiProvider>{children}</YoshikiProvider> : children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContrastArea = ({
|
||||||
|
children,
|
||||||
|
mode = "dark",
|
||||||
|
contrastText,
|
||||||
|
}: {
|
||||||
|
children: ReactNode | YoshikiFunc<ReactNode>;
|
||||||
|
mode?: "light" | "dark";
|
||||||
|
contrastText?: boolean;
|
||||||
|
}) => {
|
||||||
|
const oldTheme = useTheme();
|
||||||
|
const theme = selectMode(oldTheme.builder, mode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider
|
||||||
|
theme={
|
||||||
|
contrastText
|
||||||
|
? {
|
||||||
|
...theme,
|
||||||
|
heading: mode === "light" ? theme.colors.black : theme.colors.white,
|
||||||
|
paragraph: theme.heading,
|
||||||
|
}
|
||||||
|
: theme
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{typeof children === "function" ? <YoshikiProvider>{children}</YoshikiProvider> : children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const alpha = (color: Property.Color, alpha: number) => {
|
||||||
|
return color + (alpha * 255).toString(16);
|
||||||
|
};
|
||||||
|
@ -46,6 +46,7 @@ export const WebTooltip = ({ theme }: { theme: Theme }) => {
|
|||||||
top: 100%;
|
top: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%);
|
transform: translate(-50%);
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -34,7 +34,9 @@ import { ItemGrid } from "./grid";
|
|||||||
import { ItemList } from "./list";
|
import { ItemList } from "./list";
|
||||||
import { SortBy, SortOrd, Layout } from "./types";
|
import { SortBy, SortOrd, Layout } from "./types";
|
||||||
|
|
||||||
const itemMap = (item: WithLoading<LibraryItem>): WithLoading<ComponentProps<typeof ItemGrid>> => {
|
const itemMap = (
|
||||||
|
item: WithLoading<LibraryItem>,
|
||||||
|
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
|
||||||
if (item.isLoading) return item;
|
if (item.isLoading) return item;
|
||||||
|
|
||||||
let href;
|
let href;
|
||||||
@ -48,6 +50,7 @@ const itemMap = (item: WithLoading<LibraryItem>): WithLoading<ComponentProps<typ
|
|||||||
subtitle: item.type !== ItemType.Collection ? getDisplayDate(item) : undefined,
|
subtitle: item.type !== ItemType.Collection ? getDisplayDate(item) : undefined,
|
||||||
href,
|
href,
|
||||||
poster: item.poster,
|
poster: item.poster,
|
||||||
|
thumbnail: item.thumbnail,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -70,7 +73,7 @@ const query = (
|
|||||||
export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
|
export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
|
||||||
const [sortKey, setSort] = useState(SortBy.Name);
|
const [sortKey, setSort] = useState(SortBy.Name);
|
||||||
const [sortOrd, setSortOrd] = useState(SortOrd.Asc);
|
const [sortOrd, setSortOrd] = useState(SortOrd.Asc);
|
||||||
const [layout, setLayout] = useState(Layout.Grid);
|
const [layout, setLayout] = useState(Layout.List);
|
||||||
|
|
||||||
const LayoutComponent = layout === Layout.Grid ? ItemGrid : ItemList;
|
const LayoutComponent = layout === Layout.Grid ? ItemGrid : ItemList;
|
||||||
|
|
||||||
|
@ -18,98 +18,101 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ItemList = ({
|
import { Link, P, Skeleton, Animated, ts, ImageBackground, Heading } from "@kyoo/primitives";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||||
|
import { Layout, WithLoading } from "../fetch";
|
||||||
|
|
||||||
|
export const ItemList = ({
|
||||||
href,
|
href,
|
||||||
name,
|
name,
|
||||||
subtitle,
|
subtitle,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
poster,
|
poster,
|
||||||
loading,
|
isLoading,
|
||||||
}: {
|
}: WithLoading<{
|
||||||
href?: string;
|
href: string;
|
||||||
name?: string;
|
name: string;
|
||||||
subtitle?: string | null;
|
subtitle?: string;
|
||||||
poster?: string | null;
|
poster?: string | null;
|
||||||
thumbnail?: string | null;
|
thumbnail?: string | null;
|
||||||
loading?: boolean;
|
}>) => {
|
||||||
}) => {
|
const { css } = useYoshiki();
|
||||||
|
const [isHovered, setHovered] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<ImageBackground
|
||||||
|
src={thumbnail}
|
||||||
|
alt={name}
|
||||||
|
as={Link}
|
||||||
href={href ?? ""}
|
href={href ?? ""}
|
||||||
color="inherit"
|
onFocus={() => setHovered((i) => i + 1)}
|
||||||
sx={{
|
onBlur={() => setHovered((i) => i - 1)}
|
||||||
display: "flex",
|
onPressIn={() => setHovered((i) => i + 1)}
|
||||||
textAlign: "center",
|
onPressOut={() => setHovered((i) => i - 1)}
|
||||||
|
containerStyle={{
|
||||||
|
borderRadius: px(6),
|
||||||
|
}}
|
||||||
|
imageStyle={{
|
||||||
|
borderRadius: px(6),
|
||||||
|
}}
|
||||||
|
{...css({
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-evenly",
|
justifyContent: "space-evenly",
|
||||||
width: "100%",
|
|
||||||
height: "300px",
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
m: 1,
|
height: ItemList.layout.size,
|
||||||
position: "relative",
|
borderRadius: px(6),
|
||||||
color: "white",
|
m: ts(1),
|
||||||
"&:hover .poster": {
|
})}
|
||||||
transform: "scale(1.3)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Image
|
<View
|
||||||
src={thumbnail}
|
{...css({
|
||||||
alt={name}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
radius={px(5)}
|
|
||||||
css={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: -1,
|
|
||||||
|
|
||||||
"&::after": {
|
|
||||||
content: '""',
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
left: 0,
|
|
||||||
/* background: "rgba(0, 0, 0, 0.4)", */
|
|
||||||
background: "linear-gradient(to bottom, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0.6) 100%)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
width: { xs: "50%", lg: "30%" },
|
width: { xs: "50%", lg: "30%" },
|
||||||
}}
|
})}
|
||||||
>
|
>
|
||||||
<Typography
|
<Skeleton {...css({ height: rem(2), alignSelf: "center" })}>
|
||||||
variant="button"
|
{isLoading || (
|
||||||
sx={{
|
<Heading
|
||||||
fontSize: "2rem",
|
{...css({
|
||||||
letterSpacing: "0.002rem",
|
textAlign: "center",
|
||||||
fontWeight: 900,
|
fontSize: rem(2),
|
||||||
}}
|
letterSpacing: rem(0.002),
|
||||||
>
|
fontWeight: "900",
|
||||||
{name ?? <Skeleton />}
|
textTransform: "uppercase",
|
||||||
</Typography>
|
textDecorationLine: isHovered ? "underline" : "none",
|
||||||
{(loading || subtitle) && (
|
})}
|
||||||
<Typography variant="caption" sx={{ fontSize: "1rem" }}>
|
>
|
||||||
{subtitle ?? <Skeleton />}
|
{name}
|
||||||
</Typography>
|
</Heading>
|
||||||
|
)}
|
||||||
|
</Skeleton>
|
||||||
|
{(isLoading || subtitle) && (
|
||||||
|
<Skeleton {...css({ width: rem(5), alignSelf: "center" })}>
|
||||||
|
{isLoading || (
|
||||||
|
<P
|
||||||
|
{...css({
|
||||||
|
textAlign: "center",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</View>
|
||||||
<Poster
|
<Animated.Poster
|
||||||
src={poster}
|
src={poster}
|
||||||
alt=""
|
alt=""
|
||||||
height="80%"
|
isLoading={isLoading}
|
||||||
css={{
|
forward={{ layout: { height: percent(80) } }}
|
||||||
transition: "transform .2s",
|
// TODO: this does not work on the web...
|
||||||
}}
|
animate={{ scale: isHovered ? 1.3 : 1 }}
|
||||||
|
transition={{ type: "spring" }}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</ImageBackground>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ItemList.layout = { numColumns: 1, size: 300 } satisfies Layout;
|
||||||
|
@ -62,9 +62,11 @@ export const InfiniteFetch = <Data,>({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
overflow: "unset !important",
|
||||||
},
|
},
|
||||||
numColumns === 1 && {
|
numColumns === 1 && {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
alignItems: "stretch",
|
||||||
},
|
},
|
||||||
numColumns !== 1 && {
|
numColumns !== 1 && {
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
|
@ -58,6 +58,7 @@ export const Navbar = (props: Stylable) => {
|
|||||||
shadowOpacity: 0.3,
|
shadowOpacity: 0.3,
|
||||||
shadowRadius: 4.65,
|
shadowRadius: 4.65,
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
props,
|
props,
|
||||||
)}
|
)}
|
||||||
|
@ -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.7
|
yoshiki: 0.2.9
|
||||||
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.7
|
yoshiki: 0.2.9
|
||||||
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.7":
|
"yoshiki@npm:0.2.9":
|
||||||
version: 0.2.7
|
version: 0.2.9
|
||||||
resolution: "yoshiki@npm:0.2.7"
|
resolution: "yoshiki@npm:0.2.9"
|
||||||
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: 5589cde181d2825a9c25d0ec51b99873a335869989314c3cee3a4503444344a546ccc4705e1a0f76a4ec4faaa14433ae96fa8e2024d6550498b61b193c0fba5f
|
checksum: 41ff5ff7e4cd99b2bc7453749a1f17ceb63bc9e1123285bc62a12315cac1fb087b58366db50e08b275e7dd0b1ce862df4c3acae51872f8e3aa8765ee9b61d4bf
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user