mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-02-15 07:52:14 -05:00
Rework series header
This commit is contained in:
parent
f7f3564816
commit
e3dc2bd531
@ -105,7 +105,7 @@ export const EntryLine = ({
|
||||
>
|
||||
<Icon
|
||||
icon={MultipleVideos}
|
||||
fillClassName="accent-accent dark:accent-slate-400"
|
||||
className="fill-accent dark:fill-slate-400"
|
||||
/>
|
||||
<SubP className="ml-2">
|
||||
{t("show.videosCount", { number: videosCount })}
|
||||
|
||||
@ -2,12 +2,13 @@ import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
|
||||
import BookmarkAdded from "@material-symbols/svg-400/rounded/bookmark_added-fill.svg";
|
||||
import BookmarkRemove from "@material-symbols/svg-400/rounded/bookmark_remove.svg";
|
||||
import Bookmark from "@material-symbols/svg-400/rounded/bookmark-fill.svg";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Serie } from "~/models";
|
||||
import { IconButton, Menu, tooltip } from "~/primitives";
|
||||
import { IconButton, Menu, PressableFeedback, tooltip } from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { useMutation } from "~/query";
|
||||
import { ComponentProps } from "react";
|
||||
import { PressableProps } from "react-native";
|
||||
|
||||
type WatchStatus = NonNullable<Serie["watchStatus"]>["status"];
|
||||
const WatchStatus = [
|
||||
@ -40,7 +41,7 @@ export const WatchListInfo = ({
|
||||
kind: "movie" | "serie" | "episode";
|
||||
slug: string;
|
||||
status: WatchStatus | null;
|
||||
}) => {
|
||||
} & Partial<ComponentProps<typeof IconButton<PressableProps>>>) => {
|
||||
const account = useAccount();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -94,7 +95,7 @@ export const WatchListInfo = ({
|
||||
Trigger={IconButton}
|
||||
icon={watchListIcon(status)}
|
||||
{...tooltip(t("show.watchlistEdit"))}
|
||||
{...props}
|
||||
{...(props as any)}
|
||||
>
|
||||
{Object.values(WatchStatus).map((x) => (
|
||||
<Menu.Item
|
||||
|
||||
@ -6,24 +6,39 @@ import { cn } from "~/utils";
|
||||
export const Rating = ({
|
||||
rating,
|
||||
className,
|
||||
textClassName,
|
||||
iconClassName,
|
||||
...props
|
||||
}: {
|
||||
rating: number | null;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
iconClassName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<View className={cn("flex-row items-center", className)} {...props}>
|
||||
<Icon icon={Star} className="mr-1" />
|
||||
<P className="align-middle">{rating ? rating / 10 : "??"} / 10</P>
|
||||
<Icon icon={Star} className={cn("mr-1", iconClassName)} />
|
||||
<P className={cn("align-middle", textClassName)}>
|
||||
{rating ? rating / 10 : "??"} / 10
|
||||
</P>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Rating.Loader = ({ className, ...props }: { className?: string }) => {
|
||||
Rating.Loader = ({
|
||||
className,
|
||||
textClassName,
|
||||
iconClassName,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
iconClassName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<View className={cn("flex-row items-center", className)} {...props}>
|
||||
<Icon icon={Star} className="mr-1" />
|
||||
<Skeleton className="w-8" />
|
||||
<Icon icon={Star} className={cn("mr-1", iconClassName)} />
|
||||
<Skeleton className={cn("w-8", textClassName)} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,82 +1,58 @@
|
||||
import { type TextProps, View } from "react-native";
|
||||
import { px, rem, type Theme, useYoshiki } from "yoshiki/native";
|
||||
import { View } from "react-native";
|
||||
import { cn } from "~/utils";
|
||||
import { Link } from "./links";
|
||||
import { Skeleton } from "./skeleton";
|
||||
import { P } from "./text";
|
||||
import { capitalize, ts } from "./utils";
|
||||
import { capitalize } from "./utils";
|
||||
|
||||
export const Chip = ({
|
||||
color,
|
||||
size = "medium",
|
||||
outline = false,
|
||||
label,
|
||||
href,
|
||||
replace,
|
||||
target,
|
||||
textProps,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
color?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
outline?: boolean;
|
||||
label: string;
|
||||
href: string | null;
|
||||
replace?: boolean;
|
||||
target?: string;
|
||||
textProps?: TextProps;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { css } = useYoshiki("chip");
|
||||
|
||||
textProps ??= {};
|
||||
|
||||
const sizeMult = size === "medium" ? 1 : size === "small" ? 0.5 : 1.5;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
replace={replace}
|
||||
target={target}
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
pY: ts(1 * sizeMult),
|
||||
pX: ts(2.5 * sizeMult),
|
||||
borderRadius: ts(3),
|
||||
overflow: "hidden",
|
||||
justifyContent: "center",
|
||||
},
|
||||
outline && {
|
||||
borderColor: color ?? ((theme: Theme) => theme.accent),
|
||||
borderStyle: "solid",
|
||||
borderWidth: px(1),
|
||||
fover: {
|
||||
self: {
|
||||
bg: (theme: Theme) => theme.accent,
|
||||
},
|
||||
text: {
|
||||
color: (theme: Theme) => theme.alternate.contrast,
|
||||
},
|
||||
},
|
||||
},
|
||||
!outline && {
|
||||
bg: color ?? ((theme: Theme) => theme.accent),
|
||||
},
|
||||
],
|
||||
props,
|
||||
className={cn(
|
||||
"group justify-center overflow-hidden rounded-4xl border border-accent outline-0",
|
||||
size === "small" && "px-2.5 py-1",
|
||||
size === "medium" && "px-5 py-2",
|
||||
size === "large" && "px-10 py-4",
|
||||
outline && "hover:bg-accent focus:bg-accent",
|
||||
!outline && "bg-accent hover:bg-background focus:bg-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<P
|
||||
{...css(
|
||||
[
|
||||
"text",
|
||||
{
|
||||
marginVertical: 0,
|
||||
fontSize: rem(0.8),
|
||||
color: (theme: Theme) =>
|
||||
outline ? theme.contrast : theme.alternate.contrast,
|
||||
},
|
||||
],
|
||||
textProps,
|
||||
className={cn(
|
||||
outline &&
|
||||
cn(
|
||||
"dark:text-slate-300",
|
||||
"group-hover:text-slate-200 group-focus:text-slate-200",
|
||||
),
|
||||
!outline &&
|
||||
cn(
|
||||
"text-slate-200 dark:text-slate-300",
|
||||
"group-hover:text-slate-600 group-focus:text-slate-600",
|
||||
"dark:group-focus:text-slate-300 dark:group-hover:text-slate-300",
|
||||
),
|
||||
size === "small" && "text-sm",
|
||||
)}
|
||||
>
|
||||
{capitalize(label)}
|
||||
@ -86,42 +62,28 @@ export const Chip = ({
|
||||
};
|
||||
|
||||
Chip.Loader = ({
|
||||
color,
|
||||
size = "medium",
|
||||
outline = false,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
color?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
outline?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const sizeMult = size === "medium" ? 1 : size === "small" ? 0.5 : 1.5;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
pY: ts(1 * sizeMult),
|
||||
pX: ts(2.5 * sizeMult),
|
||||
borderRadius: ts(3),
|
||||
overflow: "hidden",
|
||||
justifyContent: "center",
|
||||
},
|
||||
outline && {
|
||||
borderColor: color ?? ((theme: Theme) => theme.accent),
|
||||
borderStyle: "solid",
|
||||
borderWidth: px(1),
|
||||
},
|
||||
!outline && {
|
||||
bg: color ?? ((theme: Theme) => theme.accent),
|
||||
},
|
||||
],
|
||||
props,
|
||||
className={cn(
|
||||
"group justify-center overflow-hidden rounded-4xl border border-accent outline-0",
|
||||
size === "small" && "px-2.5 py-1",
|
||||
size === "medium" && "px-5 py-2",
|
||||
size === "large" && "px-10 py-4",
|
||||
!outline && "bg-accent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Skeleton {...css({ width: rem(3) })} />
|
||||
<Skeleton className="w-10" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ export const HR = ({
|
||||
return (
|
||||
<EHR
|
||||
className={cn(
|
||||
"border-0 bg-gray-400 opacity-70",
|
||||
"shrink-0 border-0 bg-gray-400 opacity-70",
|
||||
orientation === "vertical" && "mx-4 my-2 h-auto w-px",
|
||||
orientation === "horizontal" && "mx-2 my-4 h-px w-auto",
|
||||
className,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ComponentProps, ComponentType } from "react";
|
||||
import { type ComponentProps, type ComponentType, useState } from "react";
|
||||
import { Animated, type PressableProps } from "react-native";
|
||||
import type { SvgProps } from "react-native-svg";
|
||||
import { withUniwind } from "uniwind";
|
||||
@ -14,38 +14,33 @@ const IconWrapper = ({ icon: Icon, ...props }: { icon: Icon } & SvgProps) => {
|
||||
|
||||
const BaseIcon = withUniwind(IconWrapper, {
|
||||
stroke: {
|
||||
fromClassName: "strokeClassName",
|
||||
fromClassName: "className",
|
||||
styleProperty: "accentColor",
|
||||
},
|
||||
fill: {
|
||||
fromClassName: "fillClassName",
|
||||
styleProperty: "accentColor",
|
||||
fromClassName: "className",
|
||||
styleProperty: "fill",
|
||||
},
|
||||
width: {
|
||||
fromClassName: "widthClassName",
|
||||
fromClassName: "className",
|
||||
styleProperty: "width",
|
||||
},
|
||||
height: {
|
||||
fromClassName: "heightClassName",
|
||||
fromClassName: "className",
|
||||
styleProperty: "height",
|
||||
},
|
||||
});
|
||||
|
||||
export const Icon = ({
|
||||
className,
|
||||
fillClassName,
|
||||
widthClassName,
|
||||
heightClassName,
|
||||
...props
|
||||
}: ComponentProps<typeof BaseIcon>) => {
|
||||
return (
|
||||
<BaseIcon
|
||||
fillClassName={
|
||||
fillClassName ? fillClassName : "accent-slate-600 dark:accent-slate-400"
|
||||
}
|
||||
widthClassName={cn("w-6", widthClassName)}
|
||||
heightClassName={cn("h-6", heightClassName)}
|
||||
className={cn("shrink-0", className)}
|
||||
className={cn(
|
||||
"h-6 w-6 shrink-0 fill-slate-600 dark:fill-slate-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -60,7 +55,7 @@ export const IconButton = <AsProps = PressableProps>({
|
||||
}: {
|
||||
as?: ComponentType<AsProps>;
|
||||
icon: Icon;
|
||||
iconProps?: Exclude<ComponentProps<typeof Icon>, "icon">;
|
||||
iconProps?: Omit<ComponentProps<typeof Icon>, "icon">;
|
||||
className?: string;
|
||||
} & AsProps) => {
|
||||
const Container = as ?? PressableFeedback;
|
||||
@ -69,18 +64,18 @@ export const IconButton = <AsProps = PressableProps>({
|
||||
<Container
|
||||
focusRipple
|
||||
className={cn(
|
||||
"m-2 h-6 w-6 self-center overflow-hidden rounded-full",
|
||||
"hover:bg-gray-300 focus-visible:bg-gray-300 focus-visible:dark:bg-gray-700 hover:dark:bg-gray-700",
|
||||
"h-10 w-10 self-center overflow-hidden rounded-full p-2",
|
||||
"outline-0 hover:bg-gray-400/50 focus-visible:bg-gray-400/50",
|
||||
className,
|
||||
)}
|
||||
{...(asProps as AsProps)}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
<Icon icon={icon} {...iconProps} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const AIconButton = Animated.createAnimatedComponent(IconButton);
|
||||
const Pressable = Animated.createAnimatedComponent(PressableFeedback);
|
||||
|
||||
export const IconFab = <AsProps = PressableProps>({
|
||||
icon,
|
||||
@ -88,21 +83,35 @@ export const IconFab = <AsProps = PressableProps>({
|
||||
iconProps,
|
||||
...props
|
||||
}: ComponentProps<typeof IconButton<AsProps>>) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [focus, setFocus] = useState(false);
|
||||
return (
|
||||
<AIconButton
|
||||
icon={icon}
|
||||
className={cn("bg-accent", className)}
|
||||
iconProps={{
|
||||
...iconProps,
|
||||
className: cn("text-slate-900", iconProps?.className),
|
||||
}}
|
||||
<Pressable
|
||||
className={cn(
|
||||
"group h-10 w-10 overflow-hidden rounded-full bg-accent p-2 outline-0",
|
||||
className,
|
||||
)}
|
||||
onHoverIn={() => setHover(true)}
|
||||
onHoverOut={() => setHover(false)}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
style={{
|
||||
transform: [{ scale: 1.3 }],
|
||||
transform: hover || focus ? [{ scale: 1.3 }] : [],
|
||||
transitionProperty: "transform",
|
||||
transitionDuration: 3000,
|
||||
transitionDuration: "150ms",
|
||||
}}
|
||||
{...(props as AsProps)}
|
||||
/>
|
||||
>
|
||||
<Icon
|
||||
icon={icon}
|
||||
{...iconProps}
|
||||
className={cn(
|
||||
"fill-slate-300",
|
||||
(hover || focus) && "fill-slate-200",
|
||||
iconProps?.className,
|
||||
)}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import { useYoshiki } from "yoshiki/native";
|
||||
import type { KImage } from "~/models";
|
||||
import { useToken } from "~/providers/account-context";
|
||||
import { cn } from "~/utils";
|
||||
import type { ImageLayout, YoshikiEnhanced } from "./image";
|
||||
|
||||
const ImgBg = withUniwind(EImageBackground);
|
||||
|
||||
@ -19,7 +18,6 @@ export const ImageBackground = ({
|
||||
src,
|
||||
quality,
|
||||
alt,
|
||||
layout,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
@ -27,7 +25,6 @@ export const ImageBackground = ({
|
||||
quality: "low" | "medium" | "high";
|
||||
alt?: string;
|
||||
style?: ImageStyle;
|
||||
layout?: ImageLayout;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
@ -57,21 +54,14 @@ export const ImageBackground = ({
|
||||
};
|
||||
export const PosterBackground = ({
|
||||
alt,
|
||||
layout,
|
||||
className,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof ImageBackground>, "layout"> & {
|
||||
style?: ImageStyle;
|
||||
layout: YoshikiEnhanced<
|
||||
{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }
|
||||
>;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
}: ComponentProps<typeof ImageBackground>) => {
|
||||
return (
|
||||
<ImageBackground
|
||||
alt={alt!}
|
||||
layout={{ aspectRatio: 2 / 3, ...layout }}
|
||||
{...css({ borderRadius: 10, overflow: "hidden" }, props)}
|
||||
className={cn("aspect-2/3 overflow-hidden rounded", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Image as EImage } from "expo-image";
|
||||
import type { ComponentProps } from "react";
|
||||
import { type ImageStyle, Platform, type ViewStyle } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { type ImageStyle, Platform } from "react-native";
|
||||
import { withUniwind } from "uniwind";
|
||||
import type { YoshikiStyle } from "yoshiki/src/type";
|
||||
import type { KImage } from "~/models";
|
||||
import { useToken } from "~/providers/account-context";
|
||||
import { cn } from "~/utils";
|
||||
import { Skeleton } from "./skeleton";
|
||||
|
||||
export type YoshikiEnhanced<Style> = Style extends any
|
||||
@ -13,11 +14,7 @@ export type YoshikiEnhanced<Style> = Style extends any
|
||||
}
|
||||
: never;
|
||||
|
||||
export type ImageLayout = YoshikiEnhanced<
|
||||
| { width: ImageStyle["width"]; height: ImageStyle["height"] }
|
||||
| { width: ImageStyle["width"]; aspectRatio: ImageStyle["aspectRatio"] }
|
||||
| { height: ImageStyle["height"]; aspectRatio: ImageStyle["aspectRatio"] }
|
||||
>;
|
||||
const Img = withUniwind(EImage);
|
||||
|
||||
// This should stay in think with `ImageBackground`.
|
||||
// (copy-pasted but change `EImageBackground` with `EImage`)
|
||||
@ -25,24 +22,23 @@ export const Image = ({
|
||||
src,
|
||||
quality,
|
||||
alt,
|
||||
layout,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
src: KImage | null;
|
||||
quality: "low" | "medium" | "high";
|
||||
alt?: string;
|
||||
style?: ImageStyle;
|
||||
layout: ImageLayout;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { apiUrl, authToken } = useToken();
|
||||
|
||||
const uri = src ? `${apiUrl}${src[quality ?? "high"]}` : null;
|
||||
return (
|
||||
<EImage
|
||||
<Img
|
||||
recyclingKey={uri}
|
||||
source={{
|
||||
uri,
|
||||
uri: uri!,
|
||||
// use cookies on web to allow `img` to make the call instead of js
|
||||
headers:
|
||||
authToken && Platform.OS !== "web"
|
||||
@ -53,41 +49,24 @@ export const Image = ({
|
||||
}}
|
||||
placeholder={{ blurhash: src?.blurhash }}
|
||||
accessibilityLabel={alt}
|
||||
{...(css(
|
||||
[layout, { borderRadius: 6, backgroundColor: theme.overlay0 }],
|
||||
props,
|
||||
) as any)}
|
||||
className={cn("overflow-hidden rounded bg-gray-300", className)}
|
||||
//imageStyle={{ width: "100%", height: "100%", margin: 0, padding: 0 }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Image.Loader = ({
|
||||
layout,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
layout?: ImageLayout;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const border = { borderRadius: 6 } satisfies ViewStyle;
|
||||
|
||||
return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||
Image.Loader = (props: { className?: string }) => {
|
||||
return <Skeleton variant="custom" {...props} />;
|
||||
};
|
||||
|
||||
export const Poster = ({
|
||||
layout,
|
||||
className,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof Image>, "layout"> & {
|
||||
layout: YoshikiEnhanced<
|
||||
{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }
|
||||
>;
|
||||
}) => <Image layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />;
|
||||
}: ComponentProps<typeof Image>) => (
|
||||
<Image className={cn("aspect-2/3", className)} {...props} />
|
||||
);
|
||||
|
||||
Poster.Loader = ({
|
||||
layout,
|
||||
...props
|
||||
}: {
|
||||
layout: YoshikiEnhanced<
|
||||
{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }
|
||||
>;
|
||||
}) => <Image.Loader layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />;
|
||||
Poster.Loader = ({ className, ...props }: { className?: string }) => (
|
||||
<Image.Loader className={cn("aspect-2/3", className)} {...props} />
|
||||
);
|
||||
|
||||
@ -60,7 +60,10 @@ export const A = ({
|
||||
return (
|
||||
<Text
|
||||
{...linkProps}
|
||||
className={cn("select-text text-accent", className)}
|
||||
className={cn(
|
||||
"select-text text-accent hover:underline focus:underline",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -121,8 +121,10 @@ const MenuItem = ({
|
||||
const icn = (icon || selected) && (
|
||||
<Icon
|
||||
icon={icon ?? Check}
|
||||
fillClassName={cn(disabled && "accent-slate-600")}
|
||||
className="mx-2"
|
||||
className={cn(
|
||||
"mx-2",
|
||||
disabled && "fill-slate-600 dark:fill-slate-600",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -103,9 +103,9 @@ const MenuItem = forwardRef<
|
||||
const icn = (icon || selected) && (
|
||||
<Icon
|
||||
icon={icon ?? Dot}
|
||||
fillClassName={cn(disabled && "accent-slate-600")}
|
||||
className={cn(
|
||||
"mx-2 group-data-highlighted:fill-slate-200",
|
||||
disabled && "fill-slate-600 dark:fill-slate-600",
|
||||
!icon && "h-2 w-2",
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -16,7 +16,8 @@ export const Skeleton = ({
|
||||
className={cn(
|
||||
"relative",
|
||||
lines === 1 && "overflow-hidden rounded",
|
||||
variant === "text" && "m-1 h-5 w-4/5",
|
||||
variant === "text" && lines === 1 && "h-5",
|
||||
variant === "text" && "my-1 w-4/5",
|
||||
variant === "round" && "rounded-full",
|
||||
className,
|
||||
)}
|
||||
@ -35,6 +36,7 @@ export const Skeleton = ({
|
||||
<Animated.View
|
||||
className="absolute inset-0 bg-linear-to-r from-transparent via-gray-500 to-transparent"
|
||||
style={{
|
||||
transform: [{ translateX: "-100%" }],
|
||||
animationName: {
|
||||
from: {
|
||||
transform: [{ translateX: "-100%" }],
|
||||
|
||||
@ -31,14 +31,13 @@ const styleText = (
|
||||
return (
|
||||
<Component
|
||||
className={cn(
|
||||
"m-0 shrink font-sans text-base text-slate-600 dark:text-slate-400",
|
||||
type === "header" && "font-headers text-slate-900 dark:text-slate-200",
|
||||
"shrink font-sans text-base text-slate-600 dark:text-slate-400",
|
||||
type === "header" &&
|
||||
"font-headers text-slate-900 dark:text-slate-200",
|
||||
type === "sub" && "font-light text-sm opacity-80",
|
||||
custom,
|
||||
className,
|
||||
)}
|
||||
// reset expo/html-elements style
|
||||
style={[{ marginVertical: 0 }, style]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -52,14 +51,22 @@ export const H3 = styleText(EH3, "header");
|
||||
export const H4 = styleText(EH4, "header");
|
||||
export const H5 = styleText(EH5, "header");
|
||||
export const H6 = styleText(EH6, "header");
|
||||
export const Heading = styleText(EP, "header");
|
||||
export const P = styleText(EP, undefined);
|
||||
export const SubP = styleText(EP, "sub");
|
||||
export const Heading = styleText(Text as any, "header");
|
||||
export const P = styleText(Text as any, undefined);
|
||||
export const SubP = styleText(Text as any, "sub");
|
||||
|
||||
export const LI = ({ children, ...props }: ComponentProps<typeof P>) => {
|
||||
export const LI = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof P>) => {
|
||||
return (
|
||||
<P role="listitem" {...props}>
|
||||
<Text className="mb-2 h-full pr-1">{String.fromCharCode(0x2022)}</Text>
|
||||
<P
|
||||
role="listitem"
|
||||
className={cn("flex items-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<Text className="h-full px-1">{String.fromCharCode(0x2022)}</Text>
|
||||
{children}
|
||||
</P>
|
||||
);
|
||||
|
||||
@ -3,49 +3,40 @@ import MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg";
|
||||
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg";
|
||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||
import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Stack } from "expo-router";
|
||||
import { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type ImageStyle, Platform, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import {
|
||||
em,
|
||||
max,
|
||||
md,
|
||||
min,
|
||||
percent,
|
||||
px,
|
||||
rem,
|
||||
type Stylable,
|
||||
type Theme,
|
||||
useYoshiki,
|
||||
vh,
|
||||
} from "yoshiki/native";
|
||||
import { WatchListInfo } from "~/components/items/watchlist-info";
|
||||
import { Rating } from "~/components/rating";
|
||||
import {
|
||||
Collection,
|
||||
type Genre,
|
||||
type KImage,
|
||||
Movie,
|
||||
Serie,
|
||||
Show,
|
||||
type Studio,
|
||||
type WatchStatusV,
|
||||
} from "~/models";
|
||||
import type { Metadata } from "~/models/utils/metadata";
|
||||
import {
|
||||
A,
|
||||
Chip,
|
||||
Container,
|
||||
ContrastArea,
|
||||
capitalize,
|
||||
DottedSeparator,
|
||||
GradientImageBackground,
|
||||
H1,
|
||||
H2,
|
||||
Head,
|
||||
HR,
|
||||
IconButton,
|
||||
IconFab,
|
||||
ImageBackground,
|
||||
LI,
|
||||
Link,
|
||||
Menu,
|
||||
@ -58,7 +49,7 @@ import {
|
||||
} from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { Fetch, type QueryIdentifier } from "~/query";
|
||||
import { displayRuntime, getDisplayDate } from "~/utils";
|
||||
import { cn, displayRuntime, getDisplayDate } from "~/utils";
|
||||
|
||||
const ButtonList = ({
|
||||
kind,
|
||||
@ -66,15 +57,16 @@ const ButtonList = ({
|
||||
playHref,
|
||||
trailerUrl,
|
||||
watchStatus,
|
||||
iconsClassName,
|
||||
}: {
|
||||
kind: "movie" | "serie" | "collection";
|
||||
slug: string;
|
||||
playHref: string | null;
|
||||
trailerUrl: string | null;
|
||||
watchStatus: WatchStatusV | null;
|
||||
iconsClassName?: string;
|
||||
}) => {
|
||||
const account = useAccount();
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// const metadataRefreshMutation = useMutation({
|
||||
@ -84,22 +76,13 @@ const ButtonList = ({
|
||||
// });
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
})}
|
||||
>
|
||||
<View className="flex-row items-center justify-center">
|
||||
{playHref !== null && (
|
||||
<IconFab
|
||||
icon={PlayArrow}
|
||||
as={Link}
|
||||
href={playHref}
|
||||
{...css({
|
||||
bg: theme.user.accent,
|
||||
fover: { self: { bg: theme.user.accent } },
|
||||
})}
|
||||
iconProps={{ className: "dark:fill-slate-200" }}
|
||||
{...tooltip(t("show.play"))}
|
||||
/>
|
||||
)}
|
||||
@ -109,6 +92,7 @@ const ButtonList = ({
|
||||
as={Link}
|
||||
href={trailerUrl}
|
||||
target="_blank"
|
||||
iconProps={{ className: iconsClassName }}
|
||||
{...tooltip(t("show.trailer"))}
|
||||
/>
|
||||
)}
|
||||
@ -117,12 +101,14 @@ const ButtonList = ({
|
||||
kind={kind}
|
||||
slug={slug}
|
||||
status={watchStatus}
|
||||
iconProps={{ className: iconsClassName }}
|
||||
/>
|
||||
)}
|
||||
{(kind === "movie" || account?.isAdmin === true) && (
|
||||
<Menu
|
||||
Trigger={IconButton}
|
||||
icon={MoreHoriz}
|
||||
iconProps={{ className: iconsClassName }}
|
||||
{...tooltip(t("misc.more"))}
|
||||
>
|
||||
{kind === "movie" && (
|
||||
@ -166,8 +152,8 @@ export const TitleLine = ({
|
||||
runtime,
|
||||
poster,
|
||||
trailerUrl,
|
||||
studios,
|
||||
watchStatus,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
kind: "movie" | "serie" | "collection";
|
||||
@ -180,199 +166,58 @@ export const TitleLine = ({
|
||||
runtime: number | null;
|
||||
poster: KImage | null;
|
||||
trailerUrl: string | null;
|
||||
studios: Studio[] | null;
|
||||
watchStatus: WatchStatusV | null;
|
||||
} & Stylable) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Container
|
||||
{...css(
|
||||
{
|
||||
flexDirection: { xs: "column", md: "row" },
|
||||
},
|
||||
props,
|
||||
)}
|
||||
className={cn("flex-1 max-sm:items-center sm:flex-row", className)}
|
||||
{...props}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "center", sm: "flex-start" },
|
||||
flexGrow: 1,
|
||||
maxWidth: percent(100),
|
||||
})}
|
||||
>
|
||||
<Poster
|
||||
src={poster}
|
||||
alt={name}
|
||||
quality="medium"
|
||||
layout={{
|
||||
width: { xs: percent(50), md: percent(25) },
|
||||
}}
|
||||
{...(css({
|
||||
maxWidth: {
|
||||
xs: px(175),
|
||||
sm: Platform.OS === "web" ? ("unset" as any) : 99999999,
|
||||
},
|
||||
flexShrink: 0,
|
||||
}) as { style: ImageStyle })}
|
||||
/>
|
||||
<View
|
||||
{...css({
|
||||
alignSelf: { xs: "center", sm: "flex-end", md: "center" },
|
||||
alignItems: { xs: "center", sm: "flex-start" },
|
||||
paddingLeft: { sm: em(2.5) },
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
})}
|
||||
>
|
||||
<P
|
||||
{...css({
|
||||
textAlign: { xs: "center", sm: "left" },
|
||||
})}
|
||||
>
|
||||
<H1
|
||||
{...css({
|
||||
color: (theme: Theme) => ({
|
||||
xs: theme.user.heading,
|
||||
md: theme.heading,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</H1>
|
||||
{date && (
|
||||
<P
|
||||
{...css({
|
||||
fontSize: rem(2.5),
|
||||
color: (theme: Theme) => ({
|
||||
xs: theme.user.paragraph,
|
||||
md: theme.paragraph,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{" "}
|
||||
({date})
|
||||
</P>
|
||||
)}
|
||||
</P>
|
||||
{tagline && (
|
||||
<P
|
||||
{...css({
|
||||
fontWeight: "300",
|
||||
fontSize: rem(1.5),
|
||||
marginTop: 0,
|
||||
letterSpacing: 0,
|
||||
textAlign: { xs: "center", sm: "left" },
|
||||
color: (theme: Theme) => ({
|
||||
xs: theme.user.heading,
|
||||
md: theme.heading,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{tagline}
|
||||
</P>
|
||||
)}
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
})}
|
||||
>
|
||||
<ButtonList
|
||||
kind={kind}
|
||||
slug={slug}
|
||||
playHref={playHref}
|
||||
trailerUrl={trailerUrl}
|
||||
watchStatus={watchStatus}
|
||||
/>
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
})}
|
||||
>
|
||||
{rating !== null && rating !== 0 && (
|
||||
<>
|
||||
<DottedSeparator
|
||||
{...css({
|
||||
color: {
|
||||
xs: theme.user.contrast,
|
||||
md: theme.colors.white,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<Rating
|
||||
rating={rating}
|
||||
color={{ xs: theme.user.contrast, md: theme.colors.white }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{runtime && (
|
||||
<>
|
||||
<DottedSeparator
|
||||
{...css({
|
||||
color: {
|
||||
xs: theme.user.contrast,
|
||||
md: theme.colors.white,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<P
|
||||
{...css({
|
||||
color: {
|
||||
xs: theme.user.contrast,
|
||||
md: theme.colors.white,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{displayRuntime(runtime)}
|
||||
</P>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
{...css([
|
||||
{
|
||||
paddingTop: { xs: ts(3), sm: ts(8) },
|
||||
alignSelf: { xs: "flex-start", md: "flex-end" },
|
||||
justifyContent: "flex-end",
|
||||
flexDirection: "column",
|
||||
},
|
||||
md({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: percent(25),
|
||||
height: percent(100),
|
||||
paddingRight: ts(3),
|
||||
}) as any,
|
||||
])}
|
||||
>
|
||||
{studios !== null && (
|
||||
<P
|
||||
{...css({
|
||||
flexWrap: "wrap",
|
||||
color: (theme: Theme) => theme.user.paragraph,
|
||||
})}
|
||||
>
|
||||
{t("show.studios")}:{" "}
|
||||
{studios.map((studio, i) => (
|
||||
<Fragment key={studio.id}>
|
||||
<P {...(css({ m: 0 }) as any)}>{i !== 0 && ", "}</P>
|
||||
<A href={`/studios/${studio.slug}`}>{studio.name}</A>
|
||||
</Fragment>
|
||||
))}
|
||||
<Poster
|
||||
src={poster}
|
||||
alt={name}
|
||||
quality="medium"
|
||||
className="w-1/2 shrink-0 max-sm:max-w-44 md:w-1/4"
|
||||
/>
|
||||
<View className="flex-1 self-center max-sm:mt-8 max-sm:items-center sm:pl-10 sm:max-md:self-end md:max-lg:mt-5">
|
||||
<P className="max-sm:text-center">
|
||||
<H1 className="sm:text-slate-200">{name}</H1>
|
||||
{date && <P className="text-3xl sm:text-slate-300"> ({date})</P>}
|
||||
</P>
|
||||
{tagline && (
|
||||
<P className="font-light text-2xl max-sm:text-center sm:text-slate-200">
|
||||
{tagline}
|
||||
</P>
|
||||
)}
|
||||
<View className="flex-warp flex-row items-center max-sm:justify-center sm:mt-8">
|
||||
<ButtonList
|
||||
kind={kind}
|
||||
slug={slug}
|
||||
playHref={playHref}
|
||||
trailerUrl={trailerUrl}
|
||||
watchStatus={watchStatus}
|
||||
iconsClassName="lg:fill-slate-200 dark:fill-slate-200"
|
||||
/>
|
||||
{rating !== null && rating !== 0 && (
|
||||
<>
|
||||
<DottedSeparator className="lg:text-slate-200 dark:text-slate-200" />
|
||||
<Rating
|
||||
rating={rating}
|
||||
textClassName="lg:text-slate-200 dark:text-slate-200"
|
||||
iconClassName="lg:fill-slate-200 dark:fill-slate-200"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{runtime && (
|
||||
<>
|
||||
<DottedSeparator className="lg:text-slate-200 dark:text-slate-200" />
|
||||
<P className="lg:text-slate-200 dark:text-slate-200">
|
||||
{displayRuntime(runtime)}
|
||||
</P>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Container>
|
||||
);
|
||||
@ -380,109 +225,44 @@ export const TitleLine = ({
|
||||
|
||||
TitleLine.Loader = ({
|
||||
kind,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
kind: "serie" | "movie" | "collection";
|
||||
className?: string;
|
||||
}) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...css(
|
||||
{
|
||||
flexDirection: { xs: "column", md: "row" },
|
||||
},
|
||||
props,
|
||||
)}
|
||||
className={cn("flex-1 max-sm:items-center sm:flex-row", className)}
|
||||
{...props}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "center", sm: "flex-start" },
|
||||
flexGrow: 1,
|
||||
maxWidth: percent(100),
|
||||
})}
|
||||
>
|
||||
<Poster.Loader
|
||||
layout={{
|
||||
width: { xs: percent(50), md: percent(25) },
|
||||
}}
|
||||
{...(css({
|
||||
maxWidth: {
|
||||
xs: px(175),
|
||||
sm: Platform.OS === "web" ? ("unset" as any) : 99999999,
|
||||
},
|
||||
flexShrink: 0,
|
||||
}) as { style: ImageStyle })}
|
||||
/>
|
||||
<View
|
||||
{...css({
|
||||
alignSelf: { xs: "center", sm: "flex-end", md: "center" },
|
||||
alignItems: { xs: "center", sm: "flex-start" },
|
||||
paddingLeft: { sm: em(2.5) },
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
})}
|
||||
>
|
||||
<Skeleton
|
||||
variant="header"
|
||||
{...css({ width: rem(15), height: rem(2.5), marginBottom: rem(1) })}
|
||||
<Poster.Loader className="w-1/2 shrink-0 max-sm:max-w-44 md:w-1/4" />
|
||||
<View className="flex-1 self-center max-sm:mt-8 max-sm:items-center sm:pl-10 sm:max-md:self-end md:max-lg:mt-5">
|
||||
<Skeleton variant="custom" className="h-10 w-2/5 max-sm:text-center" />
|
||||
<Skeleton className="h-6 w-4/5 max-sm:text-center" />
|
||||
<View className="flex-warp flex-row items-center max-sm:justify-center sm:mt-8">
|
||||
<IconFab
|
||||
icon={PlayArrow}
|
||||
iconProps={{ className: "lg:fill-slate-200" }}
|
||||
/>
|
||||
<Skeleton
|
||||
{...css({
|
||||
width: rem(5),
|
||||
height: rem(1.5),
|
||||
marginBottom: rem(0.5),
|
||||
})}
|
||||
<IconButton
|
||||
icon={Theaters}
|
||||
iconProps={{ className: "lg:fill-slate-200" }}
|
||||
/>
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
})}
|
||||
>
|
||||
<IconFab
|
||||
icon={PlayArrow}
|
||||
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
|
||||
{...css({
|
||||
bg: theme.user.accent,
|
||||
fover: { self: { bg: theme.user.accent } },
|
||||
})}
|
||||
/>
|
||||
{kind !== "collection" && (
|
||||
<IconButton
|
||||
icon={Theaters}
|
||||
color={{ xs: theme.user.contrast, md: theme.colors.white }}
|
||||
icon={BookmarkAdd}
|
||||
iconProps={{ className: "lg:fill-slate-200" }}
|
||||
/>
|
||||
{kind !== "collection" && (
|
||||
<IconButton
|
||||
icon={BookmarkAdd}
|
||||
color={{ xs: theme.user.contrast, md: theme.colors.white }}
|
||||
/>
|
||||
)}
|
||||
{kind === "movie" && <IconButton icon={MoreHoriz} />}
|
||||
<DottedSeparator
|
||||
{...css({
|
||||
color: {
|
||||
xs: theme.user.contrast,
|
||||
md: theme.colors.white,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<Rating.Loader
|
||||
color={{ xs: theme.user.contrast, md: theme.colors.white }}
|
||||
/>
|
||||
<DottedSeparator
|
||||
{...css({
|
||||
color: {
|
||||
xs: theme.user.contrast,
|
||||
md: theme.colors.white,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<Skeleton {...css({ width: rem(3) })} />
|
||||
</View>
|
||||
)}
|
||||
{kind === "movie" && <IconButton icon={MoreHoriz} />}
|
||||
<DottedSeparator className="lg:text-slate-200" />
|
||||
<Rating.Loader
|
||||
textClassName="lg:text-slate-200"
|
||||
iconClassName="lg:fill-slate-200"
|
||||
/>
|
||||
<DottedSeparator className="lg:text-slate-200" />
|
||||
<Skeleton className="w-1/5" />
|
||||
</View>
|
||||
</View>
|
||||
</Container>
|
||||
@ -493,95 +273,82 @@ const Description = ({
|
||||
description,
|
||||
tags,
|
||||
genres,
|
||||
studios,
|
||||
externalIds,
|
||||
...props
|
||||
}: {
|
||||
description: string | null;
|
||||
tags: string[];
|
||||
genres: Genre[];
|
||||
studios: Studio[];
|
||||
externalIds: Metadata;
|
||||
} & Stylable) => {
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...css(
|
||||
{ paddingBottom: ts(1), flexDirection: { xs: "column", sm: "row" } },
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<P
|
||||
{...css({
|
||||
display: { xs: "flex", sm: "none" },
|
||||
flexWrap: "wrap",
|
||||
color: (theme: Theme) => theme.user.paragraph,
|
||||
})}
|
||||
>
|
||||
{t("show.genre")}:{" "}
|
||||
{genres.map((genre, i) => (
|
||||
<Fragment key={genre}>
|
||||
<P {...(css({ m: 0 }) as any)}>{i !== 0 && ", "}</P>
|
||||
<A href={`/genres/${genre.toLowerCase()}`}>
|
||||
{t(`genres.${genre}`)}
|
||||
</A>
|
||||
</Fragment>
|
||||
))}
|
||||
</P>
|
||||
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
flexBasis: { sm: 0 },
|
||||
paddingTop: ts(4),
|
||||
})}
|
||||
>
|
||||
<P {...css({ textAlign: "justify" })}>
|
||||
<Container className="py-10" {...props}>
|
||||
<View className="flex-1 flex-col-reverse sm:flex-row">
|
||||
<P className="py-5 text-justify">
|
||||
{description ?? t("show.noOverview")}
|
||||
</P>
|
||||
<View
|
||||
{...css({
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: ts(0.5),
|
||||
})}
|
||||
>
|
||||
<P {...(css({ marginRight: ts(0.5) }) as any)}>{t("show.tags")}:</P>
|
||||
{tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag && capitalize(tag)}
|
||||
href={`/search?q=${tag}`}
|
||||
size="small"
|
||||
{...css({ m: ts(0.5) })}
|
||||
/>
|
||||
))}
|
||||
<View className="basis-1/5 flex-row xl:mt-[-100px]">
|
||||
<HR orientation="vertical" className="max-sm:hidden" />
|
||||
<View className="flex-1 items-center max-sm:flex-row">
|
||||
<H2>{t("show.genre")}</H2>
|
||||
{genres.length ? (
|
||||
<UL className="flex-1 flex-wrap max-sm:flex-row max-sm:items-center max-sm:text-center">
|
||||
{genres.map((genre) => (
|
||||
<LI key={genre}>
|
||||
<A href={`/genres/${genre.toLowerCase()}`}>
|
||||
{t(`genres.${genre}`)}
|
||||
</A>
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
) : (
|
||||
<P>{t("show.genre-none")}</P>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<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>
|
||||
{genres.length ? (
|
||||
<UL>
|
||||
{genres.map((genre) => (
|
||||
<LI key={genre}>
|
||||
<A href={`/genres/${genre.toLowerCase()}`}>
|
||||
{t(`genres.${genre}`)}
|
||||
</A>
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
) : (
|
||||
<P>{t("show.genre-none")}</P>
|
||||
)}
|
||||
<View className="mt-5 flex-row flex-wrap items-center">
|
||||
<P className="mr-1">{t("show.tags")}:</P>
|
||||
{tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag && capitalize(tag)}
|
||||
href={`/search?q=${tag}`}
|
||||
size="small"
|
||||
className="m-1"
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<P className="my-5 flex-row flex-wrap items-center">
|
||||
<P className="mr-1">{t("show.studios")}:</P>
|
||||
{studios.map((x, i) => (
|
||||
<>
|
||||
{i !== 0 && ","}
|
||||
<A key={x.id} href={x.slug} className="ml-2">
|
||||
{x.name}
|
||||
</A>
|
||||
</>
|
||||
))}
|
||||
</P>
|
||||
<View className="flex-row flex-wrap items-center">
|
||||
<P className="mr-1 text-center">{t("show.links")}:</P>
|
||||
{Object.entries(externalIds)
|
||||
.filter(([_, data]) => data.link)
|
||||
.map(([name, data]) => (
|
||||
<Chip
|
||||
key={name}
|
||||
label={name}
|
||||
href={data.link}
|
||||
target="_blank"
|
||||
size="small"
|
||||
outline
|
||||
className="m-1"
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</Container>
|
||||
);
|
||||
@ -589,72 +356,40 @@ const Description = ({
|
||||
|
||||
Description.Loader = ({ ...props }: object) => {
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...css(
|
||||
{ paddingBottom: ts(1), flexDirection: { xs: "column", sm: "row" } },
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<P
|
||||
{...css({
|
||||
display: { xs: "flex", sm: "none" },
|
||||
flexWrap: "wrap",
|
||||
color: (theme: Theme) => theme.user.paragraph,
|
||||
})}
|
||||
>
|
||||
{t("show.genre")}:{" "}
|
||||
{[...Array<Genre>(3)].map((_, i) => (
|
||||
<Fragment key={i.toString()}>
|
||||
<P {...(css({ m: 0 }) as any)}>{i !== 0 && ", "}</P>
|
||||
<Skeleton {...css({ width: rem(5) })} />
|
||||
</Fragment>
|
||||
))}
|
||||
</P>
|
||||
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
flexBasis: { sm: 0 },
|
||||
paddingTop: ts(4),
|
||||
})}
|
||||
>
|
||||
<Container className="py-10" {...props}>
|
||||
<View className="flex-1 flex-col-reverse sm:flex-row">
|
||||
<Skeleton lines={4} />
|
||||
<View
|
||||
{...css({
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: ts(0.5),
|
||||
})}
|
||||
>
|
||||
<P {...(css({ marginRight: ts(0.5) }) as any)}>{t("show.tags")}:</P>
|
||||
{[...Array<string>(3)].map((_, i) => (
|
||||
<Chip.Loader key={i} size="small" {...css({ m: ts(0.5) })} />
|
||||
))}
|
||||
<View className="basis-1/5 flex-row xl:mt-[-100px]">
|
||||
<HR orientation="vertical" className="max-sm:hidden" />
|
||||
<View className="flex-1 items-center max-sm:flex-row">
|
||||
<H2>{t("show.genre")}</H2>
|
||||
<UL className="flex-1 flex-wrap max-sm:flex-row max-sm:items-center max-sm:text-center">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<LI key={i}>
|
||||
<Skeleton className="w-25" />
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<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>
|
||||
<UL>
|
||||
{[...Array<Genre>(3)].map((_, i) => (
|
||||
<LI key={i}>
|
||||
<Skeleton {...css({ marginBottom: 0 })} />
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
<View className="mt-5 flex-row flex-wrap items-center">
|
||||
<P className="mr-1">{t("show.tags")}:</P>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Chip.Loader key={i} size="small" className="m-1" />
|
||||
))}
|
||||
</View>
|
||||
<P className="my-5 flex flex-row flex-wrap items-center">
|
||||
<P className="mr-1">{t("show.studios")}:</P>
|
||||
<Skeleton className="w-2/5" />
|
||||
</P>
|
||||
<View className="flex-row flex-wrap items-center">
|
||||
<P className="mr-1 text-center">{t("show.links")}:</P>
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<Chip.Loader key={i} size="small" outline className="m-1" />
|
||||
))}
|
||||
</View>
|
||||
</Container>
|
||||
);
|
||||
@ -667,9 +402,6 @@ export const Header = ({
|
||||
kind: "movie" | "serie";
|
||||
slug: string;
|
||||
}) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@ -681,141 +413,60 @@ export const Header = ({
|
||||
<Fetch
|
||||
query={Header.query(kind, slug)}
|
||||
Render={(data) => (
|
||||
<View {...css({ flex: 1 })}>
|
||||
<View className="flex-1">
|
||||
<Head
|
||||
title={data.name}
|
||||
description={data.description}
|
||||
image={data.thumbnail?.high}
|
||||
/>
|
||||
<GradientImageBackground
|
||||
<ImageBackground
|
||||
src={data.thumbnail}
|
||||
quality="high"
|
||||
alt=""
|
||||
layout={{
|
||||
width: percent(100),
|
||||
height: {
|
||||
xs: vh(40),
|
||||
sm: min(vh(60), px(750)),
|
||||
md: min(vh(60), px(680)),
|
||||
lg: vh(65),
|
||||
},
|
||||
}}
|
||||
{...(css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
minHeight: {
|
||||
xs: px(350),
|
||||
sm: px(300),
|
||||
md: px(400),
|
||||
lg: px(600),
|
||||
},
|
||||
}) as any)}
|
||||
/>
|
||||
<ContrastArea>
|
||||
<TitleLine
|
||||
kind={kind}
|
||||
slug={slug}
|
||||
name={data.name}
|
||||
tagline={data.tagline}
|
||||
date={getDisplayDate(data)}
|
||||
rating={data.rating}
|
||||
runtime={data.kind === "movie" ? data.runtime : null}
|
||||
poster={data.poster}
|
||||
studios={data.kind !== "collection" ? data.studios! : null}
|
||||
playHref={data.kind !== "collection" ? data.playHref : null}
|
||||
trailerUrl={data.kind !== "collection" ? data.trailerUrl : null}
|
||||
watchStatus={
|
||||
data.kind !== "collection"
|
||||
? (data.watchStatus?.status ?? null)
|
||||
: null
|
||||
}
|
||||
{...css({
|
||||
marginTop: {
|
||||
xs: max(vh(20), px(200)),
|
||||
sm: vh(45),
|
||||
md: max(vh(30), px(150)),
|
||||
lg: max(vh(35), px(200)),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</ContrastArea>
|
||||
<Description
|
||||
description={data?.description}
|
||||
genres={data?.genres}
|
||||
tags={data?.tags}
|
||||
{...css({ paddingTop: { xs: 0, md: ts(2) } })}
|
||||
/>
|
||||
<Container
|
||||
{...css({
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: ts(0.5),
|
||||
})}
|
||||
className="absolute top-0 right-0 left-0 h-[40vh] w-full sm:h-[60vh] sm:min-h-[750px] md:min-h-[680px] lg:h-[65vh]"
|
||||
>
|
||||
<P {...css({ marginRight: ts(0.5), textAlign: "center" })}>
|
||||
{t("show.links")}:
|
||||
</P>
|
||||
{Object.entries(data.externalId!)
|
||||
.filter(([_, data]) => data.link)
|
||||
.map(([name, data]) => (
|
||||
<Chip
|
||||
key={name}
|
||||
label={name}
|
||||
href={data.link}
|
||||
target="_blank"
|
||||
size="small"
|
||||
outline
|
||||
{...css({ m: ts(0.5) })}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
<View className="absolute inset-0 bg-linear-to-b from-transparent to-slate-950/70" />
|
||||
</ImageBackground>
|
||||
<TitleLine
|
||||
kind={kind}
|
||||
slug={slug}
|
||||
name={data.name}
|
||||
tagline={data.tagline}
|
||||
date={getDisplayDate(data)}
|
||||
rating={data.rating}
|
||||
runtime={data.kind === "movie" ? data.runtime : null}
|
||||
poster={data.poster}
|
||||
playHref={data.kind !== "collection" ? data.playHref : null}
|
||||
trailerUrl={data.kind !== "collection" ? data.trailerUrl : null}
|
||||
watchStatus={
|
||||
data.kind !== "collection"
|
||||
? (data.watchStatus?.status ?? null)
|
||||
: null
|
||||
}
|
||||
className="mt-[max(20vh,200px)] sm:mt-[35vh] md:mt-[max(45vh,150px)] lg:mt-[max(35vh,200px)]"
|
||||
/>
|
||||
<Description
|
||||
description={data.description}
|
||||
tags={data.tags}
|
||||
genres={data.genres}
|
||||
studios={data.kind !== "collection" ? data.studios! : []}
|
||||
externalIds={data.externalId}
|
||||
/>
|
||||
|
||||
{/* {type === "show" && ( */}
|
||||
{/* <ShowWatchStatusCard {...(data?.watchStatus as any)} /> */}
|
||||
{/* )} */}
|
||||
</View>
|
||||
)}
|
||||
Loader={() => (
|
||||
<>
|
||||
<LinearGradient
|
||||
start={{ x: 0, y: 0.25 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
colors={["transparent", theme.darkOverlay]}
|
||||
{...(css({
|
||||
width: percent(100),
|
||||
height: {
|
||||
xs: vh(40),
|
||||
sm: min(vh(60), px(750)),
|
||||
md: min(vh(60), px(680)),
|
||||
lg: vh(65),
|
||||
},
|
||||
minHeight: {
|
||||
xs: px(350),
|
||||
sm: px(300),
|
||||
md: px(400),
|
||||
lg: px(600),
|
||||
},
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}) as any)}
|
||||
/>
|
||||
<View className="flex-1">
|
||||
<View className="absolute top-0 right-0 left-0 h-[40vh] w-full bg-linear-to-b from-transparent to-slate-950/70 sm:h-[60vh] sm:min-h-[750px] md:min-h-[680px] lg:h-[65vh]" />
|
||||
<TitleLine.Loader
|
||||
kind={kind}
|
||||
{...css({
|
||||
marginTop: {
|
||||
xs: max(vh(20), px(200)),
|
||||
sm: vh(45),
|
||||
md: max(vh(30), px(150)),
|
||||
lg: max(vh(35), px(200)),
|
||||
},
|
||||
})}
|
||||
className="mt-[max(20vh,200px)] sm:mt-[35vh] md:mt-[max(45vh,150px)] lg:mt-[max(35vh,200px)]"
|
||||
/>
|
||||
<Description.Loader />
|
||||
</>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
@ -825,8 +476,8 @@ export const Header = ({
|
||||
Header.query = (
|
||||
kind: "serie" | "movie" | "collection",
|
||||
slug: string,
|
||||
): QueryIdentifier<Serie | Movie | Collection> => ({
|
||||
parser: kind === "serie" ? Serie : kind === "movie" ? Movie : Collection,
|
||||
): QueryIdentifier<Show> => ({
|
||||
parser: Show,
|
||||
path: ["api", `${kind}s`, slug],
|
||||
params: {
|
||||
with: ["studios", ...(kind === "serie" ? ["firstEntry", "nextEntry"] : [])],
|
||||
|
||||
@ -8,18 +8,18 @@ import { EntryLine, entryDisplayNumber } from "~/components/entries";
|
||||
import type { Entry, Serie } from "~/models";
|
||||
import { Container, H2 } from "~/primitives";
|
||||
import { Fetch } from "~/query";
|
||||
import { cn, useQueryState } from "~/utils";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { Header } from "./header";
|
||||
import { EntryList } from "./season";
|
||||
|
||||
export const Svg = withUniwind(RSvg, {
|
||||
stroke: {
|
||||
fromClassName: "strokeClassName",
|
||||
fromClassName: "className",
|
||||
styleProperty: "accentColor",
|
||||
},
|
||||
fill: {
|
||||
fromClassName: "fillClassName",
|
||||
styleProperty: "accentColor",
|
||||
fromClassName: "className",
|
||||
styleProperty: "fill",
|
||||
},
|
||||
});
|
||||
|
||||
@ -79,7 +79,7 @@ const SerieHeader = () => {
|
||||
/>
|
||||
{/* <DetailsCollections type="serie" slug={slug} /> */}
|
||||
{/* <Staff slug={slug} /> */}
|
||||
<SvgWave fillClassName={cn("accent-card")} className="flex-1 shrink-0" />
|
||||
<SvgWave className="flex-1 shrink-0 fill-card" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -27,14 +27,15 @@ export const useQueryState = <S>(key: string, initial: S) => {
|
||||
return [state, update] as const;
|
||||
};
|
||||
|
||||
export const getDisplayDate = (data: Show | Movie) => {
|
||||
const {
|
||||
startAir,
|
||||
endAir,
|
||||
airDate,
|
||||
}: { startAir?: Date | null; endAir?: Date | null; airDate?: Date | null } =
|
||||
data;
|
||||
|
||||
export const getDisplayDate = ({
|
||||
startAir,
|
||||
endAir,
|
||||
airDate,
|
||||
}: {
|
||||
startAir?: Date | null;
|
||||
endAir?: Date | null;
|
||||
airDate?: Date | null;
|
||||
}) => {
|
||||
if (startAir) {
|
||||
if (!endAir || startAir.getFullYear() === endAir.getFullYear()) {
|
||||
return startAir.getFullYear().toString();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user