diff --git a/front/packages/ui/src/settings/oidc.tsx b/front/packages/ui/src/settings/oidc.tsx deleted file mode 100644 index e4c383e5..00000000 --- a/front/packages/ui/src/settings/oidc.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - -import { - type QueryIdentifier, - type ServerInfo, - ServerInfoP, - queryFn, - useAccount, - useFetch, -} from "@kyoo/models"; -import { Button, IconButton, Link, Skeleton, tooltip, ts } from "@kyoo/primitives"; -import { useTranslation } from "react-i18next"; -import { ImageBackground } from "react-native"; -import { rem, useYoshiki } from "yoshiki/native"; -import { ErrorView } from "../../../../src/ui/errors"; -import { Preference, SettingsContainer } from "./base"; - -import Badge from "@material-symbols/svg-400/outlined/badge.svg"; -import Remove from "@material-symbols/svg-400/outlined/close.svg"; -import OpenProfile from "@material-symbols/svg-400/outlined/open_in_new.svg"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -export const OidcSettings = () => { - const account = useAccount()!; - const { css } = useYoshiki(); - const { t } = useTranslation(); - const { data, error } = useFetch(OidcSettings.query()); - const queryClient = useQueryClient(); - const { mutateAsync: unlinkAccount } = useMutation({ - mutationFn: async (provider: string) => - await queryFn({ - path: ["auth", "login", provider], - method: "DELETE", - }), - onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }), - }); - - return ( - - {error ? ( - - ) : data ? ( - Object.entries(data.oidc).map(([id, x]) => { - const acc = account.externalId[id]; - return ( - - ) - } - > - {acc ? ( - <> - {acc.profileUrl && ( - - )} - unlinkAccount(id)} - {...tooltip(t("settings.oidc.delete", { provider: x.displayName }))} - /> - > - ) : ( - - )} - - ); - }) - ) : ( - [...Array(3)].map((_, i) => ( - } - icon={null!} - label={} - description={} - /> - )) - )} - - ); -}; - -OidcSettings.query = (): QueryIdentifier => ({ - path: ["info"], - parser: ServerInfoP, -}); diff --git a/front/src/app/(app)/settings.tsx b/front/src/app/(app)/settings.tsx new file mode 100644 index 00000000..3cc7b47a --- /dev/null +++ b/front/src/app/(app)/settings.tsx @@ -0,0 +1,3 @@ +import { SettingsPage } from "~/ui/settings"; + +export default SettingsPage; diff --git a/front/src/app/(public)/_layout.tsx b/front/src/app/(public)/_layout.tsx index 9ce92e8e..5125a969 100644 --- a/front/src/app/(public)/_layout.tsx +++ b/front/src/app/(public)/_layout.tsx @@ -2,7 +2,7 @@ import { Stack } from "expo-router"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTheme } from "yoshiki/native"; import { ErrorConsumer } from "~/providers/error-consumer"; -import { NavbarTitle } from "~/ui/navbar"; +import { NavbarProfile, NavbarTitle } from "~/ui/navbar"; export default function Layout() { const insets = useSafeAreaInsets(); @@ -13,6 +13,7 @@ export default function Layout() { , + headerRight: () => , contentStyle: { paddingLeft: insets.left, paddingRight: insets.right, diff --git a/front/src/models/user.ts b/front/src/models/user.ts index c8a05452..2b768ff8 100644 --- a/front/src/models/user.ts +++ b/front/src/models/user.ts @@ -2,42 +2,48 @@ import { z } from "zod/v4"; export const User = z .object({ + // // keep a default for older versions of the api + // .default({}), id: z.string(), username: z.string(), email: z.string(), - // permissions: z.array(z.string()), - // hasPassword: z.boolean().default(true), - // settings: z - // .object({ - // downloadQuality: z - // .union([ - // z.literal("original"), - // z.literal("8k"), - // z.literal("4k"), - // z.literal("1440p"), - // z.literal("1080p"), - // z.literal("720p"), - // z.literal("480p"), - // z.literal("360p"), - // z.literal("240p"), - // ]) - // .default("original") - // .catch("original"), - // audioLanguage: z.string().default("default").catch("default"), - // subtitleLanguage: z.string().nullable().default(null).catch(null), - // }) - // // keep a default for older versions of the api - // .default({}), - // externalId: z - // .record( - // z.string(), - // z.object({ - // id: z.string(), - // username: z.string().nullable().default(""), - // profileUrl: z.string().nullable(), - // }), - // ) - // .default({}), + claims: z.object({ + permissions: z.array(z.string()), + // hasPassword: z.boolean().default(true), + settings: z + .object({ + downloadQuality: z + .union([ + z.literal("original"), + z.literal("8k"), + z.literal("4k"), + z.literal("1440p"), + z.literal("1080p"), + z.literal("720p"), + z.literal("480p"), + z.literal("360p"), + z.literal("240p"), + ]) + .catch("original"), + audioLanguage: z.string().catch("default"), + subtitleLanguage: z.string().nullable().catch(null), + }) + .default({ + downloadQuality: "original", + audioLanguage: "default", + subtitleLanguage: null, + }), + // externalId: z + // .record( + // z.string(), + // z.object({ + // id: z.string(), + // username: z.string().nullable().default(""), + // profileUrl: z.string().nullable(), + // }), + // ) + // .default({}), + }), }) .transform((x) => ({ ...x, diff --git a/front/src/primitives/alert.web.tsx b/front/src/primitives/alert.web.tsx index 7bc59fb7..15d96069 100644 --- a/front/src/primitives/alert.web.tsx +++ b/front/src/primitives/alert.web.tsx @@ -1,23 +1,3 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - // Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681 import type { AlertButton, AlertOptions } from "react-native"; diff --git a/front/src/primitives/index.ts b/front/src/primitives/index.ts index 63355757..39547f3d 100644 --- a/front/src/primitives/index.ts +++ b/front/src/primitives/index.ts @@ -1,4 +1,6 @@ export { Footer, Header, Main, Nav, UL } from "@expo/html-elements"; +// export * from "./snackbar"; +export * from "./alert"; export * from "./avatar"; export * from "./button"; export * from "./chip"; @@ -9,11 +11,9 @@ export * from "./image"; export * from "./image-background"; export * from "./input"; export * from "./links"; -// export * from "./snackbar"; -// export * from "./alert"; export * from "./menu"; +export * from "./popup"; export * from "./progress"; -// export * from "./popup"; export * from "./select"; export * from "./skeleton"; export * from "./slider"; diff --git a/front/src/primitives/popup.tsx b/front/src/primitives/popup.tsx index 0be13b89..e5332b07 100644 --- a/front/src/primitives/popup.tsx +++ b/front/src/primitives/popup.tsx @@ -1,30 +1,9 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - import { usePortal } from "@gorhom/portal"; import { type ReactNode, useCallback, useEffect, useState } from "react"; import { ScrollView, View } from "react-native"; import { px, vh } from "yoshiki/native"; -import { imageBorderRadius } from "./constants"; import { Container } from "./container"; -import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./themes"; +import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./theme"; import { ts } from "./utils"; export const Popup = ({ @@ -52,7 +31,7 @@ export const Popup = ({ theme.background, overflow: "hidden", diff --git a/front/src/providers/native-providers.web.tsx b/front/src/providers/native-providers.web.tsx index 87d6bf6d..2d11ab45 100644 --- a/front/src/providers/native-providers.web.tsx +++ b/front/src/providers/native-providers.web.tsx @@ -1,5 +1,6 @@ +import { PortalProvider } from "@gorhom/portal"; import type { ReactNode } from "react"; export const NativeProviders = ({ children }: { children: ReactNode }) => { - return children; + return {children}; }; diff --git a/front/src/query/query.tsx b/front/src/query/query.tsx index ed5d1f2c..b8ea38d0 100644 --- a/front/src/query/query.tsx +++ b/front/src/query/query.tsx @@ -268,7 +268,7 @@ type MutationParams = { body?: object; }; -export const useMutation = ({ +export const useMutation = ({ compute, invalidate, optimistic, diff --git a/front/packages/ui/src/settings/account.tsx b/front/src/ui/settings/account.tsx similarity index 59% rename from front/packages/ui/src/settings/account.tsx rename to front/src/ui/settings/account.tsx index 233d7cf4..43ff61a2 100644 --- a/front/packages/ui/src/settings/account.tsx +++ b/front/src/ui/settings/account.tsx @@ -1,81 +1,67 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - -import { - type Account, - type KyooErrors, - deleteAccount, - logout, - queryFn, - useAccount, -} from "@kyoo/models"; -import { Alert, Avatar, Button, H1, Icon, Input, P, Popup, ts, usePopup } from "@kyoo/primitives"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import * as ImagePicker from "expo-image-picker"; +import Username from "@material-symbols/svg-400/outlined/badge.svg"; +import Mail from "@material-symbols/svg-400/outlined/mail.svg"; +import Password from "@material-symbols/svg-400/outlined/password.svg"; +// import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg"; +import Delete from "@material-symbols/svg-400/rounded/delete.svg"; +import Logout from "@material-symbols/svg-400/rounded/logout.svg"; +// import * as ImagePicker from "expo-image-picker"; import { type ComponentProps, useState } from "react"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { rem, useYoshiki } from "yoshiki/native"; -import { PasswordInput } from "../../../../src/ui/login/password-input"; +import type { KyooError, User } from "~/models"; +import { + Alert, + Button, + H1, + Icon, + Input, + P, + Popup, + ts, + usePopup, +} from "~/primitives"; +import { useAccount } from "~/providers/account-context"; +import { deleteAccount, logout } from "../login/logic"; +import { PasswordInput } from "../login/password-input"; import { Preference, SettingsContainer } from "./base"; +import { useMutation } from "~/query"; -import Username from "@material-symbols/svg-400/outlined/badge.svg"; -import Mail from "@material-symbols/svg-400/outlined/mail.svg"; -import Password from "@material-symbols/svg-400/outlined/password.svg"; -import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg"; -import Delete from "@material-symbols/svg-400/rounded/delete.svg"; -import Logout from "@material-symbols/svg-400/rounded/logout.svg"; - -function dataURItoBlob(dataURI: string) { - const byteString = atob(dataURI.split(",")[1]); - const ab = new ArrayBuffer(byteString.length); - const ia = new Uint8Array(ab); - for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); - } - return new Blob([ab], { type: "image/jpeg" }); -} +// function dataURItoBlob(dataURI: string) { +// const byteString = atob(dataURI.split(",")[1]); +// const ab = new ArrayBuffer(byteString.length); +// const ia = new Uint8Array(ab); +// for (let i = 0; i < byteString.length; i++) { +// ia[i] = byteString.charCodeAt(i); +// } +// return new Blob([ab], { type: "image/jpeg" }); +// } export const AccountSettings = () => { const account = useAccount()!; const { css, theme } = useYoshiki(); - const { t } = useTranslation(); const [setPopup, close] = usePopup(); + const { t } = useTranslation(); - const queryClient = useQueryClient(); const { mutateAsync } = useMutation({ - mutationFn: async (update: Partial) => - await queryFn({ - path: ["auth", "me"], - method: "PATCH", - body: update, - }), - onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }), + method: "PATCH", + path: ["auth", "users", "me"], + compute: (update: Partial) => ({ body: update }), + optimistic: (update) => ({ + ...account, + ...update, + claims: { ...account.claims, ...update.claims }, + }), + invalidate: ["auth", "users", "me"], }); + const { mutateAsync: editPassword } = useMutation({ - mutationFn: async (request: { newPassword: string; oldPassword: string }) => - await queryFn({ - path: ["auth", "password-reset"], - method: "POST", - body: request, - }), + method: "PATCH", + path: ["auth", "users", "me", "password"], + compute: (body: { oldPassword: string; newPassword: string }) => ({ + body, + }), + invalidate: null, }); return ( @@ -106,7 +92,8 @@ export const AccountSettings = () => { ], { cancelable: true, - userInterfaceStyle: theme.mode === "auto" ? "light" : theme.mode, + userInterfaceStyle: + theme.mode === "auto" ? "light" : theme.mode, icon: "warning", }, ); @@ -137,43 +124,47 @@ export const AccountSettings = () => { } /> + {/* } */} + {/* label={t("settings.account.avatar.label")} */} + {/* description={t("settings.account.avatar.description")} */} + {/* > */} + {/* { */} + {/* const img = await ImagePicker.launchImageLibraryAsync({ */} + {/* mediaTypes: ImagePicker.MediaTypeOptions.Images, */} + {/* aspect: [1, 1], */} + {/* quality: 1, */} + {/* base64: true, */} + {/* }); */} + {/* if (img.canceled || img.assets.length !== 1) return; */} + {/* const data = dataURItoBlob(img.assets[0].uri); */} + {/* const formData = new FormData(); */} + {/* formData.append("picture", data); */} + {/* await queryFn({ */} + {/* method: "POST", */} + {/* path: ["auth", "me", "logo"], */} + {/* formData, */} + {/* }); */} + {/* }} */} + {/* /> */} + {/* { */} + {/* await queryFn({ */} + {/* method: "DELETE", */} + {/* path: ["auth", "me", "logo"], */} + {/* }); */} + {/* }} */} + {/* /> */} + {/* */} } - label={t("settings.account.avatar.label")} - description={t("settings.account.avatar.description")} + icon={Mail} + label={t("settings.account.email.label")} + description={account.email} > - { - const img = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - aspect: [1, 1], - quality: 1, - base64: true, - }); - if (img.canceled || img.assets.length !== 1) return; - const data = dataURItoBlob(img.assets[0].uri); - const formData = new FormData(); - formData.append("picture", data); - await queryFn({ - method: "POST", - path: ["auth", "me", "logo"], - formData, - }); - }} - /> - { - await queryFn({ - method: "DELETE", - path: ["auth", "me", "logo"], - }); - }} - /> - - @@ -202,8 +193,10 @@ export const AccountSettings = () => { await editPassword({ oldPassword: op, newPassword: np })} + hasPassword={true} + apply={async (op, np) => + await editPassword({ oldPassword: op, newPassword: np }) + } close={close} />, ) @@ -236,7 +229,9 @@ const ChangePopup = ({ {({ css }) => ( <> - + {label} @@ -246,7 +241,13 @@ const ChangePopup = ({ value={value} onChangeText={(v) => setValue(v)} /> - + close()} @@ -289,7 +290,9 @@ const ChangePasswordPopup = ({ {({ css }) => ( <> - + {label} @@ -303,14 +306,22 @@ const ChangePasswordPopup = ({ /> )} setNewValue(v)} placeholder={t("settings.account.password.newPassword")} /> - {error && theme.colors.red })}>{error}} - + {error && ( + theme.colors.red })}>{error} + )} + close()} @@ -323,7 +334,7 @@ const ChangePasswordPopup = ({ await apply(oldValue, newValue); close(); } catch (e) { - setError((e as KyooErrors).errors[0]); + setError((e as KyooError).message); } }} {...css({ minWidth: rem(6) })} diff --git a/front/src/ui/settings/base.tsx b/front/src/ui/settings/base.tsx index d22ab1c6..55c9f77a 100644 --- a/front/src/ui/settings/base.tsx +++ b/front/src/ui/settings/base.tsx @@ -115,26 +115,37 @@ export const SettingsContainer = ({ ); }; -export const useSetting = ( +export const useSetting = ( setting: Setting, ) => { const account = useAccount(); const { mutateAsync } = useMutation({ method: "PATCH", - path: ["auth", "me"], - compute: (update: Partial) => ({ - body: { settings: { ...account!.settings, ...update } }, + path: ["auth", "users", "me"], + compute: (update: Partial) => ({ + body: { + claims: { + ...account!.claims, + settings: { ...account!.claims.settings, ...update }, + }, + }, }), optimistic: (update) => ({ - body: { ...account, settings: { ...account!.settings, ...update } }, + body: { + ...account, + claims: { + ...account!.claims, + settings: { ...account!.claims.settings, ...update }, + }, + }, }), - invalidate: ["auth", "me"], + invalidate: ["auth", "users", "me"], }); if (!account) return null; return [ - account.settings[setting], - async (value: User["settings"][Setting]) => { + account.claims.settings[setting], + async (value: User["claims"]["settings"][Setting]) => { await mutateAsync({ [setting]: value }); }, ] as const; diff --git a/front/src/ui/settings/general.tsx b/front/src/ui/settings/general.tsx index f874073b..6266eff8 100644 --- a/front/src/ui/settings/general.tsx +++ b/front/src/ui/settings/general.tsx @@ -1,4 +1,4 @@ -import Theme from "@material-symbols/svg-400/outlined/dark_mode.svg"; +// import Theme from "@material-symbols/svg-400/outlined/dark_mode.svg"; import Language from "@material-symbols/svg-400/outlined/language.svg"; import Android from "@material-symbols/svg-400/rounded/android.svg"; import Public from "@material-symbols/svg-400/rounded/public.svg"; diff --git a/front/src/ui/settings/index.tsx b/front/src/ui/settings/index.tsx index 3955ae3a..f1865a6a 100644 --- a/front/src/ui/settings/index.tsx +++ b/front/src/ui/settings/index.tsx @@ -1,18 +1,18 @@ import { ScrollView } from "react-native"; import { ts } from "~/primitives"; import { useAccount } from "~/providers/account-context"; -// import { AccountSettings } from "./account"; +import { AccountSettings } from "./account"; import { About, GeneralSettings } from "./general"; // import { OidcSettings } from "./oidc"; -// import { PlaybackSettings } from "./playback"; +import { PlaybackSettings } from "./playback"; export const SettingsPage = () => { const account = useAccount(); return ( - {/* {account && } */} - {/* {account && } */} + {account && } + {account && } {/* {account && } */} diff --git a/front/src/ui/settings/oidc.tsx b/front/src/ui/settings/oidc.tsx new file mode 100644 index 00000000..0f18fd60 --- /dev/null +++ b/front/src/ui/settings/oidc.tsx @@ -0,0 +1,108 @@ +// import { +// type QueryIdentifier, +// type ServerInfo, +// ServerInfoP, +// queryFn, +// useAccount, +// useFetch, +// } from "@kyoo/models"; +// import { Button, IconButton, Link, Skeleton, tooltip, ts } from "@kyoo/primitives"; +// import { useTranslation } from "react-i18next"; +// import { ImageBackground } from "react-native"; +// import { rem, useYoshiki } from "yoshiki/native"; +// import { ErrorView } from "../errors"; +// import { Preference, SettingsContainer } from "./base"; +// +// import Badge from "@material-symbols/svg-400/outlined/badge.svg"; +// import Remove from "@material-symbols/svg-400/outlined/close.svg"; +// import OpenProfile from "@material-symbols/svg-400/outlined/open_in_new.svg"; +// import { useMutation, useQueryClient } from "@tanstack/react-query"; +// +// export const OidcSettings = () => { +// const account = useAccount()!; +// const { css } = useYoshiki(); +// const { t } = useTranslation(); +// const { data, error } = useFetch(OidcSettings.query()); +// const queryClient = useQueryClient(); +// const { mutateAsync: unlinkAccount } = useMutation({ +// mutationFn: async (provider: string) => +// await queryFn({ +// path: ["auth", "login", provider], +// method: "DELETE", +// }), +// onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }), +// }); +// +// return ( +// +// {error ? ( +// +// ) : data ? ( +// Object.entries(data.oidc).map(([id, x]) => { +// const acc = account.externalId[id]; +// return ( +// +// ) +// } +// > +// {acc ? ( +// <> +// {acc.profileUrl && ( +// +// )} +// unlinkAccount(id)} +// {...tooltip(t("settings.oidc.delete", { provider: x.displayName }))} +// /> +// > +// ) : ( +// +// )} +// +// ); +// }) +// ) : ( +// [...Array(3)].map((_, i) => ( +// } +// icon={null!} +// label={} +// description={} +// /> +// )) +// )} +// +// ); +// }; +// +// OidcSettings.query = (): QueryIdentifier => ({ +// path: ["info"], +// parser: ServerInfoP, +// }); diff --git a/front/packages/ui/src/settings/playback.tsx b/front/src/ui/settings/playback.tsx similarity index 66% rename from front/packages/ui/src/settings/playback.tsx rename to front/src/ui/settings/playback.tsx index 36fb143c..8975bf6f 100644 --- a/front/packages/ui/src/settings/playback.tsx +++ b/front/src/ui/settings/playback.tsx @@ -1,32 +1,26 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - -import { languageCodes, useLanguageName } from "../utils"; -import { Preference, SettingsContainer, useSetting } from "./base"; - -import { useLocalSetting } from "@kyoo/models"; -import { Select } from "@kyoo/primitives"; import SubtitleLanguage from "@material-symbols/svg-400/rounded/closed_caption-fill.svg"; import PlayModeI from "@material-symbols/svg-400/rounded/display_settings-fill.svg"; import AudioLanguage from "@material-symbols/svg-400/rounded/music_note-fill.svg"; import { useTranslation } from "react-i18next"; +import { Select } from "~/primitives"; +import { useLocalSetting } from "~/providers/settings"; +import { useLanguageName } from "~/track-utils"; +import { Preference, SettingsContainer, useSetting } from "./base"; +import langmap from "langmap"; + +const seenNativeNames = new Set(); +export const languageCodes = Object.keys(langmap) + .filter((x) => { + const nativeName = langmap[x]?.nativeName; + + // Only include if nativeName is unique and defined + if (nativeName && !seenNativeNames.has(nativeName)) { + seenNativeNames.add(nativeName); + return true; + } + return false; + }) + .filter((x) => !x.includes("@")); export const PlaybackSettings = () => { const { t } = useTranslation(); @@ -61,7 +55,9 @@ export const PlaybackSettings = () => { onValueChange={(value) => setAudio(value)} values={["default", ...languageCodes]} getLabel={(key) => - key === "default" ? t("mediainfo.default") : (getLanguageName(key) ?? key) + key === "default" + ? t("mediainfo.default") + : (getLanguageName(key) ?? key) } /> @@ -73,7 +69,9 @@ export const PlaybackSettings = () => { setSubtitle(value === "none" ? null : value)} + onValueChange={(value) => + setSubtitle(value === "none" ? null : value) + } values={["none", "default", ...languageCodes]} getLabel={(key) => key === "none"
theme.colors.red })}>{error}