mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-15 04:32:13 +00:00
feat: improving cloud sync manual mapping
This commit is contained in:
parent
7de6e96f63
commit
bfcf8178d8
15 changed files with 164 additions and 132 deletions
|
@ -10,7 +10,8 @@
|
||||||
"no_results": "No results found",
|
"no_results": "No results found",
|
||||||
"start_typing": "Starting typing to search...",
|
"start_typing": "Starting typing to search...",
|
||||||
"hot": "Hot now",
|
"hot": "Hot now",
|
||||||
"weekly": "📅 Top games of the week"
|
"weekly": "📅 Top games of the week",
|
||||||
|
"achievements": "🏆 Good with achievements"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Catalogue",
|
"catalogue": "Catalogue",
|
||||||
|
@ -161,7 +162,9 @@
|
||||||
"no_download_option_info": "No information available",
|
"no_download_option_info": "No information available",
|
||||||
"backup_deletion_failed": "Failed to delete backup",
|
"backup_deletion_failed": "Failed to delete backup",
|
||||||
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
|
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
|
||||||
"achievements_not_sync": "Your achievements are not synchronized"
|
"achievements_not_sync": "Your achievements are not synchronized",
|
||||||
|
"manage_files_description": "Manage which files will be backed up and restored",
|
||||||
|
"select_folder": "Select folder"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
|
export const LUDUSAVI_MANIFEST_URL = "https://cdn.losbroxas.org/manifest.yaml";
|
||||||
|
|
||||||
export const defaultDownloadsPath = app.getPath("downloads");
|
export const defaultDownloadsPath = app.getPath("downloads");
|
||||||
|
|
||||||
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
||||||
|
|
|
@ -122,12 +122,9 @@ const downloadGameArtifact = async (
|
||||||
cwd: backupPath,
|
cwd: backupPath,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
const [game] = await Ludusavi.findGames(shop, objectId);
|
|
||||||
if (!game) throw new Error("Game not found in Ludusavi manifest");
|
|
||||||
|
|
||||||
replaceLudusaviBackupWithCurrentUser(
|
replaceLudusaviBackupWithCurrentUser(
|
||||||
backupPath,
|
backupPath,
|
||||||
game.replaceAll(":", "_"),
|
objectId,
|
||||||
normalizePath(homeDir)
|
normalizePath(homeDir)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { Ludusavi } from "@main/services";
|
import { Ludusavi } from "@main/services";
|
||||||
import path from "node:path";
|
|
||||||
import { backupsPath } from "@main/constants";
|
|
||||||
|
|
||||||
const getGameBackupPreview = async (
|
const getGameBackupPreview = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => {
|
) => {
|
||||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
return Ludusavi.getBackupPreview(shop, objectId);
|
||||||
|
|
||||||
return Ludusavi.getBackupPreview(shop, objectId, backupPath);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getGameBackupPreview", getGameBackupPreview);
|
registerEvent("getGameBackupPreview", getGameBackupPreview);
|
||||||
|
|
14
src/main/events/cloud-save/select-game-backup-path.ts
Normal file
14
src/main/events/cloud-save/select-game-backup-path.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
import { Ludusavi } from "@main/services";
|
||||||
|
|
||||||
|
const selectGameBackupPath = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
_shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
backupPath: string | null
|
||||||
|
) => {
|
||||||
|
return Ludusavi.addCustomGame(objectId, backupPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("selectGameBackupPath", selectGameBackupPath);
|
|
@ -65,6 +65,7 @@ import "./cloud-save/get-game-artifacts";
|
||||||
import "./cloud-save/get-game-backup-preview";
|
import "./cloud-save/get-game-backup-preview";
|
||||||
import "./cloud-save/upload-save-game";
|
import "./cloud-save/upload-save-game";
|
||||||
import "./cloud-save/delete-game-artifact";
|
import "./cloud-save/delete-game-artifact";
|
||||||
|
import "./cloud-save/select-game-backup-path";
|
||||||
import "./notifications/publish-new-repacks-notification";
|
import "./notifications/publish-new-repacks-notification";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
import "./misc/show-item-in-folder";
|
import "./misc/show-item-in-folder";
|
||||||
|
|
|
@ -7,6 +7,7 @@ import path from "node:path";
|
||||||
import YAML from "yaml";
|
import YAML from "yaml";
|
||||||
|
|
||||||
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
|
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
|
||||||
|
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
|
||||||
|
|
||||||
export class Ludusavi {
|
export class Ludusavi {
|
||||||
private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi");
|
private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi");
|
||||||
|
@ -25,15 +26,6 @@ export class Ludusavi {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
static async findGames(shop: GameShop, objectId: string): Promise<string[]> {
|
|
||||||
const games = await this.worker.run(
|
|
||||||
{ objectId, shop },
|
|
||||||
{ name: "findGames" }
|
|
||||||
);
|
|
||||||
|
|
||||||
return games;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getConfig() {
|
static async getConfig() {
|
||||||
if (!fs.existsSync(this.ludusaviConfigPath)) {
|
if (!fs.existsSync(this.ludusaviConfigPath)) {
|
||||||
await this.worker.run(undefined, { name: "generateConfig" });
|
await this.worker.run(undefined, { name: "generateConfig" });
|
||||||
|
@ -47,36 +39,36 @@ export class Ludusavi {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async backupGame(
|
static async backupGame(
|
||||||
shop: GameShop,
|
_shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
backupPath: string,
|
backupPath: string,
|
||||||
winePrefix?: string | null
|
winePrefix?: string | null
|
||||||
): Promise<LudusaviBackup> {
|
): Promise<LudusaviBackup> {
|
||||||
const games = await this.findGames(shop, objectId);
|
|
||||||
if (!games.length) throw new Error("Game not found");
|
|
||||||
|
|
||||||
return this.worker.run(
|
return this.worker.run(
|
||||||
{ title: games[0], backupPath, winePrefix },
|
{ title: objectId, backupPath, winePrefix },
|
||||||
{ name: "backupGame" }
|
{ name: "backupGame" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getBackupPreview(
|
static async getBackupPreview(
|
||||||
shop: GameShop,
|
_shop: GameShop,
|
||||||
objectId: string,
|
objectId: string
|
||||||
backupPath: string
|
|
||||||
): Promise<LudusaviBackup | null> {
|
): Promise<LudusaviBackup | null> {
|
||||||
const games = await this.findGames(shop, objectId);
|
const config = await this.getConfig();
|
||||||
|
|
||||||
if (!games.length) return null;
|
|
||||||
const [game] = games;
|
|
||||||
|
|
||||||
const backupData = await this.worker.run(
|
const backupData = await this.worker.run(
|
||||||
{ title: game, backupPath, preview: true },
|
{ title: objectId, preview: true },
|
||||||
{ name: "backupGame" }
|
{ name: "backupGame" }
|
||||||
);
|
);
|
||||||
|
|
||||||
return backupData;
|
const customGame = config.customGames.find(
|
||||||
|
(game) => game.name === objectId
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...backupData,
|
||||||
|
customBackupPath: customGame?.files[0] || null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async restoreBackup(backupPath: string) {
|
static async restoreBackup(backupPath: string) {
|
||||||
|
@ -87,24 +79,24 @@ export class Ludusavi {
|
||||||
const config = await this.getConfig();
|
const config = await this.getConfig();
|
||||||
|
|
||||||
config.manifest.enable = false;
|
config.manifest.enable = false;
|
||||||
config.manifest.secondary = [
|
config.manifest.secondary = [{ url: LUDUSAVI_MANIFEST_URL, enable: true }];
|
||||||
{ url: "https://cdn.losbroxas.org/manifest.yaml", enable: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addCustomGame(title: string, savePath: string) {
|
static async addCustomGame(title: string, savePath: string | null) {
|
||||||
const config = await this.getConfig();
|
const config = await this.getConfig();
|
||||||
const filteredGames = config.customGames.filter(
|
const filteredGames = config.customGames.filter(
|
||||||
(game) => game.name !== title
|
(game) => game.name !== title
|
||||||
);
|
);
|
||||||
|
|
||||||
filteredGames.push({
|
if (savePath) {
|
||||||
name: title,
|
filteredGames.push({
|
||||||
files: [savePath],
|
name: title,
|
||||||
registry: [],
|
files: [savePath],
|
||||||
});
|
registry: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
config.customGames = filteredGames;
|
config.customGames = filteredGames;
|
||||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||||
|
|
|
@ -1,29 +1,10 @@
|
||||||
import type { GameShop, LudusaviBackup, LudusaviFindResult } from "@types";
|
import type { LudusaviBackup } from "@types";
|
||||||
import cp from "node:child_process";
|
import cp from "node:child_process";
|
||||||
|
|
||||||
import { workerData } from "node:worker_threads";
|
import { workerData } from "node:worker_threads";
|
||||||
|
|
||||||
const { binaryPath } = workerData;
|
const { binaryPath } = workerData;
|
||||||
|
|
||||||
export const findGames = ({
|
|
||||||
shop,
|
|
||||||
objectId,
|
|
||||||
}: {
|
|
||||||
shop: GameShop;
|
|
||||||
objectId: string;
|
|
||||||
}) => {
|
|
||||||
const args = ["find", "--api"];
|
|
||||||
|
|
||||||
if (shop === "steam") {
|
|
||||||
args.push("--steam-id", objectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = cp.execFileSync(binaryPath, args);
|
|
||||||
|
|
||||||
const games = JSON.parse(result.toString("utf-8")) as LudusaviFindResult;
|
|
||||||
return Object.keys(games.games);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const backupGame = ({
|
export const backupGame = ({
|
||||||
title,
|
title,
|
||||||
backupPath,
|
backupPath,
|
||||||
|
@ -35,7 +16,7 @@ export const backupGame = ({
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
winePrefix?: string;
|
winePrefix?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const args = ["backup", `"${title}"`, "--api", "--force"];
|
const args = ["backup", title, "--api", "--force"];
|
||||||
|
|
||||||
if (preview) args.push("--preview");
|
if (preview) args.push("--preview");
|
||||||
if (backupPath) args.push("--path", backupPath);
|
if (backupPath) args.push("--path", backupPath);
|
||||||
|
|
|
@ -178,6 +178,11 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
|
ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
|
||||||
deleteGameArtifact: (gameArtifactId: string) =>
|
deleteGameArtifact: (gameArtifactId: string) =>
|
||||||
ipcRenderer.invoke("deleteGameArtifact", gameArtifactId),
|
ipcRenderer.invoke("deleteGameArtifact", gameArtifactId),
|
||||||
|
selectGameBackupPath: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
backupPath: string | null
|
||||||
|
) => ipcRenderer.invoke("selectGameBackupPath", shop, objectId, backupPath),
|
||||||
onUploadComplete: (objectId: string, shop: GameShop, cb: () => void) => {
|
onUploadComplete: (objectId: string, shop: GameShop, cb: () => void) => {
|
||||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||||
ipcRenderer.on(`on-upload-complete-${objectId}-${shop}`, listener);
|
ipcRenderer.on(`on-upload-complete-${objectId}-${shop}`, listener);
|
||||||
|
|
|
@ -169,6 +169,7 @@ export function CloudSyncContextProvider({
|
||||||
setShowCloudSyncModal(false);
|
setShowCloudSyncModal(false);
|
||||||
setRestoringBackup(false);
|
setRestoringBackup(false);
|
||||||
setUploadingBackup(false);
|
setUploadingBackup(false);
|
||||||
|
setLoadingPreview(false);
|
||||||
}, [objectId, shop]);
|
}, [objectId, shop]);
|
||||||
|
|
||||||
const backupState = useMemo(() => {
|
const backupState = useMemo(() => {
|
||||||
|
|
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
|
@ -144,6 +144,11 @@ declare global {
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => Promise<LudusaviBackup | null>;
|
) => Promise<LudusaviBackup | null>;
|
||||||
deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>;
|
deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>;
|
||||||
|
selectGameBackupPath: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
backupPath: string | null
|
||||||
|
) => Promise<void>;
|
||||||
onBackupDownloadComplete: (
|
onBackupDownloadComplete: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
|
|
@ -1,9 +1,27 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { SPACING_UNIT } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
|
|
||||||
export const mappingMethods = style({
|
export const mappingMethods = style({
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
gridTemplateColumns: "repeat(2, 1fr)",
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fileList = style({
|
||||||
|
listStyle: "none",
|
||||||
|
margin: "0",
|
||||||
|
padding: "0",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
marginTop: `${SPACING_UNIT * 2}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fileItem = style({
|
||||||
|
flex: 1,
|
||||||
|
color: vars.color.muted,
|
||||||
|
textDecoration: "underline",
|
||||||
|
display: "flex",
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Button, Modal, ModalProps } from "@renderer/components";
|
import { Button, Modal, ModalProps, TextField } from "@renderer/components";
|
||||||
import { useContext, useMemo, useState } from "react";
|
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { cloudSyncContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CheckCircleFillIcon } from "@primer/octicons-react";
|
import { CheckCircleFillIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./cloud-sync-files-modal.css";
|
import * as styles from "./cloud-sync-files-modal.css";
|
||||||
import { formatBytes } from "@shared";
|
import { formatBytes } from "@shared";
|
||||||
import { vars } from "@renderer/theme.css";
|
import { useToast } from "@renderer/hooks";
|
||||||
// import { useToast } from "@renderer/hooks";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
export interface CloudSyncFilesModalProps
|
export interface CloudSyncFilesModalProps
|
||||||
extends Omit<ModalProps, "children" | "title"> {}
|
extends Omit<ModalProps, "children" | "title"> {}
|
||||||
|
@ -23,12 +23,30 @@ export function CloudSyncFilesModal({
|
||||||
}: CloudSyncFilesModalProps) {
|
}: CloudSyncFilesModalProps) {
|
||||||
const [selectedFileMappingMethod, setSelectedFileMappingMethod] =
|
const [selectedFileMappingMethod, setSelectedFileMappingMethod] =
|
||||||
useState<FileMappingMethod>(FileMappingMethod.Automatic);
|
useState<FileMappingMethod>(FileMappingMethod.Automatic);
|
||||||
const { backupPreview } = useContext(cloudSyncContext);
|
const { backupPreview, getGameBackupPreview } = useContext(cloudSyncContext);
|
||||||
// const { gameTitle } = useContext(gameDetailsContext);
|
const { shop, objectId } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
// const { showSuccessToast } = useToast();
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
|
const { register, setValue } = useForm<{
|
||||||
|
customBackupPath: string | null;
|
||||||
|
}>({
|
||||||
|
defaultValues: {
|
||||||
|
customBackupPath: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (backupPreview?.customBackupPath) {
|
||||||
|
setSelectedFileMappingMethod(FileMappingMethod.Manual);
|
||||||
|
} else {
|
||||||
|
setSelectedFileMappingMethod(FileMappingMethod.Automatic);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue("customBackupPath", backupPreview?.customBackupPath ?? null);
|
||||||
|
}, [visible, backupPreview]);
|
||||||
|
|
||||||
const files = useMemo(() => {
|
const files = useMemo(() => {
|
||||||
if (!backupPreview) {
|
if (!backupPreview) {
|
||||||
|
@ -44,28 +62,42 @@ export function CloudSyncFilesModal({
|
||||||
});
|
});
|
||||||
}, [backupPreview]);
|
}, [backupPreview]);
|
||||||
|
|
||||||
// const handleAddCustomPathClick = useCallback(async () => {
|
const handleAddCustomPathClick = useCallback(async () => {
|
||||||
// const { filePaths } = await window.electron.showOpenDialog({
|
const { filePaths } = await window.electron.showOpenDialog({
|
||||||
// properties: ["openDirectory"],
|
properties: ["openDirectory"],
|
||||||
// });
|
});
|
||||||
|
|
||||||
// if (filePaths && filePaths.length > 0) {
|
if (filePaths && filePaths.length > 0) {
|
||||||
// const path = filePaths[0];
|
const path = filePaths[0];
|
||||||
// await window.electron.selectGameBackupDirectory(gameTitle, path);
|
setValue("customBackupPath", path);
|
||||||
// showSuccessToast("custom_backup_location_set");
|
|
||||||
// getGameBackupPreview();
|
await window.electron.selectGameBackupPath(shop, objectId!, path);
|
||||||
// }
|
showSuccessToast("custom_backup_location_set");
|
||||||
// }, [gameTitle, showSuccessToast, getGameBackupPreview]);
|
getGameBackupPreview();
|
||||||
|
}
|
||||||
|
}, [objectId, showSuccessToast, getGameBackupPreview]);
|
||||||
|
|
||||||
|
const handleFileMappingMethodClick = useCallback(
|
||||||
|
(mappingOption: FileMappingMethod) => {
|
||||||
|
if (mappingOption === FileMappingMethod.Automatic) {
|
||||||
|
getGameBackupPreview();
|
||||||
|
window.electron.selectGameBackupPath(shop, objectId!, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedFileMappingMethod(mappingOption);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
title="Gerenciar arquivos"
|
title={t("manage_files")}
|
||||||
description="Escolha quais diretórios serão sincronizados"
|
description={t("manage_files_description")}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
<span>{t("mapping_method_label")}</span>
|
<span style={{ marginBottom: 8 }}>{t("mapping_method_label")}</span>
|
||||||
|
|
||||||
<div className={styles.mappingMethods}>
|
<div className={styles.mappingMethods}>
|
||||||
{Object.values(FileMappingMethod).map((mappingMethod) => (
|
{Object.values(FileMappingMethod).map((mappingMethod) => (
|
||||||
|
@ -76,8 +108,7 @@ export function CloudSyncFilesModal({
|
||||||
? "primary"
|
? "primary"
|
||||||
: "outline"
|
: "outline"
|
||||||
}
|
}
|
||||||
onClick={() => setSelectedFileMappingMethod(mappingMethod)}
|
onClick={() => handleFileMappingMethodClick(mappingMethod)}
|
||||||
disabled={mappingMethod === FileMappingMethod.Manual}
|
|
||||||
>
|
>
|
||||||
{selectedFileMappingMethod === mappingMethod && (
|
{selectedFileMappingMethod === mappingMethod && (
|
||||||
<CheckCircleFillIcon />
|
<CheckCircleFillIcon />
|
||||||
|
@ -89,46 +120,33 @@ export function CloudSyncFilesModal({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
{/* <TextField
|
{selectedFileMappingMethod === FileMappingMethod.Automatic ? (
|
||||||
readOnly
|
<p>{t("files_automatically_mapped")}</p>
|
||||||
theme="dark"
|
) : (
|
||||||
disabled
|
<TextField
|
||||||
placeholder={t("select_folder")}
|
{...register("customBackupPath")}
|
||||||
rightContent={
|
readOnly
|
||||||
<Button
|
theme="dark"
|
||||||
type="button"
|
disabled
|
||||||
theme="outline"
|
placeholder={t("select_folder")}
|
||||||
onClick={handleAddCustomPathClick}
|
rightContent={
|
||||||
>
|
<Button
|
||||||
<FileDirectoryIcon />
|
type="button"
|
||||||
{t("select_executable")}
|
theme="outline"
|
||||||
</Button>
|
onClick={handleAddCustomPathClick}
|
||||||
}
|
>
|
||||||
/> */}
|
<FileDirectoryIcon />
|
||||||
|
{t("select_executable")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<p>{t("files_automatically_mapped")}</p>
|
<ul className={styles.fileList}>
|
||||||
|
|
||||||
<ul
|
|
||||||
style={{
|
|
||||||
listStyle: "none",
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: 8,
|
|
||||||
marginTop: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<li key={file.path} style={{ display: "flex" }}>
|
<li key={file.path} style={{ display: "flex" }}>
|
||||||
<button
|
<button
|
||||||
style={{
|
className={styles.fileItem}
|
||||||
flex: 1,
|
|
||||||
color: vars.color.muted,
|
|
||||||
textDecoration: "underline",
|
|
||||||
display: "flex",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={() => window.electron.showItemInFolder(file.path)}
|
onClick={() => window.electron.showItemInFolder(file.path)}
|
||||||
>
|
>
|
||||||
{file.path.split("/").at(-1)}
|
{file.path.split("/").at(-1)}
|
||||||
|
|
|
@ -163,7 +163,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.manageFilesButton}
|
className={styles.manageFilesButton}
|
||||||
onClick={() => setShowCloudSyncFilesModal(true)}
|
onClick={() => setShowCloudSyncFilesModal(true)}
|
||||||
disabled={disableActions || !backupPreview?.overall.totalGames}
|
disabled={disableActions}
|
||||||
>
|
>
|
||||||
{t("manage_files")}
|
{t("manage_files")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -199,7 +199,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||||
|
|
||||||
{artifacts.length > 0 ? (
|
{artifacts.length > 0 ? (
|
||||||
<ul className={styles.artifacts}>
|
<ul className={styles.artifacts}>
|
||||||
{artifacts.map((artifact, index) => (
|
{artifacts.map((artifact) => (
|
||||||
<li key={artifact.id} className={styles.artifactButton}>
|
<li key={artifact.id} className={styles.artifactButton}>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||||
<div
|
<div
|
||||||
|
@ -210,7 +210,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3>{t("backup_title", { number: index + 1 })}</h3>
|
<h3>Backup from 22/10</h3>
|
||||||
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
|
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,9 @@ export interface LudusaviBackup {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
games: Record<string, LudusaviGame>;
|
games: Record<string, LudusaviGame>;
|
||||||
}
|
|
||||||
|
|
||||||
export interface LudusaviFindResult {
|
// Custom path for the backup, extracted from the config
|
||||||
games: Record<string, unknown>;
|
customBackupPath?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LudusaviConfig {
|
export interface LudusaviConfig {
|
||||||
|
|
Loading…
Reference in a new issue