diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index d3d68dea..4e3dcb37 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 9c51e68e..2a80084f 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -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", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 68944060..25882c3f 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -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"; diff --git a/src/main/events/library/reset-game-achievements.ts b/src/main/events/library/reset-game-achievements.ts new file mode 100644 index 00000000..8d52a3a6 --- /dev/null +++ b/src/main/events/library/reset-game-achievements.ts @@ -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); diff --git a/src/preload/index.ts b/src/preload/index.ts index 7b555000..316397d2 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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[] diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 88a16665..88f3297f 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -122,7 +122,7 @@ declare global { ) => void ) => () => Electron.IpcRenderer; onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer; - + resetGameAchievements: (gameId: number) => Promise; /* User preferences */ getUserPreferences: () => Promise; updateUserPreferences: ( diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index f299111a..b06de28a 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -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} /> + setShowResetAchievementsModal(false)} + resetAchievements={handleResetAchievements} + game={game} + /> + {t("remove_from_library")} + + + + + + + + ); +}