Fix home page horizontal bars

This commit is contained in:
Zoe Roux 2026-02-15 13:18:36 +01:00
parent 30223dfa4d
commit 082f3283f5
No known key found for this signature in database
17 changed files with 158 additions and 253 deletions

View File

@ -442,7 +442,7 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@legendapp/list": ["@legendapp/list@github:zoriya/legend-list#d5d3344", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "*" } }, "zoriya-legend-list-d5d3344"], "@legendapp/list": ["@legendapp/list@github:zoriya/legend-list#c36ff94", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "*" } }, "zoriya-legend-list-c36ff94"],
"@material-symbols/svg-400": ["@material-symbols/svg-400@0.40.2", "", {}, "sha512-e2yEgZW/OveVT1sGaZW1kkRWTPVghjsJYWy+vIea3q08Fv2o7FCYv23PESMyr5D4AaAXdM5dKWkF1e6yIm4swA=="], "@material-symbols/svg-400": ["@material-symbols/svg-400@0.40.2", "", {}, "sha512-e2yEgZW/OveVT1sGaZW1kkRWTPVghjsJYWy+vIea3q08Fv2o7FCYv23PESMyr5D4AaAXdM5dKWkF1e6yIm4swA=="],

View File

@ -17,6 +17,7 @@ import { EntryContext } from "../items/context-menus";
import { ItemProgress } from "../items/item-grid"; import { ItemProgress } from "../items/item-grid";
export const EntryBox = ({ export const EntryBox = ({
kind,
slug, slug,
serieSlug, serieSlug,
name, name,
@ -27,6 +28,7 @@ export const EntryBox = ({
className, className,
...props ...props
}: { }: {
kind: "movie" | "episode" | "special";
slug: string; slug: string;
// if serie slug is null, disable "Go to serie" in the context menu // if serie slug is null, disable "Go to serie" in the context menu
serieSlug: string | null; serieSlug: string | null;
@ -44,7 +46,7 @@ export const EntryBox = ({
<Link <Link
href={moreOpened ? undefined : href} href={moreOpened ? undefined : href}
onLongPress={() => setMoreOpened(true)} onLongPress={() => setMoreOpened(true)}
className={cn("group w-[350px] items-center outline-0", className)} className={cn("group w-[350px] items-center p-1 outline-0", className)}
{...props} {...props}
> >
<ThumbnailBackground <ThumbnailBackground
@ -58,6 +60,7 @@ export const EntryBox = ({
> >
<ItemProgress watchPercent={watchedPercent} /> <ItemProgress watchPercent={watchedPercent} />
<EntryContext <EntryContext
kind={kind}
slug={slug} slug={slug}
serieSlug={serieSlug} serieSlug={serieSlug}
isOpen={moreOpened} isOpen={moreOpened}
@ -80,9 +83,9 @@ export const EntryBox = ({
); );
}; };
EntryBox.Loader = ({ className, ...props }: { className?: string }) => { EntryBox.Loader = (props: object) => {
return ( return (
<View className={cn("items-center", className)} {...props}> <View className={"h-full w-[350px] items-center p-1"} {...props}>
<Image.Loader className="aspect-video w-full" /> <Image.Loader className="aspect-video w-full" />
<Skeleton className="w-1/2" /> <Skeleton className="w-1/2" />
<Skeleton className="h-3 w-4/5" /> <Skeleton className="h-3 w-4/5" />

View File

@ -13,11 +13,13 @@ import { watchListIcon } from "./watchlist-info";
// import { useDownloader } from "../../packages/ui/src/downloads/ui/src/downloads"; // import { useDownloader } from "../../packages/ui/src/downloads/ui/src/downloads";
export const EntryContext = ({ export const EntryContext = ({
kind,
slug, slug,
serieSlug, serieSlug,
className, className,
...props ...props
}: { }: {
kind: "movie" | "episode" | "special";
serieSlug: string | null; serieSlug: string | null;
slug: string; slug: string;
className?: string; className?: string;
@ -38,7 +40,7 @@ export const EntryContext = ({
<Menu.Item <Menu.Item
label={t("home.episodeMore.goToShow")} label={t("home.episodeMore.goToShow")}
icon={Info} icon={Info}
href={`/series/${serieSlug}`} href={`/${kind === "movie" ? "movies" : "series"}/${serieSlug}`}
/> />
)} )}
{/* <Menu.Item */} {/* <Menu.Item */}

View File

@ -1,7 +1,7 @@
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 { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScrollView, View } from "react-native"; import { ScrollView, View, type ViewProps } from "react-native";
import { ItemContext } from "~/components/items/context-menus"; import { ItemContext } from "~/components/items/context-menus";
import { ItemWatchStatus } from "~/components/items/item-helpers"; import { ItemWatchStatus } from "~/components/items/item-helpers";
import type { Genre, KImage, WatchStatusV } from "~/models"; import type { Genre, KImage, WatchStatusV } from "~/models";
@ -49,8 +49,7 @@ export const ItemDetails = ({
watchStatus: WatchStatusV | null; watchStatus: WatchStatusV | null;
availableCount?: number | null; availableCount?: number | null;
seenCount?: number | null; seenCount?: number | null;
className?: string; } & ViewProps) => {
}) => {
const [moreOpened, setMoreOpened] = useState(false); const [moreOpened, setMoreOpened] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@ -82,8 +81,8 @@ export const ItemDetails = ({
seenCount={seenCount} seenCount={seenCount}
/> />
</PosterBackground> </PosterBackground>
<View className="mb-14 flex-1 justify-end p-2"> <View className="mb-14 flex-1 justify-end">
<View className="my-2 flex-row-reverse justify-between"> <View className="my-2 flex-row-reverse justify-between p-2">
{kind !== "collection" && ( {kind !== "collection" && (
<ItemContext <ItemContext
kind={kind} kind={kind}
@ -95,7 +94,7 @@ export const ItemDetails = ({
)} )}
{tagline && <P className="p-1">{tagline}</P>} {tagline && <P className="p-1">{tagline}</P>}
</View> </View>
<ScrollView className="px-1"> <ScrollView className="p-3">
<SubP className="text-justify"> <SubP className="text-justify">
{description ?? t("show.noOverview")} {description ?? t("show.noOverview")}
</SubP> </SubP>
@ -142,10 +141,13 @@ export const ItemDetails = ({
); );
}; };
ItemDetails.Loader = (props: object) => { ItemDetails.Loader = ({ className, ...props }: ViewProps) => {
return ( return (
<View <View
className={"h-72 flex-row overflow-hidden rounded-xl bg-card"} className={cn(
"h-72 flex-row overflow-hidden rounded-xl bg-card",
className,
)}
{...props} {...props}
> >
<View className="aspect-2/3 h-full bg-gray-400"> <View className="aspect-2/3 h-full bg-gray-400">

View File

@ -71,7 +71,6 @@ export const ItemGrid = ({
> >
<PosterBackground <PosterBackground
src={poster} src={poster}
alt={name}
quality="low" quality="low"
className={cn( className={cn(
"w-full", "w-full",
@ -113,9 +112,20 @@ export const ItemGrid = ({
); );
}; };
ItemGrid.Loader = (props: object) => { ItemGrid.Loader = ({
horizontal = false,
...props
}: {
horizontal?: boolean;
}) => {
return ( return (
<View className="w-full items-center" {...props}> <View
className={cn(
"w-full items-center p-1",
horizontal && "h-full w-[200px]",
)}
{...props}
>
<Poster.Loader className="w-full" /> <Poster.Loader className="w-full" />
<Skeleton /> <Skeleton />
<Skeleton className="w-1/2" /> <Skeleton className="w-1/2" />

View File

@ -49,7 +49,8 @@ export const ItemList = ({
href={moreOpened ? undefined : href} href={moreOpened ? undefined : href}
onLongPress={() => setMoreOpened(true)} onLongPress={() => setMoreOpened(true)}
className={cn( className={cn(
"group h-80 w-full outline-0 ring-accent focus-within:ring-3 hover:ring-3", "group m-1 mx-2 h-80 overflow-hidden rounded",
"outline-0 ring-accent focus-within:ring-3 hover:ring-3",
className, className,
)} )}
{...props} {...props}
@ -57,7 +58,7 @@ export const ItemList = ({
<ImageBackground <ImageBackground
src={thumbnail} src={thumbnail}
quality="medium" quality="medium"
className="h-full w-full flex-row items-center justify-evenly overflow-hidden rounded" className="h-full w-full flex-row items-center justify-evenly"
> >
<View className="absolute inset-0 bg-linear-to-b from-transparent to-slate-950/70" /> <View className="absolute inset-0 bg-linear-to-b from-transparent to-slate-950/70" />
<View className="w-1/2 lg:w-1/3"> <View className="w-1/2 lg:w-1/3">
@ -111,7 +112,7 @@ export const ItemList = ({
ItemList.Loader = (props: object) => { ItemList.Loader = (props: object) => {
return ( return (
<View <View
className="h-80 w-full flex-row items-center justify-evenly overflow-hidden rounded bg-slate-800" className="h-80 flex-row items-center justify-evenly overflow-hidden rounded bg-slate-800"
{...props} {...props}
> >
<View className="w-1/2 justify-center lg:w-1/3"> <View className="w-1/2 justify-center lg:w-1/3">

View File

@ -67,6 +67,8 @@ export const InfiniteFetch = <Data, Type extends string = string>({
return isFetching ? [...items, ...placeholders] : items; return isFetching ? [...items, ...placeholders] : items;
}, [items, isFetching, placeholderCount, numColumns]); }, [items, isFetching, placeholderCount, numColumns]);
if (!data.length && Empty) return Empty;
return ( return (
<AnimatedLegendList <AnimatedLegendList
data={data} data={data}
@ -97,7 +99,6 @@ export const InfiniteFetch = <Data, Type extends string = string>({
ItemSeparatorComponent={ ItemSeparatorComponent={
Divider === true ? HR : (Divider as any) || undefined Divider === true ? HR : (Divider as any) || undefined
} }
ListEmptyComponent={Empty}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ contentContainerStyle={{

View File

@ -166,7 +166,6 @@ export const TitleLine = ({
> >
<Poster <Poster
src={poster} src={poster}
alt={name}
quality="medium" quality="medium"
className="w-1/2 shrink-0 max-sm:max-w-44 md:w-1/4" className="w-1/2 shrink-0 max-sm:max-w-44 md:w-1/4"
/> />

View File

@ -10,7 +10,7 @@ export const EmptyView = ({
className?: string; className?: string;
}) => { }) => {
return ( return (
<View className={cn("flex-1 items-center justify-center", className)}> <View className={cn("flex-1 items-center justify-center py-20", className)}>
<P>{message}</P> <P>{message}</P>
</View> </View>
); );

View File

@ -1,28 +1,12 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { ItemGrid, itemMap } from "~/components/items"; import { ItemGrid, itemMap } from "~/components/items";
import { type Genre, Show } from "~/models"; import { type Genre, Show } from "~/models";
import { H3, ts } from "~/primitives"; import { H3 } from "~/primitives";
import { InfiniteFetch, type QueryIdentifier } from "~/query"; import { InfiniteFetch, type QueryIdentifier } from "~/query";
import { EmptyView } from "~/ui/empty-view"; import { EmptyView } from "~/ui/empty-view";
export const Header = ({ title }: { title: string }) => { export const Header = ({ title }: { title: string }) => {
const { css } = useYoshiki(); return <H3 className="m-2 flex-row justify-between px-1">{title}</H3>;
return (
<View
{...css({
marginTop: ItemGrid.layout.gap,
marginX: ItemGrid.layout.gap,
pX: ts(0.5),
flexDirection: "row",
justifyContent: "space-between",
})}
>
<H3>{title}</H3>
</View>
);
}; };
export const GenreGrid = ({ genre }: { genre: Genre }) => { export const GenreGrid = ({ genre }: { genre: Genre }) => {
@ -34,10 +18,9 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
<InfiniteFetch <InfiniteFetch
query={GenreGrid.query(genre)} query={GenreGrid.query(genre)}
layout={{ ...ItemGrid.layout, layout: "horizontal" }} layout={{ ...ItemGrid.layout, layout: "horizontal" }}
placeholderCount={2}
Empty={<EmptyView message={t("home.none")} />} Empty={<EmptyView message={t("home.none")} />}
Render={({ item }) => <ItemGrid {...itemMap(item)} horizontal />} Render={({ item }) => <ItemGrid {...itemMap(item)} horizontal />}
Loader={ItemGrid.Loader} Loader={() => <ItemGrid.Loader horizontal />}
/> />
</> </>
); );

View File

@ -1,13 +1,10 @@
import Info from "@material-symbols/svg-400/rounded/info.svg"; import Info from "@material-symbols/svg-400/rounded/info.svg";
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 { LinearGradient } from "expo-linear-gradient";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { min, percent, px, rem, vh } from "yoshiki/native";
import { type KImage, Show } from "~/models"; import { type KImage, Show } from "~/models";
import { import {
ContrastArea,
H1, H1,
H2, H2,
IconButton, IconButton,
@ -17,7 +14,6 @@ import {
P, P,
Skeleton, Skeleton,
tooltip, tooltip,
ts,
} from "~/primitives"; } from "~/primitives";
import type { QueryIdentifier } from "~/query"; import type { QueryIdentifier } from "~/query";
import { cn } from "~/utils"; import { cn } from "~/utils";
@ -93,72 +89,35 @@ Header.Loader = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<ContrastArea mode="dark"> <View
{({ css, theme }) => ( className={cn(
<View "h-[40vh] w-full sm:h-[60vh] sm:min-h-[750px] md:min-h-[680px] lg:h-[65vh]",
{...css({
flexDirection: "column-reverse",
width: percent(100),
height: {
xs: vh(40),
sm: min(vh(60), px(750)),
md: min(vh(60), px(680)),
lg: vh(65),
},
minHeight: {
xs: px(350),
sm: px(300),
md: px(400),
lg: px(600),
},
})}
>
<LinearGradient
start={{ x: 0, y: 0.25 }}
end={{ x: 0, y: 1 }}
colors={["transparent", theme.darkOverlay]}
{...(css({
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0,
}) as any)}
/>
<View {...css({ margin: ts(2) })}>
<Skeleton {...css({ width: rem(8), height: rem(2.5) })} />
<View {...css({ flexDirection: "row", alignItems: "center" })}>
<IconFab
icon={PlayArrow}
aria-label={t("show.play")}
{...tooltip(t("show.play"))}
{...css({ marginRight: ts(1) })}
/>
<IconButton
icon={Info}
aria-label={t("home.info")}
{...tooltip(t("home.info"))}
{...css({ marginRight: ts(2) })}
/>
<Skeleton
{...css({
width: rem(25),
height: rem(2),
display: { xs: "none", sm: "flex" },
})}
/>
</View>
<Skeleton
lines={4}
{...css({
display: { xs: "none", md: "flex" },
marginTop: ts(1),
})}
/>
</View>
</View>
)} )}
</ContrastArea> >
<View className="absolute inset-0 bg-linear-to-b from-transparent to-slate-950/70" />
<View className="absolute bottom-0 m-4 md:w-3/5">
<Skeleton className="h-10 w-2/5" />
<View className="my-2 flex-row items-center">
<IconFab
icon={PlayArrow}
disabled
aria-label={t("show.play")}
className="mr-2"
{...tooltip(t("show.play"))}
/>
<IconButton
icon={Info}
disabled
aria-label={t("home.info")}
className="mr-2"
iconClassName="fill-slate-400"
{...tooltip(t("home.info"))}
/>
<Skeleton className="h-8 w-4/5 max-sm:hidden" />
</View>
<Skeleton lines={4} className="max-sm:hidden" />
</View>
</View>
); );
}; };

View File

@ -1,6 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { EntryBox, entryDisplayNumber } from "~/components/entries"; import { EntryBox, entryDisplayNumber } from "~/components/entries";
import { ItemGrid } from "~/components/items";
import { Entry } from "~/models"; import { Entry } from "~/models";
import { InfiniteFetch, type QueryIdentifier } from "~/query"; import { InfiniteFetch, type QueryIdentifier } from "~/query";
import { EmptyView } from "~/ui/empty-view"; import { EmptyView } from "~/ui/empty-view";
@ -15,15 +14,11 @@ export const NewsList = () => {
<InfiniteFetch <InfiniteFetch
query={NewsList.query()} query={NewsList.query()}
layout={{ ...EntryBox.layout, layout: "horizontal" }} layout={{ ...EntryBox.layout, layout: "horizontal" }}
// getItemType={(x, i) =>
// x?.kind === "movie" || (!x && i % 2) ? "movie" : "episode"
// }
// getItemSizeMult={(_, __, kind) => (kind === "episode" ? 2 : 1)}
Empty={<EmptyView message={t("home.none")} />} Empty={<EmptyView message={t("home.none")} />}
Render={({ item }) => { Render={({ item }) => {
// if (item.kind === "episode" || item.kind === "special") {
return ( return (
<EntryBox <EntryBox
kind={item.kind}
slug={item.slug} slug={item.slug}
serieSlug={item.show!.slug} serieSlug={item.show!.slug}
name={`${item.show!.name} ${entryDisplayNumber(item)}`} name={`${item.show!.name} ${entryDisplayNumber(item)}`}
@ -33,28 +28,8 @@ export const NewsList = () => {
watchedPercent={item.progress.percent} watchedPercent={item.progress.percent}
/> />
); );
// }
// return (
// <ItemGrid
// href={item.href ?? "#"}
// slug={item.slug}
// kind={"movie"}
// name={item.name!}
// subtitle={
// item.airDate
// ? new Date(item.airDate).getFullYear().toString()
// : null
// }
// poster={item.kind === "movie" ? item.poster : null}
// watchStatus={item.watchStatus?.status || null}
// watchPercent={item.watchStatus?.percent || null}
// unseenEpisodesCount={null}
// />
// );
}} }}
Loader={({ index }) => Loader={EntryBox.Loader}
index % 2 ? <EntryBox.Loader /> : <ItemGrid.Loader />
}
/> />
</> </>
); );

View File

@ -1,59 +1,67 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { ItemGrid } from "~/components/items";
import { ItemDetails } from "~/components/items/item-details"; import { ItemDetails } from "~/components/items/item-details";
import { Show } from "~/models"; import { Show } from "~/models";
import { H3 } from "~/primitives"; import { useBreakpointMap } from "~/primitives";
import { InfiniteFetch, type QueryIdentifier } from "~/query"; import { type QueryIdentifier, useInfiniteFetch } from "~/query";
import { getDisplayDate } from "~/utils"; import { getDisplayDate } from "~/utils";
import { Header } from "./genre";
const itemCount = 6;
export const Recommended = () => { export const Recommended = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { css } = useYoshiki(); const { numColumns, gap } = useBreakpointMap(ItemDetails.layout);
const { items } = useInfiniteFetch(Recommended.query());
return ( return (
<View <View>
{...css({ marginX: ItemGrid.layout.gap, marginTop: ItemGrid.layout.gap })} <Header title={t("home.recommended")} />
> <View className="flex-1 flex-row" style={{ gap, margin: gap }}>
<H3 className="px-1">{t("home.recommended")}</H3> {[...Array(numColumns)].map((_, x) => (
<InfiniteFetch <View key={x} className="flex-1" style={{ gap }}>
query={Recommended.query()} {[...Array(itemCount / numColumns)].map((_, y) => {
layout={ItemDetails.layout} if (!items) return <ItemDetails.Loader key={y} />;
placeholderCount={6} const item = items[x * (itemCount / numColumns) + y];
fetchMore={false} return (
contentContainerStyle={{ marginHorizontal: 0 }} <ItemDetails
Render={({ item }) => ( key={y}
<ItemDetails slug={item.slug}
slug={item.slug} kind={item.kind}
kind={item.kind} name={item.name}
name={item.name} tagline={
tagline={ item.kind !== "collection" && "tagline" in item
item.kind !== "collection" && "tagline" in item ? item.tagline
? item.tagline : null
: null }
} description={item.description}
description={item.description} poster={item.poster}
poster={item.poster} subtitle={
subtitle={item.kind !== "collection" ? getDisplayDate(item) : null} item.kind !== "collection" ? getDisplayDate(item) : null
genres={ }
item.kind !== "collection" && "genres" in item genres={
? item.genres item.kind !== "collection" && "genres" in item
: null ? item.genres
} : null
href={item.href} }
playHref={item.kind !== "collection" ? item.playHref : null} href={item.href}
watchStatus={ playHref={item.kind !== "collection" ? item.playHref : null}
(item.kind !== "collection" && item.watchStatus?.status) || null watchStatus={
} (item.kind !== "collection" && item.watchStatus?.status) ||
availableCount={item.kind === "serie" ? item.availableCount : null} null
seenCount={ }
item.kind === "serie" ? item.watchStatus?.seenCount : null availableCount={
} item.kind === "serie" ? item.availableCount : null
/> }
)} seenCount={
Loader={ItemDetails.Loader} item.kind === "serie" ? item.watchStatus?.seenCount : null
/> }
/>
);
})}
</View>
))}
</View>
</View> </View>
); );
}; };
@ -64,7 +72,7 @@ Recommended.query = (): QueryIdentifier<Show> => ({
path: ["api", "shows"], path: ["api", "shows"],
params: { params: {
sort: "random", sort: "random",
limit: 6, limit: itemCount,
with: ["firstEntry"], with: ["firstEntry"],
}, },
}); });

View File

@ -1,26 +1,22 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { ItemList, itemMap } from "~/components/items";
import { ItemGrid, ItemList, itemMap } from "~/components/items";
import { Show } from "~/models"; import { Show } from "~/models";
import { H3 } from "~/primitives"; import { type QueryIdentifier, useInfiniteFetch } from "~/query";
import { InfiniteFetch, type QueryIdentifier } from "~/query"; import { Header } from "./genre";
export const VerticalRecommended = () => { export const VerticalRecommended = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { css } = useYoshiki(); const { items } = useInfiniteFetch(VerticalRecommended.query());
return ( return (
<View {...css({ marginY: ItemGrid.layout.gap })}> <View>
<H3 {...css({ mX: ItemGrid.layout.gap })}>{t("home.recommended")}</H3> <Header title={t("home.recommended")} />
<InfiniteFetch <View className="mx-2 flex-1 gap-2">
query={VerticalRecommended.query()} {items
placeholderCount={3} ? items.map((x) => <ItemList key={x.slug} {...itemMap(x)} />)
layout={{ ...ItemList.layout, layout: "vertical" }} : [...Array(3)].map((_, i) => <ItemList.Loader key={i} />)}
fetchMore={false} </View>
Render={({ item }) => <ItemList {...itemMap(item)} />}
Loader={() => <ItemList.Loader />}
/>
</View> </View>
); );
}; };

View File

@ -1,32 +1,29 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { EntryBox, entryDisplayNumber } from "~/components/entries"; import { EntryBox, entryDisplayNumber } from "~/components/entries";
import { ItemGrid } from "~/components/items"; import { ItemGrid, itemMap } from "~/components/items";
import { Show } from "~/models"; import { Show } from "~/models";
import { Button, Link, P, ts } from "~/primitives"; import { Button, Link, P } from "~/primitives";
import { useAccount } from "~/providers/account-context"; import { useAccount } from "~/providers/account-context";
import { InfiniteFetch, type QueryIdentifier } from "~/query"; import { InfiniteFetch, type QueryIdentifier } from "~/query";
import { EmptyView } from "~/ui/empty-view"; import { EmptyView } from "~/ui/empty-view";
import { getDisplayDate } from "~/utils";
import { Header } from "./genre"; import { Header } from "./genre";
export const WatchlistList = () => { export const WatchlistList = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { css } = useYoshiki();
const account = useAccount(); const account = useAccount();
if (!account) { if (!account) {
return ( return (
<> <>
<Header title={t("home.watchlist")} /> <Header title={t("home.watchlist")} />
<View {...css({ justifyContent: "center", alignItems: "center" })}> <View className="items-center justify-center">
<P>{t("home.watchlistLogin")}</P> <P>{t("home.watchlistLogin")}</P>
<Button <Button
as={Link} as={Link}
href={"/login"} href={"/login"}
text={t("login.login")} text={t("login.login")}
{...css({ minWidth: ts(24), margin: ts(2) })} className="m-4 min-w-md"
/> />
</View> </View>
</> </>
@ -51,6 +48,7 @@ export const WatchlistList = () => {
if (entry) { if (entry) {
return ( return (
<EntryBox <EntryBox
kind={entry.kind}
slug={entry.slug} slug={entry.slug}
serieSlug={item.slug} serieSlug={item.slug}
name={`${item.name} ${entryDisplayNumber(entry)}`} name={`${item.name} ${entryDisplayNumber(entry)}`}
@ -61,31 +59,10 @@ export const WatchlistList = () => {
/> />
); );
} }
return ( return <ItemGrid {...itemMap(item)} horizontal />;
<ItemGrid
href={item.href}
slug={item.slug}
kind={item.kind}
name={item.name!}
subtitle={getDisplayDate(item)}
poster={item.poster}
watchStatus={
item.kind !== "collection"
? (item.watchStatus?.status ?? null)
: null
}
watchPercent={
item.kind === "movie" && item.watchStatus
? item.watchStatus.percent
: null
}
unseenEpisodesCount={null}
horizontal
/>
);
}} }}
Loader={({ index }) => Loader={({ index }) =>
index % 2 ? <EntryBox.Loader /> : <ItemGrid.Loader /> index % 2 ? <EntryBox.Loader /> : <ItemGrid.Loader horizontal />
} }
/> />
</> </>

View File

@ -1,6 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { VideoInfo } from "~/models"; import { VideoInfo } from "~/models";
import { HR, P, Skeleton } from "~/primitives"; import { HR, P, Skeleton } from "~/primitives";
import { type QueryIdentifier, useFetch } from "~/query"; import { type QueryIdentifier, useFetch } from "~/query";
@ -10,23 +9,19 @@ import { useQueryState } from "~/utils";
const formatBitrate = (b: number) => `${(b / 1000000).toFixed(2)} Mbps`; const formatBitrate = (b: number) => `${(b / 1000000).toFixed(2)} Mbps`;
const Row = ({ label, value }: { label: string; value: string }) => { const Row = ({ label, value }: { label: string; value: string }) => {
const { css } = useYoshiki();
return ( return (
<View {...css({ flexDirection: "row" })}> <View className="flex-row">
<P {...css({ flex: 1 })}>{label}</P> <P className="flex-1">{label}</P>
<P {...css({ flex: 3 })}>{value}</P> <P className="flex-3">{value}</P>
</View> </View>
); );
}; };
Row.Loading = ({ label }: { label: string }) => { Row.Loading = ({ label }: { label: string }) => {
const { css } = useYoshiki();
return ( return (
<View {...css({ flexDirection: "row" })}> <View className="flex-row">
<P {...css({ flex: 1 })}>{label}</P> <P className="flex-1">{label}</P>
<Skeleton {...css({ flex: 3 })} /> <Skeleton className="flex-3" />
</View> </View>
); );
}; };

View File

@ -6,16 +6,10 @@ import Logout from "@material-symbols/svg-400/rounded/logout.svg";
import Search from "@material-symbols/svg-400/rounded/search-fill.svg"; import Search from "@material-symbols/svg-400/rounded/search-fill.svg";
import Settings from "@material-symbols/svg-400/rounded/settings.svg"; import Settings from "@material-symbols/svg-400/rounded/settings.svg";
import { useIsFocused } from "@react-navigation/native"; import { useIsFocused } from "@react-navigation/native";
import { import { useNavigation, usePathname, useRouter } from "expo-router";
useGlobalSearchParams,
useNavigation,
usePathname,
useRouter,
} from "expo-router";
import KyooLongLogo from "public/icon-long.svg"; import KyooLongLogo from "public/icon-long.svg";
import { import {
type ComponentProps, type ComponentProps,
type Ref,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useRef, useRef,
@ -24,9 +18,8 @@ import {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Platform, Platform,
TextInput,
type PressableProps, type PressableProps,
type TextInputProps, TextInput,
View, View,
type ViewProps, type ViewProps,
} from "react-native"; } from "react-native";
@ -43,7 +36,6 @@ import {
Avatar, Avatar,
HR, HR,
IconButton, IconButton,
Input,
Link, Link,
Menu, Menu,
PressableFeedback, PressableFeedback,
@ -146,9 +138,10 @@ const SearchBar = () => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const path = usePathname(); const path = usePathname();
const shouldExpand = useRef(false);
useEffect(() => { useEffect(() => {
console.log(path); if (path === "/browse" && shouldExpand.current) {
if (path === "/browse") { shouldExpand.current = false;
// Small delay to allow animation to start before focusing // Small delay to allow animation to start before focusing
setTimeout(() => { setTimeout(() => {
setExpanded(true); setExpanded(true);
@ -207,6 +200,7 @@ const SearchBar = () => {
setQuery(""); setQuery("");
router.setParams({ q: undefined }); router.setParams({ q: undefined });
} else { } else {
shouldExpand.current = true;
setExpanded(true); setExpanded(true);
// Small delay to allow animation to start before focusing // Small delay to allow animation to start before focusing
setTimeout(() => inputRef.current?.focus(), 100); setTimeout(() => inputRef.current?.focus(), 100);