Merge branch 'main' into feature/go-up-buttom

This commit is contained in:
Kelvin 2025-01-21 23:23:48 -03:00 committed by GitHub
commit 2f2d949015
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 451 additions and 207 deletions

View File

@ -125,6 +125,10 @@ cd hydra
yarn
```
### <a name="install-openssl-11"></a> Instale OpenSSL 1.1
[OpenSSL 1.1](https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe) é exigido pelo libtorrent em ambientes Windows.
### <a name="install-python-39"></a> Instale Python 3.9
Certifique-se de ter o Python 3.9 instalado em sua máquina. Você pode baixá-lo e instalá-lo em [python.org](https://www.python.org/downloads/release/python-3913/).

View File

@ -281,7 +281,23 @@
"launch_minimized": "Launch Hydra minimized",
"disable_nsfw_alert": "Disable NSFW alert",
"seed_after_download_complete": "Seed after download complete",
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them"
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
"account": "Account",
"no_users_blocked": "You have no blocked users",
"subscription_active_until": "Your Hydra Cloud is active until {{date}}",
"manage_subscription": "Manage subscription",
"update_email": "Update email",
"update_password": "Update password",
"current_email": "Current email:",
"no_email_account": "You have not set an email yet",
"account_data_updated_successfully": "Account data updated successfully",
"renew_subscription": "Renew Hydra Cloud",
"subscription_expired_at": "Your subscription expired at {{date}}",
"no_subscription": "Enjoy Hydra in the best possible way",
"become_subscriber": "Be Hydra Cloud",
"subscription_renew_cancelled": "Automatic renewal is disabled",
"subscription_renews_on": "Your subscription renews on {{date}}",
"bill_sent_until": "Your next bill will be sent until this day"
},
"notifications": {
"download_complete": "Download complete",

View File

@ -268,7 +268,23 @@
"launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
"seed_after_download_complete": "Semear após a conclusão do download",
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las"
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las",
"account": "Conta",
"no_users_blocked": "Você não bloqueou nenhum usuário",
"subscription_active_until": "Sua assinatura Hydra Cloud ficará ativa até {{date}}",
"manage_subscription": "Gerenciar assinatura",
"update_email": "Atualizar email",
"update_password": "Atualizar senha",
"current_email": "Email atual:",
"no_email_account": "Você ainda não adicionou um email a sua conta",
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
"renew_subscription": "Renovar Hydra Cloud",
"subscription_expired_at": "Sua assinatura expirou em {{date}}",
"no_subscription": "Aproveite o Hydra da melhor forma possível",
"become_subscriber": "Seja Hydra Cloud",
"subscription_renew_cancelled": "A renovação automática está desativada",
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
},
"notifications": {
"download_complete": "Download concluído",
@ -398,7 +414,7 @@
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
"hidden_achievement_tooltip": "Está é uma conquista oculta",
"hidden_achievement_tooltip": "Esta é uma conquista oculta",
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
"earned_points": "Pontos ganhos:",
"available_points": "Pontos disponíveis:",

View File

@ -279,7 +279,23 @@
"source_already_exists": "Этот источник уже добавлен",
"user_unblocked": "Пользователь разблокирован",
"seed_after_download_complete": "Раздавать после завершения загрузки",
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением"
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
"account": "Аккаунт",
"no_users_blocked": "У вас нет заблокированных пользователей",
"subscription_active_until": "Ваша подписка на Hydra Cloud активна до {{date}}",
"manage_subscription": "Управлять подпиской",
"update_email": "Обновить электронную почту",
"update_password": "Обновить пароль",
"current_email": "Текущий email:",
"no_email_account": "Вы еще не установили электронную почту",
"account_data_updated_successfully": "Данные учетной записи успешно обновлены",
"renew_subscription": "Обновить подписку Hydra Cloud",
"subscription_expired_at": "Срок действия вашей подписки истек в {{date}}",
"no_subscription": "Наслаждайтесь Hydra по максимуму",
"become_subscriber": "Станьте обладателем Hydra Cloud",
"subscription_renew_cancelled": "Автоматическое продление отключено",
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня"
},
"notifications": {
"download_complete": "Загрузка завершена",

View File

@ -1,7 +1,24 @@
import i18next from "i18next";
import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services";
import { HydraApi, WindowManager } from "@main/services";
import { AuthPage } from "@shared";
const openAuthWindow = async (_event: Electron.IpcMainInvokeEvent) =>
WindowManager.openAuthWindow();
const openAuthWindow = async (
_event: Electron.IpcMainInvokeEvent,
page: AuthPage
) => {
const searchParams = new URLSearchParams({
lng: i18next.language,
});
if ([AuthPage.UpdateEmail, AuthPage.UpdatePassword].includes(page)) {
const { accessToken } = await HydraApi.refreshToken().catch(() => {
return { accessToken: "" };
});
searchParams.set("token", accessToken);
}
WindowManager.openAuthWindow(page, searchParams);
};
registerEvent("openAuthWindow", openAuthWindow);

View File

@ -215,38 +215,42 @@ export class HydraApi {
}
}
public static async refreshToken() {
const { accessToken, expiresIn } = await this.instance
.post<{ accessToken: string; expiresIn: number }>(`/auth/refresh`, {
refreshToken: this.userAuth.refreshToken,
})
.then((response) => response.data);
const tokenExpirationTimestamp =
Date.now() +
this.secondsToMilliseconds(expiresIn) -
this.EXPIRATION_OFFSET_IN_MS;
this.userAuth.authToken = accessToken;
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
logger.log(
"Token refreshed. New expiration:",
this.userAuth.expirationTimestamp
);
userAuthRepository.upsert(
{
id: 1,
accessToken,
tokenExpirationTimestamp,
},
["id"]
);
return { accessToken, expiresIn };
}
private static async revalidateAccessTokenIfExpired() {
const now = new Date();
if (this.userAuth.expirationTimestamp < now.getTime()) {
if (this.userAuth.expirationTimestamp < Date.now()) {
try {
const response = await this.instance.post(`/auth/refresh`, {
refreshToken: this.userAuth.refreshToken,
});
const { accessToken, expiresIn } = response.data;
const tokenExpirationTimestamp =
now.getTime() +
this.secondsToMilliseconds(expiresIn) -
this.EXPIRATION_OFFSET_IN_MS;
this.userAuth.authToken = accessToken;
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
logger.log(
"Token refreshed. New expiration:",
this.userAuth.expirationTimestamp
);
userAuthRepository.upsert(
{
id: 1,
accessToken,
tokenExpirationTimestamp,
},
["id"]
);
await this.refreshToken();
} catch (err) {
this.handleUnauthorizedError(err);
}
@ -261,7 +265,7 @@ export class HydraApi {
};
}
private static handleUnauthorizedError = (err) => {
private static readonly handleUnauthorizedError = (err) => {
if (err instanceof AxiosError && err.response?.status === 401) {
logger.error(
"401 - Current credentials:",

View File

@ -9,7 +9,7 @@ import {
shell,
} from "electron";
import { is } from "@electron-toolkit/utils";
import i18next, { t } from "i18next";
import { t } from "i18next";
import path from "node:path";
import icon from "@resources/icon.png?asset";
import trayIcon from "@resources/tray-icon.png?asset";
@ -17,6 +17,7 @@ import { gameRepository, userPreferencesRepository } from "@main/repository";
import { IsNull, Not } from "typeorm";
import { HydraApi } from "./hydra-api";
import UserAgent from "user-agents";
import { AuthPage } from "@shared";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
@ -142,7 +143,7 @@ export class WindowManager {
});
}
public static openAuthWindow() {
public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {
if (this.mainWindow) {
const authWindow = new BrowserWindow({
width: 600,
@ -164,12 +165,8 @@ export class WindowManager {
if (!app.isPackaged) authWindow.webContents.openDevTools();
const searchParams = new URLSearchParams({
lng: i18next.language,
});
authWindow.loadURL(
`${import.meta.env.MAIN_VITE_AUTH_URL}/?${searchParams.toString()}`
`${import.meta.env.MAIN_VITE_AUTH_URL}${page}?${searchParams.toString()}`
);
authWindow.once("ready-to-show", () => {
@ -181,6 +178,13 @@ export class WindowManager {
authWindow.close();
HydraApi.handleExternalAuth(url);
return;
}
if (url.startsWith("hydralauncher://update-account")) {
authWindow.close();
WindowManager.mainWindow?.webContents.send("on-account-updated");
}
});
}

View File

@ -15,7 +15,7 @@ import type {
SeedingStatus,
GameAchievement,
} from "@types";
import type { CatalogueCategory } from "@shared";
import type { AuthPage, CatalogueCategory } from "@shared";
import type { AxiosProgressEvent } from "axios";
contextBridge.exposeInMainWorld("electron", {
@ -291,13 +291,19 @@ contextBridge.exposeInMainWorld("electron", {
/* Auth */
signOut: () => ipcRenderer.invoke("signOut"),
openAuthWindow: () => ipcRenderer.invoke("openAuthWindow"),
openAuthWindow: (page: AuthPage) =>
ipcRenderer.invoke("openAuthWindow", page),
getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
onSignIn: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-signin", listener);
return () => ipcRenderer.removeListener("on-signin", listener);
},
onAccountUpdated: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-account-updated", listener);
return () => ipcRenderer.removeListener("on-account-updated", listener);
},
onSignOut: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-signout", listener);

View File

@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { Avatar } from "../avatar/avatar";
import { AuthPage } from "@shared";
const LONG_POLLING_INTERVAL = 120_000;
@ -26,11 +27,11 @@ export function SidebarProfile() {
const handleProfileClick = () => {
if (userDetails === null) {
window.electron.openAuthWindow();
window.electron.openAuthWindow(AuthPage.SignIn);
return;
}
navigate(`/profile/${userDetails!.id}`);
navigate(`/profile/${userDetails.id}`);
};
useEffect(() => {

View File

@ -1,4 +1,4 @@
import type { CatalogueCategory } from "@shared";
import type { AuthPage, CatalogueCategory } from "@shared";
import type {
AppUpdaterEvent,
Game,
@ -208,9 +208,10 @@ declare global {
/* Auth */
signOut: () => Promise<void>;
openAuthWindow: () => Promise<void>;
openAuthWindow: (page: AuthPage) => Promise<void>;
getSessionHash: () => Promise<string | null>;
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
/* User */

View File

@ -10,7 +10,7 @@ import { Sidebar } from "./sidebar/sidebar";
import * as styles from "./game-details.css";
import { useTranslation } from "react-i18next";
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
import { steamUrlBuilder } from "@shared";
import { AuthPage, steamUrlBuilder } from "@shared";
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
import { useUserDetails } from "@renderer/hooks";
@ -69,7 +69,7 @@ export function GameDetailsContent() {
});
const backgroundColor = output
? (new Color(output).darken(0.7).toString() as string)
? new Color(output).darken(0.7).toString()
: "";
setGameColor(backgroundColor);
@ -101,7 +101,7 @@ export function GameDetailsContent() {
const handleCloudSaveButtonClick = () => {
if (!userDetails) {
window.electron.openAuthWindow();
window.electron.openAuthWindow(AuthPage.SignIn);
return;
}

View File

@ -1,5 +1,5 @@
import { ChevronDownIcon } from "@primer/octicons-react";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import * as styles from "./sidebar-section.css";
@ -11,6 +11,15 @@ export interface SidebarSectionProps {
export function SidebarSection({ title, children }: SidebarSectionProps) {
const content = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(true);
const [height, setHeight] = useState(0);
useEffect(() => {
if (content.current && content.current.scrollHeight !== height) {
setHeight(isOpen ? content.current.scrollHeight : 0);
} else if (!isOpen) {
setHeight(0);
}
}, [isOpen, children, height]);
return (
<div>
@ -26,7 +35,7 @@ export function SidebarSection({ title, children }: SidebarSectionProps) {
<div
ref={content}
style={{
maxHeight: isOpen ? `${content.current?.scrollHeight}px` : "0",
maxHeight: `${height}px`,
overflow: "hidden",
transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)",
position: "relative",

View File

@ -5,14 +5,7 @@ import { SPACING_UNIT, vars } from "../../theme.css";
export const form = style({
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
});
export const blockedUserAvatar = style({
width: "32px",
height: "32px",
borderRadius: "4px",
filter: "grayscale(100%)",
gap: `${SPACING_UNIT * 3}px`,
});
export const blockedUser = style({
@ -43,5 +36,4 @@ export const blockedUsersList = style({
flexDirection: "column",
alignItems: "flex-start",
gap: `${SPACING_UNIT}px`,
marginTop: `${SPACING_UNIT}px`,
});

View File

@ -0,0 +1,291 @@
import { Avatar, Button, SelectField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import * as styles from "./settings-account.css";
import { useDate, useToast, useUserDetails } from "@renderer/hooks";
import { useCallback, useContext, useEffect, useState } from "react";
import {
CloudIcon,
KeyIcon,
MailIcon,
XCircleFillIcon,
} from "@primer/octicons-react";
import { settingsContext } from "@renderer/context";
import { AuthPage } from "@shared";
interface FormValues {
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
}
export function SettingsAccount() {
const { t } = useTranslation("settings");
const [isUnblocking, setIsUnblocking] = useState(false);
const { showSuccessToast } = useToast();
const { blockedUsers, fetchBlockedUsers } = useContext(settingsContext);
const { formatDate } = useDate();
const {
control,
formState: { isSubmitting },
setValue,
handleSubmit,
} = useForm<FormValues>();
const {
userDetails,
hasActiveSubscription,
patchUser,
fetchUserDetails,
updateUserDetails,
unblockUser,
} = useUserDetails();
useEffect(() => {
if (userDetails?.profileVisibility) {
setValue("profileVisibility", userDetails.profileVisibility);
}
}, [userDetails, setValue]);
useEffect(() => {
const unsubscribe = window.electron.onAccountUpdated(() => {
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
}
});
showSuccessToast(t("account_data_updated_successfully"));
});
return () => {
unsubscribe();
};
}, [fetchUserDetails, updateUserDetails]);
const visibilityOptions = [
{ value: "PUBLIC", label: t("public") },
{ value: "FRIENDS", label: t("friends_only") },
{ value: "PRIVATE", label: t("private") },
];
const onSubmit = async (values: FormValues) => {
await patchUser(values);
showSuccessToast(t("changes_saved"));
};
const handleUnblockClick = useCallback(
(id: string) => {
setIsUnblocking(true);
unblockUser(id)
.then(() => {
fetchBlockedUsers();
showSuccessToast(t("user_unblocked"));
})
.finally(() => {
setIsUnblocking(false);
});
},
[unblockUser, fetchBlockedUsers, t, showSuccessToast]
);
const getHydraCloudSectionContent = () => {
const hasSubscribedBefore = Boolean(userDetails?.subscription?.expiresAt);
const isRenewalActive = userDetails?.subscription?.status === "active";
if (!hasSubscribedBefore) {
return {
description: <small>{t("no_subscription")}</small>,
callToAction: t("become_subscriber"),
};
}
if (hasActiveSubscription) {
return {
description: isRenewalActive ? (
<>
<small>
{t("subscription_renews_on", {
date: formatDate(userDetails.subscription!.expiresAt!),
})}
</small>
<small>{t("bill_sent_until")}</small>
</>
) : (
<>
<small>{t("subscription_renew_cancelled")}</small>
<small>
{t("subscription_active_until", {
date: formatDate(userDetails!.subscription!.expiresAt!),
})}
</small>
</>
),
callToAction: t("manage_subscription"),
};
}
return {
description: (
<small>
{t("subscription_expired_at", {
date: formatDate(userDetails!.subscription!.expiresAt!),
})}
</small>
),
callToAction: t("renew_subscription"),
};
};
if (!userDetails) return null;
return (
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="profileVisibility"
render={({ field }) => {
const handleChange = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
field.onChange(event);
handleSubmit(onSubmit)();
};
return (
<section>
<SelectField
label={t("profile_visibility")}
value={field.value}
onChange={handleChange}
options={visibilityOptions.map((visiblity) => ({
key: visiblity.value,
value: visiblity.value,
label: visiblity.label,
}))}
disabled={isSubmitting}
/>
<small>{t("profile_visibility_description")}</small>
</section>
);
}}
/>
<section>
<h4>{t("current_email")}</h4>
<p>{userDetails?.email ?? t("no_email_account")}</p>
<div
style={{
display: "flex",
justifyContent: "start",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
marginTop: `${SPACING_UNIT * 2}px`,
}}
>
<Button
theme="outline"
onClick={() => window.electron.openAuthWindow(AuthPage.UpdateEmail)}
>
<MailIcon />
{t("update_email")}
</Button>
<Button
theme="outline"
onClick={() =>
window.electron.openAuthWindow(AuthPage.UpdatePassword)
}
>
<KeyIcon />
{t("update_password")}
</Button>
</div>
</section>
<section
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<h3>Hydra Cloud</h3>
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
}}
>
{getHydraCloudSectionContent().description}
</div>
<Button
style={{
placeSelf: "flex-start",
}}
theme="outline"
onClick={() => window.electron.openCheckout()}
>
<CloudIcon />
{getHydraCloudSectionContent().callToAction}
</Button>
</section>
<section
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<h3>{t("blocked_users")}</h3>
{blockedUsers.length > 0 ? (
<ul className={styles.blockedUsersList}>
{blockedUsers.map((user) => {
return (
<li key={user.id} className={styles.blockedUser}>
<div
style={{
display: "flex",
gap: `${SPACING_UNIT}px`,
alignItems: "center",
}}
>
<Avatar
style={{ filter: "grayscale(100%)" }}
size={32}
src={user.profileImageUrl}
alt={user.displayName}
/>
<span>{user.displayName}</span>
</div>
<button
type="button"
className={styles.unblockButton}
onClick={() => handleUnblockClick(user.id)}
disabled={isUnblocking}
>
<XCircleFillIcon />
</button>
</li>
);
})}
</ul>
) : (
<small>{t("no_users_blocked")}</small>
)}
</section>
</form>
);
}

View File

@ -1,139 +0,0 @@
import { SelectField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import * as styles from "./settings-privacy.css";
import { useToast, useUserDetails } from "@renderer/hooks";
import { useCallback, useContext, useEffect, useState } from "react";
import { XCircleFillIcon } from "@primer/octicons-react";
import { settingsContext } from "@renderer/context";
interface FormValues {
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
}
export function SettingsPrivacy() {
const { t } = useTranslation("settings");
const [isUnblocking, setIsUnblocking] = useState(false);
const { showSuccessToast } = useToast();
const { blockedUsers, fetchBlockedUsers } = useContext(settingsContext);
const {
control,
formState: { isSubmitting },
setValue,
handleSubmit,
} = useForm<FormValues>();
const { patchUser, userDetails } = useUserDetails();
const { unblockUser } = useUserDetails();
useEffect(() => {
if (userDetails?.profileVisibility) {
setValue("profileVisibility", userDetails.profileVisibility);
}
}, [userDetails, setValue]);
const visibilityOptions = [
{ value: "PUBLIC", label: t("public") },
{ value: "FRIENDS", label: t("friends_only") },
{ value: "PRIVATE", label: t("private") },
];
const onSubmit = async (values: FormValues) => {
await patchUser(values);
showSuccessToast(t("changes_saved"));
};
const handleUnblockClick = useCallback(
(id: string) => {
setIsUnblocking(true);
unblockUser(id)
.then(() => {
fetchBlockedUsers();
showSuccessToast(t("user_unblocked"));
})
.finally(() => {
setIsUnblocking(false);
});
},
[unblockUser, fetchBlockedUsers, t, showSuccessToast]
);
return (
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="profileVisibility"
render={({ field }) => {
const handleChange = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
field.onChange(event);
handleSubmit(onSubmit)();
};
return (
<>
<SelectField
label={t("profile_visibility")}
value={field.value}
onChange={handleChange}
options={visibilityOptions.map((visiblity) => ({
key: visiblity.value,
value: visiblity.value,
label: visiblity.label,
}))}
disabled={isSubmitting}
/>
<small>{t("profile_visibility_description")}</small>
</>
);
}}
/>
<h3 style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
{t("blocked_users")}
</h3>
<ul className={styles.blockedUsersList}>
{blockedUsers.map((user) => {
return (
<li key={user.id} className={styles.blockedUser}>
<div
style={{
display: "flex",
gap: `${SPACING_UNIT}px`,
alignItems: "center",
}}
>
<img
src={user.profileImageUrl!}
alt={user.displayName}
className={styles.blockedUserAvatar}
/>
<span>{user.displayName}</span>
</div>
<button
type="button"
className={styles.unblockButton}
onClick={() => handleUnblockClick(user.id)}
disabled={isUnblocking}
>
<XCircleFillIcon />
</button>
</li>
);
})}
</ul>
</form>
);
}

View File

@ -11,7 +11,7 @@ import {
SettingsContextConsumer,
SettingsContextProvider,
} from "@renderer/context";
import { SettingsPrivacy } from "./settings-privacy";
import { SettingsAccount } from "./settings-account";
import { useUserDetails } from "@renderer/hooks";
import { useMemo } from "react";
@ -28,7 +28,7 @@ export default function Settings() {
"Real-Debrid",
];
if (userDetails) return [...categories, t("privacy")];
if (userDetails) return [...categories, t("account")];
return categories;
}, [userDetails, t]);
@ -53,7 +53,7 @@ export default function Settings() {
return <SettingsRealDebrid />;
}
return <SettingsPrivacy />;
return <SettingsAccount />;
};
return (

View File

@ -42,3 +42,9 @@ export enum Cracker {
rle = "RLE",
razor1911 = "RAZOR1911",
}
export enum AuthPage {
SignIn = "/",
UpdateEmail = "/update-email",
UpdatePassword = "/update-password",
}