From dac29767bd4c79cc282381a8389297e344ee409f Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Fri, 30 May 2025 12:28:27 +0100 Subject: [PATCH] feat: adding backup freezing --- .../Steam/CODEX/268910/achievements.ini | 73 +++++ .../Steam/CODEX/268910/leaderboards.ini | 3 + .../Documents/Steam/CODEX/268910/stats.ini | 6 + src/locales/en/translation.json | 14 +- src/locales/pt-BR/translation.json | 14 +- .../events/cloud-save/rename-game-artifact.ts | 14 + src/main/events/index.ts | 1 + src/preload/index.ts | 2 + src/renderer/src/declaration.d.ts | 4 + .../cloud-sync-modal/cloud-sync-modal.scss | 17 ++ .../cloud-sync-modal/cloud-sync-modal.tsx | 276 ++++++++++-------- .../cloud-sync-rename-artifact-modal.scss | 8 + .../cloud-sync-rename-artifact-modal.tsx | 101 +++++++ 13 files changed, 406 insertions(+), 127 deletions(-) create mode 100644 C:/users/Public/Documents/Steam/CODEX/268910/achievements.ini create mode 100644 C:/users/Public/Documents/Steam/CODEX/268910/leaderboards.ini create mode 100644 C:/users/Public/Documents/Steam/CODEX/268910/stats.ini create mode 100644 src/main/events/cloud-save/rename-game-artifact.ts create mode 100644 src/renderer/src/pages/game-details/cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal.scss create mode 100644 src/renderer/src/pages/game-details/cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal.tsx diff --git a/C:/users/Public/Documents/Steam/CODEX/268910/achievements.ini b/C:/users/Public/Documents/Steam/CODEX/268910/achievements.ini new file mode 100644 index 00000000..3a1c79d3 --- /dev/null +++ b/C:/users/Public/Documents/Steam/CODEX/268910/achievements.ini @@ -0,0 +1,73 @@ +[ARankWorldDLC] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740593973 + +[BoughtAllItems] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740592806 + +[CompleteWorldDLC] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740593973 + +[DefeatBoss] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740593973 + +[DefeatBossAsChalice] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740593973 + +[DefeatBossDLCWeapon] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740593973 + +[DefeatSaltbaker] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740595074 + +[DefeatXBossesAsChalice] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740593973 + +[NewGamePlus] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740594289 + +[NoHitsTaken] +Achieved=1 +CurProgress=0 +MaxProgress=0 +UnlockTime=1740593973 + +[SteamAchievements] +00000=BoughtAllItems +00001=CompleteWorldDLC +00002=ARankWorldDLC +00003=NoHitsTaken +00004=DefeatBossDLCWeapon +00005=DefeatBoss +00006=DefeatBossAsChalice +00007=DefeatXBossesAsChalice +00008=NewGamePlus +00009=DefeatSaltbaker +Count=10 + diff --git a/C:/users/Public/Documents/Steam/CODEX/268910/leaderboards.ini b/C:/users/Public/Documents/Steam/CODEX/268910/leaderboards.ini new file mode 100644 index 00000000..8c93dafd --- /dev/null +++ b/C:/users/Public/Documents/Steam/CODEX/268910/leaderboards.ini @@ -0,0 +1,3 @@ +[SteamLeaderboards] +Count=0 + diff --git a/C:/users/Public/Documents/Steam/CODEX/268910/stats.ini b/C:/users/Public/Documents/Steam/CODEX/268910/stats.ini new file mode 100644 index 00000000..3a99be67 --- /dev/null +++ b/C:/users/Public/Documents/Steam/CODEX/268910/stats.ini @@ -0,0 +1,6 @@ +[UserStats] +ARanks=25 +BossesDefeatedNormal=17 +HangTime=2.75 +Parries=7 + diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ee4254e2..4b5f7768 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -205,7 +205,19 @@ "create_start_menu_shortcut": "Create Start Menu shortcut", "invalid_wine_prefix_path": "Invalid Wine prefix path", "invalid_wine_prefix_path_description": "The path to the Wine prefix is invalid. Please check the path and try again.", - "missing_wine_prefix": "Wine prefix is required to create a backup on Linux" + "missing_wine_prefix": "Wine prefix is required to create a backup on Linux", + "artifact_renamed": "Backup renamed successfully", + "rename_artifact": "Rename Backup", + "rename_artifact_description": "Rename the backup to a more descriptive name", + "artifact_name_label": "Backup name", + "artifact_name_placeholder": "Enter a name for the backup", + "save_changes": "Save changes", + "required_field": "This field is required", + "max_length_field": "This field must be less than {{lenght}} characters", + "freeze_backup": "Pin it so it's not overwritten by automatic backups", + "unfreeze_backup": "Unpin it", + "backup_frozen": "Backup pinned", + "backup_unfrozen": "Backup unpinned" }, "activation": { "title": "Activate Hydra", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 6b9546ec..2bd54dac 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -192,7 +192,19 @@ "automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados", "create_start_menu_shortcut": "Criar atalho no Menu Iniciar", "invalid_wine_prefix_path": "Caminho do prefixo Wine inválido", - "invalid_wine_prefix_path_description": "O caminho para o prefixo Wine é inválido. Por favor, verifique o caminho e tente novamente." + "invalid_wine_prefix_path_description": "O caminho para o prefixo Wine é inválido. Por favor, verifique o caminho e tente novamente.", + "artifact_renamed": "Backup renomeado com sucesso", + "rename_artifact": "Renomear Backup", + "rename_artifact_description": "Renomeie o backup para um nome mais descritivo", + "artifact_name_label": "Nome do backup", + "artifact_name_placeholder": "Insira um nome para o backup", + "save_changes": "Salvar mudanças", + "required_field": "Este campo é obrigatório", + "max_length_field": "Este campo deve ter menos de {{lenght}} caracteres", + "freeze_backup": "Fixar para não ser apagado por backups automáticos", + "unfreeze_backup": "Remover dos fixados", + "backup_frozen": "Backup fixado", + "backup_unfrozen": "Backup removido dos fixados" }, "activation": { "title": "Ativação", diff --git a/src/main/events/cloud-save/rename-game-artifact.ts b/src/main/events/cloud-save/rename-game-artifact.ts new file mode 100644 index 00000000..f8257c4b --- /dev/null +++ b/src/main/events/cloud-save/rename-game-artifact.ts @@ -0,0 +1,14 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; + +const renameGameArtifact = async ( + _event: Electron.IpcMainInvokeEvent, + gameArtifactId: string, + label: string +) => { + await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}`, { + label, + }); +}; + +registerEvent("renameGameArtifact", renameGameArtifact); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 28ea70a9..e4e6ed2e 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -88,6 +88,7 @@ import "./cloud-save/upload-save-game"; import "./cloud-save/delete-game-artifact"; import "./cloud-save/select-game-backup-path"; import "./cloud-save/toggle-artifact-freeze"; +import "./cloud-save/rename-game-artifact"; import "./notifications/publish-new-repacks-notification"; import "./notifications/update-achievement-notification-window"; import "./notifications/show-achievement-test-notification"; diff --git a/src/preload/index.ts b/src/preload/index.ts index b9834676..2a8297f1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -238,6 +238,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("uploadSaveGame", objectId, shop, downloadOptionTitle), toggleArtifactFreeze: (gameArtifactId: string, freeze: boolean) => ipcRenderer.invoke("toggleArtifactFreeze", gameArtifactId, freeze), + renameGameArtifact: (gameArtifactId: string, label: string) => + ipcRenderer.invoke("renameGameArtifact", gameArtifactId, label), downloadGameArtifact: ( objectId: string, shop: GameShop, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 29186007..79c72a1c 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -205,6 +205,10 @@ declare global { gameArtifactId: string, freeze: boolean ) => Promise; + renameGameArtifact: ( + gameArtifactId: string, + label: string + ) => Promise; downloadGameArtifact: ( objectId: string, shop: GameShop, diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.scss b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.scss index d096d16f..451e863c 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.scss +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.scss @@ -31,6 +31,23 @@ gap: globals.$spacing-unit; } + &__artifact-label { + display: flex; + align-items: center; + gap: 4px; + color: globals.$body-color; + font-size: 16px; + font-weight: 600; + text-align: left; + background-color: transparent; + border: none; + padding: 0; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + } + &__artifacts { display: flex; gap: globals.$spacing-unit; diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index 0f6a2a92..743bf5b2 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -7,19 +7,21 @@ import { formatBytes } from "@shared"; import { ClockIcon, DeviceDesktopIcon, - DownloadIcon, + HistoryIcon, InfoIcon, - LockIcon, PencilIcon, + PinIcon, + PinSlashIcon, SyncIcon, TrashIcon, - UnlockIcon, UploadIcon, } from "@primer/octicons-react"; import { useAppSelector, useDate, useToast } from "@renderer/hooks"; import { useTranslation } from "react-i18next"; import { AxiosProgressEvent } from "axios"; import { formatDownloadProgress } from "@renderer/helpers"; +import { CloudSyncRenameArtifactModal } from "../cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal"; +import { GameArtifact } from "@types"; export interface CloudSyncModalProps extends Omit {} @@ -28,6 +30,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { const [deletingArtifact, setDeletingArtifact] = useState(false); const [backupDownloadProgress, setBackupDownloadProgress] = useState(null); + const [artifactToRename, setArtifactToRename] = useState( + null + ); const { t } = useTranslation("game_details"); @@ -87,11 +92,17 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { downloadGameArtifact(artifactId); }; - const handleFreezeArtifactClick = async (artifactId: string) => { - await toggleArtifactFreeze( - artifactId, - !artifacts.find((artifact) => artifact.id === artifactId)?.isFrozen - ); + const handleFreezeArtifactClick = async ( + artifactId: string, + isFrozen: boolean + ) => { + await toggleArtifactFreeze(artifactId, isFrozen); + + if (isFrozen) { + showSuccessToast(t("backup_frozen")); + } else { + showSuccessToast(t("backup_unfrozen")); + } }; useEffect(() => { @@ -165,132 +176,147 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { window.electron.platform === "linux" && !game?.winePrefixPath; return ( - -
-
-

{gameTitle}

-

{backupStateLabel}

+ <> + setArtifactToRename(null)} + artifact={artifactToRename} + /> - +
+ + + {uploadingBackup ? ( + + ) : ( + + )} + {t("create_backup")} +
- - +
+

{t("backups")}

+ + {artifacts.length} / {backupsPerGameLimit} + +
-
-

{t("backups")}

- - {artifacts.length} / {backupsPerGameLimit} - -
+ {artifacts.length > 0 ? ( +
    + {artifacts.map((artifact) => ( +
  • +
    +
    + + {formatBytes(artifact.artifactLengthInBytes)} +
    - - - {formatBytes(artifact.artifactLengthInBytes)} + + + {artifact.hostname} + + + + + {artifact.downloadOptionTitle ?? + t("no_download_option_info")} + + + + + {formatDateTime(artifact.createdAt)} +
    - - - {artifact.hostname} - - - - - {artifact.downloadOptionTitle ?? t("no_download_option_info")} - - - - - {formatDateTime(artifact.createdAt)} - - - -
    - - - -
    -
  • - ))} -
- ) : ( -

{t("no_backups_created")}

- )} -
+
+ + + +
+ + ))} + + ) : ( +

{t("no_backups_created")}

+ )} + + ); } diff --git a/src/renderer/src/pages/game-details/cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal.scss b/src/renderer/src/pages/game-details/cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal.scss new file mode 100644 index 00000000..a0cf2a80 --- /dev/null +++ b/src/renderer/src/pages/game-details/cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal.scss @@ -0,0 +1,8 @@ +.cloud-sync-rename-artifact-modal { + &__form-actions { + display: flex; + gap: 8px; + margin-top: 16px; + justify-content: flex-end; + } +} diff --git a/src/renderer/src/pages/game-details/cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal.tsx new file mode 100644 index 00000000..3d2690fa --- /dev/null +++ b/src/renderer/src/pages/game-details/cloud-sync-rename-artifact-modal/cloud-sync-rename-artifact-modal.tsx @@ -0,0 +1,101 @@ +import { useCallback, useContext, useEffect } from "react"; +import { Button, Modal, ModalProps, TextField } from "@renderer/components"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import type { GameArtifact } from "@types"; +import { cloudSyncContext } from "@renderer/context"; +import { logger } from "@renderer/logger"; +import * as yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { InferType } from "yup"; +import { useToast } from "@renderer/hooks"; + +import "./cloud-sync-rename-artifact-modal.scss"; + +export interface CloudSyncRenameArtifactModalProps + extends Omit { + artifact: GameArtifact | null; +} + +export function CloudSyncRenameArtifactModal({ + visible, + onClose, + artifact, +}: CloudSyncRenameArtifactModalProps) { + const { t } = useTranslation("game_details"); + + const validationSchema = yup.object({ + label: yup + .string() + .required(t("required_field")) + .max(255, t("max_length_field", { lenght: 255 })), + }); + + const { getGameArtifacts } = useContext(cloudSyncContext); + + const { + register, + handleSubmit, + setValue, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + label: artifact?.label ?? "", + }, + resolver: yupResolver(validationSchema), + }); + + const { showSuccessToast } = useToast(); + + useEffect(() => { + if (artifact) { + setValue("label", artifact.label ?? ""); + } + }, [artifact, setValue]); + + const onSubmit = useCallback( + async (data: InferType) => { + try { + if (!artifact) return; + + await window.electron.renameGameArtifact(artifact.id, data.label); + await getGameArtifacts(); + + showSuccessToast(t("artifact_renamed")); + + onClose(); + } catch (err) { + logger.error("Failed to rename artifact", err); + } + }, + [artifact, getGameArtifacts, onClose, showSuccessToast, t] + ); + + return ( + +
+ + +
+ + + +
+ +
+ ); +}