mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
@@ -57,6 +57,7 @@
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-updater": "^6.6.2",
|
||||
"file-type": "^20.5.0",
|
||||
"framer-motion": "^12.15.0",
|
||||
"i18next": "^23.11.2",
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
|
||||
@@ -205,7 +205,21 @@
|
||||
"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 {{length}} 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",
|
||||
"backup_freeze_failed": "Failed to freeze backup",
|
||||
"backup_freeze_failed_description": "You must leave at least one free slot for automatic backups"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
|
||||
@@ -192,7 +192,21 @@
|
||||
"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 {{length}} 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",
|
||||
"backup_freeze_failed": "Falha ao fixar backup",
|
||||
"backup_freeze_failed_description": "Você deve deixar pelo menos um espaço livre para backups automáticos"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
|
||||
14
src/main/events/cloud-save/rename-game-artifact.ts
Normal file
14
src/main/events/cloud-save/rename-game-artifact.ts
Normal file
@@ -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);
|
||||
16
src/main/events/cloud-save/toggle-artifact-freeze.ts
Normal file
16
src/main/events/cloud-save/toggle-artifact-freeze.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const toggleArtifactFreeze = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameArtifactId: string,
|
||||
freeze: boolean
|
||||
) => {
|
||||
if (freeze) {
|
||||
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}/freeze`);
|
||||
} else {
|
||||
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}/unfreeze`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("toggleArtifactFreeze", toggleArtifactFreeze);
|
||||
@@ -87,6 +87,8 @@ import "./cloud-save/get-game-backup-preview";
|
||||
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";
|
||||
|
||||
@@ -16,7 +16,10 @@ export class Ludusavi {
|
||||
SystemPath.getPath("userData"),
|
||||
"ludusavi"
|
||||
);
|
||||
private static binaryPath = path.join(this.configPath, "ludusavi");
|
||||
private static binaryName =
|
||||
process.platform === "win32" ? "ludusavi.exe" : "ludusavi";
|
||||
|
||||
private static binaryPath = path.join(this.configPath, this.binaryName);
|
||||
|
||||
public static async getConfig() {
|
||||
const config = YAML.parse(
|
||||
@@ -40,7 +43,7 @@ export class Ludusavi {
|
||||
public static async copyBinaryToUserData() {
|
||||
if (!fs.existsSync(this.binaryPath)) {
|
||||
fs.cpSync(
|
||||
path.join(this.ludusaviResourcesPath, "ludusavi"),
|
||||
path.join(this.ludusaviResourcesPath, this.binaryName),
|
||||
this.binaryPath
|
||||
);
|
||||
}
|
||||
|
||||
@@ -236,6 +236,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
downloadOptionTitle: string | null
|
||||
) =>
|
||||
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,
|
||||
|
||||
@@ -30,9 +30,14 @@ export interface CloudSyncContext {
|
||||
setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
getGameBackupPreview: () => Promise<void>;
|
||||
getGameArtifacts: () => Promise<void>;
|
||||
toggleArtifactFreeze: (
|
||||
gameArtifactId: string,
|
||||
freeze: boolean
|
||||
) => Promise<void>;
|
||||
restoringBackup: boolean;
|
||||
uploadingBackup: boolean;
|
||||
loadingPreview: boolean;
|
||||
freezingArtifact: boolean;
|
||||
}
|
||||
|
||||
export const cloudSyncContext = createContext<CloudSyncContext>({
|
||||
@@ -47,10 +52,12 @@ export const cloudSyncContext = createContext<CloudSyncContext>({
|
||||
showCloudSyncFilesModal: false,
|
||||
setShowCloudSyncFilesModal: () => {},
|
||||
getGameBackupPreview: async () => {},
|
||||
toggleArtifactFreeze: async () => {},
|
||||
getGameArtifacts: async () => {},
|
||||
restoringBackup: false,
|
||||
uploadingBackup: false,
|
||||
loadingPreview: false,
|
||||
freezingArtifact: false,
|
||||
});
|
||||
|
||||
const { Provider } = cloudSyncContext;
|
||||
@@ -78,6 +85,7 @@ export function CloudSyncContextProvider({
|
||||
const [uploadingBackup, setUploadingBackup] = useState(false);
|
||||
const [showCloudSyncFilesModal, setShowCloudSyncFilesModal] = useState(false);
|
||||
const [loadingPreview, setLoadingPreview] = useState(false);
|
||||
const [freezingArtifact, setFreezingArtifact] = useState(false);
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
@@ -119,6 +127,22 @@ export function CloudSyncContextProvider({
|
||||
[objectId, shop]
|
||||
);
|
||||
|
||||
const toggleArtifactFreeze = useCallback(
|
||||
async (gameArtifactId: string, freeze: boolean) => {
|
||||
setFreezingArtifact(true);
|
||||
try {
|
||||
await window.electron.toggleArtifactFreeze(gameArtifactId, freeze);
|
||||
getGameArtifacts();
|
||||
} catch (err) {
|
||||
logger.error("Failed to toggle artifact freeze", objectId, shop, err);
|
||||
throw err;
|
||||
} finally {
|
||||
setFreezingArtifact(false);
|
||||
}
|
||||
},
|
||||
[objectId, shop, getGameArtifacts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const removeUploadCompleteListener = window.electron.onUploadComplete(
|
||||
objectId,
|
||||
@@ -192,6 +216,7 @@ export function CloudSyncContextProvider({
|
||||
uploadingBackup,
|
||||
showCloudSyncFilesModal,
|
||||
loadingPreview,
|
||||
freezingArtifact,
|
||||
setShowCloudSyncModal,
|
||||
uploadSaveGame,
|
||||
downloadGameArtifact,
|
||||
@@ -199,6 +224,7 @@ export function CloudSyncContextProvider({
|
||||
setShowCloudSyncFilesModal,
|
||||
getGameBackupPreview,
|
||||
getGameArtifacts,
|
||||
toggleArtifactFreeze,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
@@ -201,6 +201,14 @@ declare global {
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null
|
||||
) => Promise<void>;
|
||||
toggleArtifactFreeze: (
|
||||
gameArtifactId: string,
|
||||
freeze: boolean
|
||||
) => Promise<void>;
|
||||
renameGameArtifact: (
|
||||
gameArtifactId: string,
|
||||
label: string
|
||||
) => Promise<void>;
|
||||
downloadGameArtifact: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
|
||||
@@ -31,6 +31,20 @@
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__artifact-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: globals.$body-color;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__artifacts {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button, Modal, ModalProps } from "@renderer/components";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
|
||||
import "./cloud-sync-modal.scss";
|
||||
import { formatBytes } from "@shared";
|
||||
import {
|
||||
@@ -9,6 +8,9 @@ import {
|
||||
DeviceDesktopIcon,
|
||||
HistoryIcon,
|
||||
InfoIcon,
|
||||
PencilIcon,
|
||||
PinIcon,
|
||||
PinSlashIcon,
|
||||
SyncIcon,
|
||||
TrashIcon,
|
||||
UploadIcon,
|
||||
@@ -17,6 +19,10 @@ 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";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { orderBy } from "lodash-es";
|
||||
|
||||
export interface CloudSyncModalProps
|
||||
extends Omit<ModalProps, "children" | "title"> {}
|
||||
@@ -25,9 +31,11 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
const [deletingArtifact, setDeletingArtifact] = useState(false);
|
||||
const [backupDownloadProgress, setBackupDownloadProgress] =
|
||||
useState<AxiosProgressEvent | null>(null);
|
||||
const [artifactToRename, setArtifactToRename] = useState<GameArtifact | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { formatDate, formatDateTime } = useDate();
|
||||
|
||||
const {
|
||||
@@ -36,9 +44,11 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
uploadingBackup,
|
||||
restoringBackup,
|
||||
loadingPreview,
|
||||
freezingArtifact,
|
||||
uploadSaveGame,
|
||||
downloadGameArtifact,
|
||||
deleteGameArtifact,
|
||||
toggleArtifactFreeze,
|
||||
setShowCloudSyncFilesModal,
|
||||
getGameBackupPreview,
|
||||
} = useContext(cloudSyncContext);
|
||||
@@ -50,10 +60,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
|
||||
const handleDeleteArtifactClick = async (gameArtifactId: string) => {
|
||||
setDeletingArtifact(true);
|
||||
|
||||
try {
|
||||
await deleteGameArtifact(gameArtifactId);
|
||||
|
||||
showSuccessToast(t("backup_deleted"));
|
||||
} catch (err) {
|
||||
showErrorToast("backup_deletion_failed");
|
||||
@@ -71,7 +79,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
setBackupDownloadProgress(progressEvent);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
removeBackupDownloadProgressListener();
|
||||
};
|
||||
@@ -82,6 +89,21 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
downloadGameArtifact(artifactId);
|
||||
};
|
||||
|
||||
const handleFreezeArtifactClick = async (
|
||||
artifactId: string,
|
||||
isFrozen: boolean
|
||||
) => {
|
||||
try {
|
||||
await toggleArtifactFreeze(artifactId, isFrozen);
|
||||
showSuccessToast(isFrozen ? t("backup_frozen") : t("backup_unfrozen"));
|
||||
} catch (err) {
|
||||
showErrorToast(
|
||||
t("backup_freeze_failed"),
|
||||
t("backup_freeze_failed_description")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
getGameBackupPreview();
|
||||
@@ -100,7 +122,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (restoringBackup) {
|
||||
return (
|
||||
<span className="cloud-sync-modal__backup-state-label">
|
||||
@@ -113,7 +134,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (loadingPreview) {
|
||||
return (
|
||||
<span className="cloud-sync-modal__backup-state-label">
|
||||
@@ -122,19 +142,15 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (artifacts.length >= backupsPerGameLimit) {
|
||||
return t("max_number_of_artifacts_reached");
|
||||
}
|
||||
|
||||
if (!backupPreview) {
|
||||
return t("no_backup_preview");
|
||||
}
|
||||
|
||||
if (artifacts.length === 0) {
|
||||
return t("no_backups");
|
||||
}
|
||||
|
||||
return "";
|
||||
}, [
|
||||
uploadingBackup,
|
||||
@@ -147,11 +163,19 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
t,
|
||||
]);
|
||||
|
||||
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
||||
const disableActions =
|
||||
uploadingBackup || restoringBackup || deletingArtifact || freezingArtifact;
|
||||
const isMissingWinePrefix =
|
||||
window.electron.platform === "linux" && !game?.winePrefixPath;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CloudSyncRenameArtifactModal
|
||||
visible={!!artifactToRename}
|
||||
onClose={() => setArtifactToRename(null)}
|
||||
artifact={artifactToRename}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("cloud_save")}
|
||||
@@ -163,7 +187,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
<div className="cloud-sync-modal__title-container">
|
||||
<h2>{gameTitle}</h2>
|
||||
<p>{backupStateLabel}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="cloud-sync-modal__manage-files-button"
|
||||
@@ -204,17 +227,34 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
|
||||
{artifacts.length > 0 ? (
|
||||
<ul className="cloud-sync-modal__artifacts">
|
||||
{artifacts.map((artifact) => (
|
||||
<li key={artifact.id} className="cloud-sync-modal__artifact">
|
||||
<AnimatePresence>
|
||||
{orderBy(artifacts, [(a) => !a.isFrozen], ["asc"]).map(
|
||||
(artifact) => (
|
||||
<motion.li
|
||||
key={artifact.id}
|
||||
className="cloud-sync-modal__artifact"
|
||||
layout
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="cloud-sync-modal__artifact-info">
|
||||
<div className="cloud-sync-modal__artifact-header">
|
||||
<h3>
|
||||
<button
|
||||
type="button"
|
||||
className="cloud-sync-modal__artifact-label"
|
||||
onClick={() => setArtifactToRename(artifact)}
|
||||
>
|
||||
{artifact.label ??
|
||||
t("backup_from", {
|
||||
date: formatDate(artifact.createdAt),
|
||||
})}
|
||||
</h3>
|
||||
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
|
||||
<PencilIcon />
|
||||
</button>
|
||||
<small>
|
||||
{formatBytes(artifact.artifactLengthInBytes)}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<span className="cloud-sync-modal__artifact-meta">
|
||||
@@ -224,7 +264,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
|
||||
<span className="cloud-sync-modal__artifact-meta">
|
||||
<InfoIcon size={14} />
|
||||
{artifact.downloadOptionTitle ?? t("no_download_option_info")}
|
||||
{artifact.downloadOptionTitle ??
|
||||
t("no_download_option_info")}
|
||||
</span>
|
||||
|
||||
<span className="cloud-sync-modal__artifact-meta">
|
||||
@@ -234,10 +275,29 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
</div>
|
||||
|
||||
<div className="cloud-sync-modal__artifact-actions">
|
||||
<Button
|
||||
type="button"
|
||||
tooltip={
|
||||
artifact.isFrozen
|
||||
? t("unfreeze_backup")
|
||||
: t("freeze_backup")
|
||||
}
|
||||
theme={artifact.isFrozen ? "primary" : "outline"}
|
||||
onClick={() =>
|
||||
handleFreezeArtifactClick(
|
||||
artifact.id,
|
||||
!artifact.isFrozen
|
||||
)
|
||||
}
|
||||
disabled={disableActions}
|
||||
>
|
||||
{artifact.isFrozen ? <PinSlashIcon /> : <PinIcon />}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleBackupInstallClick(artifact.id)}
|
||||
disabled={disableActions}
|
||||
theme="outline"
|
||||
>
|
||||
{restoringBackup ? (
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
@@ -249,19 +309,22 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteArtifactClick(artifact.id)}
|
||||
theme="danger"
|
||||
disabled={disableActions}
|
||||
disabled={disableActions || artifact.isFrozen}
|
||||
theme="outline"
|
||||
tooltip={t("delete_backup")}
|
||||
>
|
||||
<TrashIcon />
|
||||
{t("delete_backup")}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</motion.li>
|
||||
)
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ul>
|
||||
) : (
|
||||
<p>{t("no_backups_created")}</p>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.cloud-sync-rename-artifact-modal {
|
||||
&__form-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -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<ModalProps, "children" | "title"> {
|
||||
artifact: GameArtifact | null;
|
||||
}
|
||||
|
||||
export function CloudSyncRenameArtifactModal({
|
||||
visible,
|
||||
onClose,
|
||||
artifact,
|
||||
}: Readonly<CloudSyncRenameArtifactModalProps>) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const validationSchema = yup.object({
|
||||
label: yup
|
||||
.string()
|
||||
.required(t("required_field"))
|
||||
.max(255, t("max_length_field", { length: 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<typeof validationSchema>) => {
|
||||
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 (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("rename_artifact")}
|
||||
description={t("rename_artifact_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextField
|
||||
label={t("artifact_name_label")}
|
||||
placeholder={t("artifact_name_placeholder")}
|
||||
{...register("label")}
|
||||
error={errors.label?.message}
|
||||
/>
|
||||
|
||||
<div className="cloud-sync-rename-artifact-modal__form-actions">
|
||||
<Button theme="outline" onClick={onClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{t("save_changes")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -282,6 +282,7 @@ export interface GameArtifact {
|
||||
hostname: string;
|
||||
downloadCount: number;
|
||||
label?: string;
|
||||
isFrozen: boolean;
|
||||
}
|
||||
|
||||
export interface ComparedAchievements {
|
||||
|
||||
23
yarn.lock
23
yarn.lock
@@ -5646,6 +5646,15 @@ formdata-polyfill@^4.0.10:
|
||||
dependencies:
|
||||
fetch-blob "^3.1.2"
|
||||
|
||||
framer-motion@^12.15.0:
|
||||
version "12.15.0"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.15.0.tgz#6892283fc7967b071f537d6d160ab49e3d5e73ae"
|
||||
integrity sha512-XKg/LnKExdLGugZrDILV7jZjI599785lDIJZLxMiiIFidCsy0a4R2ZEf+Izm67zyOuJgQYTHOmodi7igQsw3vg==
|
||||
dependencies:
|
||||
motion-dom "^12.15.0"
|
||||
motion-utils "^12.12.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
fs-extra@^10.0.0, fs-extra@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
|
||||
@@ -7304,6 +7313,18 @@ module-error@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86"
|
||||
integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==
|
||||
|
||||
motion-dom@^12.15.0:
|
||||
version "12.15.0"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.15.0.tgz#eca7c9d8c28976b8c920f175f92d5288f5a17785"
|
||||
integrity sha512-D2ldJgor+2vdcrDtKJw48k3OddXiZN1dDLLWrS8kiHzQdYVruh0IoTwbJBslrnTXIPgFED7PBN2Zbwl7rNqnhA==
|
||||
dependencies:
|
||||
motion-utils "^12.12.1"
|
||||
|
||||
motion-utils@^12.12.1:
|
||||
version "12.12.1"
|
||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.12.1.tgz#63e28751325cb9d1cd684f3c273a570022b0010e"
|
||||
integrity sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==
|
||||
|
||||
ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
@@ -9041,7 +9062,7 @@ ts-node@^10.9.2:
|
||||
v8-compile-cache-lib "^3.0.1"
|
||||
yn "3.1.1"
|
||||
|
||||
tslib@^2.0.0, tslib@^2.1.0:
|
||||
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
Reference in New Issue
Block a user