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"
},
"settings": {
"username": "Username",
"downloads_path": "Downloads path",
"change": "Update",
"notifications": "Notifications",

View file

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

View file

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

View file

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

View file

@ -1,255 +1,255 @@
import { Avatar, Button, SelectField } from "@renderer/components";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
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";
import "./settings-account.scss";
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, t, showSuccessToast]);
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="settings-account__form" onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="profileVisibility"
render={({ field }) => {
const handleChange = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
field.onChange(event);
handleSubmit(onSubmit)();
};
return (
<section className="settings-account__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 className="settings-account__section">
<h4>{t("current_email")}</h4>
<p>{userDetails?.email ?? t("no_email_account")}</p>
<div className="settings-account__actions">
<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 className="settings-account__section">
<h3>Hydra Cloud</h3>
<div className="settings-account__subscription-info">
{getHydraCloudSectionContent().description}
</div>
<Button
className="settings-account__subscription-button"
theme="outline"
onClick={() => window.electron.openCheckout()}
>
<CloudIcon />
{getHydraCloudSectionContent().callToAction}
</Button>
</section>
<section className="settings-account__section">
<h3>{t("blocked_users")}</h3>
{blockedUsers.length > 0 ? (
<ul className="settings-account__blocked-users">
{blockedUsers.map((user) => {
return (
<li key={user.id} className="settings-account__blocked-user">
<div className="settings-account__user-info">
<Avatar
className="settings-account__user-avatar"
size={32}
src={user.profileImageUrl}
alt={user.displayName}
/>
<span>{user.displayName}</span>
</div>
<button
type="button"
className="settings-account__unblock-button"
onClick={() => handleUnblockClick(user.id)}
disabled={isUnblocking}
>
<XCircleFillIcon />
</button>
</li>
);
})}
</ul>
) : (
<small>{t("no_users_blocked")}</small>
)}
</section>
</form>
);
}
import { Avatar, Button, SelectField } from "@renderer/components";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
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";
import "./settings-account.scss";
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, t, showSuccessToast]);
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="settings-account__form" onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="profileVisibility"
render={({ field }) => {
const handleChange = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
field.onChange(event);
handleSubmit(onSubmit)();
};
return (
<section className="settings-account__section">
<SelectField
label={t("profile_visibility")}
value={field.value}
onChange={handleChange}
options={visibilityOptions.map((visibility) => ({
key: visibility.value,
value: visibility.value,
label: visibility.label,
}))}
disabled={isSubmitting}
/>
<small>{t("profile_visibility_description")}</small>
</section>
);
}}
/>
<section className="settings-account__section">
<h4>{t("current_email")}</h4>
<p>{userDetails?.email ?? t("no_email_account")}</p>
<h4>{t("username")}:</h4>
<p>{userDetails?.username}</p>
<div className="settings-account__actions">
<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 className="settings-account__section">
<h3>Hydra Cloud</h3>
<div className="settings-account__subscription-info">
{getHydraCloudSectionContent().description}
</div>
<Button
className="settings-account__subscription-button"
theme="outline"
onClick={() => window.electron.openCheckout()}
>
<CloudIcon />
{getHydraCloudSectionContent().callToAction}
</Button>
</section>
<section className="settings-account__section">
<h3>{t("blocked_users")}</h3>
{blockedUsers.length > 0 ? (
<ul className="settings-account__blocked-users">
{blockedUsers.map((user) => {
return (
<li key={user.id} className="settings-account__blocked-user">
<div className="settings-account__user-info">
<Avatar
className="settings-account__user-avatar"
size={32}
src={user.profileImageUrl}
alt={user.displayName}
/>
<span>{user.displayName}</span>
</div>
<button
type="button"
className="settings-account__unblock-button"
onClick={() => handleUnblockClick(user.id)}
disabled={isUnblocking}
>
<XCircleFillIcon />
</button>
</li>
);
})}
</ul>
) : (
<small>{t("no_users_blocked")}</small>
)}
</section>
</form>
);
}