mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-11-21 05:53:11 -05:00
348 lines
8.7 KiB
TypeScript
348 lines
8.7 KiB
TypeScript
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 type { KyooError, User } from "~/models";
|
|
import {
|
|
Alert,
|
|
Button,
|
|
H1,
|
|
Icon,
|
|
Input,
|
|
P,
|
|
Popup,
|
|
ts,
|
|
usePopup,
|
|
} from "~/primitives";
|
|
import { useAccount } from "~/providers/account-context";
|
|
import { useMutation } from "~/query";
|
|
import { deleteAccount, logout } from "../login/logic";
|
|
import { PasswordInput } from "../login/password-input";
|
|
import { Preference, SettingsContainer } from "./base";
|
|
|
|
// 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 [setPopup, close] = usePopup();
|
|
const { t } = useTranslation();
|
|
|
|
const { mutateAsync } = useMutation({
|
|
method: "PATCH",
|
|
path: ["auth", "users", "me"],
|
|
compute: (update: Partial<User>) => ({ body: update }),
|
|
optimistic: (update) => ({
|
|
...account,
|
|
...update,
|
|
claims: { ...account.claims, ...update.claims },
|
|
}),
|
|
invalidate: ["auth", "users", "me"],
|
|
});
|
|
|
|
const { mutateAsync: editPassword } = useMutation({
|
|
method: "PATCH",
|
|
path: ["auth", "users", "me", "password"],
|
|
compute: (body: { oldPassword: string; newPassword: string }) => ({
|
|
body,
|
|
}),
|
|
invalidate: null,
|
|
});
|
|
|
|
return (
|
|
<SettingsContainer
|
|
title={t("settings.account.label")}
|
|
extra={
|
|
<View {...css({ marginTop: ts(2), gap: ts(2), flexDirection: "row" })}>
|
|
<Button
|
|
licon={<Icon icon={Logout} {...css({ marginX: ts(1) })} />}
|
|
text={t("login.logout")}
|
|
onPress={logout}
|
|
{...css({ flexGrow: 1, flexShrink: 1, flexBasis: 0 })}
|
|
/>
|
|
<Button
|
|
licon={<Icon icon={Delete} {...css({ marginX: ts(1) })} />}
|
|
text={t("login.delete")}
|
|
onPress={async () => {
|
|
Alert.alert(
|
|
t("login.delete"),
|
|
t("login.delete-confirmation"),
|
|
[
|
|
{ text: t("misc.cancel"), style: "cancel" },
|
|
{
|
|
text: t("misc.delete"),
|
|
onPress: deleteAccount,
|
|
style: "destructive",
|
|
},
|
|
],
|
|
{
|
|
cancelable: true,
|
|
userInterfaceStyle:
|
|
theme.mode === "auto" ? "light" : theme.mode,
|
|
icon: "warning",
|
|
},
|
|
);
|
|
}}
|
|
{...css({ flexGrow: 1, flexShrink: 1, flexBasis: 0 })}
|
|
/>
|
|
</View>
|
|
}
|
|
>
|
|
<Preference
|
|
icon={Username}
|
|
label={t("settings.account.username.label")}
|
|
description={account.username}
|
|
>
|
|
<Button
|
|
text={t("misc.edit")}
|
|
onPress={() =>
|
|
setPopup(
|
|
<ChangePopup
|
|
icon={Username}
|
|
autoComplete="username-new"
|
|
label={t("settings.account.username.label")}
|
|
inital={account.username}
|
|
apply={async (v) => await mutateAsync({ username: v })}
|
|
close={close}
|
|
/>,
|
|
)
|
|
}
|
|
/>
|
|
</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
|
|
icon={Mail}
|
|
label={t("settings.account.email.label")}
|
|
description={account.email}
|
|
>
|
|
<Button
|
|
text={t("misc.edit")}
|
|
onPress={() =>
|
|
setPopup(
|
|
<ChangePopup
|
|
icon={Mail}
|
|
autoComplete="email"
|
|
label={t("settings.account.email.label")}
|
|
inital={account.email}
|
|
apply={async (v) => await mutateAsync({ email: v })}
|
|
close={close}
|
|
/>,
|
|
)
|
|
}
|
|
/>
|
|
</Preference>
|
|
<Preference
|
|
icon={Password}
|
|
label={t("settings.account.password.label")}
|
|
description={t("settings.account.password.description")}
|
|
>
|
|
<Button
|
|
text={t("misc.edit")}
|
|
onPress={() =>
|
|
setPopup(
|
|
<ChangePasswordPopup
|
|
icon={Password}
|
|
label={t("settings.account.password.label")}
|
|
hasPassword={true}
|
|
apply={async (op, np) =>
|
|
await editPassword({ oldPassword: op, newPassword: np })
|
|
}
|
|
close={close}
|
|
/>,
|
|
)
|
|
}
|
|
/>
|
|
</Preference>
|
|
</SettingsContainer>
|
|
);
|
|
};
|
|
|
|
const ChangePopup = ({
|
|
label,
|
|
icon,
|
|
inital,
|
|
autoComplete,
|
|
apply,
|
|
close,
|
|
}: {
|
|
label: string;
|
|
icon: Icon;
|
|
inital: string;
|
|
autoComplete: ComponentProps<typeof Input>["autoComplete"];
|
|
apply: (v: string) => Promise<unknown>;
|
|
close: () => void;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const [value, setValue] = useState(inital);
|
|
|
|
return (
|
|
<Popup>
|
|
{({ css }) => (
|
|
<>
|
|
<View
|
|
{...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}
|
|
>
|
|
<Icon icon={icon} />
|
|
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
|
|
</View>
|
|
<Input
|
|
autoComplete={autoComplete}
|
|
variant="big"
|
|
value={value}
|
|
onChangeText={(v) => setValue(v)}
|
|
/>
|
|
<View
|
|
{...css({
|
|
flexDirection: "row",
|
|
alignSelf: "flex-end",
|
|
gap: ts(1),
|
|
})}
|
|
>
|
|
<Button
|
|
text={t("misc.cancel")}
|
|
onPress={() => close()}
|
|
{...css({ minWidth: rem(6) })}
|
|
/>
|
|
<Button
|
|
text={t("misc.edit")}
|
|
onPress={async () => {
|
|
await apply(value);
|
|
close();
|
|
}}
|
|
{...css({ minWidth: rem(6) })}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</Popup>
|
|
);
|
|
};
|
|
|
|
const ChangePasswordPopup = ({
|
|
label,
|
|
icon,
|
|
hasPassword,
|
|
apply,
|
|
close,
|
|
}: {
|
|
label: string;
|
|
icon: Icon;
|
|
hasPassword: boolean;
|
|
apply: (oldPassword: string, newPassword: string) => Promise<unknown>;
|
|
close: () => void;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const [oldValue, setOldValue] = useState("");
|
|
const [newValue, setNewValue] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
return (
|
|
<Popup>
|
|
{({ css }) => (
|
|
<>
|
|
<View
|
|
{...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}
|
|
>
|
|
<Icon icon={icon} />
|
|
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
|
|
</View>
|
|
{hasPassword && (
|
|
<PasswordInput
|
|
autoComplete="current-password"
|
|
variant="big"
|
|
value={oldValue}
|
|
onChangeText={(v) => setOldValue(v)}
|
|
placeholder={t("settings.account.password.oldPassword")}
|
|
/>
|
|
)}
|
|
<PasswordInput
|
|
autoComplete="new-password"
|
|
variant="big"
|
|
value={newValue}
|
|
onChangeText={(v) => setNewValue(v)}
|
|
placeholder={t("settings.account.password.newPassword")}
|
|
/>
|
|
{error && (
|
|
<P {...css({ color: (theme) => theme.colors.red })}>{error}</P>
|
|
)}
|
|
<View
|
|
{...css({
|
|
flexDirection: "row",
|
|
alignSelf: "flex-end",
|
|
gap: ts(1),
|
|
})}
|
|
>
|
|
<Button
|
|
text={t("misc.cancel")}
|
|
onPress={() => close()}
|
|
{...css({ minWidth: rem(6) })}
|
|
/>
|
|
<Button
|
|
text={t("misc.edit")}
|
|
onPress={async () => {
|
|
try {
|
|
await apply(oldValue, newValue);
|
|
close();
|
|
} catch (e) {
|
|
setError((e as KyooError).message);
|
|
}
|
|
}}
|
|
{...css({ minWidth: rem(6) })}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</Popup>
|
|
);
|
|
};
|