Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add reset achievements modal #1301

Merged
merged 25 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
47a5f4d
feat: add reset achievements modal
Hachi-R Dec 17, 2024
ac6eb24
feat: implement reset game achievements functionality
Hachi-R Dec 17, 2024
afcfcbf
refactor: clean up reset game achievements logic
Hachi-R Dec 17, 2024
c60cd4b
Merge remote-tracking branch 'origin/main' into feature/reset-achieve…
Hachi-R Jan 2, 2025
bfdc278
feat: remove hame achievements from remote db
Hachi-R Jan 2, 2025
1076652
refactor: streamline resetGameAchievements with a single try catch
Hachi-R Jan 2, 2025
addc2a7
lint
Hachi-R Jan 2, 2025
9849fbb
refactor: change ResetAchievementsModalProps to use Readonly type for…
Hachi-R Jan 2, 2025
52c159f
fix: replace console.error with achievementsLogger.error
Hachi-R Jan 2, 2025
e2f798c
refactor: simplify resetGameAchievements by replacing Promise.all wit…
Hachi-R Jan 2, 2025
9672e64
feat: log deleted achievement files
Hachi-R Jan 2, 2025
f3d617a
feat: log response after deleting game achievements
Hachi-R Jan 2, 2025
257a71d
fix: change console.info to console.log
Hachi-R Jan 2, 2025
8cf549f
refactor: enhance logging in resetGameAchievement
Hachi-R Jan 3, 2025
93b86f8
refactor: improve reset achievements handling and modal state management
Hachi-R Jan 3, 2025
29ba0cc
fix: update button disabled state logic
Hachi-R Jan 3, 2025
b68fe30
refactor: rename state variable for clarity
Hachi-R Jan 3, 2025
ef3bf98
feat: add success and error toast
Hachi-R Jan 3, 2025
2ddda4e
refactor: remove error logging
Hachi-R Jan 3, 2025
e6d76a5
lint
Hachi-R Jan 3, 2025
190ddeb
refactor: improve logging for deleted game achievements
Hachi-R Jan 3, 2025
5061695
lint
Hachi-R Jan 3, 2025
2df57b0
feat: disable reset achievement button if has no achievements
Hachi-R Jan 3, 2025
3efb142
lint
Hachi-R Jan 3, 2025
cade56b
feat: disable reset achievements button if user is not logged in
Hachi-R Jan 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
);
}
Loading