Fix browse layout switch

This commit is contained in:
Zoe Roux 2025-06-22 20:20:19 +02:00
parent 0ad9c86756
commit 552926d2cb
No known key found for this signature in database
7 changed files with 155 additions and 128 deletions

View File

@ -187,7 +187,7 @@ ItemGrid.Loader = (props: object) => {
}; };
ItemGrid.layout = { ItemGrid.layout = {
size: px(150), size: 150,
numColumns: { xs: 3, sm: 4, md: 5, lg: 6, xl: 8 }, numColumns: { xs: 3, sm: 4, md: 5, lg: 6, xl: 8 },
gap: { xs: ts(1), sm: ts(2), md: ts(4) }, gap: { xs: ts(1), sm: ts(2), md: ts(4) },
layout: "grid", layout: "grid",

View File

@ -3,6 +3,7 @@ import { Platform, View } from "react-native";
import { percent, px, rem, useYoshiki } from "yoshiki/native"; import { percent, px, rem, useYoshiki } from "yoshiki/native";
import type { KImage, WatchStatusV } from "~/models"; import type { KImage, WatchStatusV } from "~/models";
import { import {
ContrastArea,
GradientImageBackground, GradientImageBackground,
Heading, Heading,
important, important,
@ -39,114 +40,117 @@ export const ItemList = ({
watchStatus: WatchStatusV | null; watchStatus: WatchStatusV | null;
unseenEpisodesCount: number | null; unseenEpisodesCount: number | null;
}) => { }) => {
const { css } = useYoshiki("line");
const [moreOpened, setMoreOpened] = useState(false); const [moreOpened, setMoreOpened] = useState(false);
return ( return (
<Link <ContrastArea>
href={moreOpened ? undefined : href} {({ css }) => (
onLongPress={() => setMoreOpened(true)} <Link
{...css({ href={moreOpened ? undefined : href}
child: { onLongPress={() => setMoreOpened(true)}
more: {
opacity: 0,
},
},
fover: {
title: {
textDecorationLine: "underline",
},
more: {
opacity: 100,
},
},
})}
>
<GradientImageBackground
src={thumbnail}
alt={name}
quality="medium"
layout={{ width: percent(100), height: ItemList.layout.size }}
{...(css(
{
alignItems: "center",
justifyContent: "space-evenly",
flexDirection: "row",
borderRadius: px(10),
overflow: "hidden",
},
props,
) as any)}
>
<View
{...css({ {...css({
width: { xs: "50%", lg: "30%" }, child: {
more: {
opacity: 0,
},
},
fover: {
title: {
textDecorationLine: "underline",
},
more: {
opacity: 100,
},
},
})} })}
{...props}
> >
<View <GradientImageBackground
{...css({ src={thumbnail}
alt={name}
quality="medium"
layout={{ width: percent(100), height: ItemList.layout.size }}
gradientStyle={{
alignItems: "center",
justifyContent: "space-evenly",
flexDirection: "row", flexDirection: "row",
justifyContent: "center", }}
})} {...(css({
borderRadius: px(10),
overflow: "hidden",
}) as any)}
> >
<Heading <View
{...css([
"title",
{
textAlign: "center",
fontSize: rem(2),
letterSpacing: rem(0.002),
fontWeight: "900",
textTransform: "uppercase",
},
])}
>
{name}
</Heading>
{kind !== "collection" && (
<ItemContext
kind={kind}
slug={slug}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
{...css([
{
// I dont know why marginLeft gets overwritten by the margin: px(2) so we important
marginLeft: important(ts(2)),
bg: (theme) => theme.darkOverlay,
},
"more",
Platform.OS === "web" &&
moreOpened && { opacity: important(100) },
])}
/>
)}
</View>
{subtitle && (
<P
{...css({ {...css({
textAlign: "center", width: { xs: "50%", lg: "30%" },
marginRight: ts(4),
})} })}
> >
{subtitle} <View
</P> {...css({
)} flexDirection: "row",
</View> justifyContent: "center",
<PosterBackground })}
src={poster} >
alt="" <Heading
quality="low" {...css([
layout={{ height: percent(80) }} "title",
> {
<ItemWatchStatus textAlign: "center",
watchStatus={watchStatus} fontSize: rem(2),
unseenEpisodesCount={unseenEpisodesCount} letterSpacing: rem(0.002),
/> fontWeight: "900",
</PosterBackground> textTransform: "uppercase",
</GradientImageBackground> },
</Link> ])}
>
{name}
</Heading>
{kind !== "collection" && (
<ItemContext
kind={kind}
slug={slug}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
{...css([
{
// I dont know why marginLeft gets overwritten by the margin: px(2) so we important
marginLeft: important(ts(2)),
bg: (theme) => theme.darkOverlay,
},
"more",
Platform.OS === "web" &&
moreOpened && { opacity: important(100) },
])}
/>
)}
</View>
{subtitle && (
<P
{...css({
textAlign: "center",
marginRight: ts(4),
})}
>
{subtitle}
</P>
)}
</View>
<PosterBackground
src={poster}
alt=""
quality="low"
layout={{ height: percent(80) }}
>
<ItemWatchStatus
watchStatus={watchStatus}
unseenEpisodesCount={unseenEpisodesCount}
/>
</PosterBackground>
</GradientImageBackground>
</Link>
)}
</ContrastArea>
); );
}; };

View File

@ -52,7 +52,9 @@ export const PosterBackground = ({
...props ...props
}: Omit<ComponentProps<typeof ImageBackground>, "layout"> & { }: Omit<ComponentProps<typeof ImageBackground>, "layout"> & {
style?: ImageStyle; style?: ImageStyle;
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; layout: YoshikiEnhanced<
{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }
>;
}) => { }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
@ -67,10 +69,12 @@ export const PosterBackground = ({
export const GradientImageBackground = ({ export const GradientImageBackground = ({
gradient, gradient,
gradientStyle,
children, children,
...props ...props
}: ComponentProps<typeof ImageBackground> & { }: ComponentProps<typeof ImageBackground> & {
gradient?: Partial<LinearGradientProps>; gradient?: Partial<LinearGradientProps>;
gradientStyle?: Parameters<ReturnType<typeof useYoshiki>["css"]>[0];
}) => { }) => {
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
@ -81,13 +85,16 @@ export const GradientImageBackground = ({
end={{ x: 0, y: 1 }} end={{ x: 0, y: 1 }}
colors={["transparent", theme.darkOverlay]} colors={["transparent", theme.darkOverlay]}
{...css( {...css(
{ [
position: "absolute", {
top: 0, position: "absolute",
bottom: 0, top: 0,
left: 0, bottom: 0,
right: 0, left: 0,
}, right: 0,
},
gradientStyle,
],
typeof gradient === "object" ? gradient : undefined, typeof gradient === "object" ? gradient : undefined,
)} )}
> >

View File

@ -1,5 +1,5 @@
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { type ReactNode, forwardRef } from "react"; import type { ReactNode } from "react";
import { import {
Linking, Linking,
Platform, Platform,
@ -7,7 +7,6 @@ import {
type PressableProps, type PressableProps,
Text, Text,
type TextProps, type TextProps,
type View,
} from "react-native"; } from "react-native";
import { useTheme, useYoshiki } from "yoshiki/native"; import { useTheme, useYoshiki } from "yoshiki/native";
import { alpha } from "./theme"; import { alpha } from "./theme";
@ -26,7 +25,9 @@ function useLinkTo({
onPress: (e) => { onPress: (e) => {
if (e?.defaultPrevented) return; if (e?.defaultPrevented) return;
if (href.startsWith("http")) { if (href.startsWith("http")) {
Platform.OS === "web" ? window.open(href, "_blank") : Linking.openURL(href); Platform.OS === "web"
? window.open(href, "_blank")
: Linking.openURL(href);
} else { } else {
replace ? router.replace(href) : router.push(href); replace ? router.replace(href) : router.push(href);
} }
@ -65,25 +66,26 @@ export const A = ({
); );
}; };
export const PressableFeedback = forwardRef<View, PressableProps>(function Feedback( export const PressableFeedback = ({ children, ...props }: PressableProps) => {
{ children, ...props },
ref,
) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Pressable <Pressable
ref={ref}
// TODO: Enable ripple on tv. Waiting for https://github.com/react-native-tvos/react-native-tvos/issues/440 // TODO: Enable ripple on tv. Waiting for https://github.com/react-native-tvos/react-native-tvos/issues/440
{...(Platform.isTV {...(Platform.isTV
? {} ? {}
: { android_ripple: { foreground: true, color: alpha(theme.contrast, 0.5) as any } })} : {
android_ripple: {
foreground: true,
color: alpha(theme.contrast, 0.5) as any,
},
})}
{...props} {...props}
> >
{children} {children}
</Pressable> </Pressable>
); );
}); };
export const Link = ({ export const Link = ({
href, href,

View File

@ -48,11 +48,19 @@ export const InfiniteFetch = <Data, Props, _, Kind extends number | string>({
const { numColumns, size, gap } = useBreakpointMap(layout); const { numColumns, size, gap } = useBreakpointMap(layout);
const [setOffline, clearOffline] = useSetError("offline"); const [setOffline, clearOffline] = useSetError("offline");
const oldItems = useRef<Data[] | undefined>(undefined); const oldItems = useRef<Data[] | undefined>(undefined);
let { items, isPaused, error, fetchNextPage, isFetching, refetch, isRefetching } = let {
useInfiniteFetch(query); items,
isPaused,
error,
fetchNextPage,
isFetching,
refetch,
isRefetching,
} = useInfiniteFetch(query);
if (incremental && items) oldItems.current = items; if (incremental && items) oldItems.current = items;
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch."); if (!query.infinite)
console.warn("A non infinite query was passed to an InfiniteFetch.");
if (isPaused) setOffline(); if (isPaused) setOffline();
else clearOffline(); else clearOffline();
@ -60,10 +68,13 @@ export const InfiniteFetch = <Data, Props, _, Kind extends number | string>({
if (error) return <ErrorView error={error} />; if (error) return <ErrorView error={error} />;
if (incremental) items ??= oldItems.current; if (incremental) items ??= oldItems.current;
const count = items ? numColumns - (items.length % numColumns) : placeholderCount; const count = items
console.log(numColumns, count); ? numColumns - (items.length % numColumns)
: placeholderCount;
const placeholders = [...Array(count === 0 ? numColumns : count)].fill(null); const placeholders = [...Array(count === 0 ? numColumns : count)].fill(null);
const data = isFetching || !items ? [...(items || []), ...placeholders] : items; const data =
isFetching || !items ? [...(items || []), ...placeholders] : items;
return ( return (
<LegendList <LegendList
data={data} data={data}
@ -71,9 +82,8 @@ export const InfiniteFetch = <Data, Props, _, Kind extends number | string>({
renderItem={({ item, index }) => renderItem={({ item, index }) =>
item ? <Render index={index} item={item} /> : <Loader index={index} /> item ? <Render index={index} item={item} /> : <Loader index={index} />
} }
// keyExtractor={(item: any, index) => (item ? item.id : index)} keyExtractor={(item: any, index) => (item ? item.id : index)}
// estimatedItemSize={size} estimatedItemSize={size}
horizontal={layout.layout === "horizontal"} horizontal={layout.layout === "horizontal"}
numColumns={layout.layout === "horizontal" ? 1 : numColumns} numColumns={layout.layout === "horizontal" ? 1 : numColumns}
onEndReached={fetchMore ? () => fetchNextPage() : undefined} onEndReached={fetchMore ? () => fetchNextPage() : undefined}
@ -81,7 +91,9 @@ export const InfiniteFetch = <Data, Props, _, Kind extends number | string>({
onRefresh={layout.layout !== "horizontal" ? refetch : undefined} onRefresh={layout.layout !== "horizontal" ? refetch : undefined}
refreshing={isRefetching} refreshing={isRefetching}
ListHeaderComponent={Header} ListHeaderComponent={Header}
ItemSeparatorComponent={divider === true ? HR : (divider as any) || undefined} ItemSeparatorComponent={
divider === true ? HR : (divider as any) || undefined
}
ListEmptyComponent={Empty} ListEmptyComponent={Empty}
contentContainerStyle={{ gap, marginHorizontal: gap }} contentContainerStyle={{ gap, marginHorizontal: gap }}
{...props} {...props}

View File

@ -149,7 +149,9 @@ export const BrowseSettings = ({
{Object.keys(MediaTypeIcons).map((x) => ( {Object.keys(MediaTypeIcons).map((x) => (
<Menu.Item <Menu.Item
key={x} key={x}
label={t(`browse.mediatypekey.${x}`)} label={t(
`browse.mediatypekey.${x as keyof typeof MediaTypeIcons}`,
)}
selected={mediaType === x} selected={mediaType === x}
icon={MediaTypeIcons[x as keyof typeof MediaTypeIcons]} icon={MediaTypeIcons[x as keyof typeof MediaTypeIcons]}
onSelect={() => setMediaType(x)} onSelect={() => setMediaType(x)}

View File

@ -1,4 +1,3 @@
import { useState } from "react";
import { ItemGrid, ItemList, itemMap } from "~/components/items"; import { ItemGrid, ItemList, itemMap } from "~/components/items";
import { Show } from "~/models"; import { Show } from "~/models";
import { InfiniteFetch, type QueryIdentifier } from "~/query"; import { InfiniteFetch, type QueryIdentifier } from "~/query";
@ -17,6 +16,7 @@ export const BrowsePage = () => {
return ( return (
<InfiniteFetch <InfiniteFetch
key={layout}
query={BrowsePage.query(filter, sortBy, sortOrd)} query={BrowsePage.query(filter, sortBy, sortOrd)}
layout={LayoutComponent.layout} layout={LayoutComponent.layout}
Header={ Header={