Update code for biome v2

This commit is contained in:
Zoe Roux 2025-07-14 01:10:18 +02:00
parent bbe1ad4ef1
commit 7b3f3cc1c1
No known key found for this signature in database
90 changed files with 605 additions and 325 deletions

View File

@ -1,4 +1,4 @@
import { type SQL, and, desc, eq, isNotNull, ne, sql } from "drizzle-orm";
import { and, desc, eq, isNotNull, ne, type SQL, sql } from "drizzle-orm";
import { Elysia, t } from "elysia";
import { auth } from "~/auth";
import { db } from "~/db";
@ -31,14 +31,14 @@ import { KError } from "~/models/error";
import { madeInAbyss } from "~/models/examples";
import {
AcceptLanguage,
createPage,
Filter,
type FilterDef,
Page,
Sort,
createPage,
isUuid,
keysetPaginate,
Page,
processLanguages,
Sort,
sortToSql,
} from "~/models/utils";
import { desc as description } from "~/models/utils/descriptions";

View File

@ -11,10 +11,10 @@ import { KError } from "~/models/error";
import { SeedHistory } from "~/models/history";
import {
AcceptLanguage,
Filter,
Page,
createPage,
Filter,
isUuid,
Page,
processLanguages,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -8,13 +8,13 @@ import { getColumns, sqlarr } from "~/db/utils";
import { Entry } from "~/models/entry";
import {
AcceptLanguage,
createPage,
Filter,
type FilterDef,
Page,
Sort,
createPage,
keysetPaginate,
Page,
processLanguages,
Sort,
sortToSql,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -18,11 +18,11 @@ import { Movie } from "~/models/movie";
import { Serie } from "~/models/serie";
import {
AcceptLanguage,
createPage,
DbMetadata,
Filter,
Page,
createPage,
isUuid,
Page,
processLanguages,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -1,20 +1,20 @@
import { and, eq, sql } from "drizzle-orm";
import { Elysia, t } from "elysia";
import { db } from "~/db";
import { seasonTranslations, seasons, shows } from "~/db/schema";
import { seasons, seasonTranslations, shows } from "~/db/schema";
import { getColumns, sqlarr } from "~/db/utils";
import { KError } from "~/models/error";
import { madeInAbyss } from "~/models/examples";
import {
AcceptLanguage,
createPage,
Filter,
type FilterDef,
Page,
Sort,
createPage,
isUuid,
keysetPaginate,
Page,
processLanguages,
Sort,
sortToSql,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -1,11 +1,11 @@
import path from "node:path";
import { encode } from "blurhash";
import { type SQL, and, eq, is, lt, sql } from "drizzle-orm";
import { and, eq, is, lt, type SQL, sql } from "drizzle-orm";
import { PgColumn, type PgTable } from "drizzle-orm/pg-core";
import { version } from "package.json";
import type { PoolClient } from "pg";
import sharp from "sharp";
import { type Transaction, db } from "~/db";
import { db, type Transaction } from "~/db";
import { mqueue } from "~/db/schema/mqueue";
import type { Image } from "~/models/utils";
import { getFile } from "~/utils";

View File

@ -1,6 +1,6 @@
import { sql } from "drizzle-orm";
import { db } from "~/db";
import { showTranslations, shows } from "~/db/schema";
import { shows, showTranslations } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedCollection } from "~/models/collections";
import type { SeedMovie } from "~/models/movie";

View File

@ -1,4 +1,4 @@
import { type Column, type SQL, eq, sql } from "drizzle-orm";
import { type Column, eq, type SQL, sql } from "drizzle-orm";
import { db } from "~/db";
import {
entries,

View File

@ -1,5 +1,5 @@
import { db } from "~/db";
import { seasonTranslations, seasons } from "~/db/schema";
import { seasons, seasonTranslations } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedSeason } from "~/models/season";
import { enqueueOptImage } from "../images";

View File

@ -1,15 +1,15 @@
import {
type SQLWrapper,
and,
count,
eq,
exists,
isNull,
ne,
type SQLWrapper,
sql,
} from "drizzle-orm";
import { type Transaction, db } from "~/db";
import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
import { db, type Transaction } from "~/db";
import { entries, entryVideoJoin, shows, showTranslations } from "~/db/schema";
import { conflictUpdateAllExcept, sqlarr } from "~/db/utils";
import type { SeedCollection } from "~/models/collections";
import type { SeedMovie } from "~/models/movie";

View File

@ -1,5 +1,5 @@
import { db } from "~/db";
import { showStudioJoin, studioTranslations, studios } from "~/db/schema";
import { showStudioJoin, studios, studioTranslations } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedStudio } from "~/models/studio";
import { enqueueOptImage } from "../images";

View File

@ -1,7 +1,7 @@
// oh i hate js dates so much.
export const guessNextRefresh = (airDate: Date | string) => {
if (typeof airDate === "string") airDate = new Date(airDate);
const diff = new Date().getTime() - airDate.getTime();
const diff = Date.now() - airDate.getTime();
const days = diff / (24 * 60 * 60 * 1000);
const ret = new Date();

View File

@ -16,10 +16,10 @@ import { Serie } from "~/models/serie";
import { Show } from "~/models/show";
import {
AcceptLanguage,
Filter,
Page,
createPage,
Filter,
isUuid,
Page,
processLanguages,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -1,4 +1,4 @@
import { and, eq, exists, gt, ne, type SQL, sql } from "drizzle-orm";
import { and, eq, exists, ne, type SQL, sql } from "drizzle-orm";
import { db } from "~/db";
import {
entries,

View File

@ -9,10 +9,10 @@ import { bubble } from "~/models/examples";
import { FullMovie, Movie, MovieTranslation } from "~/models/movie";
import {
AcceptLanguage,
Filter,
Page,
createPage,
Filter,
isUuid,
Page,
processLanguages,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -9,10 +9,10 @@ import { madeInAbyss } from "~/models/examples";
import { FullSerie, Serie, SerieTranslation } from "~/models/serie";
import {
AcceptLanguage,
Filter,
Page,
createPage,
Filter,
isUuid,
Page,
processLanguages,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -8,9 +8,9 @@ import { KError } from "~/models/error";
import { Show } from "~/models/show";
import {
AcceptLanguage,
createPage,
Filter,
Page,
createPage,
processLanguages,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -1,9 +1,9 @@
import { type SQL, and, eq, sql } from "drizzle-orm";
import { and, eq, type SQL, sql } from "drizzle-orm";
import Elysia, { t } from "elysia";
import { auth } from "~/auth";
import { prefix } from "~/base";
import { db } from "~/db";
import { profiles, showTranslations, shows } from "~/db/schema";
import { profiles, shows, showTranslations } from "~/db/schema";
import { roles, staff } from "~/db/schema/staff";
import { watchlist } from "~/db/schema/watchlist";
import { getColumns, jsonbBuildObject, sqlarr } from "~/db/utils";
@ -13,15 +13,15 @@ import { Role, Staff } from "~/models/staff";
import { RoleWShow, RoleWStaff } from "~/models/staff-roles";
import {
AcceptLanguage,
createPage,
Filter,
type FilterDef,
type Image,
Page,
Sort,
createPage,
isUuid,
keysetPaginate,
Page,
processLanguages,
Sort,
sortToSql,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -1,4 +1,4 @@
import { type SQL, and, eq, exists, sql } from "drizzle-orm";
import { and, eq, exists, type SQL, sql } from "drizzle-orm";
import Elysia, { t } from "elysia";
import { auth } from "~/auth";
import { prefix } from "~/base";
@ -6,8 +6,8 @@ import { db } from "~/db";
import {
showStudioJoin,
shows,
studioTranslations,
studios,
studioTranslations,
} from "~/db/schema";
import {
getColumns,
@ -22,14 +22,14 @@ import { Show } from "~/models/show";
import { Studio, StudioTranslation } from "~/models/studio";
import {
AcceptLanguage,
Filter,
Page,
Sort,
buildRelations,
createPage,
Filter,
isUuid,
keysetPaginate,
Page,
processLanguages,
Sort,
sortToSql,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";

View File

@ -1,6 +1,6 @@
import { and, eq, notExists, or, sql } from "drizzle-orm";
import { Elysia, t } from "elysia";
import { type Transaction, db } from "~/db";
import { db, type Transaction } from "~/db";
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
import {
conflictUpdateAllExcept,
@ -13,12 +13,12 @@ import {
import { KError } from "~/models/error";
import { bubbleVideo } from "~/models/examples";
import {
Page,
type Resource,
Sort,
createPage,
isUuid,
keysetPaginate,
Page,
type Resource,
Sort,
sortToSql,
} from "~/models/utils";
import { desc as description } from "~/models/utils/descriptions";

View File

@ -1,9 +1,9 @@
export * from "./entries";
export * from "./seasons";
export * from "./shows";
export * from "./studios";
export * from "./staff";
export * from "./videos";
export * from "./profiles";
export * from "./history";
export * from "./mqueue";
export * from "./profiles";
export * from "./seasons";
export * from "./shows";
export * from "./staff";
export * from "./studios";
export * from "./videos";

View File

@ -1,5 +1,5 @@
import { t } from "elysia";
import { type Prettify, comment } from "~/utils";
import { comment, type Prettify } from "~/utils";
import { madeInAbyss, registerExamples } from "../examples";
import { Progress } from "../history";
import { DbMetadata, SeedImage } from "../utils";

View File

@ -13,6 +13,6 @@ export type SeedEntry = SeedEpisode | SeedMovieEntry | SeedSpecial;
export type EntryKind = Entry["kind"] | Extra["kind"];
export * from "./episode";
export * from "./extra";
export * from "./movie-entry";
export * from "./special";
export * from "./extra";

View File

@ -1,5 +1,5 @@
import { t } from "elysia";
import { type Prettify, comment } from "~/utils";
import { comment, type Prettify } from "~/utils";
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
import { Progress } from "../history";
import {

View File

@ -1,5 +1,5 @@
import { t } from "elysia";
import { type Prettify, comment } from "~/utils";
import { comment, type Prettify } from "~/utils";
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
import { Progress } from "../history";
import {

View File

@ -31,7 +31,7 @@ export const registerExamples = <T extends TSchema>(
};
export * from "./bubble";
export * from "./made-in-abyss";
export * from "./dune-1984";
export * from "./dune-2021";
export * from "./dune-collection";
export * from "./made-in-abyss";

View File

@ -23,7 +23,10 @@ export type FilterDef = {
export const Filter = ({
def,
description = "Filters to apply to the query.",
}: { def: FilterDef; description?: string }) =>
}: {
def: FilterDef;
description?: string;
}) =>
t
.Transform(
t.String({

View File

@ -1,11 +1,11 @@
import {
type Parjser,
anyStringOf,
digit,
float,
int,
letter,
noCharOf,
type Parjser,
string,
} from "parjs";
import {

View File

@ -1,7 +1,6 @@
import {
type BinaryOperator,
type SQL,
and,
type BinaryOperator,
eq,
gt,
gte,
@ -10,6 +9,7 @@ import {
ne,
not,
or,
type SQL,
sql,
} from "drizzle-orm";
import { KErrorT } from "~/models/error";

View File

@ -1,12 +1,12 @@
export * from "./db-metadata";
export * from "./external-id";
export * from "./filters";
export * from "./genres";
export * from "./image";
export * from "./language";
export * from "./resource";
export * from "./filters";
export * from "./page";
export * from "./sort";
export * from "./keyset-paginate";
export * from "./db-metadata";
export * from "./language";
export * from "./original";
export * from "./page";
export * from "./relations";
export * from "./resource";
export * from "./sort";

View File

@ -4,9 +4,7 @@ import {
type TSchema,
type TString,
} from "@sinclair/typebox";
import { type Column, eq, sql, type Table } from "drizzle-orm";
import { t } from "elysia";
import { sqlarr } from "~/db/utils";
import { comment } from "../../utils";
import { KErrorT } from "../error";

View File

@ -1,6 +1,6 @@
import { PatternStringExact, type TSchema } from "@sinclair/typebox";
import { t } from "elysia";
import { type Prettify, comment } from "~/utils";
import { comment, type Prettify } from "~/utils";
import { ExtraType } from "./entry/extra";
import { bubble, bubbleVideo, registerExamples } from "./examples";
import { DbMetadata, EpisodeId, ExternalId, Resource } from "./utils";

View File

@ -1,8 +1,7 @@
export * from "~/base";
export * from "./movies-helper";
export * from "./series-helper";
export * from "./shows-helper";
export * from "./studio-helper";
export * from "./staff-helper";
export * from "./studio-helper";
export * from "./videos-helper";
export * from "~/base";

View File

@ -45,7 +45,11 @@ export const getSerie = async (
export const getSeries = async ({
langs,
...query
}: { langs?: string; preferOriginal?: boolean; with?: string[] }) => {
}: {
langs?: string;
preferOriginal?: boolean;
with?: string[];
}) => {
const resp = await handlers.handle(
new Request(buildUrl("series", query), {
method: "GET",

View File

@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from "bun:test";
import { eq } from "drizzle-orm";
import { expectStatus } from "tests/utils";
import { db } from "~/db";
import { showTranslations, shows, videos } from "~/db/schema";
import { shows, showTranslations, videos } from "~/db/schema";
import { bubble } from "~/models/examples";
import { dune, duneVideo } from "~/models/examples/dune-2021";
import { createMovie, createVideo } from "../helpers";

View File

@ -10,7 +10,7 @@ import {
} from "tests/helpers";
import { expectStatus } from "tests/utils";
import { db } from "~/db";
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
import { entries, shows, videos } from "~/db/schema";
import { bubble, madeInAbyss } from "~/models/examples";
beforeAll(async () => {

View File

@ -21,13 +21,18 @@
},
"suspicious": {
"noExplicitAny": "off",
"noArrayIndexKey": "off"
"noArrayIndexKey": "off",
"noTemplateCurlyInString": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"complexity": {
"noBannedTypes": "off"
"noBannedTypes": "off",
"noUselessUndefinedInitialization": "off"
},
"correctness": {
"noUnusedVariables": "off"
}
}
},

View File

@ -1,3 +1,6 @@
{
"extends": "//"
"extends": "//",
"files": {
"includes": ["src/**"]
}
}

View File

@ -8,15 +8,46 @@ export default function Root({ children }: PropsWithChildren) {
<head>
<title>Kyoo</title>
<meta charSet="utf-8" />
<meta name="description" content="A portable and vast media library solution." />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta
name="description"
content="A portable and vast media library solution."
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<link rel="icon" type="image/png" sizes="16x16" href="/icon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="64x64" href="/icon-64x64.png" />
<link rel="icon" type="image/png" sizes="128x128" href="/icon-128x128.png" />
<link rel="icon" type="image/png" sizes="256x256" href="/icon-256x256.png" />
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/icon-16x16.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/icon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="64x64"
href="/icon-64x64.png"
/>
<link
rel="icon"
type="image/png"
sizes="128x128"
href="/icon-128x128.png"
/>
<link
rel="icon"
type="image/png"
sizes="256x256"
href="/icon-256x256.png"
/>
<ScrollViewStyleReset />
</head>

View File

@ -1,12 +1,6 @@
import { useState } from "react";
import { type ImageStyle, Platform, View } from "react-native";
import {
percent,
px,
type Stylable,
type Theme,
useYoshiki,
} from "yoshiki/native";
import { percent, type Stylable, type Theme, useYoshiki } from "yoshiki/native";
import type { KImage, WatchStatusV } from "~/models";
import {
focusReset,

View File

@ -14,7 +14,8 @@ export const ItemWatchStatus = ({
}) => {
const { css } = useYoshiki();
if (watchStatus !== WatchStatusV.Completed && !unseenEpisodesCount) return null;
if (watchStatus !== WatchStatusV.Completed && !unseenEpisodesCount)
return null;
return (
<View
@ -38,7 +39,13 @@ export const ItemWatchStatus = ({
{watchStatus === WatchStatusV.Completed ? (
<Icon icon={Done} size={16} />
) : (
<P {...css({ marginVertical: 0, verticalAlign: "middle", textAlign: "center" })}>
<P
{...css({
marginVertical: 0,
verticalAlign: "middle",
textAlign: "center",
})}
>
{unseenEpisodesCount}
</P>
)}

View File

@ -1,7 +1,7 @@
import Bookmark from "@material-symbols/svg-400/rounded/bookmark-fill.svg";
import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
import BookmarkAdded from "@material-symbols/svg-400/rounded/bookmark_added-fill.svg";
import BookmarkRemove from "@material-symbols/svg-400/rounded/bookmark_remove.svg";
import Bookmark from "@material-symbols/svg-400/rounded/bookmark-fill.svg";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import type { Serie } from "~/models";
@ -10,7 +10,13 @@ import { useAccount } from "~/providers/account-context";
import { useMutation } from "~/query";
type WatchStatus = NonNullable<Serie["watchStatus"]>["status"];
const WatchStatus = ["completed", "watching", "rewatching", "dropped", "planned"] as const;
const WatchStatus = [
"completed",
"watching",
"rewatching",
"dropped",
"planned",
] as const;
export const watchListIcon = (status: WatchStatus | null) => {
switch (status) {
@ -51,7 +57,12 @@ export const WatchListInfo = ({
if (account == null) {
return (
<IconButton icon={BookmarkAdd} disabled {...tooltip(t("show.watchlistLogin"))} {...props} />
<IconButton
icon={BookmarkAdd}
disabled
{...tooltip(t("show.watchlistLogin"))}
{...props}
/>
);
}
@ -93,7 +104,10 @@ export const WatchListInfo = ({
selected={x === status}
/>
))}
<Menu.Item label={t("show.watchlistMark.null")} onSelect={() => mutation.mutate(null)} />
<Menu.Item
label={t("show.watchlistMark.null")}
onSelect={() => mutation.mutate(null)}
/>
</Menu>
);
default:

View File

@ -1,15 +1,12 @@
export * from "./utils/page";
export * from "./collection";
export * from "./entry";
export * from "./kyoo-error";
export * from "./movie";
export * from "./serie";
export * from "./collection";
export * from "./show";
export * from "./entry";
export * from "./studio";
export * from "./video";
export * from "./user";
export * from "./utils/images";
export * from "./utils/genre";
export * from "./utils/images";
export * from "./utils/page";
export * from "./video";

View File

@ -23,7 +23,8 @@ export const EpisodeP = BaseEpisodeP.and(
watchStatus: WatchStatusP.optional().nullable(),
}),
).transform((x) => {
if (x.show && !x.thumbnail && x.show.thumbnail) x.thumbnail = x.show.thumbnail;
if (x.show && !x.thumbnail && x.show.thumbnail)
x.thumbnail = x.show.thumbnail;
return x;
});

View File

@ -169,9 +169,11 @@ export const WatchInfoP = z
// from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
const humanFileSize = (size: number): string => {
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (
// @ts-ignore I'm not gonna fix stackoverflow's working code.
// biome-ignore lint: same as above
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
// biome-ignore lint/style/useTemplate: same as above
(size / 1024 ** i).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i]
);
};
/**

View File

@ -11,4 +11,10 @@ export const Show = z.union([
export type Show = z.infer<typeof Show>;
export type WatchStatusV = NonNullable<Serie["watchStatus"]>["status"];
export const WatchStatusV = ["completed", "watching", "rewatching", "dropped", "planned"] as const;
export const WatchStatusV = [
"completed",
"watching",
"rewatching",
"dropped",
"planned",
] as const;

View File

@ -1,6 +1,10 @@
// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681
import { type AlertButton, type AlertOptions, Alert as RNAlert } from "react-native";
import {
type AlertButton,
type AlertOptions,
Alert as RNAlert,
} from "react-native";
import type { SweetAlertIcon } from "sweetalert2";
export interface ExtendedAlertStatic {

View File

@ -37,7 +37,9 @@ export class Alert {
const denyButton = buttons
? buttons.find((button) => button.style === "destructive")
: undefined;
const cancelButton = buttons ? buttons.find((button) => button.style === "cancel") : undefined;
const cancelButton = buttons
? buttons.find((button) => button.style === "cancel")
: undefined;
Swal.fire({
title: title,

View File

@ -1,7 +1,7 @@
import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
import { type ComponentType, type RefAttributes, forwardRef } from "react";
import { type ComponentType, forwardRef, type RefAttributes } from "react";
import { Image, type ImageProps, View, type ViewStyle } from "react-native";
import { type Stylable, px, useYoshiki } from "yoshiki/native";
import { px, type Stylable, useYoshiki } from "yoshiki/native";
import { Icon } from "./icons";
import { Skeleton } from "./skeleton";
import { P } from "./text";
@ -73,7 +73,11 @@ const AvatarC = forwardRef<
{placeholder[0]}
</P>
) : (
<Icon icon={AccountCircle} size={size} color={fill ? col : theme.colors.white} />
<Icon
icon={AccountCircle}
size={size}
color={fill ? col : theme.colors.white}
/>
)}
<Image
resizeMode="cover"

View File

@ -1,4 +1,9 @@
import { type ComponentType, type ForwardedRef, type ReactElement, forwardRef } from "react";
import {
type ComponentType,
type ForwardedRef,
forwardRef,
type ReactElement,
} from "react";
import { type Falsy, type PressableProps, View } from "react-native";
import { type Theme, useYoshiki } from "yoshiki/native";
import { PressableFeedback } from "./links";

View File

@ -1,5 +1,5 @@
import { HR as EHR } from "@expo/html-elements";
import { type Stylable, px, useYoshiki } from "yoshiki/native";
import { px, type Stylable, useYoshiki } from "yoshiki/native";
import { ts } from "./utils";
export const HR = ({

View File

@ -43,7 +43,9 @@ export const Sprite = ({
style?: object;
}) => {
return (
<View style={{ width, height, overflow: "hidden", flexGrow: 0, flexShrink: 0 }}>
<View
style={{ width, height, overflow: "hidden", flexGrow: 0, flexShrink: 0 }}
>
<Image
source={{ uri: src }}
alt={alt}

View File

@ -1,24 +1,24 @@
export { Header, Main, Nav, Footer, UL } from "@expo/html-elements";
export * from "./text";
export * from "./theme";
export * from "./icons";
export * from "./links";
export { Footer, Header, Main, Nav, UL } from "@expo/html-elements";
export * from "./avatar";
export * from "./image";
export * from "./image-background";
export * from "./skeleton";
export * from "./tooltip";
export * from "./button";
export * from "./chip";
export * from "./container";
export * from "./divider";
export * from "./icons";
export * from "./image";
export * from "./image-background";
// export * from "./popup";
// export * from "./select";
export * from "./input";
export * from "./links";
// export * from "./progress";
// export * from "./slider";
// export * from "./snackbar";
// export * from "./alert";
export * from "./menu";
// export * from "./popup";
// export * from "./select";
export * from "./input";
export * from "./button";
export * from "./chip";
export * from "./skeleton";
export * from "./text";
export * from "./theme";
export * from "./tooltip";
export * from "./utils";

View File

@ -1,6 +1,11 @@
import { type ReactNode, forwardRef, useState } from "react";
import { TextInput, type TextInputProps, View, type ViewStyle } from "react-native";
import { type Theme, px, useYoshiki } from "yoshiki/native";
import { forwardRef, type ReactNode, useState } from "react";
import {
TextInput,
type TextInputProps,
View,
type ViewStyle,
} from "react-native";
import { px, type Theme, useYoshiki } from "yoshiki/native";
import type { YoshikiEnhanced } from "./image/base-image";
import { focusReset, ts } from "./utils";

View File

@ -4,9 +4,9 @@ import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
import { useRouter } from "expo-router";
import {
type ComponentType,
createContext,
type ReactElement,
type ReactNode,
createContext,
useContext,
useEffect,
useRef,
@ -22,7 +22,9 @@ import { P } from "./text";
import { ContrastArea, SwitchVariant } from "./theme";
import { ts } from "./utils";
const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined);
const MenuContext = createContext<((open: boolean) => void) | undefined>(
undefined,
);
type Optional<T, K extends keyof any> = Omit<T, K> & Partial<T>;
@ -45,7 +47,10 @@ const Menu = <AsProps,>({
const insets = useSafeAreaInsets();
const alreadyRendered = useRef(false);
const [isOpen, setOpen] =
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
outerOpen !== undefined && outerSetOpen
? [outerOpen, outerSetOpen]
: // biome-ignore lint/correctness/useHookAtTopLevel: const
useState(false);
// does the same as a useMemo but for props.
const memoRef = useRef({ onMenuOpen, onMenuClose });
@ -73,7 +78,11 @@ const Menu = <AsProps,>({
<Pressable
onPress={() => setOpen(false)}
tabIndex={-1}
{...css({ ...StyleSheet.absoluteFillObject, flexGrow: 1, bg: "transparent" })}
{...css({
...StyleSheet.absoluteFillObject,
flexGrow: 1,
bg: "transparent",
})}
/>
<View
{...css([
@ -107,7 +116,10 @@ const Menu = <AsProps,>({
icon={Close}
color={theme.colors.black}
onPress={() => setOpen(false)}
{...css({ alignSelf: "flex-end", display: { xs: "none", xl: "flex" } })}
{...css({
alignSelf: "flex-end",
display: { xs: "none", xl: "flex" },
})}
/>
{children}
</ScrollView>
@ -137,7 +149,10 @@ const MenuItem = ({
left?: ReactElement;
disabled?: boolean;
icon?: ComponentType<SvgProps>;
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
} & (
| { onSelect: () => void; href?: undefined }
| { href: string; onSelect?: undefined }
)) => {
const { css, theme } = useYoshiki();
const setOpen = useContext(MenuContext);
const router = useRouter();
@ -174,7 +189,10 @@ const MenuItem = ({
{!left && icn && icn}
<P
{...css([
{ paddingLeft: ts(2) + +!(icon || selected || left) * px(24), flexGrow: 1 },
{
paddingLeft: ts(2) + +!(icon || selected || left) * px(24),
flexGrow: 1,
},
disabled && { color: theme.overlay0 },
])}
>

View File

@ -27,7 +27,12 @@ import { Container } from "./container";
import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./themes";
import { ts } from "./utils";
export const Popup = ({ children, ...props }: { children: ReactNode | YoshikiFunc<ReactNode> }) => {
export const Popup = ({
children,
...props
}: {
children: ReactNode | YoshikiFunc<ReactNode>;
}) => {
return (
<ContrastArea mode="user">
<SwitchVariant>
@ -67,7 +72,9 @@ export const Popup = ({ children, ...props }: { children: ReactNode | YoshikiFun
flexShrink: 1,
})}
>
{typeof children === "function" ? children({ css, theme }) : children}
{typeof children === "function"
? children({ css, theme })
: children}
</ScrollView>
</Container>
</View>

View File

@ -20,7 +20,7 @@
import { ActivityIndicator, Platform, View } from "react-native";
import { Circle, Svg } from "react-native-svg";
import { type Stylable, px, useYoshiki } from "yoshiki/native";
import { px, type Stylable, useYoshiki } from "yoshiki/native";
export const CircularProgress = ({
size = 48,
@ -31,7 +31,9 @@ export const CircularProgress = ({
const { css, theme } = useYoshiki();
if (Platform.OS !== "web")
return <ActivityIndicator size={size} color={color ?? theme.accent} {...props} />;
return (
<ActivityIndicator size={size} color={color ?? theme.accent} {...props} />
);
return (
<View {...css({ width: size, height: size, overflow: "hidden" }, props)}>
@ -63,7 +65,9 @@ export const CircularProgress = ({
viewBox={`${size / 2} ${size / 2} ${size} ${size}`}
{...css(
// @ts-ignore Web only
Platform.OS === "web" && { animation: "circularProgress-svg 1.4s ease-in-out infinite" },
Platform.OS === "web" && {
animation: "circularProgress-svg 1.4s ease-in-out infinite",
},
)}
>
<Circle

View File

@ -24,7 +24,6 @@ import { Icon } from "./icons";
import { Menu } from "./menu";
export const Select = <Value extends string>({
label,
value,
onValueChange,
values,
@ -37,7 +36,11 @@ 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={<Icon icon={ExpandMore} />}
>
{values.map((x) => (
<Menu.Item
key={x}

View File

@ -25,7 +25,7 @@ import * as RSelect from "@radix-ui/react-select";
import { forwardRef } from "react";
import { View } from "react-native";
import { useYoshiki } from "yoshiki";
import { type Theme, px, useYoshiki as useNativeYoshiki } from "yoshiki/native";
import { px, type Theme, useYoshiki as useNativeYoshiki } from "yoshiki/native";
import { Icon } from "./icons";
import { PressableFeedback } from "./links";
import { InternalTriger, YoshikiProvider } from "./menu.web";
@ -78,7 +78,9 @@ export const Select = ({
})}
>
<P {...css({ textAlign: "center" }, "text")}>{<RSelect.Value />}</P>
<RSelect.Icon {...wCss({ display: "flex", justifyContent: "center" })}>
<RSelect.Icon
{...wCss({ display: "flex", justifyContent: "center" })}
>
<Icon icon={ExpandMore} />
</RSelect.Icon>
</View>
@ -98,7 +100,8 @@ export const Select = ({
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
zIndex: 2,
maxHeight: "calc(var(--radix-dropdown-menu-content-available-height) * 0.8)",
maxHeight:
"calc(var(--radix-dropdown-menu-content-available-height) * 0.8)",
})}
>
<RSelect.ScrollUpButton>
@ -122,10 +125,8 @@ export const Select = ({
);
};
const Item = forwardRef<HTMLDivElement, { label: string; value: string }>(function Item(
{ label, value, ...props },
ref,
) {
const Item = forwardRef<HTMLDivElement, { label: string; value: string }>(
function Item({ label, value, ...props }, ref) {
const { css, theme } = useYoshiki();
const { css: nCss } = useNativeYoshiki();
return (
@ -156,7 +157,9 @@ const Item = forwardRef<HTMLDivElement, { label: string; value: string }>(functi
props as any,
)}
>
<RSelect.ItemText {...css({ color: (theme) => theme.paragraph })}>{label}</RSelect.ItemText>
<RSelect.ItemText {...css({ color: (theme) => theme.paragraph })}>
{label}
</RSelect.ItemText>
<RSelect.ItemIndicator asChild>
<InternalTriger
Component={Icon}
@ -173,4 +176,5 @@ const Item = forwardRef<HTMLDivElement, { label: string; value: string }>(functi
</RSelect.Item>
</>
);
});
},
);

View File

@ -32,7 +32,10 @@ export const Skeleton = ({
}));
useEffect(() => {
mult.value = withRepeat(withDelay(800, withTiming(1, { duration: 800 })), 0);
mult.value = withRepeat(
withDelay(800, withTiming(1, { duration: 800 })),
0,
);
});
return (

View File

@ -21,7 +21,7 @@
import { useRef, useState } from "react";
import { type GestureResponderEvent, Platform, View } from "react-native";
import type { ViewProps } from "react-native-svg/lib/typescript/fabric/utils";
import { Stylable, percent, px, useYoshiki } from "yoshiki/native";
import { percent, px, useYoshiki } from "yoshiki/native";
import { focusReset } from "./utils";
export const Slider = ({
@ -80,7 +80,10 @@ export const Slider = ({
}}
// @ts-ignore Web only
onMouseMove={(e) =>
onHover?.(Math.max(0, Math.min((e.clientX - layout.x) / layout.width, 1) * max), layout)
onHover?.(
Math.max(0, Math.min((e.clientX - layout.x) / layout.width, 1) * max),
layout,
)
}
tabIndex={0}
onFocus={() => setFocus(true)}

View File

@ -19,7 +19,13 @@
*/
import { usePortal } from "@gorhom/portal";
import { type ReactElement, createContext, useCallback, useContext, useRef } from "react";
import {
createContext,
type ReactElement,
useCallback,
useContext,
useRef,
} from "react";
import { View } from "react-native";
import { percent, px } from "yoshiki/native";
import { Button } from "./button";
@ -43,14 +49,21 @@ export type Action = {
const SnackbarContext = createContext<(snackbar: Snackbar) => void>(null!);
export const SnackbarProvider = ({ children }: { children: ReactElement | ReactElement[] }) => {
export const SnackbarProvider = ({
children,
}: {
children: ReactElement | ReactElement[];
}) => {
const { addPortal, removePortal } = usePortal();
const snackbars = useRef<Snackbar[]>([]);
const timeout = useRef<NodeJS.Timeout | null>(null);
const createSnackbar = useCallback(
(snackbar: Snackbar) => {
if (snackbar.key) snackbars.current = snackbars.current.filter((x) => snackbar.key !== x.key);
if (snackbar.key)
snackbars.current = snackbars.current.filter(
(x) => snackbar.key !== x.key,
);
snackbars.current.unshift(snackbar);
if (timeout.current) return;
@ -73,7 +86,11 @@ export const SnackbarProvider = ({ children }: { children: ReactElement | ReactE
[addPortal, removePortal],
);
return <SnackbarContext.Provider value={createSnackbar}>{children}</SnackbarContext.Provider>;
return (
<SnackbarContext.Provider value={createSnackbar}>
{children}
</SnackbarContext.Provider>
);
};
export const useSnackbar = () => {

View File

@ -8,7 +8,13 @@ import {
P as EP,
} from "@expo/html-elements";
import type { ComponentProps, ComponentType } from "react";
import { Platform, type StyleProp, Text, type TextProps, type TextStyle } from "react-native";
import {
Platform,
type StyleProp,
Text,
type TextProps,
type TextStyle,
} from "react-native";
import { percent, rem, useYoshiki } from "yoshiki/native";
import { ts } from "./utils/spacing";
@ -55,7 +61,10 @@ const styleText = (
return Text;
};
export const H1 = styleText(EH1, "header", { fontSize: rem(3), fontWeight: "900" });
export const H1 = styleText(EH1, "header", {
fontSize: rem(3),
fontWeight: "900",
});
export const H2 = styleText(EH2, "header", { fontSize: rem(2) });
export const H3 = styleText(EH3, "header");
export const H4 = styleText(EH4, "header");

View File

@ -1,2 +1,2 @@
export * from "./theme";
export * from "./catppuccin";
export * from "./theme";

View File

@ -1,7 +1,11 @@
import type { Property } from "csstype";
import type { ReactNode } from "react";
import { Platform, type TextStyle } from "react-native";
import { type Theme, ThemeProvider as WebThemeProvider, useAutomaticTheme } from "yoshiki";
import {
type Theme,
useAutomaticTheme,
ThemeProvider as WebThemeProvider,
} from "yoshiki";
import "yoshiki";
import { ThemeProvider, useTheme, useYoshiki } from "yoshiki/native";
import "yoshiki/native";
@ -52,7 +56,9 @@ declare module "yoshiki" {
export type { Theme } from "yoshiki";
export type ThemeBuilder = {
light: Omit<Mode, "contrast" | "mode" | "themeOverlay"> & { default: Variant };
light: Omit<Mode, "contrast" | "mode" | "themeOverlay"> & {
default: Variant;
};
dark: Omit<Mode, "contrast" | "mode" | "themeOverlay"> & { default: Variant };
};
@ -88,8 +94,13 @@ const selectMode = (
};
}
// biome-ignore lint/correctness/useHookAtTopLevel: const
const auto = useAutomaticTheme("theme", { light, dark });
const alternate = useAutomaticTheme("alternate", { dark: light, light: dark });
// biome-ignore lint/correctness/useHookAtTopLevel: const
const alternate = useAutomaticTheme("alternate", {
dark: light,
light: dark,
});
return {
...options,
...auto,
@ -136,12 +147,20 @@ export const ThemeSelector = ({
export type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T;
const YoshikiProvider = ({ children }: { children: YoshikiFunc<ReactNode> }) => {
const YoshikiProvider = ({
children,
}: {
children: YoshikiFunc<ReactNode>;
}) => {
const yoshiki = useYoshiki();
return <>{children(yoshiki)}</>;
};
export const SwitchVariant = ({ children }: { children: ReactNode | YoshikiFunc<ReactNode> }) => {
export const SwitchVariant = ({
children,
}: {
children: ReactNode | YoshikiFunc<ReactNode>;
}) => {
const theme = useTheme();
return (

View File

@ -1,7 +1,12 @@
import { useWindowDimensions } from "react-native";
import { breakpoints, isBreakpoints, type Breakpoints as YoshikiBreakpoint } from "yoshiki/native";
import {
breakpoints,
isBreakpoints,
type Breakpoints as YoshikiBreakpoint,
} from "yoshiki/native";
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> &
U[keyof U];
export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>;
// copied from yoshiki.
@ -36,6 +41,9 @@ export const useBreakpointMap = <T extends Record<string, unknown>>(
const breakpoint = useBreakpoint();
// @ts-ignore
return Object.fromEntries(
Object.entries(value).map(([key, val]) => [key, getBreakpointValue(val, breakpoint)]),
Object.entries(value).map(([key, val]) => [
key,
getBreakpointValue(val, breakpoint),
]),
);
};

View File

@ -1,7 +1,7 @@
export * from "./nojs";
export * from "./head";
export * from "./spacing";
export * from "./capitalize";
export * from "./touchonly";
export * from "./page-style";
export * from "./breakpoint";
export * from "./capitalize";
export * from "./head";
export * from "./nojs";
export * from "./page-style";
export * from "./spacing";
export * from "./touchonly";

View File

@ -1,6 +1,8 @@
import type { ViewProps } from "react-native";
export const hiddenIfNoJs: ViewProps = { style: { $$css: true, noJs: "noJsHidden" } as any };
export const hiddenIfNoJs: ViewProps = {
style: { $$css: true, noJs: "noJsHidden" } as any,
};
export const HiddenIfNoJs = () => (
<noscript>

View File

@ -14,10 +14,16 @@ export const TouchOnlyCss = () => {
};
export const touchOnly: ViewProps = {
style: Platform.OS === "web" ? ({ $$css: true, touchOnly: "touchOnly" } as any) : {},
style:
Platform.OS === "web"
? ({ $$css: true, touchOnly: "touchOnly" } as any)
: {},
};
export const noTouch: ViewProps = {
style: Platform.OS === "web" ? ({ $$css: true, noTouch: "noTouch" } as any) : { display: "none" },
style:
Platform.OS === "web"
? ({ $$css: true, noTouch: "noTouch" } as any)
: { display: "none" },
};
export const useIsTouch = () => {

View File

@ -2,7 +2,13 @@ import { type ReactNode, useContext } from "react";
import { ErrorView, errorHandlers } from "~/ui/errors";
import { ErrorContext } from "./error-provider";
export const ErrorConsumer = ({ children, scope }: { children: ReactNode; scope: string }) => {
export const ErrorConsumer = ({
children,
scope,
}: {
children: ReactNode;
scope: string;
}) => {
const error = useContext(ErrorContext);
if (!error) return children;

View File

@ -1,6 +1,6 @@
import {
type ReactNode,
createContext,
type ReactNode,
useCallback,
useContext,
useMemo,
@ -46,7 +46,10 @@ export const ErrorProvider = ({ children }: { children: ReactNode }) => {
export const useSetError = (key: string) => {
const { setError, clearError } = useContext(ErrorSetterContext);
const set = ({ key: nKey, ...obj }: Omit<Error, "key"> & { key?: Error["key"] } = {}) =>
const set = ({
key: nKey,
...obj
}: Omit<Error, "key"> & { key?: Error["key"] } = {}) =>
setError({ key: nKey ?? key, ...obj });
const clear = () => clearError(key);
return [set, clear] as const;

View File

@ -1,5 +1,5 @@
import { useFetch } from "~/query";
import { useAccount } from "./account-context";
// import { useFetch } from "~/query";
// import { useAccount } from "./account-context";
// import { ServerInfoP } from "~/models/resources/server-info";
// export const useHasPermission = (perms?: string[]) => {

View File

@ -23,6 +23,7 @@ export const setCookie = (key: string, val?: unknown) => {
const expires = value
? `expires=${d.toUTCString()}`
: "expires=Thu, 01 Jan 1970 00:00:01 GMT";
// biome-ignore lint/suspicious/noDocumentCookie: idk
document.cookie = `${key}=${value};${expires};path=/;samesite=strict`;
};
@ -43,6 +44,7 @@ export const useStoreValue = <T extends ZodType>(key: string, parser: T) => {
if (Platform.OS === "web" && typeof window === "undefined") {
return readCookie(key, parser);
}
// biome-ignore lint/correctness/useHookAtTopLevel: constant
const [val] = useMMKVString(key);
if (val === undefined) return val;
return parser.parse(JSON.parse(val)) as z.infer<T>;

View File

@ -1,8 +1,8 @@
import i18next from "i18next";
import { type ReactNode, useMemo } from "react";
import { I18nextProvider } from "react-i18next";
import { resources, supportedLanguages } from "./translations.compile";
import { setServerData } from "~/utils";
import { resources, supportedLanguages } from "./translations.compile";
export const TranslationsProvider = ({ children }: { children: ReactNode }) => {
const val = useMemo(() => {

View File

@ -2,8 +2,8 @@ import i18next from "i18next";
import HttpApi, { type HttpBackendOptions } from "i18next-http-backend";
import { type ReactNode, useMemo } from "react";
import { I18nextProvider } from "react-i18next";
import { supportedLanguages } from "./translations.compile";
import { getServerData } from "~/utils";
import { supportedLanguages } from "./translations.compile";
export const TranslationsProvider = ({ children }: { children: ReactNode }) => {
const val = useMemo(() => {

View File

@ -22,7 +22,7 @@ export const Fetch = <Data,>({
if (error && (error.status === 401 || error.status === 403)) {
setError({ key: "unauthorized", error });
}
}, [error, isPaused]);
}, [error, isPaused, setError]);
if (error) {
return <ErrorView error={error} />;

View File

@ -1,3 +1,3 @@
export * from "./query";
export * from "./fetch";
export * from "./fetch-infinite";
export * from "./query";

View File

@ -1,3 +1,9 @@
export const availableSorts = ["name", "startAir", "endAir", "createdAt", "rating"] as const;
export const availableSorts = [
"name",
"startAir",
"endAir",
"createdAt",
"rating",
] as const;
export type SortBy = (typeof availableSorts)[number];
export type SortOrd = "asc" | "desc";

View File

@ -18,6 +18,8 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
/** biome-ignore-all lint/correctness/noUnusedImports: TODO */
import {
type Collection,
CollectionP,
@ -27,12 +29,12 @@ import {
} from "@kyoo/models";
import {
Container,
focusReset,
GradientImageBackground,
H2,
ImageBackground,
Link,
P,
focusReset,
ts,
} from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
@ -90,8 +92,16 @@ export const PartOf = ({
);
};
export const DetailsCollections = ({ type, slug }: { type: "movie" | "show"; slug: string }) => {
const { items, error } = useInfiniteFetch(DetailsCollections.query(type, slug));
export const DetailsCollections = ({
type,
slug,
}: {
type: "movie" | "show";
slug: string;
}) => {
const { items, error } = useInfiniteFetch(
DetailsCollections.query(type, slug),
);
const { css } = useYoshiki();
if (error) return <ErrorView error={error} />;
@ -114,7 +124,10 @@ export const DetailsCollections = ({ type, slug }: { type: "movie" | "show"; slu
);
};
DetailsCollections.query = (type: "movie" | "show", slug: string): QueryIdentifier<Collection> => ({
DetailsCollections.query = (
type: "movie" | "show",
slug: string,
): QueryIdentifier<Collection> => ({
parser: CollectionP,
path: [type, slug, "collections"],
params: {

View File

@ -18,20 +18,22 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
/** biome-ignore-all lint/correctness/noUnusedImports: TODO */
import { type KyooImage, WatchStatusV } from "@kyoo/models";
import {
focusReset,
H6,
IconButton,
Image,
ImageBackground,
type ImageProps,
imageBorderRadius,
important,
Link,
P,
Skeleton,
SubP,
focusReset,
imageBorderRadius,
important,
tooltip,
ts,
} from "@kyoo/primitives";
@ -39,8 +41,19 @@ import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fi
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { type ImageStyle, Platform, type PressableProps, View } from "react-native";
import { type Stylable, type Theme, percent, rem, useYoshiki } from "yoshiki/native";
import {
type ImageStyle,
Platform,
type PressableProps,
View,
} from "react-native";
import {
percent,
rem,
type Stylable,
type Theme,
useYoshiki,
} from "yoshiki/native";
import { ItemProgress } from "../browse/grid";
import { EpisodesContext } from "../components/context-menus";
import type { Layout } from "../fetch";
@ -50,7 +63,10 @@ export const episodeDisplayNumber = (episode: {
episodeNumber?: number | null;
absoluteNumber?: number | null;
}) => {
if (typeof episode.seasonNumber === "number" && typeof episode.episodeNumber === "number")
if (
typeof episode.seasonNumber === "number" &&
typeof episode.episodeNumber === "number"
)
return `S${episode.seasonNumber}:E${episode.episodeNumber}`;
if (episode.absoluteNumber) return episode.absoluteNumber.toString();
return "??";
@ -139,7 +155,8 @@ export const EpisodeBox = ({
bg: (theme) => theme.darkOverlay,
},
"more",
Platform.OS === "web" && moreOpened && { display: important("flex") },
Platform.OS === "web" &&
moreOpened && { display: important("flex") },
])}
/>
</ImageBackground>
@ -295,7 +312,9 @@ export const EpisodeLine = ({
<SubP>
{[
// @ts-ignore Source https://www.i18next.com/translation-function/formatting#datetime
releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null,
releaseDate
? t("{{val, datetime}}", { val: releaseDate })
: null,
displayRuntime(runtime),
]
.filter((item) => item != null)
@ -310,17 +329,22 @@ export const EpisodeLine = ({
{...css([
"more",
{ display: "flex", marginLeft: ts(3) },
Platform.OS === "web" && moreOpened && { display: important("flex") },
Platform.OS === "web" &&
moreOpened && { display: important("flex") },
])}
/>
</View>
</View>
<View {...css({ flexDirection: "row", justifyContent: "space-between" })}>
<View
{...css({ flexDirection: "row", justifyContent: "space-between" })}
>
<P numberOfLines={descriptionExpanded ? undefined : 3}>{overview}</P>
<IconButton
{...css(["more", Platform.OS !== "web" && { opacity: 1 }])}
icon={descriptionExpanded ? ExpandLess : ExpandMore}
{...tooltip(t(descriptionExpanded ? "misc.collapse" : "misc.expand"))}
{...tooltip(
t(descriptionExpanded ? "misc.collapse" : "misc.expand"),
)}
onPress={(e) => {
e.preventDefault();
setDescriptionExpanded((isExpanded) => !isExpanded);

View File

@ -1,6 +1,5 @@
import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg";
import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
import Download from "@material-symbols/svg-400/rounded/download.svg";
import MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg";
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg";
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";

View File

@ -77,7 +77,7 @@ export const ShowWatchStatusCard = ({
);
};
const ShowHeader = ({ children, slug, ...props }) => {
const ShowHeader = ({ children, slug, ...props }: any) => {
const { css, theme } = useYoshiki();
return (
@ -107,8 +107,7 @@ const ShowHeader = ({ children, slug, ...props }) => {
</View>
</View>
);
},
)
};
export const ShowDetails = () => {
const { css, theme } = useYoshiki();

View File

@ -4,7 +4,13 @@ import { useYoshiki } from "yoshiki/native";
import type { KyooError } from "~/models";
import { Button, H1, Link, P, ts } from "~/primitives";
export const ConnectionError = ({ error, retry }: { error: KyooError; retry: () => void }) => {
export const ConnectionError = ({
error,
retry,
}: {
error: KyooError;
retry: () => void;
}) => {
const { css } = useYoshiki();
const { t } = useTranslation();
@ -13,8 +19,17 @@ export const ConnectionError = ({ error, retry }: { error: KyooError; retry: ()
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
<P>{error?.message ?? t("errors.unknown")}</P>
<P>{t("errors.connection-tips")}</P>
<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
<Button as={Link} href="/login" text={t("errors.re-login")} {...css({ m: ts(1) })} />
<Button
onPress={retry}
text={t("errors.try-again")}
{...css({ m: ts(1) })}
/>
<Button
as={Link}
href="/login"
text={t("errors.re-login")}
{...css({ m: ts(1) })}
/>
</View>
);
};

View File

@ -3,11 +3,7 @@ import { useYoshiki } from "yoshiki/native";
import type { KyooError } from "~/models";
import { P } from "~/primitives";
export const ErrorView = ({
error,
}: {
error: KyooError;
}) => {
export const ErrorView = ({ error }: { error: KyooError }) => {
const { css } = useYoshiki();
return (

View File

@ -16,7 +16,9 @@ export const OfflineView = () => {
alignItems: "center",
})}
>
<P {...css({ color: (theme) => theme.colors.white })}>{t("errors.offline")}</P>
<P {...css({ color: (theme) => theme.colors.white })}>
{t("errors.offline")}
</P>
</View>
);
};

View File

@ -13,7 +13,12 @@ export const Unauthorized = ({ missing }: { missing: string[] }) => {
if (!account) {
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
{...css({
flexGrow: 1,
flexShrink: 1,
justifyContent: "center",
alignItems: "center",
})}
>
<P>{t("errors.needAccount")}</P>
<Button
@ -29,7 +34,12 @@ export const Unauthorized = ({ missing }: { missing: string[] }) => {
if (!account.isVerified) {
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
{...css({
flexGrow: 1,
flexShrink: 1,
justifyContent: "center",
alignItems: "center",
})}
>
<P>{t("errors.needVerification")}</P>
</View>
@ -45,7 +55,9 @@ export const Unauthorized = ({ missing }: { missing: string[] }) => {
alignItems: "center",
})}
>
<P>{t("errors.unauthorized", { permission: missing?.join(", ") ?? "" })}</P>
<P>
{t("errors.unauthorized", { permission: missing?.join(", ") ?? "" })}
</P>
</View>
);
};

View File

@ -1,9 +1,9 @@
import {
oidcLogin,
type QueryIdentifier,
type QueryPage,
type ServerInfo,
ServerInfoP,
oidcLogin,
useFetch,
} from "@kyoo/models";
import { Button, HR, Link, P, Skeleton, ts } from "@kyoo/primitives";
@ -14,12 +14,24 @@ import { useRouter } from "solito/router";
import { percent, rem, useYoshiki } from "yoshiki/native";
import { ErrorView } from "../errors";
export const OidcLogin = ({ apiUrl, hideOr }: { apiUrl?: string; hideOr?: boolean }) => {
export const OidcLogin = ({
apiUrl,
hideOr,
}: {
apiUrl?: string;
hideOr?: boolean;
}) => {
const { css } = useYoshiki();
const { t } = useTranslation();
const { data, error } = useFetch({ options: { apiUrl }, ...OidcLogin.query() });
const { data, error } = useFetch({
options: { apiUrl },
...OidcLogin.query(),
});
const btn = css({ width: { xs: percent(100), sm: percent(75) }, marginY: ts(1) });
const btn = css({
width: { xs: percent(100), sm: percent(75) },
marginY: ts(1),
});
return (
<View {...css({ alignItems: "center", marginY: ts(1) })}>
@ -86,16 +98,26 @@ export const OidcCallbackPage: QueryPage<{
hasRun.current = true;
function onError(error: string) {
router.replace({ pathname: "/login", query: { error, apiUrl } }, undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
});
router.replace(
{ pathname: "/login", query: { error, apiUrl } },
undefined,
{
experimental: {
nativeBehavior: "stack-replace",
isNestedNavigator: false,
},
},
);
}
async function run() {
const { error: loginError } = await oidcLogin(provider, code, apiUrl);
if (loginError) onError(loginError);
else {
router.replace("/", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: {
nativeBehavior: "stack-replace",
isNestedNavigator: false,
},
});
}
}

View File

@ -1,4 +1,4 @@
import Svg, { type SvgProps, Path } from "react-native-svg";
import Svg, { Path, type SvgProps } from "react-native-svg";
import { useYoshiki } from "yoshiki/native";
/* export const KyooLogo = (props: SvgProps) => ( */
@ -24,7 +24,12 @@ export const KyooLongLogo = ({
const { theme } = useYoshiki();
const textColor = theme.contrast;
return (
<Svg viewBox="49.954 131.833 318.13 108.676" height={height} width={height * 2.9272} {...props}>
<Svg
viewBox="49.954 131.833 318.13 108.676"
height={height}
width={height * 2.9272}
{...props}
>
<Path
d="m164.844 186.759-114.89-53.76v107.51l114.89-53.75Z"
fill="#121327"

View File

@ -2,7 +2,7 @@ import { NavigationContext, useRoute } from "@react-navigation/native";
import { useContext } from "react";
import type { Movie, Show } from "~/models";
export function setServerData(key: string, val: any) {}
export function setServerData(_key: string, _val: any) {}
export function getServerData(key: string) {
return key;
}

View File

@ -2,9 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": [
"./src/*"
]
"~/*": ["./src/*"]
},
"strict": true,
"rootDir": ".",
@ -16,21 +14,10 @@
"skipLibCheck": true,
"jsx": "react-jsx",
"forceConsistentCasingInFileNames": true,
"types": [
"node",
"react",
],
"lib": [
"dom",
"esnext"
]
"types": ["node", "react"],
"lib": ["dom", "esnext"]
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
],
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
"exclude": [
"node_modules",
".expo",