mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-04-07 17:51:55 -04:00
Fix home page horizontal bars
This commit is contained in:
parent
30223dfa4d
commit
082f3283f5
@ -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=="],
|
||||||
|
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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={{
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 />}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 />
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user