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 { 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 = ({

View File

@ -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: {

View File

@ -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>
)} )}

View File

@ -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;

View File

@ -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,16 +76,24 @@ 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 (
<View
{...css({
height: ItemDetails.layout.size,
})}
>
<Link <Link
href={href} href={href}
{...css( {...css(
{ {
height: ItemDetails.layout.size, position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
flexDirection: "row", flexDirection: "row",
bg: (theme) => theme.variant.background, bg: (theme) => theme.variant.background,
borderRadius: calc(px(imageBorderRadius), "+", ts(0.25)), borderRadius: calc(px(imageBorderRadius), "+", ts(0.25)),
@ -141,7 +147,9 @@ export const ItemDetails = ({
</View> </View>
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} /> <ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
</PosterBackground> </PosterBackground>
<View {...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end" })}> <View
{...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end", marginBottom: px(50) })}
>
{(isLoading || tagline) && ( {(isLoading || tagline) && (
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}> <Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
{isLoading || <P {...css({ p: ts(1) })}>{tagline}</P>} {isLoading || <P {...css({ p: ts(1) })}>{tagline}</P>}
@ -154,17 +162,31 @@ export const ItemDetails = ({
)} )}
</Skeleton> </Skeleton>
</ScrollView> </ScrollView>
</View>
</Link>
{/* This view needs to be out of the Link because nested <a> are not allowed on the web */}
<View <View
{...css({ {...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, bg: (theme) => theme.themeOverlay,
flexDirection: "row", flexDirection: "row",
pX: 4, pX: 4,
justifyContent: "flex-end", justifyContent: "flex-end",
minHeight: px(50), height: px(50),
})} })}
> >
{(isLoading || genres) && ( {(isLoading || genres) && (
<ScrollView horizontal {...css({ alignItems: "center" })}> <ScrollView horizontal contentContainerStyle={{ alignItems: "center" }}>
{(genres || [...Array(3)])?.map((x, i) => ( {(genres || [...Array(3)])?.map((x, i) => (
<Chip key={x ?? i} label={x} size="small" {...css({ mX: ts(0.5) })} /> <Chip key={x ?? i} label={x} size="small" {...css({ mX: ts(0.5) })} />
))} ))}
@ -174,15 +196,14 @@ export const ItemDetails = ({
<IconFab <IconFab
icon={PlayArrow} icon={PlayArrow}
size={20} size={20}
as={Pressable} as={Link}
onPress={() => push(playHref ?? "")} href={playHref}
{...tooltip(t("show.play"))} {...tooltip(t("show.play"))}
{...css({ fover: { self: { transform: "scale(1.2)" as any, mX: ts(0.5) } } })} {...css({ fover: { self: { transform: "scale(1.2)" as any, mX: ts(0.5) } } })}
/> />
)} )}
</View> </View>
</View> </View>
</Link>
); );
}; };
@ -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