Cleanup recommanded card for android and nested links

This commit is contained in:
Zoe Roux 2023-12-11 20:55:56 +01:00
parent dfe061e611
commit 545e729f2e
5 changed files with 157 additions and 105 deletions

View File

@ -22,8 +22,7 @@ import { px, rem, Theme, useYoshiki } from "yoshiki/native";
import { Link } from "./links";
import { P } from "./text";
import { capitalize, ts } from "./utils";
import { EnhancedStyle } from "yoshiki/src/native/type";
import { TextProps, TextStyle, ViewStyle } from "react-native";
import { TextProps } from "react-native";
import { Skeleton } from "./skeleton";
export const Chip = ({

View File

@ -87,6 +87,7 @@ export const IconButton = forwardRef(function IconButton<AsProps = PressableProp
alignSelf: "center",
p: ts(1),
m: px(2),
overflow: "hidden",
borderRadius: 9999,
fover: {
self: {

View File

@ -20,10 +20,37 @@
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
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 { 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, _>({
query,
@ -37,6 +64,7 @@ export const InfiniteFetchList = <Data, Props, _>({
headerProps,
getItemType,
fetchMore = true,
contentContainerStyle,
...props
}: {
query: ReturnType<typeof useInfiniteFetch<_, Data>>;
@ -54,10 +82,11 @@ export const InfiniteFetchList = <Data, Props, _>({
headerProps?: Props;
getItemType?: (item: WithLoading<Data>, index: number) => string | number;
fetchMore?: boolean;
contentContainerStyle?: ContentStyle;
}): JSX.Element | null => {
const { numColumns, size, gap } = useBreakpointMap(layout);
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 (error) return <ErrorView error={error} />;
@ -77,15 +106,11 @@ export const InfiniteFetchList = <Data, Props, _>({
return (
<FlashList
contentContainerStyle={{
paddingHorizontal: layout.layout !== "vertical" ? gap / 2 : 0,
paddingHorizontal: layout.layout !== "vertical" ? gap : 0,
...contentContainerStyle,
}}
renderItem={({ item, index }) => (
<View
style={{
paddingHorizontal: layout.layout !== "vertical" ? gap / 2 : 0,
paddingVertical: layout.layout !== "horizontal" ? gap / 2 : 0,
}}
>
<View style={emulateGap(layout.layout, gap, numColumns, index)}>
{children({ isLoading: false, ...item } as any, index)}
</View>
)}

View File

@ -32,6 +32,8 @@ import {
} from "react";
import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki";
import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch";
import { ViewStyle } from "react-native";
import type { ContentStyle } from "@shopify/flash-list";
const InfiniteScroll = <Props,>({
children,
@ -43,6 +45,7 @@ const InfiniteScroll = <Props,>({
Header,
headerProps,
fetchMore = true,
contentContainerStyle,
...props
}: {
children?: ReactElement | (ReactElement | null)[] | null;
@ -54,6 +57,7 @@ const InfiniteScroll = <Props,>({
Header?: ComponentType<Props & { children: JSX.Element }> | ReactElement;
headerProps?: Props;
fetchMore?: boolean;
contentContainerStyle?: ContentStyle;
} & Stylable) => {
const ref = useRef<HTMLDivElement>(null);
const { css } = useYoshiki();
@ -110,6 +114,7 @@ const InfiniteScroll = <Props,>({
overflowY: "auto",
padding: layout.gap as any,
},
contentContainerStyle as any,
],
nativeStyleToCss(props),
)}
@ -162,6 +167,7 @@ export const InfiniteFetchList = <Data, _, HeaderProps>({
headerProps: HeaderProps;
getItemType?: (item: WithLoading<Data>, index: number) => string | number;
fetchMore?: boolean;
contentContainerStyle?: ContentStyle;
}): JSX.Element | null => {
const oldItems = useRef<Data[] | undefined>();
const { items, error, fetchNextPage, hasNextPage, isFetching } = query;

View File

@ -44,14 +44,12 @@ import {
ts,
} from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { Pressable, ScrollView, View } from "react-native";
import { useRouter } from "solito/router";
import { ScrollView, View } from "react-native";
import { Theme, calc, percent, px, rem, useYoshiki } from "yoshiki/native";
import { Layout, WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite";
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
import { ItemGrid, ItemWatchStatus } from "../browse/grid";
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
export const ItemDetails = ({
isLoading,
@ -78,111 +76,134 @@ export const ItemDetails = ({
watchStatus: WatchStatusV | null;
unseenEpisodesCount: number | null;
}>) => {
const { push } = useRouter();
const { t } = useTranslation();
const { css } = useYoshiki("recommanded-card");
return (
<Link
href={href}
{...css(
{
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,
)}
<View
{...css({
height: ItemDetails.layout.size,
})}
>
<PosterBackground
src={poster}
alt=""
quality="low"
forcedLoading={isLoading}
layout={{ height: percent(100) }}
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
>
<View
{...css({
bg: (theme) => theme.darkOverlay,
<Link
href={href}
{...css(
{
position: "absolute",
top: 0,
left: 0,
right: 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) })}>
{isLoading || (
<P {...css([{ m: 0, color: (theme: Theme) => theme.colors.white }, "title"])}>
{name}
</P>
<View
{...css({
bg: (theme) => theme.darkOverlay,
position: "absolute",
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>
{(subtitle || isLoading) && (
<Skeleton {...css({ height: rem(0.8) })}>
{isLoading || <SubP {...css({ m: 0 })}>{subtitle}</SubP>}
</View>
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
</PosterBackground>
<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>
)}
<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>
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
</PosterBackground>
<View {...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end" })}>
{(isLoading || tagline) && (
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
{isLoading || <P {...css({ p: ts(1) })}>{tagline}</P>}
</Skeleton>
</Link>
{/* This view needs to be out of the Link because nested <a> are not allowed on the web */}
<View
{...css({
position: "absolute",
// 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>
</Link>
</View>
);
};
@ -205,7 +226,7 @@ export const Recommanded = () => {
layout={ItemDetails.layout}
placeholderCount={6}
fetchMore={false}
{...css({ padding: 0 })}
contentContainerStyle={{ padding: 0, paddingHorizontal: 0 }}
>
{(x) => (
<ItemDetails