mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge 67fe5ccdef
into af2896efc3
This commit is contained in:
commit
08190d0d18
9 changed files with 297 additions and 257 deletions
5
.env
Normal file
5
.env
Normal 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
|
|
@ -1,2 +0,0 @@
|
|||
MAIN_VITE_API_URL=API_URL
|
||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
1
.github/workflows/main.yml
vendored
Normal file
1
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
|
32
.github/workflows/manual.yml
vendored
Normal file
32
.github/workflows/manual.yml
vendored
Normal 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 }}"
|
|
@ -231,6 +231,7 @@
|
|||
"options": "Manage"
|
||||
},
|
||||
"settings": {
|
||||
"username": "Username",
|
||||
"downloads_path": "Downloads path",
|
||||
"change": "Update",
|
||||
"notifications": "Notifications",
|
||||
|
|
|
@ -225,6 +225,7 @@
|
|||
"options": "Gestionar"
|
||||
},
|
||||
"settings": {
|
||||
"username": "Nombre de usuario",
|
||||
"downloads_path": "Ruta de descarga",
|
||||
"change": "Cambiar",
|
||||
"notifications": "Notificaciones",
|
||||
|
|
|
@ -221,6 +221,7 @@
|
|||
"options": "Gerenciar"
|
||||
},
|
||||
"settings": {
|
||||
"username": "Nome de usuário",
|
||||
"downloads_path": "Diretório dos downloads",
|
||||
"change": "Explorar...",
|
||||
"notifications": "Notificações",
|
||||
|
|
|
@ -229,6 +229,7 @@
|
|||
"options": "Управлять"
|
||||
},
|
||||
"settings": {
|
||||
"username": "Имя пользователя",
|
||||
"downloads_path": "Путь загрузок",
|
||||
"change": "Изменить",
|
||||
"notifications": "Уведомления",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue