Merge pull request #1301 from hydralauncher/feature/reset-achievements
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled

feat: add reset achievements modal
This commit is contained in:
Eight 2025-01-03 19:41:24 -03:00 committed by GitHub
commit 385db5c936
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 171 additions and 6 deletions

View File

@ -179,7 +179,12 @@
"backup_from": "Backup from {{date}}",
"custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected",
"no_write_permission": "Cannot download into this directory. Click here to learn more."
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
"reset_achievements": "Reset achievements",
"reset_achievements_description": "This will reset all achievements for {{game}}",
"reset_achievements_title": "Are you sure?",
"reset_achievements_success": "Achievements successfully reset",
"reset_achievements_error": "Failed to reset achievements"
},
"activation": {
"title": "Activate Hydra",

View File

@ -168,7 +168,11 @@
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado",
"no_write_permission": "O download não pode ser feito neste diretório. Clique aqui para saber mais."
"reset_achievements": "Resetar conquistas",
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
"reset_achievements_title": "Tem certeza?",
"reset_achievements_success": "Conquistas resetadas com sucesso",
"reset_achievements_error": "Falha ao resetar conquistas"
},
"activation": {
"title": "Ativação",

View File

@ -28,6 +28,7 @@ import "./library/verify-executable-path";
import "./library/remove-game";
import "./library/remove-game-from-library";
import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";

View File

@ -0,0 +1,56 @@
import { gameAchievementRepository, gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
import fs from "fs";
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
const resetGameAchievements = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
try {
const game = await gameRepository.findOne({ where: { id: gameId } });
if (!game) return;
const achievementFiles = findAchievementFiles(game);
if (achievementFiles.length) {
for (const achievementFile of achievementFiles) {
achievementsLogger.log(`deleting ${achievementFile.filePath}`);
await fs.promises.rm(achievementFile.filePath);
}
}
await gameAchievementRepository.update(
{ objectId: game.objectID },
{
unlockedAchievements: null,
}
);
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
() =>
achievementsLogger.log(
`Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}`
)
);
const gameAchievements = await getUnlockedAchievements(
game.objectID,
game.shop,
true
);
WindowManager.mainWindow?.webContents.send(
`on-update-achievements-${game.objectID}-${game.shop}`,
gameAchievements
);
} catch (error) {
achievementsLogger.error(error);
throw error;
}
};
registerEvent("resetGameAchievements", resetGameAchievements);

View File

@ -130,6 +130,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("deleteGameFolder", gameId),
getGameByObjectId: (objectId: string) =>
ipcRenderer.invoke("getGameByObjectId", objectId),
resetGameAchievements: (gameId: number) =>
ipcRenderer.invoke("resetGameAchievements", gameId),
onGamesRunning: (
cb: (
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]

View File

@ -122,7 +122,7 @@ declare global {
) => void
) => () => Electron.IpcRenderer;
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
resetGameAchievements: (gameId: number) => Promise<void>;
/* User preferences */
getUserPreferences: () => Promise<UserPreferences | null>;
updateUserPreferences: (

View File

@ -5,8 +5,9 @@ import type { Game } from "@types";
import * as styles from "./game-options-modal.css";
import { gameDetailsContext } from "@renderer/context";
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
import { useDownload, useToast } from "@renderer/hooks";
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
import { ResetAchievementsModal } from "./reset-achievements-modal";
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
import { debounce } from "lodash-es";
@ -25,12 +26,20 @@ export function GameOptionsModal({
const { showSuccessToast, showErrorToast } = useToast();
const { updateGame, setShowRepacksModal, repacks, selectGameExecutable } =
useContext(gameDetailsContext);
const {
updateGame,
setShowRepacksModal,
repacks,
selectGameExecutable,
achievements,
} = useContext(gameDetailsContext);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
const [showResetAchievementsModal, setShowResetAchievementsModal] =
useState(false);
const [isDeletingAchievements, setIsDeletingAchievements] = useState(false);
const {
removeGameInstaller,
@ -39,6 +48,12 @@ export function GameOptionsModal({
cancelDownload,
} = useDownload();
const { userDetails } = useUserDetails();
const hasAchievements =
(achievements?.filter((achievement) => achievement.unlocked).length ?? 0) >
0;
const deleting = isGameDeleting(game.id);
const { lastPacket } = useDownload();
@ -141,6 +156,19 @@ export function GameOptionsModal({
const shouldShowWinePrefixConfiguration =
window.electron.platform === "linux";
const handleResetAchievements = async () => {
setIsDeletingAchievements(true);
try {
await window.electron.resetGameAchievements(game.id);
await updateGame();
showSuccessToast(t("reset_achievements_success"));
} catch (error) {
showErrorToast(t("reset_achievements_error"));
} finally {
setIsDeletingAchievements(false);
}
};
const shouldShowLaunchOptionsConfiguration = false;
return (
@ -158,6 +186,13 @@ export function GameOptionsModal({
game={game}
/>
<ResetAchievementsModal
visible={showResetAchievementsModal}
onClose={() => setShowResetAchievementsModal(false)}
resetAchievements={handleResetAchievements}
game={game}
/>
<Modal
visible={visible}
title={game.title}
@ -313,6 +348,20 @@ export function GameOptionsModal({
>
{t("remove_from_library")}
</Button>
<Button
onClick={() => setShowResetAchievementsModal(true)}
theme="danger"
disabled={
deleting ||
isDeletingAchievements ||
!hasAchievements ||
!userDetails
}
>
{t("reset_achievements")}
</Button>
<Button
onClick={() => {
setShowDeleteModal(true);

View File

@ -0,0 +1,48 @@
import { useTranslation } from "react-i18next";
import { Button, Modal } from "@renderer/components";
import * as styles from "./remove-from-library-modal.css";
import type { Game } from "@types";
type ResetAchievementsModalProps = Readonly<{
visible: boolean;
game: Game;
onClose: () => void;
resetAchievements: () => Promise<void>;
}>;
export function ResetAchievementsModal({
onClose,
game,
visible,
resetAchievements,
}: ResetAchievementsModalProps) {
const { t } = useTranslation("game_details");
const handleResetAchievements = async () => {
try {
await resetAchievements();
} finally {
onClose();
}
};
return (
<Modal
visible={visible}
onClose={onClose}
title={t("reset_achievements_title")}
description={t("reset_achievements_description", {
game: game.title,
})}
>
<div className={styles.deleteActionsButtonsCtn}>
<Button onClick={handleResetAchievements} theme="outline">
{t("reset_achievements")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
</Button>
</div>
</Modal>
);
}