mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 13:34:54 +03:00
Merge pull request #1402 from hydralauncher/feat/manage-account-buttons
feat: manage account buttons
This commit is contained in:
commit
69787ee068
@ -280,7 +280,23 @@
|
|||||||
"launch_minimized": "Launch Hydra minimized",
|
"launch_minimized": "Launch Hydra minimized",
|
||||||
"disable_nsfw_alert": "Disable NSFW alert",
|
"disable_nsfw_alert": "Disable NSFW alert",
|
||||||
"seed_after_download_complete": "Seed after download complete",
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
@ -268,7 +268,23 @@
|
|||||||
"launch_minimized": "Iniciar o Hydra minimizado",
|
"launch_minimized": "Iniciar o Hydra minimizado",
|
||||||
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
|
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
|
||||||
"seed_after_download_complete": "Semear após a conclusão do download",
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
@ -397,7 +413,7 @@
|
|||||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
||||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
||||||
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
|
"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",
|
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
|
||||||
"earned_points": "Pontos ganhos:",
|
"earned_points": "Pontos ganhos:",
|
||||||
"available_points": "Pontos disponíveis:",
|
"available_points": "Pontos disponíveis:",
|
||||||
|
@ -1,7 +1,24 @@
|
|||||||
|
import i18next from "i18next";
|
||||||
import { registerEvent } from "../register-event";
|
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) =>
|
const openAuthWindow = async (
|
||||||
WindowManager.openAuthWindow();
|
_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);
|
registerEvent("openAuthWindow", openAuthWindow);
|
||||||
|
@ -33,7 +33,8 @@ export class DatanodesApi {
|
|||||||
"User-Agent":
|
"User-Agent":
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||||
},
|
},
|
||||||
maxRedirects: 0, validateStatus: (status: number) => status === 302 || status < 400,
|
maxRedirects: 0,
|
||||||
|
validateStatus: (status: number) => status === 302 || status < 400,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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() {
|
private static async revalidateAccessTokenIfExpired() {
|
||||||
const now = new Date();
|
if (this.userAuth.expirationTimestamp < Date.now()) {
|
||||||
|
|
||||||
if (this.userAuth.expirationTimestamp < now.getTime()) {
|
|
||||||
try {
|
try {
|
||||||
const response = await this.instance.post(`/auth/refresh`, {
|
await this.refreshToken();
|
||||||
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"]
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.handleUnauthorizedError(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) {
|
if (err instanceof AxiosError && err.response?.status === 401) {
|
||||||
logger.error(
|
logger.error(
|
||||||
"401 - Current credentials:",
|
"401 - Current credentials:",
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
shell,
|
shell,
|
||||||
} from "electron";
|
} from "electron";
|
||||||
import { is } from "@electron-toolkit/utils";
|
import { is } from "@electron-toolkit/utils";
|
||||||
import i18next, { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import icon from "@resources/icon.png?asset";
|
import icon from "@resources/icon.png?asset";
|
||||||
import trayIcon from "@resources/tray-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 { IsNull, Not } from "typeorm";
|
||||||
import { HydraApi } from "./hydra-api";
|
import { HydraApi } from "./hydra-api";
|
||||||
import UserAgent from "user-agents";
|
import UserAgent from "user-agents";
|
||||||
|
import { AuthPage } from "@shared";
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
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) {
|
if (this.mainWindow) {
|
||||||
const authWindow = new BrowserWindow({
|
const authWindow = new BrowserWindow({
|
||||||
width: 600,
|
width: 600,
|
||||||
@ -164,12 +165,8 @@ export class WindowManager {
|
|||||||
|
|
||||||
if (!app.isPackaged) authWindow.webContents.openDevTools();
|
if (!app.isPackaged) authWindow.webContents.openDevTools();
|
||||||
|
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
lng: i18next.language,
|
|
||||||
});
|
|
||||||
|
|
||||||
authWindow.loadURL(
|
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", () => {
|
authWindow.once("ready-to-show", () => {
|
||||||
@ -181,6 +178,13 @@ export class WindowManager {
|
|||||||
authWindow.close();
|
authWindow.close();
|
||||||
|
|
||||||
HydraApi.handleExternalAuth(url);
|
HydraApi.handleExternalAuth(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.startsWith("hydralauncher://update-account")) {
|
||||||
|
authWindow.close();
|
||||||
|
|
||||||
|
WindowManager.mainWindow?.webContents.send("on-account-updated");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import type {
|
|||||||
SeedingStatus,
|
SeedingStatus,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { CatalogueCategory } from "@shared";
|
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
@ -291,13 +291,19 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
openAuthWindow: () => ipcRenderer.invoke("openAuthWindow"),
|
openAuthWindow: (page: AuthPage) =>
|
||||||
|
ipcRenderer.invoke("openAuthWindow", page),
|
||||||
getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
|
getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
|
||||||
onSignIn: (cb: () => void) => {
|
onSignIn: (cb: () => void) => {
|
||||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||||
ipcRenderer.on("on-signin", listener);
|
ipcRenderer.on("on-signin", listener);
|
||||||
return () => ipcRenderer.removeListener("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) => {
|
onSignOut: (cb: () => void) => {
|
||||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||||
ipcRenderer.on("on-signout", listener);
|
ipcRenderer.on("on-signout", listener);
|
||||||
|
@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { Avatar } from "../avatar/avatar";
|
import { Avatar } from "../avatar/avatar";
|
||||||
|
import { AuthPage } from "@shared";
|
||||||
|
|
||||||
const LONG_POLLING_INTERVAL = 120_000;
|
const LONG_POLLING_INTERVAL = 120_000;
|
||||||
|
|
||||||
@ -26,11 +27,11 @@ export function SidebarProfile() {
|
|||||||
|
|
||||||
const handleProfileClick = () => {
|
const handleProfileClick = () => {
|
||||||
if (userDetails === null) {
|
if (userDetails === null) {
|
||||||
window.electron.openAuthWindow();
|
window.electron.openAuthWindow(AuthPage.SignIn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(`/profile/${userDetails!.id}`);
|
navigate(`/profile/${userDetails.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import type { CatalogueCategory } from "@shared";
|
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||||
import type {
|
import type {
|
||||||
AppUpdaterEvent,
|
AppUpdaterEvent,
|
||||||
Game,
|
Game,
|
||||||
@ -208,9 +208,10 @@ declare global {
|
|||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
signOut: () => Promise<void>;
|
signOut: () => Promise<void>;
|
||||||
openAuthWindow: () => Promise<void>;
|
openAuthWindow: (page: AuthPage) => Promise<void>;
|
||||||
getSessionHash: () => Promise<string | null>;
|
getSessionHash: () => Promise<string | null>;
|
||||||
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
|
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
|
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
|
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
|
@ -10,7 +10,7 @@ import { Sidebar } from "./sidebar/sidebar";
|
|||||||
import * as styles from "./game-details.css";
|
import * as styles from "./game-details.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
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 cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
@ -69,7 +69,7 @@ export function GameDetailsContent() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const backgroundColor = output
|
const backgroundColor = output
|
||||||
? (new Color(output).darken(0.7).toString() as string)
|
? new Color(output).darken(0.7).toString()
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
setGameColor(backgroundColor);
|
setGameColor(backgroundColor);
|
||||||
@ -101,7 +101,7 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
const handleCloudSaveButtonClick = () => {
|
const handleCloudSaveButtonClick = () => {
|
||||||
if (!userDetails) {
|
if (!userDetails) {
|
||||||
window.electron.openAuthWindow();
|
window.electron.openAuthWindow(AuthPage.SignIn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,14 +5,7 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
|||||||
export const form = style({
|
export const form = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
});
|
|
||||||
|
|
||||||
export const blockedUserAvatar = style({
|
|
||||||
width: "32px",
|
|
||||||
height: "32px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
filter: "grayscale(100%)",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const blockedUser = style({
|
export const blockedUser = style({
|
||||||
@ -43,5 +36,4 @@ export const blockedUsersList = style({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
marginTop: `${SPACING_UNIT}px`,
|
|
||||||
});
|
});
|
291
src/renderer/src/pages/settings/settings-account.tsx
Normal file
291
src/renderer/src/pages/settings/settings-account.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -11,7 +11,7 @@ import {
|
|||||||
SettingsContextConsumer,
|
SettingsContextConsumer,
|
||||||
SettingsContextProvider,
|
SettingsContextProvider,
|
||||||
} from "@renderer/context";
|
} from "@renderer/context";
|
||||||
import { SettingsPrivacy } from "./settings-privacy";
|
import { SettingsAccount } from "./settings-account";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ export default function Settings() {
|
|||||||
"Real-Debrid",
|
"Real-Debrid",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (userDetails) return [...categories, t("privacy")];
|
if (userDetails) return [...categories, t("account")];
|
||||||
return categories;
|
return categories;
|
||||||
}, [userDetails, t]);
|
}, [userDetails, t]);
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ export default function Settings() {
|
|||||||
return <SettingsRealDebrid />;
|
return <SettingsRealDebrid />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SettingsPrivacy />;
|
return <SettingsAccount />;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -42,3 +42,9 @@ export enum Cracker {
|
|||||||
rle = "RLE",
|
rle = "RLE",
|
||||||
razor1911 = "RAZOR1911",
|
razor1911 = "RAZOR1911",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AuthPage {
|
||||||
|
SignIn = "/",
|
||||||
|
UpdateEmail = "/update-email",
|
||||||
|
UpdatePassword = "/update-password",
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user