mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Cleanup recommanded card for android and nested links
This commit is contained in:
parent
dfe061e611
commit
545e729f2e
@ -22,8 +22,7 @@ import { px, rem, Theme, useYoshiki } from "yoshiki/native";
|
|||||||
import { Link } from "./links";
|
import { Link } from "./links";
|
||||||
import { P } from "./text";
|
import { P } from "./text";
|
||||||
import { capitalize, ts } from "./utils";
|
import { capitalize, ts } from "./utils";
|
||||||
import { EnhancedStyle } from "yoshiki/src/native/type";
|
import { TextProps } from "react-native";
|
||||||
import { TextProps, TextStyle, ViewStyle } from "react-native";
|
|
||||||
import { Skeleton } from "./skeleton";
|
import { Skeleton } from "./skeleton";
|
||||||
|
|
||||||
export const Chip = ({
|
export const Chip = ({
|
||||||
|
@ -87,6 +87,7 @@ export const IconButton = forwardRef(function IconButton<AsProps = PressableProp
|
|||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
p: ts(1),
|
p: ts(1),
|
||||||
m: px(2),
|
m: px(2),
|
||||||
|
overflow: "hidden",
|
||||||
borderRadius: 9999,
|
borderRadius: 9999,
|
||||||
fover: {
|
fover: {
|
||||||
self: {
|
self: {
|
||||||
|
@ -20,10 +20,37 @@
|
|||||||
|
|
||||||
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
|
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
|
||||||
import { useBreakpointMap, HR } from "@kyoo/primitives";
|
import { useBreakpointMap, HR } from "@kyoo/primitives";
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { ContentStyle, FlashList } from "@shopify/flash-list";
|
||||||
import { ComponentProps, ComponentType, isValidElement, ReactElement, useRef } from "react";
|
import { ComponentProps, ComponentType, isValidElement, ReactElement, useRef } from "react";
|
||||||
import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch";
|
import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch";
|
||||||
import { View, DimensionValue } from "react-native";
|
import { View, ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
const emulateGap = (
|
||||||
|
layout: "grid" | "vertical" | "horizontal",
|
||||||
|
gap: number,
|
||||||
|
numColumns: number,
|
||||||
|
index: number,
|
||||||
|
): ViewStyle => {
|
||||||
|
let marginLeft = 0;
|
||||||
|
let marginRight = 0;
|
||||||
|
|
||||||
|
if (layout !== "vertical" && numColumns > 1) {
|
||||||
|
if (index % numColumns === 0) {
|
||||||
|
marginRight = (gap * 2) / 3;
|
||||||
|
} else if ((index + 1) % numColumns === 0) {
|
||||||
|
marginLeft = (gap * 2) / 3;
|
||||||
|
} else {
|
||||||
|
marginLeft = gap / 3;
|
||||||
|
marginRight = gap / 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
marginLeft,
|
||||||
|
marginRight,
|
||||||
|
marginTop: layout !== "horizontal" && index >= numColumns ? gap : 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const InfiniteFetchList = <Data, Props, _>({
|
export const InfiniteFetchList = <Data, Props, _>({
|
||||||
query,
|
query,
|
||||||
@ -37,6 +64,7 @@ export const InfiniteFetchList = <Data, Props, _>({
|
|||||||
headerProps,
|
headerProps,
|
||||||
getItemType,
|
getItemType,
|
||||||
fetchMore = true,
|
fetchMore = true,
|
||||||
|
contentContainerStyle,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
query: ReturnType<typeof useInfiniteFetch<_, Data>>;
|
query: ReturnType<typeof useInfiniteFetch<_, Data>>;
|
||||||
@ -54,10 +82,11 @@ export const InfiniteFetchList = <Data, Props, _>({
|
|||||||
headerProps?: Props;
|
headerProps?: Props;
|
||||||
getItemType?: (item: WithLoading<Data>, index: number) => string | number;
|
getItemType?: (item: WithLoading<Data>, index: number) => string | number;
|
||||||
fetchMore?: boolean;
|
fetchMore?: boolean;
|
||||||
|
contentContainerStyle?: ContentStyle;
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
const { numColumns, size, gap } = useBreakpointMap(layout);
|
const { numColumns, size, gap } = useBreakpointMap(layout);
|
||||||
const oldItems = useRef<Data[] | undefined>();
|
const oldItems = useRef<Data[] | undefined>();
|
||||||
let { items, error, fetchNextPage, hasNextPage, isFetching, refetch, isRefetching } = query;
|
let { items, error, fetchNextPage, isFetching, refetch, isRefetching } = query;
|
||||||
if (incremental && items) oldItems.current = items;
|
if (incremental && items) oldItems.current = items;
|
||||||
|
|
||||||
if (error) return <ErrorView error={error} />;
|
if (error) return <ErrorView error={error} />;
|
||||||
@ -77,15 +106,11 @@ export const InfiniteFetchList = <Data, Props, _>({
|
|||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingHorizontal: layout.layout !== "vertical" ? gap / 2 : 0,
|
paddingHorizontal: layout.layout !== "vertical" ? gap : 0,
|
||||||
|
...contentContainerStyle,
|
||||||
}}
|
}}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
<View
|
<View style={emulateGap(layout.layout, gap, numColumns, index)}>
|
||||||
style={{
|
|
||||||
paddingHorizontal: layout.layout !== "vertical" ? gap / 2 : 0,
|
|
||||||
paddingVertical: layout.layout !== "horizontal" ? gap / 2 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children({ isLoading: false, ...item } as any, index)}
|
{children({ isLoading: false, ...item } as any, index)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
@ -32,6 +32,8 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki";
|
import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki";
|
||||||
import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch";
|
import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch";
|
||||||
|
import { ViewStyle } from "react-native";
|
||||||
|
import type { ContentStyle } from "@shopify/flash-list";
|
||||||
|
|
||||||
const InfiniteScroll = <Props,>({
|
const InfiniteScroll = <Props,>({
|
||||||
children,
|
children,
|
||||||
@ -43,6 +45,7 @@ const InfiniteScroll = <Props,>({
|
|||||||
Header,
|
Header,
|
||||||
headerProps,
|
headerProps,
|
||||||
fetchMore = true,
|
fetchMore = true,
|
||||||
|
contentContainerStyle,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
children?: ReactElement | (ReactElement | null)[] | null;
|
children?: ReactElement | (ReactElement | null)[] | null;
|
||||||
@ -54,6 +57,7 @@ const InfiniteScroll = <Props,>({
|
|||||||
Header?: ComponentType<Props & { children: JSX.Element }> | ReactElement;
|
Header?: ComponentType<Props & { children: JSX.Element }> | ReactElement;
|
||||||
headerProps?: Props;
|
headerProps?: Props;
|
||||||
fetchMore?: boolean;
|
fetchMore?: boolean;
|
||||||
|
contentContainerStyle?: ContentStyle;
|
||||||
} & Stylable) => {
|
} & Stylable) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
@ -110,6 +114,7 @@ const InfiniteScroll = <Props,>({
|
|||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
padding: layout.gap as any,
|
padding: layout.gap as any,
|
||||||
},
|
},
|
||||||
|
contentContainerStyle as any,
|
||||||
],
|
],
|
||||||
nativeStyleToCss(props),
|
nativeStyleToCss(props),
|
||||||
)}
|
)}
|
||||||
@ -162,6 +167,7 @@ export const InfiniteFetchList = <Data, _, HeaderProps>({
|
|||||||
headerProps: HeaderProps;
|
headerProps: HeaderProps;
|
||||||
getItemType?: (item: WithLoading<Data>, index: number) => string | number;
|
getItemType?: (item: WithLoading<Data>, index: number) => string | number;
|
||||||
fetchMore?: boolean;
|
fetchMore?: boolean;
|
||||||
|
contentContainerStyle?: ContentStyle;
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
const oldItems = useRef<Data[] | undefined>();
|
const oldItems = useRef<Data[] | undefined>();
|
||||||
const { items, error, fetchNextPage, hasNextPage, isFetching } = query;
|
const { items, error, fetchNextPage, hasNextPage, isFetching } = query;
|
||||||
|
@ -44,14 +44,12 @@ import {
|
|||||||
ts,
|
ts,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Pressable, ScrollView, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
import { useRouter } from "solito/router";
|
|
||||||
import { Theme, calc, percent, px, rem, useYoshiki } from "yoshiki/native";
|
import { Theme, calc, percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||||
import { Layout, WithLoading } from "../fetch";
|
import { Layout, WithLoading } from "../fetch";
|
||||||
import { InfiniteFetch } from "../fetch-infinite";
|
import { InfiniteFetch } from "../fetch-infinite";
|
||||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||||
import { ItemGrid, ItemWatchStatus } from "../browse/grid";
|
import { ItemGrid, ItemWatchStatus } from "../browse/grid";
|
||||||
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
|
|
||||||
|
|
||||||
export const ItemDetails = ({
|
export const ItemDetails = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -78,111 +76,134 @@ export const ItemDetails = ({
|
|||||||
watchStatus: WatchStatusV | null;
|
watchStatus: WatchStatusV | null;
|
||||||
unseenEpisodesCount: number | null;
|
unseenEpisodesCount: number | null;
|
||||||
}>) => {
|
}>) => {
|
||||||
const { push } = useRouter();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { css } = useYoshiki("recommanded-card");
|
const { css } = useYoshiki("recommanded-card");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<View
|
||||||
href={href}
|
{...css({
|
||||||
{...css(
|
height: ItemDetails.layout.size,
|
||||||
{
|
})}
|
||||||
height: ItemDetails.layout.size,
|
|
||||||
flexDirection: "row",
|
|
||||||
bg: (theme) => theme.variant.background,
|
|
||||||
borderRadius: calc(px(imageBorderRadius), "+", ts(0.25)),
|
|
||||||
overflow: "hidden",
|
|
||||||
borderColor: (theme) => theme.background,
|
|
||||||
borderWidth: ts(0.25),
|
|
||||||
borderStyle: "solid",
|
|
||||||
fover: {
|
|
||||||
self: {
|
|
||||||
...focusReset,
|
|
||||||
borderColor: (theme: Theme) => theme.accent,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textDecorationLine: "underline",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
props,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<PosterBackground
|
<Link
|
||||||
src={poster}
|
href={href}
|
||||||
alt=""
|
{...css(
|
||||||
quality="low"
|
{
|
||||||
forcedLoading={isLoading}
|
|
||||||
layout={{ height: percent(100) }}
|
|
||||||
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
{...css({
|
|
||||||
bg: (theme) => theme.darkOverlay,
|
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
p: ts(1),
|
flexDirection: "row",
|
||||||
})}
|
bg: (theme) => theme.variant.background,
|
||||||
|
borderRadius: calc(px(imageBorderRadius), "+", ts(0.25)),
|
||||||
|
overflow: "hidden",
|
||||||
|
borderColor: (theme) => theme.background,
|
||||||
|
borderWidth: ts(0.25),
|
||||||
|
borderStyle: "solid",
|
||||||
|
fover: {
|
||||||
|
self: {
|
||||||
|
...focusReset,
|
||||||
|
borderColor: (theme: Theme) => theme.accent,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
textDecorationLine: "underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PosterBackground
|
||||||
|
src={poster}
|
||||||
|
alt=""
|
||||||
|
quality="low"
|
||||||
|
forcedLoading={isLoading}
|
||||||
|
layout={{ height: percent(100) }}
|
||||||
|
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
|
||||||
>
|
>
|
||||||
<Skeleton {...css({ width: percent(100) })}>
|
<View
|
||||||
{isLoading || (
|
{...css({
|
||||||
<P {...css([{ m: 0, color: (theme: Theme) => theme.colors.white }, "title"])}>
|
bg: (theme) => theme.darkOverlay,
|
||||||
{name}
|
position: "absolute",
|
||||||
</P>
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
p: ts(1),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Skeleton {...css({ width: percent(100) })}>
|
||||||
|
{isLoading || (
|
||||||
|
<P {...css([{ m: 0, color: (theme: Theme) => theme.colors.white }, "title"])}>
|
||||||
|
{name}
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
</Skeleton>
|
||||||
|
{(subtitle || isLoading) && (
|
||||||
|
<Skeleton {...css({ height: rem(0.8) })}>
|
||||||
|
{isLoading || <SubP {...css({ m: 0 })}>{subtitle}</SubP>}
|
||||||
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
</Skeleton>
|
</View>
|
||||||
{(subtitle || isLoading) && (
|
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
|
||||||
<Skeleton {...css({ height: rem(0.8) })}>
|
</PosterBackground>
|
||||||
{isLoading || <SubP {...css({ m: 0 })}>{subtitle}</SubP>}
|
<View
|
||||||
|
{...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end", marginBottom: px(50) })}
|
||||||
|
>
|
||||||
|
{(isLoading || tagline) && (
|
||||||
|
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
|
||||||
|
{isLoading || <P {...css({ p: ts(1) })}>{tagline}</P>}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
|
<ScrollView {...css({ pX: ts(1) })}>
|
||||||
|
<Skeleton lines={5} {...css({ height: rem(0.8) })}>
|
||||||
|
{isLoading || (
|
||||||
|
<SubP {...css({ textAlign: "justify" })}>{overview ?? t("show.noOverview")}</SubP>
|
||||||
|
)}
|
||||||
|
</Skeleton>
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
|
</Link>
|
||||||
</PosterBackground>
|
|
||||||
<View {...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end" })}>
|
{/* This view needs to be out of the Link because nested <a> are not allowed on the web */}
|
||||||
{(isLoading || tagline) && (
|
<View
|
||||||
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
|
{...css({
|
||||||
{isLoading || <P {...css({ p: ts(1) })}>{tagline}</P>}
|
position: "absolute",
|
||||||
</Skeleton>
|
// Take the border into account
|
||||||
|
bottom: ts(0.25),
|
||||||
|
right: ts(0.25),
|
||||||
|
borderWidth: ts(0.25),
|
||||||
|
borderColor: "transparent",
|
||||||
|
borderBottomEndRadius: px(imageBorderRadius),
|
||||||
|
overflow: "hidden",
|
||||||
|
// Calculate the size of the poster
|
||||||
|
left: calc(ItemDetails.layout.size, "*", 2 / 3),
|
||||||
|
bg: (theme) => theme.themeOverlay,
|
||||||
|
flexDirection: "row",
|
||||||
|
pX: 4,
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
height: px(50),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(isLoading || genres) && (
|
||||||
|
<ScrollView horizontal contentContainerStyle={{ alignItems: "center" }}>
|
||||||
|
{(genres || [...Array(3)])?.map((x, i) => (
|
||||||
|
<Chip key={x ?? i} label={x} size="small" {...css({ mX: ts(0.5) })} />
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
{playHref !== null && (
|
||||||
|
<IconFab
|
||||||
|
icon={PlayArrow}
|
||||||
|
size={20}
|
||||||
|
as={Link}
|
||||||
|
href={playHref}
|
||||||
|
{...tooltip(t("show.play"))}
|
||||||
|
{...css({ fover: { self: { transform: "scale(1.2)" as any, mX: ts(0.5) } } })}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<ScrollView {...css({ pX: ts(1) })}>
|
|
||||||
<Skeleton lines={5} {...css({ height: rem(0.8) })}>
|
|
||||||
{isLoading || (
|
|
||||||
<SubP {...css({ textAlign: "justify" })}>{overview ?? t("show.noOverview")}</SubP>
|
|
||||||
)}
|
|
||||||
</Skeleton>
|
|
||||||
</ScrollView>
|
|
||||||
<View
|
|
||||||
{...css({
|
|
||||||
bg: (theme) => theme.themeOverlay,
|
|
||||||
flexDirection: "row",
|
|
||||||
pX: 4,
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
minHeight: px(50),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{(isLoading || genres) && (
|
|
||||||
<ScrollView horizontal {...css({ alignItems: "center" })}>
|
|
||||||
{(genres || [...Array(3)])?.map((x, i) => (
|
|
||||||
<Chip key={x ?? i} label={x} size="small" {...css({ mX: ts(0.5) })} />
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
)}
|
|
||||||
{playHref !== null && (
|
|
||||||
<IconFab
|
|
||||||
icon={PlayArrow}
|
|
||||||
size={20}
|
|
||||||
as={Pressable}
|
|
||||||
onPress={() => push(playHref ?? "")}
|
|
||||||
{...tooltip(t("show.play"))}
|
|
||||||
{...css({ fover: { self: { transform: "scale(1.2)" as any, mX: ts(0.5) } } })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -205,7 +226,7 @@ export const Recommanded = () => {
|
|||||||
layout={ItemDetails.layout}
|
layout={ItemDetails.layout}
|
||||||
placeholderCount={6}
|
placeholderCount={6}
|
||||||
fetchMore={false}
|
fetchMore={false}
|
||||||
{...css({ padding: 0 })}
|
contentContainerStyle={{ padding: 0, paddingHorizontal: 0 }}
|
||||||
>
|
>
|
||||||
{(x) => (
|
{(x) => (
|
||||||
<ItemDetails
|
<ItemDetails
|
||||||
|
Loading…
x
Reference in New Issue
Block a user