mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-06 06:36:25 +00:00
Rework settings page
This commit is contained in:
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<SettingsContainer title={t("settings.oidc.label")}>
|
|
||||||
{error ? (
|
|
||||||
<ErrorView error={error} />
|
|
||||||
) : data ? (
|
|
||||||
Object.entries(data.oidc).map(([id, x]) => {
|
|
||||||
const acc = account.externalId[id];
|
|
||||||
return (
|
|
||||||
<Preference
|
|
||||||
key={x.displayName}
|
|
||||||
icon={Badge}
|
|
||||||
label={x.displayName}
|
|
||||||
description={
|
|
||||||
acc
|
|
||||||
? t("settings.oidc.connected", { username: acc.username })
|
|
||||||
: t("settings.oidc.not-connected")
|
|
||||||
}
|
|
||||||
customIcon={
|
|
||||||
x.logoUrl != null && (
|
|
||||||
<ImageBackground
|
|
||||||
source={{ uri: x.logoUrl }}
|
|
||||||
{...css({ width: ts(3), height: ts(3), marginRight: ts(2) })}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{acc ? (
|
|
||||||
<>
|
|
||||||
{acc.profileUrl && (
|
|
||||||
<IconButton
|
|
||||||
icon={OpenProfile}
|
|
||||||
as={Link}
|
|
||||||
href={acc.profileUrl}
|
|
||||||
target="_blank"
|
|
||||||
{...tooltip(t("settings.oidc.open-profile", { provider: x.displayName }))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
icon={Remove}
|
|
||||||
onPress={() => unlinkAccount(id)}
|
|
||||||
{...tooltip(t("settings.oidc.delete", { provider: x.displayName }))}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
text={t("settings.oidc.link")}
|
|
||||||
as={Link}
|
|
||||||
href={x.link}
|
|
||||||
{...css({ minWidth: rem(6) })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Preference>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
[...Array(3)].map((_, i) => (
|
|
||||||
<Preference
|
|
||||||
key={i}
|
|
||||||
customIcon={<Skeleton {...css({ width: ts(3), height: ts(3) })} />}
|
|
||||||
icon={null!}
|
|
||||||
label={<Skeleton {...css({ width: rem(6) })} />}
|
|
||||||
description={<Skeleton {...css({ width: rem(7), height: rem(0.8) })} />}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SettingsContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
OidcSettings.query = (): QueryIdentifier<ServerInfo> => ({
|
|
||||||
path: ["info"],
|
|
||||||
parser: ServerInfoP,
|
|
||||||
});
|
|
||||||
3
front/src/app/(app)/settings.tsx
Normal file
3
front/src/app/(app)/settings.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { SettingsPage } from "~/ui/settings";
|
||||||
|
|
||||||
|
export default SettingsPage;
|
||||||
@@ -2,7 +2,7 @@ import { Stack } from "expo-router";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useTheme } from "yoshiki/native";
|
import { useTheme } from "yoshiki/native";
|
||||||
import { ErrorConsumer } from "~/providers/error-consumer";
|
import { ErrorConsumer } from "~/providers/error-consumer";
|
||||||
import { NavbarTitle } from "~/ui/navbar";
|
import { NavbarProfile, NavbarTitle } from "~/ui/navbar";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -13,6 +13,7 @@ export default function Layout() {
|
|||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerTitle: () => <NavbarTitle />,
|
headerTitle: () => <NavbarTitle />,
|
||||||
|
headerRight: () => <NavbarProfile />,
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
paddingLeft: insets.left,
|
paddingLeft: insets.left,
|
||||||
paddingRight: insets.right,
|
paddingRight: insets.right,
|
||||||
|
|||||||
@@ -2,42 +2,48 @@ import { z } from "zod/v4";
|
|||||||
|
|
||||||
export const User = z
|
export const User = z
|
||||||
.object({
|
.object({
|
||||||
|
// // keep a default for older versions of the api
|
||||||
|
// .default({}),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
// permissions: z.array(z.string()),
|
claims: z.object({
|
||||||
// hasPassword: z.boolean().default(true),
|
permissions: z.array(z.string()),
|
||||||
// settings: z
|
// hasPassword: z.boolean().default(true),
|
||||||
// .object({
|
settings: z
|
||||||
// downloadQuality: z
|
.object({
|
||||||
// .union([
|
downloadQuality: z
|
||||||
// z.literal("original"),
|
.union([
|
||||||
// z.literal("8k"),
|
z.literal("original"),
|
||||||
// z.literal("4k"),
|
z.literal("8k"),
|
||||||
// z.literal("1440p"),
|
z.literal("4k"),
|
||||||
// z.literal("1080p"),
|
z.literal("1440p"),
|
||||||
// z.literal("720p"),
|
z.literal("1080p"),
|
||||||
// z.literal("480p"),
|
z.literal("720p"),
|
||||||
// z.literal("360p"),
|
z.literal("480p"),
|
||||||
// z.literal("240p"),
|
z.literal("360p"),
|
||||||
// ])
|
z.literal("240p"),
|
||||||
// .default("original")
|
])
|
||||||
// .catch("original"),
|
.catch("original"),
|
||||||
// audioLanguage: z.string().default("default").catch("default"),
|
audioLanguage: z.string().catch("default"),
|
||||||
// subtitleLanguage: z.string().nullable().default(null).catch(null),
|
subtitleLanguage: z.string().nullable().catch(null),
|
||||||
// })
|
})
|
||||||
// // keep a default for older versions of the api
|
.default({
|
||||||
// .default({}),
|
downloadQuality: "original",
|
||||||
// externalId: z
|
audioLanguage: "default",
|
||||||
// .record(
|
subtitleLanguage: null,
|
||||||
// z.string(),
|
}),
|
||||||
// z.object({
|
// externalId: z
|
||||||
// id: z.string(),
|
// .record(
|
||||||
// username: z.string().nullable().default(""),
|
// z.string(),
|
||||||
// profileUrl: z.string().nullable(),
|
// z.object({
|
||||||
// }),
|
// id: z.string(),
|
||||||
// )
|
// username: z.string().nullable().default(""),
|
||||||
// .default({}),
|
// profileUrl: z.string().nullable(),
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// .default({}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.transform((x) => ({
|
.transform((x) => ({
|
||||||
...x,
|
...x,
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 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, AlertOptions } from "react-native";
|
import type { AlertButton, AlertOptions } from "react-native";
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export { Footer, Header, Main, Nav, UL } from "@expo/html-elements";
|
export { Footer, Header, Main, Nav, UL } from "@expo/html-elements";
|
||||||
|
// export * from "./snackbar";
|
||||||
|
export * from "./alert";
|
||||||
export * from "./avatar";
|
export * from "./avatar";
|
||||||
export * from "./button";
|
export * from "./button";
|
||||||
export * from "./chip";
|
export * from "./chip";
|
||||||
@@ -9,11 +11,9 @@ export * from "./image";
|
|||||||
export * from "./image-background";
|
export * from "./image-background";
|
||||||
export * from "./input";
|
export * from "./input";
|
||||||
export * from "./links";
|
export * from "./links";
|
||||||
// export * from "./snackbar";
|
|
||||||
// export * from "./alert";
|
|
||||||
export * from "./menu";
|
export * from "./menu";
|
||||||
|
export * from "./popup";
|
||||||
export * from "./progress";
|
export * from "./progress";
|
||||||
// export * from "./popup";
|
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
export * from "./skeleton";
|
export * from "./skeleton";
|
||||||
export * from "./slider";
|
export * from "./slider";
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { usePortal } from "@gorhom/portal";
|
import { usePortal } from "@gorhom/portal";
|
||||||
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
||||||
import { ScrollView, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
import { px, vh } from "yoshiki/native";
|
import { px, vh } from "yoshiki/native";
|
||||||
import { imageBorderRadius } from "./constants";
|
|
||||||
import { Container } from "./container";
|
import { Container } from "./container";
|
||||||
import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./themes";
|
import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./theme";
|
||||||
import { ts } from "./utils";
|
import { ts } from "./utils";
|
||||||
|
|
||||||
export const Popup = ({
|
export const Popup = ({
|
||||||
@@ -52,7 +31,7 @@ export const Popup = ({
|
|||||||
<Container
|
<Container
|
||||||
{...css(
|
{...css(
|
||||||
{
|
{
|
||||||
borderRadius: px(imageBorderRadius),
|
borderRadius: px(6),
|
||||||
paddingHorizontal: 0,
|
paddingHorizontal: 0,
|
||||||
bg: (theme) => theme.background,
|
bg: (theme) => theme.background,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { PortalProvider } from "@gorhom/portal";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export const NativeProviders = ({ children }: { children: ReactNode }) => {
|
export const NativeProviders = ({ children }: { children: ReactNode }) => {
|
||||||
return children;
|
return <PortalProvider>{children}</PortalProvider>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ type MutationParams = {
|
|||||||
body?: object;
|
body?: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMutation = <T = void, QueryRet>({
|
export const useMutation = <T = void, QueryRet = void>({
|
||||||
compute,
|
compute,
|
||||||
invalidate,
|
invalidate,
|
||||||
optimistic,
|
optimistic,
|
||||||
|
|||||||
@@ -1,81 +1,67 @@
|
|||||||
/*
|
import Username from "@material-symbols/svg-400/outlined/badge.svg";
|
||||||
* Kyoo - A portable and vast media library solution.
|
import Mail from "@material-symbols/svg-400/outlined/mail.svg";
|
||||||
* Copyright (c) Kyoo.
|
import Password from "@material-symbols/svg-400/outlined/password.svg";
|
||||||
*
|
// import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
|
||||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
|
||||||
*
|
import Logout from "@material-symbols/svg-400/rounded/logout.svg";
|
||||||
* Kyoo is free software: you can redistribute it and/or modify
|
// import * as ImagePicker from "expo-image-picker";
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 { type ComponentProps, useState } from "react";
|
import { type ComponentProps, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { rem, useYoshiki } from "yoshiki/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 { Preference, SettingsContainer } from "./base";
|
||||||
|
import { useMutation } from "~/query";
|
||||||
|
|
||||||
import Username from "@material-symbols/svg-400/outlined/badge.svg";
|
// function dataURItoBlob(dataURI: string) {
|
||||||
import Mail from "@material-symbols/svg-400/outlined/mail.svg";
|
// const byteString = atob(dataURI.split(",")[1]);
|
||||||
import Password from "@material-symbols/svg-400/outlined/password.svg";
|
// const ab = new ArrayBuffer(byteString.length);
|
||||||
import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
|
// const ia = new Uint8Array(ab);
|
||||||
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
|
// for (let i = 0; i < byteString.length; i++) {
|
||||||
import Logout from "@material-symbols/svg-400/rounded/logout.svg";
|
// ia[i] = byteString.charCodeAt(i);
|
||||||
|
// }
|
||||||
function dataURItoBlob(dataURI: string) {
|
// return new Blob([ab], { type: "image/jpeg" });
|
||||||
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 = () => {
|
export const AccountSettings = () => {
|
||||||
const account = useAccount()!;
|
const account = useAccount()!;
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
|
||||||
const [setPopup, close] = usePopup();
|
const [setPopup, close] = usePopup();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { mutateAsync } = useMutation({
|
const { mutateAsync } = useMutation({
|
||||||
mutationFn: async (update: Partial<Account>) =>
|
method: "PATCH",
|
||||||
await queryFn({
|
path: ["auth", "users", "me"],
|
||||||
path: ["auth", "me"],
|
compute: (update: Partial<User>) => ({ body: update }),
|
||||||
method: "PATCH",
|
optimistic: (update) => ({
|
||||||
body: update,
|
...account,
|
||||||
}),
|
...update,
|
||||||
onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }),
|
claims: { ...account.claims, ...update.claims },
|
||||||
|
}),
|
||||||
|
invalidate: ["auth", "users", "me"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: editPassword } = useMutation({
|
const { mutateAsync: editPassword } = useMutation({
|
||||||
mutationFn: async (request: { newPassword: string; oldPassword: string }) =>
|
method: "PATCH",
|
||||||
await queryFn({
|
path: ["auth", "users", "me", "password"],
|
||||||
path: ["auth", "password-reset"],
|
compute: (body: { oldPassword: string; newPassword: string }) => ({
|
||||||
method: "POST",
|
body,
|
||||||
body: request,
|
}),
|
||||||
}),
|
invalidate: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -106,7 +92,8 @@ export const AccountSettings = () => {
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
userInterfaceStyle: theme.mode === "auto" ? "light" : theme.mode,
|
userInterfaceStyle:
|
||||||
|
theme.mode === "auto" ? "light" : theme.mode,
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -137,43 +124,47 @@ export const AccountSettings = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Preference>
|
</Preference>
|
||||||
|
{/* <Preference */}
|
||||||
|
{/* icon={AccountCircle} */}
|
||||||
|
{/* customIcon={<Avatar src={account.logo} />} */}
|
||||||
|
{/* label={t("settings.account.avatar.label")} */}
|
||||||
|
{/* description={t("settings.account.avatar.description")} */}
|
||||||
|
{/* > */}
|
||||||
|
{/* <Button */}
|
||||||
|
{/* text={t("misc.edit")} */}
|
||||||
|
{/* onPress={async () => { */}
|
||||||
|
{/* 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, */}
|
||||||
|
{/* }); */}
|
||||||
|
{/* }} */}
|
||||||
|
{/* /> */}
|
||||||
|
{/* <Button */}
|
||||||
|
{/* text={t("misc.delete")} */}
|
||||||
|
{/* onPress={async () => { */}
|
||||||
|
{/* await queryFn({ */}
|
||||||
|
{/* method: "DELETE", */}
|
||||||
|
{/* path: ["auth", "me", "logo"], */}
|
||||||
|
{/* }); */}
|
||||||
|
{/* }} */}
|
||||||
|
{/* /> */}
|
||||||
|
{/* </Preference> */}
|
||||||
<Preference
|
<Preference
|
||||||
icon={AccountCircle}
|
icon={Mail}
|
||||||
customIcon={<Avatar src={account.logo} />}
|
label={t("settings.account.email.label")}
|
||||||
label={t("settings.account.avatar.label")}
|
description={account.email}
|
||||||
description={t("settings.account.avatar.description")}
|
|
||||||
>
|
>
|
||||||
<Button
|
|
||||||
text={t("misc.edit")}
|
|
||||||
onPress={async () => {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
text={t("misc.delete")}
|
|
||||||
onPress={async () => {
|
|
||||||
await queryFn({
|
|
||||||
method: "DELETE",
|
|
||||||
path: ["auth", "me", "logo"],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Preference>
|
|
||||||
<Preference icon={Mail} label={t("settings.account.email.label")} description={account.email}>
|
|
||||||
<Button
|
<Button
|
||||||
text={t("misc.edit")}
|
text={t("misc.edit")}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
@@ -202,8 +193,10 @@ export const AccountSettings = () => {
|
|||||||
<ChangePasswordPopup
|
<ChangePasswordPopup
|
||||||
icon={Password}
|
icon={Password}
|
||||||
label={t("settings.account.password.label")}
|
label={t("settings.account.password.label")}
|
||||||
hasPassword={account.hasPassword}
|
hasPassword={true}
|
||||||
apply={async (op, np) => await editPassword({ oldPassword: op, newPassword: np })}
|
apply={async (op, np) =>
|
||||||
|
await editPassword({ oldPassword: op, newPassword: np })
|
||||||
|
}
|
||||||
close={close}
|
close={close}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
@@ -236,7 +229,9 @@ const ChangePopup = ({
|
|||||||
<Popup>
|
<Popup>
|
||||||
{({ css }) => (
|
{({ css }) => (
|
||||||
<>
|
<>
|
||||||
<View {...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}>
|
<View
|
||||||
|
{...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}
|
||||||
|
>
|
||||||
<Icon icon={icon} />
|
<Icon icon={icon} />
|
||||||
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
|
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
|
||||||
</View>
|
</View>
|
||||||
@@ -246,7 +241,13 @@ const ChangePopup = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChangeText={(v) => setValue(v)}
|
onChangeText={(v) => setValue(v)}
|
||||||
/>
|
/>
|
||||||
<View {...css({ flexDirection: "row", alignSelf: "flex-end", gap: ts(1) })}>
|
<View
|
||||||
|
{...css({
|
||||||
|
flexDirection: "row",
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
gap: ts(1),
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
text={t("misc.cancel")}
|
text={t("misc.cancel")}
|
||||||
onPress={() => close()}
|
onPress={() => close()}
|
||||||
@@ -289,7 +290,9 @@ const ChangePasswordPopup = ({
|
|||||||
<Popup>
|
<Popup>
|
||||||
{({ css }) => (
|
{({ css }) => (
|
||||||
<>
|
<>
|
||||||
<View {...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}>
|
<View
|
||||||
|
{...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}
|
||||||
|
>
|
||||||
<Icon icon={icon} />
|
<Icon icon={icon} />
|
||||||
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
|
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
|
||||||
</View>
|
</View>
|
||||||
@@ -303,14 +306,22 @@ const ChangePasswordPopup = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
autoComplete="password-new"
|
autoComplete="new-password"
|
||||||
variant="big"
|
variant="big"
|
||||||
value={newValue}
|
value={newValue}
|
||||||
onChangeText={(v) => setNewValue(v)}
|
onChangeText={(v) => setNewValue(v)}
|
||||||
placeholder={t("settings.account.password.newPassword")}
|
placeholder={t("settings.account.password.newPassword")}
|
||||||
/>
|
/>
|
||||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
{error && (
|
||||||
<View {...css({ flexDirection: "row", alignSelf: "flex-end", gap: ts(1) })}>
|
<P {...css({ color: (theme) => theme.colors.red })}>{error}</P>
|
||||||
|
)}
|
||||||
|
<View
|
||||||
|
{...css({
|
||||||
|
flexDirection: "row",
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
gap: ts(1),
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
text={t("misc.cancel")}
|
text={t("misc.cancel")}
|
||||||
onPress={() => close()}
|
onPress={() => close()}
|
||||||
@@ -323,7 +334,7 @@ const ChangePasswordPopup = ({
|
|||||||
await apply(oldValue, newValue);
|
await apply(oldValue, newValue);
|
||||||
close();
|
close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError((e as KyooErrors).errors[0]);
|
setError((e as KyooError).message);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{...css({ minWidth: rem(6) })}
|
{...css({ minWidth: rem(6) })}
|
||||||
@@ -115,26 +115,37 @@ export const SettingsContainer = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSetting = <Setting extends keyof User["settings"]>(
|
export const useSetting = <Setting extends keyof User["claims"]["settings"]>(
|
||||||
setting: Setting,
|
setting: Setting,
|
||||||
) => {
|
) => {
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const { mutateAsync } = useMutation({
|
const { mutateAsync } = useMutation({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
path: ["auth", "me"],
|
path: ["auth", "users", "me"],
|
||||||
compute: (update: Partial<User["settings"]>) => ({
|
compute: (update: Partial<User["claims"]["settings"]>) => ({
|
||||||
body: { settings: { ...account!.settings, ...update } },
|
body: {
|
||||||
|
claims: {
|
||||||
|
...account!.claims,
|
||||||
|
settings: { ...account!.claims.settings, ...update },
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
optimistic: (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;
|
if (!account) return null;
|
||||||
return [
|
return [
|
||||||
account.settings[setting],
|
account.claims.settings[setting],
|
||||||
async (value: User["settings"][Setting]) => {
|
async (value: User["claims"]["settings"][Setting]) => {
|
||||||
await mutateAsync({ [setting]: value });
|
await mutateAsync({ [setting]: value });
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -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 Language from "@material-symbols/svg-400/outlined/language.svg";
|
||||||
import Android from "@material-symbols/svg-400/rounded/android.svg";
|
import Android from "@material-symbols/svg-400/rounded/android.svg";
|
||||||
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ScrollView } from "react-native";
|
import { ScrollView } from "react-native";
|
||||||
import { ts } from "~/primitives";
|
import { ts } from "~/primitives";
|
||||||
import { useAccount } from "~/providers/account-context";
|
import { useAccount } from "~/providers/account-context";
|
||||||
// import { AccountSettings } from "./account";
|
import { AccountSettings } from "./account";
|
||||||
import { About, GeneralSettings } from "./general";
|
import { About, GeneralSettings } from "./general";
|
||||||
// import { OidcSettings } from "./oidc";
|
// import { OidcSettings } from "./oidc";
|
||||||
// import { PlaybackSettings } from "./playback";
|
import { PlaybackSettings } from "./playback";
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
return (
|
return (
|
||||||
<ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}>
|
<ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}>
|
||||||
<GeneralSettings />
|
<GeneralSettings />
|
||||||
{/* {account && <PlaybackSettings />} */}
|
{account && <PlaybackSettings />}
|
||||||
{/* {account && <AccountSettings />} */}
|
{account && <AccountSettings />}
|
||||||
{/* {account && <OidcSettings />} */}
|
{/* {account && <OidcSettings />} */}
|
||||||
<About />
|
<About />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
108
front/src/ui/settings/oidc.tsx
Normal file
108
front/src/ui/settings/oidc.tsx
Normal file
@@ -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 (
|
||||||
|
// <SettingsContainer title={t("settings.oidc.label")}>
|
||||||
|
// {error ? (
|
||||||
|
// <ErrorView error={error} />
|
||||||
|
// ) : data ? (
|
||||||
|
// Object.entries(data.oidc).map(([id, x]) => {
|
||||||
|
// const acc = account.externalId[id];
|
||||||
|
// return (
|
||||||
|
// <Preference
|
||||||
|
// key={x.displayName}
|
||||||
|
// icon={Badge}
|
||||||
|
// label={x.displayName}
|
||||||
|
// description={
|
||||||
|
// acc
|
||||||
|
// ? t("settings.oidc.connected", { username: acc.username })
|
||||||
|
// : t("settings.oidc.not-connected")
|
||||||
|
// }
|
||||||
|
// customIcon={
|
||||||
|
// x.logoUrl != null && (
|
||||||
|
// <ImageBackground
|
||||||
|
// source={{ uri: x.logoUrl }}
|
||||||
|
// {...css({ width: ts(3), height: ts(3), marginRight: ts(2) })}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {acc ? (
|
||||||
|
// <>
|
||||||
|
// {acc.profileUrl && (
|
||||||
|
// <IconButton
|
||||||
|
// icon={OpenProfile}
|
||||||
|
// as={Link}
|
||||||
|
// href={acc.profileUrl}
|
||||||
|
// target="_blank"
|
||||||
|
// {...tooltip(t("settings.oidc.open-profile", { provider: x.displayName }))}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// <IconButton
|
||||||
|
// icon={Remove}
|
||||||
|
// onPress={() => unlinkAccount(id)}
|
||||||
|
// {...tooltip(t("settings.oidc.delete", { provider: x.displayName }))}
|
||||||
|
// />
|
||||||
|
// </>
|
||||||
|
// ) : (
|
||||||
|
// <Button
|
||||||
|
// text={t("settings.oidc.link")}
|
||||||
|
// as={Link}
|
||||||
|
// href={x.link}
|
||||||
|
// {...css({ minWidth: rem(6) })}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// </Preference>
|
||||||
|
// );
|
||||||
|
// })
|
||||||
|
// ) : (
|
||||||
|
// [...Array(3)].map((_, i) => (
|
||||||
|
// <Preference
|
||||||
|
// key={i}
|
||||||
|
// customIcon={<Skeleton {...css({ width: ts(3), height: ts(3) })} />}
|
||||||
|
// icon={null!}
|
||||||
|
// label={<Skeleton {...css({ width: rem(6) })} />}
|
||||||
|
// description={<Skeleton {...css({ width: rem(7), height: rem(0.8) })} />}
|
||||||
|
// />
|
||||||
|
// ))
|
||||||
|
// )}
|
||||||
|
// </SettingsContainer>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// OidcSettings.query = (): QueryIdentifier<ServerInfo> => ({
|
||||||
|
// path: ["info"],
|
||||||
|
// parser: ServerInfoP,
|
||||||
|
// });
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 SubtitleLanguage from "@material-symbols/svg-400/rounded/closed_caption-fill.svg";
|
||||||
import PlayModeI from "@material-symbols/svg-400/rounded/display_settings-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 AudioLanguage from "@material-symbols/svg-400/rounded/music_note-fill.svg";
|
||||||
import { useTranslation } from "react-i18next";
|
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 = () => {
|
export const PlaybackSettings = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -61,7 +55,9 @@ export const PlaybackSettings = () => {
|
|||||||
onValueChange={(value) => setAudio(value)}
|
onValueChange={(value) => setAudio(value)}
|
||||||
values={["default", ...languageCodes]}
|
values={["default", ...languageCodes]}
|
||||||
getLabel={(key) =>
|
getLabel={(key) =>
|
||||||
key === "default" ? t("mediainfo.default") : (getLanguageName(key) ?? key)
|
key === "default"
|
||||||
|
? t("mediainfo.default")
|
||||||
|
: (getLanguageName(key) ?? key)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Preference>
|
</Preference>
|
||||||
@@ -73,7 +69,9 @@ export const PlaybackSettings = () => {
|
|||||||
<Select
|
<Select
|
||||||
label={t("settings.playback.subtitleLanguage.label")}
|
label={t("settings.playback.subtitleLanguage.label")}
|
||||||
value={subtitle ?? "none"}
|
value={subtitle ?? "none"}
|
||||||
onValueChange={(value) => setSubtitle(value === "none" ? null : value)}
|
onValueChange={(value) =>
|
||||||
|
setSubtitle(value === "none" ? null : value)
|
||||||
|
}
|
||||||
values={["none", "default", ...languageCodes]}
|
values={["none", "default", ...languageCodes]}
|
||||||
getLabel={(key) =>
|
getLabel={(key) =>
|
||||||
key === "none"
|
key === "none"
|
||||||
Reference in New Issue
Block a user