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 { 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 = ({
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user