This commit is contained in:
s.p.e.c.t.r.e 2025-03-05 00:32:40 +00:00 committed by GitHub
commit 08190d0d18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 297 additions and 257 deletions

5
.env Normal file
View file

@ -0,0 +1,5 @@
MAIN_VITE_API_URL=https://hydra-api-us-east-1.losbroxas.org
MAIN_VITE_AUTH_URL=https://auth.hydralauncher.gg
MAIN_VITE_CHECKOUT_URL=https://checkout.hydralauncher.gg
MAIN_VITE_EXTERNAL_RESOURCES_URL=https://assets.hydralauncher.gg
RENDERER_VITE_EXTERNAL_RESOURCES_URL=https://assets.hydralauncher.gg

View file

@ -1,2 +0,0 @@
MAIN_VITE_API_URL=API_URL
MAIN_VITE_AUTH_URL=AUTH_URL

1
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1 @@

32
.github/workflows/manual.yml vendored Normal file
View file

@ -0,0 +1,32 @@
# This is a basic workflow that is manually triggered
name: Manual workflow
# Controls when the action will run. Workflow runs when manually triggered using the UI
# or API.
on:
workflow_dispatch:
# Inputs the workflow accepts.
inputs:
name:
# Friendly description to be shown in the UI instead of 'name'
description: 'Person to greet'
# Default value if no value is explicitly provided
default: 'World'
# Input has to be provided for the workflow to run
required: true
# The data type of the input
type: string
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "greet"
greet:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Runs a single command using the runners shell
- name: Send greeting
run: echo "Hello ${{ inputs.name }}"

View file

@ -231,6 +231,7 @@
"options": "Manage" "options": "Manage"
}, },
"settings": { "settings": {
"username": "Username",
"downloads_path": "Downloads path", "downloads_path": "Downloads path",
"change": "Update", "change": "Update",
"notifications": "Notifications", "notifications": "Notifications",

View file

@ -225,6 +225,7 @@
"options": "Gestionar" "options": "Gestionar"
}, },
"settings": { "settings": {
"username": "Nombre de usuario",
"downloads_path": "Ruta de descarga", "downloads_path": "Ruta de descarga",
"change": "Cambiar", "change": "Cambiar",
"notifications": "Notificaciones", "notifications": "Notificaciones",

View file

@ -221,6 +221,7 @@
"options": "Gerenciar" "options": "Gerenciar"
}, },
"settings": { "settings": {
"username": "Nome de usuário",
"downloads_path": "Diretório dos downloads", "downloads_path": "Diretório dos downloads",
"change": "Explorar...", "change": "Explorar...",
"notifications": "Notificações", "notifications": "Notificações",

View file

@ -229,6 +229,7 @@
"options": "Управлять" "options": "Управлять"
}, },
"settings": { "settings": {
"username": "Имя пользователя",
"downloads_path": "Путь загрузок", "downloads_path": "Путь загрузок",
"change": "Изменить", "change": "Изменить",
"notifications": "Уведомления", "notifications": "Уведомления",

View file

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