feat: Changing settings ui. Added new section achievements

This commit is contained in:
Moyasee
2025-10-23 09:55:04 +03:00
parent 2634bec292
commit 594d56db5c
10 changed files with 335 additions and 160 deletions

View File

@@ -413,6 +413,7 @@
"launch_with_system": "Launch Hydra on system start-up",
"general": "General",
"behavior": "Behavior",
"achievements": "Achievements",
"download_sources": "Download sources",
"language": "Language",
"api_token": "API Token",

View File

@@ -1,4 +1,9 @@
import { appVersion, defaultDownloadsPath, isStaging, screenshotsPath } from "@main/constants";
import {
appVersion,
defaultDownloadsPath,
isStaging,
screenshotsPath,
} from "@main/constants";
import { ipcMain } from "electron";
import "./catalogue/get-game-shop-details";

View File

@@ -8,4 +8,4 @@ const openFolder = async (
return shell.openPath(folderPath);
};
registerEvent("openFolder", openFolder);
registerEvent("openFolder", openFolder);

View File

@@ -0,0 +1,60 @@
@use "../../scss/globals.scss";
.settings-achievements {
&__checkbox-container {
opacity: globals.$disabled-opacity;
cursor: not-allowed;
&--enabled {
opacity: 1;
cursor: pointer;
}
&--with-tooltip {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
}
&--tooltip {
cursor: pointer;
}
}
&__button-container {
margin-top: 16px;
}
&__section {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
&-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
> * + * {
margin-top: 16px;
}
&--achievements {
// First section sits flush with container top
margin-top: 0;
padding-top: 0;
border-top: none;
}
}
&__achievement-custom-notification-position__select-variation {
max-width: 300px;
}
&__test-achievement-notification-button {
margin-top: 8px;
}
}

View File

@@ -0,0 +1,181 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { CheckboxField, Button, SelectField } from "@renderer/components";
import { useAppSelector } from "@renderer/hooks";
import { settingsContext } from "@renderer/context";
import "./settings-achievements.scss";
import { QuestionIcon } from "@primer/octicons-react";
import { AchievementCustomNotificationPosition } from "@types";
export function SettingsAchievements() {
const { t } = useTranslation("settings");
const userPreferences = useAppSelector((state) => state.userPreferences.value);
const { updateUserPreferences } = useContext(settingsContext);
const [form, setForm] = useState({
showHiddenAchievementsDescription: false,
enableSteamAchievements: false,
enableAchievementScreenshots: false,
achievementNotificationsEnabled: true,
achievementCustomNotificationsEnabled: true,
achievementCustomNotificationPosition: "top-left" as AchievementCustomNotificationPosition,
});
useEffect(() => {
if (userPreferences) {
setForm((prev) => ({
...prev,
showHiddenAchievementsDescription:
userPreferences.showHiddenAchievementsDescription ?? false,
enableSteamAchievements: userPreferences.enableSteamAchievements ?? false,
enableAchievementScreenshots:
userPreferences.enableAchievementScreenshots ?? false,
achievementNotificationsEnabled:
userPreferences.achievementNotificationsEnabled ?? true,
achievementCustomNotificationsEnabled:
userPreferences.achievementCustomNotificationsEnabled ?? true,
achievementCustomNotificationPosition:
userPreferences.achievementCustomNotificationPosition ?? "top-left",
}));
}
}, [userPreferences]);
const achievementCustomNotificationPositionOptions = useMemo(() => {
return [
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
].map((position) => ({ key: position, value: position, label: t(position) }));
}, [t]);
const handleChange = async (values: Partial<typeof form>) => {
setForm((prev) => ({ ...prev, ...values }));
await updateUserPreferences(values);
};
const handleChangeAchievementCustomNotificationPosition = async (
event: React.ChangeEvent<HTMLSelectElement>
) => {
const value = event.target.value as AchievementCustomNotificationPosition;
await handleChange({ achievementCustomNotificationPosition: value });
window.electron.updateAchievementCustomNotificationWindow();
};
return (
<div className="settings-achievements">
<div className="settings-achievements__section settings-achievements__section--achievements">
<CheckboxField
label={t("show_hidden_achievement_description")}
checked={form.showHiddenAchievementsDescription}
onChange={() =>
handleChange({
showHiddenAchievementsDescription: !form.showHiddenAchievementsDescription,
})
}
/>
<div className="settings-achievements__checkbox-container--with-tooltip">
<CheckboxField
label={t("enable_steam_achievements")}
checked={form.enableSteamAchievements}
onChange={() =>
handleChange({ enableSteamAchievements: !form.enableSteamAchievements })
}
/>
<small
className="settings-achievements__checkbox-container--tooltip"
data-open-article="steam-achievements"
>
<QuestionIcon size={12} />
</small>
</div>
<div className="settings-achievements__checkbox-container--with-tooltip">
<CheckboxField
label={t("enable_achievement_screenshots")}
checked={form.enableAchievementScreenshots}
disabled={window.electron.platform === "linux"}
onChange={() =>
handleChange({
enableAchievementScreenshots: !form.enableAchievementScreenshots,
})
}
/>
<small
className="settings-achievements__checkbox-container--tooltip"
data-open-article="achievement-souvenirs"
>
<QuestionIcon size={12} />
</small>
</div>
<div className="settings-achievements__button-container">
<Button
theme="outline"
onClick={async () => {
const screenshotsPath = await window.electron.getScreenshotsPath();
window.electron.openFolder(screenshotsPath);
}}
>
{t("open_screenshots_directory")}
</Button>
</div>
</div>
<div className="settings-achievements__section settings-achievements__section--notifications">
<h3 className="settings-achievements__section-title">{t("notifications")}</h3>
<CheckboxField
label={t("enable_achievement_notifications")}
checked={form.achievementNotificationsEnabled}
onChange={async () => {
await handleChange({
achievementNotificationsEnabled: !form.achievementNotificationsEnabled,
});
window.electron.updateAchievementCustomNotificationWindow();
}}
/>
<CheckboxField
label={t("enable_achievement_custom_notifications")}
checked={form.achievementCustomNotificationsEnabled}
disabled={!form.achievementNotificationsEnabled}
onChange={async () => {
await handleChange({
achievementCustomNotificationsEnabled:
!form.achievementCustomNotificationsEnabled,
});
window.electron.updateAchievementCustomNotificationWindow();
}}
/>
{form.achievementNotificationsEnabled && form.achievementCustomNotificationsEnabled && (
<>
<SelectField
className="settings-achievements__achievement-custom-notification-position__select-variation"
label={t("achievement_custom_notification_position")}
value={form.achievementCustomNotificationPosition}
onChange={handleChangeAchievementCustomNotificationPosition}
options={achievementCustomNotificationPositionOptions}
/>
<Button
className="settings-achievements__test-achievement-notification-button"
onClick={() => window.electron.showAchievementTestNotification()}
>
{t("test_notification")}
</Button>
</>
)}
</div>
</div>
);
}

View File

@@ -25,4 +25,21 @@
&__button-container {
margin-top: 16px;
}
&__section {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
&-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
// Add spacing between elements in the section
> * + * {
margin-top: 16px;
}
}
}

View File

@@ -1,11 +1,10 @@
import { useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { CheckboxField, Button } from "@renderer/components";
import { CheckboxField } from "@renderer/components";
import { useAppSelector } from "@renderer/hooks";
import { settingsContext } from "@renderer/context";
import "./settings-behavior.scss";
import { QuestionIcon } from "@primer/octicons-react";
export function SettingsBehavior() {
const userPreferences = useAppSelector(
@@ -141,17 +140,6 @@ export function SettingsBehavior() {
}
/>
<CheckboxField
label={t("show_hidden_achievement_description")}
checked={form.showHiddenAchievementsDescription}
onChange={() =>
handleChange({
showHiddenAchievementsDescription:
!form.showHiddenAchievementsDescription,
})
}
/>
<CheckboxField
label={t("show_download_speed_in_megabytes")}
checked={form.showDownloadSpeedInMegabytes}
@@ -172,56 +160,7 @@ export function SettingsBehavior() {
}
/>
<div className={`settings-behavior__checkbox-container--with-tooltip`}>
<CheckboxField
label={t("enable_steam_achievements")}
checked={form.enableSteamAchievements}
onChange={() =>
handleChange({
enableSteamAchievements: !form.enableSteamAchievements,
})
}
/>
<small
className="settings-behavior__checkbox-container--tooltip"
data-open-article="steam-achievements"
>
<QuestionIcon size={12} />
</small>
</div>
<div className={`settings-behavior__checkbox-container--with-tooltip`}>
<CheckboxField
label={t("enable_achievement_screenshots")}
checked={form.enableAchievementScreenshots}
disabled={window.electron.platform === "linux"}
onChange={() =>
handleChange({
enableAchievementScreenshots: !form.enableAchievementScreenshots,
})
}
/>
<small
className="settings-behavior__checkbox-container--tooltip"
data-open-article="achievement-souvenirs"
>
<QuestionIcon size={12} />
</small>
</div>
<div className="settings-behavior__button-container">
<Button
theme="outline"
onClick={async () => {
const screenshotsPath = await window.electron.getScreenshotsPath();
window.electron.openFolder(screenshotsPath);
}}
>
{t("open_screenshots_directory")}
</Button>
</div>
</>
);
}

View File

@@ -1,4 +1,4 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { useContext, useEffect, useState } from "react";
import {
TextField,
Button,
@@ -119,20 +119,6 @@ export function SettingsGeneral() {
}
}, [userPreferences, defaultDownloadsPath]);
const achievementCustomNotificationPositionOptions = useMemo(() => {
return [
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
].map((position) => ({
key: position,
value: position,
label: t(position),
}));
}, [t]);
const handleLanguageChange = (
event: React.ChangeEvent<HTMLSelectElement>
@@ -148,15 +134,6 @@ export function SettingsGeneral() {
await updateUserPreferences(values);
};
const handleChangeAchievementCustomNotificationPosition = async (
event: React.ChangeEvent<HTMLSelectElement>
) => {
const value = event.target.value as AchievementCustomNotificationPosition;
await handleChange({ achievementCustomNotificationPosition: value });
window.electron.updateAchievementCustomNotificationWindow();
};
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
@@ -262,52 +239,6 @@ export function SettingsGeneral() {
}
/>
<CheckboxField
label={t("enable_achievement_notifications")}
checked={form.achievementNotificationsEnabled}
onChange={async () => {
await handleChange({
achievementNotificationsEnabled:
!form.achievementNotificationsEnabled,
});
window.electron.updateAchievementCustomNotificationWindow();
}}
/>
<CheckboxField
label={t("enable_achievement_custom_notifications")}
checked={form.achievementCustomNotificationsEnabled}
disabled={!form.achievementNotificationsEnabled}
onChange={async () => {
await handleChange({
achievementCustomNotificationsEnabled:
!form.achievementCustomNotificationsEnabled,
});
window.electron.updateAchievementCustomNotificationWindow();
}}
/>
{form.achievementNotificationsEnabled &&
form.achievementCustomNotificationsEnabled && (
<>
<SelectField
className="settings-general__achievement-custom-notification-position__select-variation"
label={t("achievement_custom_notification_position")}
value={form.achievementCustomNotificationPosition}
onChange={handleChangeAchievementCustomNotificationPosition}
options={achievementCustomNotificationPositionOptions}
/>
<Button
className="settings-general__test-achievement-notification-button"
onClick={() => window.electron.showAchievementTestNotification()}
>
{t("test_notification")}
</Button>
</>
)}
<h2 className="settings-general__section-title">{t("common_redist")}</h2>

View File

@@ -2,26 +2,36 @@
.settings {
&__container {
padding: 24px;
padding: 16px;
width: 100%;
display: flex;
align-items: flex-start;
}
&__sidebar {
width: 240px;
min-width: 240px;
margin-right: 12px;
background-color: globals.$background-color;
border: solid 1px globals.$border-color;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
align-self: flex-start;
}
&__content {
background-color: globals.$background-color;
width: 100%;
height: 100%;
flex: 1;
padding: calc(globals.$spacing-unit * 3);
border: solid 1px globals.$border-color;
box-shadow: 0px 0px 15px 0px #000000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
gap: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
}
&__categories {
display: flex;
gap: globals.$spacing-unit;
}
}

View File

@@ -1,8 +1,8 @@
import { Button } from "@renderer/components";
import { useTranslation } from "react-i18next";
import { SettingsGeneral } from "./settings-general";
import { SettingsBehavior } from "./settings-behavior";
import { SettingsDownloadSources } from "./settings-download-sources";
import { SettingsAchievements } from "./settings-achievements";
import {
SettingsContextConsumer,
SettingsContextProvider,
@@ -13,6 +13,16 @@ import { useMemo } from "react";
import "./settings.scss";
import { SettingsAppearance } from "./aparence/settings-appearance";
import { SettingsDebrid } from "./settings-debrid";
import cn from "classnames";
import {
GearIcon,
ToolsIcon,
TrophyIcon,
DownloadIcon,
PaintbrushIcon,
CloudIcon,
PersonIcon,
} from "@primer/octicons-react";
export default function Settings() {
const { t } = useTranslation("settings");
@@ -21,20 +31,26 @@ export default function Settings() {
const categories = useMemo(() => {
const categories = [
{ tabLabel: t("general"), contentTitle: t("general") },
{ tabLabel: t("behavior"), contentTitle: t("behavior") },
{ tabLabel: t("download_sources"), contentTitle: t("download_sources") },
{ tabLabel: t("general"), contentTitle: t("general"), Icon: GearIcon },
{ tabLabel: t("behavior"), contentTitle: t("behavior"), Icon: ToolsIcon },
{ tabLabel: t("achievements"), contentTitle: t("achievements"), Icon: TrophyIcon },
{
tabLabel: t("download_sources"),
contentTitle: t("download_sources"),
Icon: DownloadIcon,
},
{
tabLabel: t("appearance"),
contentTitle: t("appearance"),
Icon: PaintbrushIcon,
},
{ tabLabel: t("debrid"), contentTitle: t("debrid") },
{ tabLabel: t("debrid"), contentTitle: t("debrid"), Icon: CloudIcon },
];
if (userDetails)
return [
...categories,
{ tabLabel: t("account"), contentTitle: t("account") },
{ tabLabel: t("account"), contentTitle: t("account"), Icon: PersonIcon },
];
return categories;
}, [userDetails, t]);
@@ -53,14 +69,18 @@ export default function Settings() {
}
if (currentCategoryIndex === 2) {
return <SettingsDownloadSources />;
return <SettingsAchievements />;
}
if (currentCategoryIndex === 3) {
return <SettingsAppearance appearance={appearance} />;
return <SettingsDownloadSources />;
}
if (currentCategoryIndex === 4) {
return <SettingsAppearance appearance={appearance} />;
}
if (currentCategoryIndex === 5) {
return <SettingsDebrid />;
}
@@ -69,21 +89,32 @@ export default function Settings() {
return (
<section className="settings__container">
<div className="settings__content">
<section className="settings__categories">
<aside className="settings__sidebar">
<ul className="settings__categories sidebar__menu">
{categories.map((category, index) => (
<Button
<li
key={category.contentTitle}
theme={
currentCategoryIndex === index ? "primary" : "outline"
}
onClick={() => setCurrentCategoryIndex(index)}
className={cn("sidebar__menu-item", {
"sidebar__menu-item--active":
currentCategoryIndex === index,
})}
>
{category.tabLabel}
</Button>
<button
type="button"
className="sidebar__menu-item-button"
onClick={() => setCurrentCategoryIndex(index)}
>
<category.Icon size={16} />
<span className="sidebar__menu-item-button-label">
{category.tabLabel}
</span>
</button>
</li>
))}
</section>
</ul>
</aside>
<div className="settings__content">
<h2>{categories[currentCategoryIndex].contentTitle}</h2>
{renderCategory()}
</div>