Type error cleanups

This commit is contained in:
Zoe Roux 2026-02-03 23:17:17 +01:00
parent e1d1eb3bef
commit 3bfadc673e
No known key found for this signature in database
15 changed files with 172 additions and 332 deletions

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import type { KImage, WatchStatusV } from "~/models";
import type { KImage } from "~/models";
import {
Image,
ImageBackground,
@ -24,7 +24,6 @@ export const EntryBox = ({
thumbnail,
href,
watchedPercent,
watchedStatus,
className,
...props
}: {
@ -35,8 +34,7 @@ export const EntryBox = ({
description: string | null;
href: string;
thumbnail: KImage | null;
watchedPercent: number | null;
watchedStatus: WatchStatusV | null;
watchedPercent: number;
className?: string;
}) => {
const [moreOpened, setMoreOpened] = useState(false);
@ -58,9 +56,7 @@ export const EntryBox = ({
"ring-accent group-hover:ring-3 group-focus-visible:ring-3",
)}
>
{(watchedPercent || watchedStatus === "completed") && (
<ItemProgress watchPercent={watchedPercent ?? 100} />
)}
<ItemProgress watchPercent={watchedPercent} />
<EntryContext
slug={slug}
serieSlug={serieSlug}

View File

@ -18,8 +18,8 @@ export const itemMap = (
item.kind !== "collection" ? (item.watchStatus?.status ?? null) : null,
watchPercent:
item.kind === "movie" ? (item.watchStatus?.percent ?? null) : null,
unseenEpisodesCount: 0,
// item.kind === "serie" ? (item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!) : null,
availableCount: item.kind === "serie" ? item.availableCount : null,
seenCount: item.kind === "serie" ? item.watchStatus?.seenCount : null,
});
export { ItemGrid, ItemList };

View File

@ -16,6 +16,7 @@ import { ItemContext } from "./context-menus";
import { ItemWatchStatus } from "./item-helpers";
export const ItemProgress = ({ watchPercent }: { watchPercent: number }) => {
if (!watchPercent) return null;
return (
<>
<View className="absolute bottom-0 h-1 w-full bg-slate-400" />
@ -36,7 +37,8 @@ export const ItemGrid = ({
poster,
watchStatus,
watchPercent,
unseenEpisodesCount,
availableCount,
seenCount,
horizontal = false,
className,
...props
@ -49,8 +51,9 @@ export const ItemGrid = ({
watchStatus: WatchStatusV | null;
watchPercent: number | null;
kind: "movie" | "serie" | "collection";
unseenEpisodesCount: number | null;
horizontal: boolean;
availableCount?: number | null;
seenCount?: number | null;
horizontal?: boolean;
className?: string;
}) => {
const [moreOpened, setMoreOpened] = useState(false);
@ -77,7 +80,8 @@ export const ItemGrid = ({
>
<ItemWatchStatus
watchStatus={watchStatus}
unseenEpisodesCount={unseenEpisodesCount}
availableCount={availableCount}
seenCount={seenCount}
/>
{kind === "movie" && watchPercent && (
<ItemProgress watchPercent={watchPercent} />

View File

@ -1,51 +1,30 @@
import Done from "@material-symbols/svg-400/rounded/check-fill.svg";
import { View } from "react-native";
import { max, rem, useYoshiki } from "yoshiki/native";
import type { WatchStatusV } from "~/models";
import { Icon, P, ts } from "~/primitives";
import { Icon, P } from "~/primitives";
export const ItemWatchStatus = ({
watchStatus,
unseenEpisodesCount,
availableCount,
seenCount,
...props
}: {
watchStatus?: WatchStatusV | null;
unseenEpisodesCount?: number | null;
availableCount?: number | null;
seenCount?: number | null;
}) => {
const { css } = useYoshiki();
if (watchStatus !== "completed" && !unseenEpisodesCount) return null;
if (watchStatus !== "completed" && !availableCount) return null;
return (
<View
{...css(
{
position: "absolute",
top: 0,
right: 0,
minWidth: max(rem(1), ts(3.5)),
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
m: ts(0.5),
pX: ts(0.5),
bg: (theme) => theme.darkOverlay,
borderRadius: 999999,
},
props,
)}
className="absolute top-0 right-0 m-1 aspect-square min-w-8 items-center justify-center rounded-full bg-gray-800/70 p-1"
{...props}
>
{watchStatus === "completed" ? (
<Icon icon={Done} size={16} />
<Icon icon={Done} />
) : (
<P
{...css({
marginVertical: 0,
verticalAlign: "middle",
textAlign: "center",
})}
>
{unseenEpisodesCount}
<P className="text-center">
{seenCount ?? 0}/{availableCount}
</P>
)}
</View>

View File

@ -1,12 +1,9 @@
import { useState } from "react";
import { Platform, View } from "react-native";
import { percent, px, rem, useYoshiki } from "yoshiki/native";
import { View } from "react-native";
import type { KImage, WatchStatusV } from "~/models";
import {
ContrastArea,
GradientImageBackground,
Heading,
important,
ImageBackground,
Link,
P,
Poster,
@ -15,6 +12,7 @@ import {
ts,
} from "~/primitives";
import type { Layout } from "~/query";
import { cn } from "~/utils";
import { ItemContext } from "./context-menus";
import { ItemWatchStatus } from "./item-helpers";
@ -27,7 +25,9 @@ export const ItemList = ({
thumbnail,
poster,
watchStatus,
unseenEpisodesCount,
availableCount,
seenCount,
className,
...props
}: {
href: string;
@ -38,159 +38,92 @@ export const ItemList = ({
poster: KImage | null;
thumbnail: KImage | null;
watchStatus: WatchStatusV | null;
unseenEpisodesCount: number | null;
availableCount?: number | null;
seenCount?: number | null;
className?: string;
}) => {
const [moreOpened, setMoreOpened] = useState(false);
return (
<ContrastArea>
{({ css }) => (
<Link
href={moreOpened ? undefined : href}
onLongPress={() => setMoreOpened(true)}
{...css({
child: {
more: {
opacity: 0,
},
},
fover: {
title: {
textDecorationLine: "underline",
},
more: {
opacity: 100,
},
},
})}
{...props}
>
<GradientImageBackground
src={thumbnail}
alt={name}
quality="medium"
layout={{ width: percent(100), height: ItemList.layout.size }}
gradientStyle={{
alignItems: "center",
justifyContent: "space-evenly",
flexDirection: "row",
}}
{...(css({
borderRadius: px(10),
overflow: "hidden",
}) as any)}
>
<View
{...css({
width: { xs: "50%", lg: "30%" },
})}
>
<View
{...css({
flexDirection: "row",
justifyContent: "center",
})}
>
<Heading
{...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({
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>
<Link
href={moreOpened ? undefined : href}
onLongPress={() => setMoreOpened(true)}
className={cn(
"group h-80 w-full outline-0 ring-accent focus-within:ring-3 hover:ring-3",
className,
)}
</ContrastArea>
{...props}
>
<ImageBackground
src={thumbnail}
quality="medium"
className="h-full w-full flex-row items-center justify-evenly overflow-hidden rounded"
>
<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="flex-row justify-center">
<Heading
className={cn(
"text-center text-3xl uppercase",
"group-focus-within:underline group-hover:underline",
)}
>
{name}
</Heading>
{kind !== "collection" && (
<ItemContext
kind={kind}
slug={slug}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
className={cn(
"ml-4",
"bg-gray-800/70 hover:bg-gray-800 focus-visible:bg-gray-800",
"native:hidden opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100",
moreOpened && "opacity-100",
)}
iconClassName="fill-slate-200 dark:fill-slate-200"
/>
)}
</View>
{subtitle && <P className="mr-8 text-center">{subtitle}</P>}
</View>
<PosterBackground
src={poster}
alt=""
quality="low"
className="h-4/5 ring-accent group-focus-within:ring-4 group-hover:ring-4"
>
<ItemWatchStatus
watchStatus={watchStatus}
availableCount={availableCount}
seenCount={seenCount}
/>
</PosterBackground>
</ImageBackground>
</Link>
);
};
ItemList.Loader = (props: object) => {
const { css } = useYoshiki();
return (
<View
{...css(
{
alignItems: "center",
justifyContent: "space-evenly",
flexDirection: "row",
height: ItemList.layout.size,
borderRadius: px(10),
overflow: "hidden",
bg: (theme) => theme.dark.background,
marginX: ItemList.layout.gap,
},
props,
)}
className="h-80 w-full flex-row items-center justify-evenly overflow-hidden rounded bg-slate-800"
{...props}
>
<View
{...css({
width: { xs: "50%", lg: "30%" },
flexDirection: "column",
justifyContent: "center",
})}
>
<Skeleton {...css({ height: rem(2), alignSelf: "center" })} />
<Skeleton {...css({ width: rem(5), alignSelf: "center" })} />
<View className="w-1/2 justify-center lg:w-1/3">
<Skeleton className="h-8" />
<Skeleton className="w-2/5" />
</View>
<Poster.Loader layout={{ height: percent(80) }} />
<Poster.Loader className="h-4/5" />
</View>
);
};
ItemList.layout = {
numColumns: 1,
size: 300,
size: 320,
layout: "vertical",
gap: ts(2),
} satisfies Layout;

View File

@ -1,8 +1,10 @@
import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
import type { ComponentType } from "react";
import { Image, View, type ViewProps, type ViewStyle } from "react-native";
import { cn } from "~/utils";
import { Skeleton } from "./skeleton";
import { P } from "./text";
import { Icon } from "./icons";
const stringToColor = (string: string) => {
let hash = 0;
@ -51,12 +53,20 @@ export const Avatar = <AsProps = ViewProps>({
{placeholder[0]}
</P>
)}
<Image
resizeMode="cover"
source={{ uri: src }}
alt={alt}
className="absolute inset-0"
/>
{src && (
<Image
resizeMode="cover"
source={{ uri: src }}
alt={alt}
className="absolute inset-0"
/>
)}
{!src && !placeholder && (
<Icon
icon={AccountCircle}
className="fill-slate-200 dark:fill-slate-200"
/>
)}
</Container>
);
};

View File

@ -35,7 +35,7 @@ export const Button = <AsProps = PressableProps>({
disabled={disabled}
className={cn(
"flex-row items-center justify-center overflow-hidden",
"rounded-4xl border-3 border-accent p-1",
"rounded-4xl border-3 border-accent p-1 outline-0",
disabled && "border-slate-600",
"group focus-within:bg-accent hover:bg-accent",
className,

View File

@ -124,8 +124,8 @@ export const IconFab = <AsProps = PressableProps>({
<Icon
icon={icon}
className={cn(
"fill-slate-300",
(hover || focus) && "fill-slate-200",
"fill-slate-300 dark:fill-slate-300",
(hover || focus) && "fill-slate-200 dark:fill-slate-200",
iconClassName,
)}
/>

View File

@ -1,10 +1,8 @@
import { ImageBackground as EImageBackground } from "expo-image";
import { LinearGradient, type LinearGradientProps } from "expo-linear-gradient";
import type { ComponentProps, ReactNode } from "react";
import type { ImageStyle } from "react-native";
import { Platform } from "react-native";
import { withUniwind } from "uniwind";
import { useYoshiki } from "yoshiki/native";
import type { KImage } from "~/models";
import { useToken } from "~/providers/account-context";
import { cn } from "~/utils";
@ -65,40 +63,3 @@ export const PosterBackground = ({
/>
);
};
export const GradientImageBackground = ({
gradient,
gradientStyle,
children,
...props
}: ComponentProps<typeof ImageBackground> & {
gradient?: Partial<LinearGradientProps>;
gradientStyle?: Parameters<ReturnType<typeof useYoshiki>["css"]>[0];
}) => {
const { css, theme } = useYoshiki();
return (
<ImageBackground {...props}>
<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,
},
gradientStyle,
],
typeof gradient === "object" ? gradient : undefined,
)}
>
{children}
</LinearGradient>
</ImageBackground>
);
};

View File

@ -1,6 +1,5 @@
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
import { Button } from "./button";
import { Icon } from "./icons";
import { Menu } from "./menu";
export const Select = <Value extends string>({
@ -16,11 +15,7 @@ export const Select = <Value extends string>({
getLabel: (key: Value) => string;
}) => {
return (
<Menu
Trigger={Button}
text={getLabel(value)}
icon={<Icon icon={ExpandMore} />}
>
<Menu Trigger={Button} text={getLabel(value)} icon={ExpandMore}>
{values.map((x) => (
<Menu.Item
key={x}

View File

@ -7,13 +7,6 @@ import { Stack } from "expo-router";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import {
percent,
rem,
type Stylable,
type Theme,
useYoshiki,
} from "yoshiki/native";
import { WatchListInfo } from "~/components/items/watchlist-info";
import { Rating } from "~/components/rating";
import {
@ -44,7 +37,6 @@ import {
Poster,
Skeleton,
tooltip,
ts,
UL,
} from "~/primitives";
import { useAccount } from "~/providers/account-context";
@ -82,7 +74,6 @@ const ButtonList = ({
icon={PlayArrow}
as={Link}
href={playHref}
iconClassName="dark:fill-slate-200"
{...tooltip(t("show.play"))}
/>
)}
@ -273,7 +264,7 @@ const Description = ({
genres: Genre[];
studios: Studio[];
externalIds: Metadata;
} & Stylable) => {
}) => {
const { t } = useTranslation();
return (

View File

@ -7,11 +7,11 @@ import { min, percent, px, rem, vh } from "yoshiki/native";
import { type KImage, Show } from "~/models";
import {
ContrastArea,
GradientImageBackground,
H1,
H2,
IconButton,
IconFab,
ImageBackground,
Link,
P,
Skeleton,
@ -19,6 +19,7 @@ import {
ts,
} from "~/primitives";
import type { QueryIdentifier } from "~/query";
import { cn } from "~/utils";
export const Header = ({
name,
@ -27,6 +28,7 @@ export const Header = ({
tagline,
link,
infoLink,
className,
...props
}: {
name: string;
@ -35,86 +37,55 @@ export const Header = ({
tagline: string | null;
link: string | null;
infoLink: string;
className?: string;
}) => {
const { t } = useTranslation();
return (
<ContrastArea mode="dark">
{({ css }) => (
<GradientImageBackground
src={thumbnail}
alt=""
quality="high"
layout={{
width: percent(100),
height: {
xs: vh(40),
sm: min(vh(60), px(750)),
md: min(vh(60), px(680)),
lg: vh(65),
},
}}
{...(css(
{
minHeight: {
xs: px(350),
sm: px(300),
md: px(400),
lg: px(600),
},
},
props,
) as any)}
>
<View
{...css({
width: { md: percent(70) },
position: "absolute",
bottom: 0,
margin: ts(2),
})}
>
<H1
numberOfLines={4}
{...css({ fontSize: { xs: rem(2), sm: rem(3) } })}
>
{name}
</H1>
<View {...css({ flexDirection: "row", alignItems: "center" })}>
{link !== null && (
<IconFab
icon={PlayArrow}
aria-label={t("show.play")}
as={Link}
href={link ?? "#"}
{...tooltip(t("show.play"))}
{...css({ marginRight: ts(1) })}
/>
)}
<IconButton
icon={Info}
as={Link}
aria-label={t("home.info")}
href={infoLink ?? "#"}
{...tooltip(t("home.info"))}
{...css({ marginRight: ts(2) })}
/>
{tagline && (
<H2 {...css({ display: { xs: "none", sm: "flex" } })}>
{tagline}
</H2>
)}
</View>
<P
numberOfLines={4}
{...css({ display: { xs: "none", md: "flex" } })}
>
{description}
</P>
</View>
</GradientImageBackground>
<ImageBackground
src={thumbnail}
alt=""
quality="high"
className={cn(
"h-[40vh] w-full sm:h-[60vh] sm:min-h-[750px] md:min-h-[680px] lg:h-[65vh]",
className,
)}
</ContrastArea>
{...props}
>
<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">
<H1 numberOfLines={4} className="text-3xl text-slate-200 sm:text-5xl">
{name}
</H1>
<View className="my-2 flex-row items-center">
{link !== null && (
<IconFab
icon={PlayArrow}
aria-label={t("show.play")}
as={Link}
href={link}
className="mr-2"
{...tooltip(t("show.play"))}
/>
)}
<IconButton
icon={Info}
as={Link}
aria-label={t("home.info")}
href={infoLink}
className="mr-2"
iconClassName="fill-slate-400"
{...tooltip(t("home.info"))}
/>
{tagline && (
<H2 className="text-slate-200 max-sm:hidden">{tagline}</H2>
)}
</View>
<P numberOfLines={4} className="text-slate-400 max-sm:hidden">
{description}
</P>
</View>
</ImageBackground>
);
};

View File

@ -35,7 +35,8 @@ export const ItemDetails = ({
href,
playHref,
watchStatus,
unseenEpisodesCount,
availableCount,
seenCount,
...props
}: {
slug: string;
@ -49,7 +50,8 @@ export const ItemDetails = ({
href: string;
playHref: string | null;
watchStatus: WatchStatusV | null;
unseenEpisodesCount: number | null;
availableCount?: number | null;
seenCount?: number | null;
}) => {
const [moreOpened, setMoreOpened] = useState(false);
const { css } = useYoshiki("recommended-card");
@ -120,7 +122,8 @@ export const ItemDetails = ({
</View>
<ItemWatchStatus
watchStatus={watchStatus}
unseenEpisodesCount={unseenEpisodesCount}
availableCount={availableCount}
seenCount={seenCount}
/>
</PosterBackground>
<View

View File

@ -57,7 +57,7 @@ export const WatchlistList = () => {
description={entry.name}
thumbnail={entry.thumbnail ?? item.thumbnail}
href={entry.href ?? "#"}
watchedPercent={entry.watchStatus?.percent || null}
watchedPercent={entry.progress.percent}
/>
);
}

View File

@ -237,7 +237,6 @@ const ChangePopup = ({
</View>
<Input
autoComplete={autoComplete}
variant="big"
value={value}
onChangeText={(v) => setValue(v)}
/>
@ -299,7 +298,6 @@ const ChangePasswordPopup = ({
{hasPassword && (
<PasswordInput
autoComplete="current-password"
variant="big"
value={oldValue}
onChangeText={(v) => setOldValue(v)}
placeholder={t("settings.account.password.oldPassword")}
@ -307,7 +305,6 @@ const ChangePasswordPopup = ({
)}
<PasswordInput
autoComplete="new-password"
variant="big"
value={newValue}
onChangeText={(v) => setNewValue(v)}
placeholder={t("settings.account.password.newPassword")}