Finish a basic recommanded card

This commit is contained in:
Zoe Roux 2023-10-18 22:56:07 +02:00
parent 941137ff51
commit e4562648ba
5 changed files with 93 additions and 38 deletions

View File

@ -162,12 +162,14 @@ export type QueryIdentifier<T = unknown, Ret = T> = {
getNext?: (item: unknown) => string | undefined; getNext?: (item: unknown) => string | undefined;
}; };
export type QueryPage<Props = {}, Items = unknown> = ComponentType<Props & { randomItems: Items[]}> & { export type QueryPage<Props = {}, Items = unknown> = ComponentType<
Props & { randomItems: Items[] }
> & {
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier<any>[]; getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier<any>[];
getLayout?: getLayout?:
| QueryPage<{ page: ReactElement }> | QueryPage<{ page: ReactElement }>
| { Layout: QueryPage<{ page: ReactElement }>; props: object }; | { Layout: QueryPage<{ page: ReactElement }>; props: object };
randomItems?: Items[] randomItems?: Items[];
}; };
const toQueryKey = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => { const toQueryKey = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {

View File

@ -26,6 +26,7 @@ export const catppuccin: ThemeBuilder = {
// Catppuccin latte // Catppuccin latte
overlay0: "#9ca0b0", overlay0: "#9ca0b0",
overlay1: "#7c7f93", overlay1: "#7c7f93",
lightOverlay: "#eff1f599",
darkOverlay: "#4c4f6999", darkOverlay: "#4c4f6999",
link: "#1e66f5", link: "#1e66f5",
default: { default: {
@ -57,6 +58,7 @@ export const catppuccin: ThemeBuilder = {
// Catppuccin mocha // Catppuccin mocha
overlay0: "#6c7086", overlay0: "#6c7086",
overlay1: "#9399b2", overlay1: "#9399b2",
lightOverlay: "#f5f0f899",
darkOverlay: "#11111b99", darkOverlay: "#11111b99",
link: "#89b4fa", link: "#89b4fa",
default: { default: {

View File

@ -38,7 +38,9 @@ type Mode = {
mode: "light" | "dark" | "auto"; mode: "light" | "dark" | "auto";
overlay0: Property.Color; overlay0: Property.Color;
overlay1: Property.Color; overlay1: Property.Color;
lightOverlay: Property.Color;
darkOverlay: Property.Color; darkOverlay: Property.Color;
themeOverlay: Property.Color;
link: Property.Color; link: Property.Color;
contrast: Property.Color; contrast: Property.Color;
variant: Variant; variant: Variant;
@ -73,8 +75,8 @@ declare module "yoshiki" {
export type { Theme } from "yoshiki"; export type { Theme } from "yoshiki";
export type ThemeBuilder = { export type ThemeBuilder = {
light: Omit<Mode, "contrast" | "mode"> & { default: Variant }; light: Omit<Mode, "contrast" | "mode" | "themeOverlay"> & { default: Variant };
dark: Omit<Mode, "contrast" | "mode"> & { default: Variant }; dark: Omit<Mode, "contrast" | "mode" | "themeOverlay"> & { default: Variant };
}; };
const selectMode = ( const selectMode = (
@ -86,12 +88,14 @@ const selectMode = (
...lightBuilder, ...lightBuilder,
...lightBuilder.default, ...lightBuilder.default,
contrast: lightBuilder.colors.black, contrast: lightBuilder.colors.black,
themeOverlay: lightBuilder.lightOverlay,
mode: "light", mode: "light",
}; };
const dark: Mode & Variant = { const dark: Mode & Variant = {
...darkBuilder, ...darkBuilder,
...darkBuilder.default, ...darkBuilder.default,
contrast: darkBuilder.colors.white, contrast: darkBuilder.colors.white,
themeOverlay: darkBuilder.darkOverlay,
mode: "dark", mode: "dark",
}; };
if (Platform.OS !== "web" || mode !== "auto") { if (Platform.OS !== "web" || mode !== "auto") {

View File

@ -36,6 +36,7 @@ import { Fetch } from "../fetch";
import { ItemGrid } from "../browse/grid"; import { ItemGrid } from "../browse/grid";
import ChevronLeft from "@material-symbols/svg-400/rounded/chevron_left-fill.svg"; import ChevronLeft from "@material-symbols/svg-400/rounded/chevron_left-fill.svg";
import ChevronRight from "@material-symbols/svg-400/rounded/chevron_right-fill.svg"; import ChevronRight from "@material-symbols/svg-400/rounded/chevron_right-fill.svg";
import { InfiniteFetch } from "../fetch-infinite";
export const GenreGrid = ({ genre }: { genre: Genre }) => { export const GenreGrid = ({ genre }: { genre: Genre }) => {
const ref = useRef<ScrollView>(null); const ref = useRef<ScrollView>(null);
@ -43,7 +44,7 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
return ( return (
<View> <View>
<View {...css({ marginX: ts(1), flexDirection: "row", justifyContent: "space-between" })}> <View {...css({ marginX: ts(1), flexDirection: "row", justifyContent: "space-between" })}>
<H3>{genre}</H3> <H3>{genre}</H3>
<View {...css({ flexDirection: "row" })}> <View {...css({ flexDirection: "row" })}>
<IconButton <IconButton
@ -56,28 +57,30 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
/> />
</View> </View>
</View> </View>
<ScrollView ref={ref} horizontal> <InfiniteFetch
<Fetch query={GenreGrid.query(genre)}> query={GenreGrid.query(genre)}
{(x, i) => ( layout={{ ...ItemGrid.layout, layout: "horizontal" }}
<ItemGrid >
key={x.id ?? i} {(x, i) => (
isLoading={x.isLoading as any} <ItemGrid
href={x.href} key={x.id ?? i}
name={x.name} isLoading={x.isLoading as any}
subtitle={ href={x.href}
x.kind !== ItemKind.Collection && !x.isLoading ? getDisplayDate(x) : undefined name={x.name}
} subtitle={
poster={x.poster} x.kind !== ItemKind.Collection && !x.isLoading ? getDisplayDate(x) : undefined
/> }
)} poster={x.poster}
</Fetch> />
</ScrollView> )}
</InfiniteFetch>
</View> </View>
); );
}; };
GenreGrid.query = (genre: Genre): QueryIdentifier<Page<LibraryItem>> => ({ GenreGrid.query = (genre: Genre): QueryIdentifier<LibraryItem> => ({
parser: Paged(LibraryItemP) as any, parser: LibraryItemP,
infinite: true,
path: ["items"], path: ["items"],
params: { params: {
genres: genre, genres: genre,

View File

@ -19,6 +19,7 @@
*/ */
import { import {
Genre,
ItemKind, ItemKind,
KyooImage, KyooImage,
LibraryItem, LibraryItem,
@ -28,11 +29,12 @@ import {
QueryIdentifier, QueryIdentifier,
getDisplayDate, getDisplayDate,
} from "@kyoo/models"; } from "@kyoo/models";
import { Container, H3, ImageBackground, P, Poster, SubP, ts } from "@kyoo/primitives"; import { Chip, Container, H3, ImageBackground, P, Poster, SubP, alpha, ts } from "@kyoo/primitives";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { ScrollView, View } from "react-native";
import { percent, useYoshiki } from "yoshiki/native"; import { percent, px, useYoshiki } from "yoshiki/native";
import { Fetch, WithLoading } from "../fetch"; import { Fetch, Layout, WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite";
export const ItemDetails = ({ export const ItemDetails = ({
isLoading, isLoading,
@ -41,35 +43,76 @@ export const ItemDetails = ({
subtitle, subtitle,
overview, overview,
poster, poster,
genres,
...props ...props
}: WithLoading<{ }: WithLoading<{
name: string; name: string;
tagline: string | null; tagline: string | null;
subtitle: string; subtitle: string;
poster: KyooImage | null; poster: KyooImage | null;
genres: Genre[] | null;
overview: string | null; overview: string | null;
}>) => { }>) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
return ( return (
<View {...css({ flexDirection: "row", bg: (theme) => theme.variant.background }, props)}> <View
{...css(
{
height: ItemDetails.layout.size,
flexDirection: "row",
bg: (theme) => theme.variant.background,
borderRadius: 6,
overflow: "hidden",
},
props,
)}
>
<ImageBackground <ImageBackground
src={poster} src={poster}
alt="" alt=""
quality="low" quality="low"
gradient={false}
{...css({ height: percent(100), aspectRatio: 2 / 3 })} {...css({ height: percent(100), aspectRatio: 2 / 3 })}
> >
<P>{name}</P> <View
{subtitle && <SubP>{subtitle}</SubP>} {...css({
bg: (theme) => theme.darkOverlay,
position: "absolute",
left: 0,
right: 0,
bottom: 0,
p: ts(1),
})}
>
<P {...css({ m: 0 })}>{name}</P>
{subtitle && <SubP {...css({ m: 0 })}>{subtitle}</SubP>}
</View>
</ImageBackground> </ImageBackground>
<View> <View {...css({ flexShrink: 1, flexGrow: 1 })}>
{tagline && <P>{tagline}</P>} {tagline && <P {...css({ p: ts(1) })}>{tagline}</P>}
{overview && <SubP>{overview}</SubP>} {overview && (
<ScrollView>
<SubP {...css({ pX: ts(1) })}>{overview}</SubP>
</ScrollView>
)}
<View {...css({ bg: (theme) => theme.themeOverlay, flexDirection: "row" })}>
{genres?.map((x) => (
<Chip key={x} label={x} {...css({ mX: ts(.5) })} />
))}
</View>
</View> </View>
</View> </View>
); );
}; };
ItemDetails.layout = {
size: ts(36),
numColumns: { xs: 1, md: 2, xl: 3 },
layout: "grid",
gap: ts(8),
} satisfies Layout;
export const Recommanded = () => { export const Recommanded = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { css } = useYoshiki(); const { css } = useYoshiki();
@ -77,7 +120,7 @@ export const Recommanded = () => {
return ( return (
<View {...css({ marginX: ts(1) })}> <View {...css({ marginX: ts(1) })}>
<H3>{t("home.recommanded")}</H3> <H3>{t("home.recommanded")}</H3>
<Fetch query={Recommanded.query()}> <InfiniteFetch query={Recommanded.query()} layout={ItemDetails.layout}>
{(x, i) => ( {(x, i) => (
<ItemDetails <ItemDetails
key={x.id ?? i} key={x.id ?? i}
@ -89,16 +132,17 @@ export const Recommanded = () => {
subtitle={ subtitle={
x.kind !== ItemKind.Collection && !x.isLoading ? getDisplayDate(x) : undefined x.kind !== ItemKind.Collection && !x.isLoading ? getDisplayDate(x) : undefined
} }
{...css({ height: { xs: ts(15), md: ts(20) } })} genres={"genres" in x ? x.genres : null}
/> />
)} )}
</Fetch> </InfiniteFetch>
</View> </View>
); );
}; };
Recommanded.query = (): QueryIdentifier<Page<LibraryItem>> => ({ Recommanded.query = (): QueryIdentifier<LibraryItem> => ({
parser: Paged(LibraryItemP) as any, parser: LibraryItemP,
infinite: true,
path: ["items"], path: ["items"],
params: { params: {
sortBy: "random", sortBy: "random",