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 { Elysia, t } from "elysia";
import { auth } from "~/auth"; import { auth } from "~/auth";
import { db } from "~/db"; import { db } from "~/db";
@ -31,14 +31,14 @@ import { KError } from "~/models/error";
import { madeInAbyss } from "~/models/examples"; import { madeInAbyss } from "~/models/examples";
import { import {
AcceptLanguage, AcceptLanguage,
createPage,
Filter, Filter,
type FilterDef, type FilterDef,
Page,
Sort,
createPage,
isUuid, isUuid,
keysetPaginate, keysetPaginate,
Page,
processLanguages, processLanguages,
Sort,
sortToSql, sortToSql,
} from "~/models/utils"; } from "~/models/utils";
import { desc as description } from "~/models/utils/descriptions"; 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 { SeedHistory } from "~/models/history";
import { import {
AcceptLanguage, AcceptLanguage,
Filter,
Page,
createPage, createPage,
Filter,
isUuid, isUuid,
Page,
processLanguages, processLanguages,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; import { desc } from "~/models/utils/descriptions";

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import path from "node:path"; import path from "node:path";
import { encode } from "blurhash"; 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 { PgColumn, type PgTable } from "drizzle-orm/pg-core";
import { version } from "package.json"; import { version } from "package.json";
import type { PoolClient } from "pg"; import type { PoolClient } from "pg";
import sharp from "sharp"; import sharp from "sharp";
import { type Transaction, db } from "~/db"; import { db, type Transaction } from "~/db";
import { mqueue } from "~/db/schema/mqueue"; import { mqueue } from "~/db/schema/mqueue";
import type { Image } from "~/models/utils"; import type { Image } from "~/models/utils";
import { getFile } from "~/utils"; import { getFile } from "~/utils";

View File

@ -1,6 +1,6 @@
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { db } from "~/db"; import { db } from "~/db";
import { showTranslations, shows } from "~/db/schema"; import { shows, showTranslations } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils"; import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedCollection } from "~/models/collections"; import type { SeedCollection } from "~/models/collections";
import type { SeedMovie } from "~/models/movie"; 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 { db } from "~/db";
import { import {
entries, entries,

View File

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

View File

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

View File

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

View File

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

View File

@ -16,10 +16,10 @@ import { Serie } from "~/models/serie";
import { Show } from "~/models/show"; import { Show } from "~/models/show";
import { import {
AcceptLanguage, AcceptLanguage,
Filter,
Page,
createPage, createPage,
Filter,
isUuid, isUuid,
Page,
processLanguages, processLanguages,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; 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 { db } from "~/db";
import { import {
entries, entries,

View File

@ -9,10 +9,10 @@ import { bubble } from "~/models/examples";
import { FullMovie, Movie, MovieTranslation } from "~/models/movie"; import { FullMovie, Movie, MovieTranslation } from "~/models/movie";
import { import {
AcceptLanguage, AcceptLanguage,
Filter,
Page,
createPage, createPage,
Filter,
isUuid, isUuid,
Page,
processLanguages, processLanguages,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; 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 { FullSerie, Serie, SerieTranslation } from "~/models/serie";
import { import {
AcceptLanguage, AcceptLanguage,
Filter,
Page,
createPage, createPage,
Filter,
isUuid, isUuid,
Page,
processLanguages, processLanguages,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; import { desc } from "~/models/utils/descriptions";

View File

@ -8,9 +8,9 @@ import { KError } from "~/models/error";
import { Show } from "~/models/show"; import { Show } from "~/models/show";
import { import {
AcceptLanguage, AcceptLanguage,
createPage,
Filter, Filter,
Page, Page,
createPage,
processLanguages, processLanguages,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; 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 Elysia, { t } from "elysia";
import { auth } from "~/auth"; import { auth } from "~/auth";
import { prefix } from "~/base"; import { prefix } from "~/base";
import { db } from "~/db"; 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 { roles, staff } from "~/db/schema/staff";
import { watchlist } from "~/db/schema/watchlist"; import { watchlist } from "~/db/schema/watchlist";
import { getColumns, jsonbBuildObject, sqlarr } from "~/db/utils"; 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 { RoleWShow, RoleWStaff } from "~/models/staff-roles";
import { import {
AcceptLanguage, AcceptLanguage,
createPage,
Filter, Filter,
type FilterDef, type FilterDef,
type Image, type Image,
Page,
Sort,
createPage,
isUuid, isUuid,
keysetPaginate, keysetPaginate,
Page,
processLanguages, processLanguages,
Sort,
sortToSql, sortToSql,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; 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 Elysia, { t } from "elysia";
import { auth } from "~/auth"; import { auth } from "~/auth";
import { prefix } from "~/base"; import { prefix } from "~/base";
@ -6,8 +6,8 @@ import { db } from "~/db";
import { import {
showStudioJoin, showStudioJoin,
shows, shows,
studioTranslations,
studios, studios,
studioTranslations,
} from "~/db/schema"; } from "~/db/schema";
import { import {
getColumns, getColumns,
@ -22,14 +22,14 @@ import { Show } from "~/models/show";
import { Studio, StudioTranslation } from "~/models/studio"; import { Studio, StudioTranslation } from "~/models/studio";
import { import {
AcceptLanguage, AcceptLanguage,
Filter,
Page,
Sort,
buildRelations, buildRelations,
createPage, createPage,
Filter,
isUuid, isUuid,
keysetPaginate, keysetPaginate,
Page,
processLanguages, processLanguages,
Sort,
sortToSql, sortToSql,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; import { desc } from "~/models/utils/descriptions";

View File

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

View File

@ -1,9 +1,9 @@
export * from "./entries"; 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 "./history";
export * from "./mqueue"; 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 { t } from "elysia";
import { type Prettify, comment } from "~/utils"; import { comment, type Prettify } from "~/utils";
import { madeInAbyss, registerExamples } from "../examples"; import { madeInAbyss, registerExamples } from "../examples";
import { Progress } from "../history"; import { Progress } from "../history";
import { DbMetadata, SeedImage } from "../utils"; 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 type EntryKind = Entry["kind"] | Extra["kind"];
export * from "./episode"; export * from "./episode";
export * from "./extra";
export * from "./movie-entry"; export * from "./movie-entry";
export * from "./special"; export * from "./special";
export * from "./extra";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import {
} from "tests/helpers"; } from "tests/helpers";
import { expectStatus } from "tests/utils"; import { expectStatus } from "tests/utils";
import { db } from "~/db"; 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"; import { bubble, madeInAbyss } from "~/models/examples";
beforeAll(async () => { beforeAll(async () => {

View File

@ -21,13 +21,18 @@
}, },
"suspicious": { "suspicious": {
"noExplicitAny": "off", "noExplicitAny": "off",
"noArrayIndexKey": "off" "noArrayIndexKey": "off",
"noTemplateCurlyInString": "off"
}, },
"security": { "security": {
"noDangerouslySetInnerHtml": "off" "noDangerouslySetInnerHtml": "off"
}, },
"complexity": { "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> <head>
<title>Kyoo</title> <title>Kyoo</title>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="description" content="A portable and vast media library solution." /> <meta
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 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" /> <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<link rel="icon" type="image/png" sizes="16x16" href="/icon-16x16.png" /> <link
<link rel="icon" type="image/png" sizes="32x32" href="/icon-32x32.png" /> rel="icon"
<link rel="icon" type="image/png" sizes="64x64" href="/icon-64x64.png" /> type="image/png"
<link rel="icon" type="image/png" sizes="128x128" href="/icon-128x128.png" /> sizes="16x16"
<link rel="icon" type="image/png" sizes="256x256" href="/icon-256x256.png" /> 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 /> <ScrollViewStyleReset />
</head> </head>

View File

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

View File

@ -14,7 +14,8 @@ export const ItemWatchStatus = ({
}) => { }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
if (watchStatus !== WatchStatusV.Completed && !unseenEpisodesCount) return null; if (watchStatus !== WatchStatusV.Completed && !unseenEpisodesCount)
return null;
return ( return (
<View <View
@ -38,7 +39,13 @@ export const ItemWatchStatus = ({
{watchStatus === WatchStatusV.Completed ? ( {watchStatus === WatchStatusV.Completed ? (
<Icon icon={Done} size={16} /> <Icon icon={Done} size={16} />
) : ( ) : (
<P {...css({ marginVertical: 0, verticalAlign: "middle", textAlign: "center" })}> <P
{...css({
marginVertical: 0,
verticalAlign: "middle",
textAlign: "center",
})}
>
{unseenEpisodesCount} {unseenEpisodesCount}
</P> </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 BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
import BookmarkAdded from "@material-symbols/svg-400/rounded/bookmark_added-fill.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 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 type { ComponentProps } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { Serie } from "~/models"; import type { Serie } from "~/models";
@ -10,7 +10,13 @@ import { useAccount } from "~/providers/account-context";
import { useMutation } from "~/query"; import { useMutation } from "~/query";
type WatchStatus = NonNullable<Serie["watchStatus"]>["status"]; 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) => { export const watchListIcon = (status: WatchStatus | null) => {
switch (status) { switch (status) {
@ -51,7 +57,12 @@ export const WatchListInfo = ({
if (account == null) { if (account == null) {
return ( 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} 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> </Menu>
); );
default: default:

View File

@ -1,15 +1,12 @@
export * from "./utils/page"; export * from "./collection";
export * from "./entry";
export * from "./kyoo-error"; export * from "./kyoo-error";
export * from "./movie"; export * from "./movie";
export * from "./serie"; export * from "./serie";
export * from "./collection";
export * from "./show"; export * from "./show";
export * from "./entry";
export * from "./studio"; export * from "./studio";
export * from "./video";
export * from "./user"; export * from "./user";
export * from "./utils/images";
export * from "./utils/genre"; 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(), watchStatus: WatchStatusP.optional().nullable(),
}), }),
).transform((x) => { ).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; 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 // from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
const humanFileSize = (size: number): string => { const humanFileSize = (size: number): string => {
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); 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. // @ts-ignore I'm not gonna fix stackoverflow's working code.
// biome-ignore lint: same as above // biome-ignore lint/style/useTemplate: same as above
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i]; (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 Show = z.infer<typeof Show>;
export type WatchStatusV = NonNullable<Serie["watchStatus"]>["status"]; 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 // 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"; import type { SweetAlertIcon } from "sweetalert2";
export interface ExtendedAlertStatic { export interface ExtendedAlertStatic {

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { HR as EHR } from "@expo/html-elements"; 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"; import { ts } from "./utils";
export const HR = ({ export const HR = ({

View File

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

View File

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

View File

@ -1,6 +1,11 @@
import { type ReactNode, forwardRef, useState } from "react"; import { forwardRef, type ReactNode, useState } from "react";
import { TextInput, type TextInputProps, View, type ViewStyle } from "react-native"; import {
import { type Theme, px, useYoshiki } from "yoshiki/native"; 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 type { YoshikiEnhanced } from "./image/base-image";
import { focusReset, ts } from "./utils"; 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 { useRouter } from "expo-router";
import { import {
type ComponentType, type ComponentType,
createContext,
type ReactElement, type ReactElement,
type ReactNode, type ReactNode,
createContext,
useContext, useContext,
useEffect, useEffect,
useRef, useRef,
@ -22,7 +22,9 @@ import { P } from "./text";
import { ContrastArea, SwitchVariant } from "./theme"; import { ContrastArea, SwitchVariant } from "./theme";
import { ts } from "./utils"; 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>; type Optional<T, K extends keyof any> = Omit<T, K> & Partial<T>;
@ -45,7 +47,10 @@ const Menu = <AsProps,>({
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const alreadyRendered = useRef(false); const alreadyRendered = useRef(false);
const [isOpen, setOpen] = 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. // does the same as a useMemo but for props.
const memoRef = useRef({ onMenuOpen, onMenuClose }); const memoRef = useRef({ onMenuOpen, onMenuClose });
@ -73,7 +78,11 @@ const Menu = <AsProps,>({
<Pressable <Pressable
onPress={() => setOpen(false)} onPress={() => setOpen(false)}
tabIndex={-1} tabIndex={-1}
{...css({ ...StyleSheet.absoluteFillObject, flexGrow: 1, bg: "transparent" })} {...css({
...StyleSheet.absoluteFillObject,
flexGrow: 1,
bg: "transparent",
})}
/> />
<View <View
{...css([ {...css([
@ -107,7 +116,10 @@ const Menu = <AsProps,>({
icon={Close} icon={Close}
color={theme.colors.black} color={theme.colors.black}
onPress={() => setOpen(false)} onPress={() => setOpen(false)}
{...css({ alignSelf: "flex-end", display: { xs: "none", xl: "flex" } })} {...css({
alignSelf: "flex-end",
display: { xs: "none", xl: "flex" },
})}
/> />
{children} {children}
</ScrollView> </ScrollView>
@ -137,7 +149,10 @@ const MenuItem = ({
left?: ReactElement; left?: ReactElement;
disabled?: boolean; disabled?: boolean;
icon?: ComponentType<SvgProps>; icon?: ComponentType<SvgProps>;
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => { } & (
| { onSelect: () => void; href?: undefined }
| { href: string; onSelect?: undefined }
)) => {
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
const setOpen = useContext(MenuContext); const setOpen = useContext(MenuContext);
const router = useRouter(); const router = useRouter();
@ -174,7 +189,10 @@ const MenuItem = ({
{!left && icn && icn} {!left && icn && icn}
<P <P
{...css([ {...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 }, disabled && { color: theme.overlay0 },
])} ])}
> >

View File

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

View File

@ -20,7 +20,7 @@
import { ActivityIndicator, Platform, View } from "react-native"; import { ActivityIndicator, Platform, View } from "react-native";
import { Circle, Svg } from "react-native-svg"; 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 = ({ export const CircularProgress = ({
size = 48, size = 48,
@ -31,7 +31,9 @@ export const CircularProgress = ({
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
if (Platform.OS !== "web") if (Platform.OS !== "web")
return <ActivityIndicator size={size} color={color ?? theme.accent} {...props} />; return (
<ActivityIndicator size={size} color={color ?? theme.accent} {...props} />
);
return ( return (
<View {...css({ width: size, height: size, overflow: "hidden" }, props)}> <View {...css({ width: size, height: size, overflow: "hidden" }, props)}>
@ -63,7 +65,9 @@ export const CircularProgress = ({
viewBox={`${size / 2} ${size / 2} ${size} ${size}`} viewBox={`${size / 2} ${size / 2} ${size} ${size}`}
{...css( {...css(
// @ts-ignore Web only // @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 <Circle

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { type GestureResponderEvent, Platform, View } from "react-native"; import { type GestureResponderEvent, Platform, View } from "react-native";
import type { ViewProps } from "react-native-svg/lib/typescript/fabric/utils"; 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"; import { focusReset } from "./utils";
export const Slider = ({ export const Slider = ({
@ -80,7 +80,10 @@ export const Slider = ({
}} }}
// @ts-ignore Web only // @ts-ignore Web only
onMouseMove={(e) => 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} tabIndex={0}
onFocus={() => setFocus(true)} onFocus={() => setFocus(true)}

View File

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

View File

@ -8,7 +8,13 @@ import {
P as EP, P as EP,
} from "@expo/html-elements"; } from "@expo/html-elements";
import type { ComponentProps, ComponentType } from "react"; 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 { percent, rem, useYoshiki } from "yoshiki/native";
import { ts } from "./utils/spacing"; import { ts } from "./utils/spacing";
@ -55,7 +61,10 @@ const styleText = (
return Text; 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 H2 = styleText(EH2, "header", { fontSize: rem(2) });
export const H3 = styleText(EH3, "header"); export const H3 = styleText(EH3, "header");
export const H4 = styleText(EH4, "header"); export const H4 = styleText(EH4, "header");

View File

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

View File

@ -1,7 +1,11 @@
import type { Property } from "csstype"; import type { Property } from "csstype";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { Platform, type TextStyle } from "react-native"; 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 "yoshiki";
import { ThemeProvider, useTheme, useYoshiki } from "yoshiki/native"; import { ThemeProvider, useTheme, useYoshiki } from "yoshiki/native";
import "yoshiki/native"; import "yoshiki/native";
@ -52,7 +56,9 @@ declare module "yoshiki" {
export type { Theme } from "yoshiki"; export type { Theme } from "yoshiki";
export type ThemeBuilder = { 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 }; 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 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 { return {
...options, ...options,
...auto, ...auto,
@ -136,12 +147,20 @@ export const ThemeSelector = ({
export type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T; export type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T;
const YoshikiProvider = ({ children }: { children: YoshikiFunc<ReactNode> }) => { const YoshikiProvider = ({
children,
}: {
children: YoshikiFunc<ReactNode>;
}) => {
const yoshiki = useYoshiki(); const yoshiki = useYoshiki();
return <>{children(yoshiki)}</>; return <>{children(yoshiki)}</>;
}; };
export const SwitchVariant = ({ children }: { children: ReactNode | YoshikiFunc<ReactNode> }) => { export const SwitchVariant = ({
children,
}: {
children: ReactNode | YoshikiFunc<ReactNode>;
}) => {
const theme = useTheme(); const theme = useTheme();
return ( return (

View File

@ -1,7 +1,12 @@
import { useWindowDimensions } from "react-native"; 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>>; export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>;
// copied from yoshiki. // copied from yoshiki.
@ -36,6 +41,9 @@ export const useBreakpointMap = <T extends Record<string, unknown>>(
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
// @ts-ignore // @ts-ignore
return Object.fromEntries( 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 "./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"; 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 = () => ( export const HiddenIfNoJs = () => (
<noscript> <noscript>

View File

@ -14,10 +14,16 @@ export const TouchOnlyCss = () => {
}; };
export const touchOnly: ViewProps = { 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 = { 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 = () => { export const useIsTouch = () => {

View File

@ -2,7 +2,13 @@ import { type ReactNode, useContext } from "react";
import { ErrorView, errorHandlers } from "~/ui/errors"; import { ErrorView, errorHandlers } from "~/ui/errors";
import { ErrorContext } from "./error-provider"; 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); const error = useContext(ErrorContext);
if (!error) return children; if (!error) return children;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
export * from "./query";
export * from "./fetch"; export * from "./fetch";
export * from "./fetch-infinite"; 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 SortBy = (typeof availableSorts)[number];
export type SortOrd = "asc" | "desc"; export type SortOrd = "asc" | "desc";

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg"; import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg";
import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.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 MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg";
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; import MovieInfo from "@material-symbols/svg-400/rounded/movie_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";

View File

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

View File

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

View File

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

View File

@ -16,7 +16,9 @@ export const OfflineView = () => {
alignItems: "center", 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>
); );
}; };

View File

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

View File

@ -1,9 +1,9 @@
import { import {
oidcLogin,
type QueryIdentifier, type QueryIdentifier,
type QueryPage, type QueryPage,
type ServerInfo, type ServerInfo,
ServerInfoP, ServerInfoP,
oidcLogin,
useFetch, useFetch,
} from "@kyoo/models"; } from "@kyoo/models";
import { Button, HR, Link, P, Skeleton, ts } from "@kyoo/primitives"; 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 { percent, rem, useYoshiki } from "yoshiki/native";
import { ErrorView } from "../errors"; 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 { css } = useYoshiki();
const { t } = useTranslation(); 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 ( return (
<View {...css({ alignItems: "center", marginY: ts(1) })}> <View {...css({ alignItems: "center", marginY: ts(1) })}>
@ -86,16 +98,26 @@ export const OidcCallbackPage: QueryPage<{
hasRun.current = true; hasRun.current = true;
function onError(error: string) { function onError(error: string) {
router.replace({ pathname: "/login", query: { error, apiUrl } }, undefined, { router.replace(
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, { pathname: "/login", query: { error, apiUrl } },
}); undefined,
{
experimental: {
nativeBehavior: "stack-replace",
isNestedNavigator: false,
},
},
);
} }
async function run() { async function run() {
const { error: loginError } = await oidcLogin(provider, code, apiUrl); const { error: loginError } = await oidcLogin(provider, code, apiUrl);
if (loginError) onError(loginError); if (loginError) onError(loginError);
else { else {
router.replace("/", undefined, { 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"; import { useYoshiki } from "yoshiki/native";
/* export const KyooLogo = (props: SvgProps) => ( */ /* export const KyooLogo = (props: SvgProps) => ( */
@ -24,7 +24,12 @@ export const KyooLongLogo = ({
const { theme } = useYoshiki(); const { theme } = useYoshiki();
const textColor = theme.contrast; const textColor = theme.contrast;
return ( 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 <Path
d="m164.844 186.759-114.89-53.76v107.51l114.89-53.75Z" d="m164.844 186.759-114.89-53.76v107.51l114.89-53.75Z"
fill="#121327" fill="#121327"

View File

@ -2,7 +2,7 @@ import { NavigationContext, useRoute } from "@react-navigation/native";
import { useContext } from "react"; import { useContext } from "react";
import type { Movie, Show } from "~/models"; 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) { export function getServerData(key: string) {
return key; return key;
} }

View File

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