Skip to content

Commit

Permalink
Merge pull request #1301 from hydralauncher/feature/reset-achievements
Browse files Browse the repository at this point in the history
feat: add reset achievements modal
  • Loading branch information
Hachi-R authored Jan 3, 2025
2 parents 59bc23b + cade56b commit 385db5c
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 6 deletions.
7 changes: 6 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion src/locales/pt-BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/main/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
56 changes: 56 additions & 0 deletions src/main/events/library/reset-game-achievements.ts
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 2 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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">[]
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand Down
55 changes: 52 additions & 3 deletions src/renderer/src/pages/game-details/modals/game-options-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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 (
Expand All @@ -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}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
}

0 comments on commit 385db5c

Please sign in to comment.