mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-09 12:56:19 +00:00
feat: adding cross cloud save
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,7 +7,8 @@ out
|
||||
*.log*
|
||||
.env
|
||||
.vite
|
||||
ludusavi/
|
||||
ludusavi/**
|
||||
!ludusavi/config.yaml
|
||||
hydra-python-rpc/
|
||||
.python-version
|
||||
|
||||
|
||||
10
index.ts
Normal file
10
index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import vdf from "vdf-parser";
|
||||
import fs from "node:fs";
|
||||
|
||||
const vdfData = fs.readFileSync(
|
||||
"/home/chubby/.local/share/Steam/userdata/1126196664/config/localconfig.vdf",
|
||||
"utf-8"
|
||||
);
|
||||
const data = vdf.parse(vdfData);
|
||||
|
||||
console.log(data);
|
||||
6
ludusavi/config.yaml
Normal file
6
ludusavi/config.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
manifest:
|
||||
enable: false
|
||||
secondary:
|
||||
- url: https://cdn.losbroxas.org/manifest.yaml
|
||||
enable: true
|
||||
customGames: []
|
||||
@@ -62,7 +62,6 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parse-torrent": "^11.0.17",
|
||||
"piscina": "^4.7.0",
|
||||
"rc-virtual-list": "^3.16.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
|
||||
@@ -2,8 +2,6 @@ import { app } from "electron";
|
||||
import path from "node:path";
|
||||
import { SystemPath } from "./services/system-path";
|
||||
|
||||
export const LUDUSAVI_MANIFEST_URL = "https://cdn.losbroxas.org/manifest.yaml";
|
||||
|
||||
export const defaultDownloadsPath = SystemPath.getPath("downloads");
|
||||
|
||||
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
|
||||
|
||||
@@ -1,74 +1,73 @@
|
||||
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
|
||||
import { CloudSync, HydraApi, logger, WindowManager } from "@main/services";
|
||||
import fs from "node:fs";
|
||||
import * as tar from "tar";
|
||||
import { registerEvent } from "../register-event";
|
||||
import axios from "axios";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import type { GameShop } from "@types";
|
||||
import type { GameShop, LudusaviBackupMapping } from "@types";
|
||||
|
||||
import YAML from "yaml";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { SystemPath } from "@main/services/system-path";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export interface LudusaviBackup {
|
||||
files: {
|
||||
[key: string]: {
|
||||
hash: string;
|
||||
size: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
export const transformLudusaviBackupPathIntoWindowsPath = (
|
||||
backupPath: string,
|
||||
winePrefixPath?: string | null
|
||||
) => {
|
||||
return backupPath.replace(winePrefixPath ?? "", "").replace("drive_c", "C:");
|
||||
};
|
||||
|
||||
const replaceLudusaviBackupWithCurrentUser = (
|
||||
const restoreLudusaviBackup = (
|
||||
backupPath: string,
|
||||
title: string,
|
||||
homeDir: string
|
||||
homeDir: string,
|
||||
winePrefixPath?: string | null
|
||||
) => {
|
||||
const gameBackupPath = path.join(backupPath, title);
|
||||
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
|
||||
|
||||
const data = fs.readFileSync(mappingYamlPath, "utf8");
|
||||
const manifest = YAML.parse(data) as {
|
||||
backups: LudusaviBackup[];
|
||||
backups: LudusaviBackupMapping[];
|
||||
drives: Record<string, string>;
|
||||
};
|
||||
|
||||
const currentHomeDir = normalizePath(SystemPath.getPath("home"));
|
||||
const { userProfilePath, publicProfilePath } =
|
||||
CloudSync.getProfilePaths(winePrefixPath);
|
||||
|
||||
/* Renaming logic */
|
||||
if (os.platform() === "win32") {
|
||||
const mappedHomeDir = path.join(
|
||||
gameBackupPath,
|
||||
path.join("drive-C", homeDir.replace("C:", ""))
|
||||
);
|
||||
|
||||
if (fs.existsSync(mappedHomeDir)) {
|
||||
fs.renameSync(
|
||||
mappedHomeDir,
|
||||
path.join(gameBackupPath, "drive-C", currentHomeDir.replace("C:", ""))
|
||||
manifest.backups.forEach((backup) => {
|
||||
Object.keys(backup.files).forEach((key) => {
|
||||
const sourcePathWithDrives = Object.entries(manifest.drives).reduce(
|
||||
(prev, [driveKey, driveValue]) => {
|
||||
return prev.replace(driveValue, driveKey);
|
||||
},
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const backups = manifest.backups.map((backup: LudusaviBackup) => {
|
||||
const files = Object.entries(backup.files).reduce((prev, [key, value]) => {
|
||||
const updatedKey = key.replace(homeDir, currentHomeDir);
|
||||
const sourcePath = path.join(gameBackupPath, sourcePathWithDrives);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[updatedKey]: value,
|
||||
};
|
||||
}, {});
|
||||
logger.info(`Source path: ${sourcePath}`);
|
||||
|
||||
return {
|
||||
...backup,
|
||||
files,
|
||||
};
|
||||
const destinationPath = transformLudusaviBackupPathIntoWindowsPath(
|
||||
key,
|
||||
null
|
||||
)
|
||||
.replace(homeDir, userProfilePath)
|
||||
.replace("C:/Users/Public", publicProfilePath);
|
||||
|
||||
logger.info(`Moving ${sourcePath} to ${destinationPath}`);
|
||||
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||
|
||||
if (fs.existsSync(destinationPath)) {
|
||||
fs.unlinkSync(destinationPath);
|
||||
}
|
||||
|
||||
fs.renameSync(sourcePath, destinationPath);
|
||||
});
|
||||
});
|
||||
|
||||
fs.writeFileSync(mappingYamlPath, YAML.stringify({ ...manifest, backups }));
|
||||
};
|
||||
|
||||
const downloadGameArtifact = async (
|
||||
@@ -78,6 +77,8 @@ const downloadGameArtifact = async (
|
||||
gameArtifactId: string
|
||||
) => {
|
||||
try {
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
|
||||
downloadUrl: string;
|
||||
objectKey: string;
|
||||
@@ -109,34 +110,33 @@ const downloadGameArtifact = async (
|
||||
response.data.pipe(writer);
|
||||
|
||||
writer.on("error", (err) => {
|
||||
logger.error("Failed to write zip", err);
|
||||
logger.error("Failed to write tar file", err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
fs.mkdirSync(backupPath, { recursive: true });
|
||||
|
||||
writer.on("close", () => {
|
||||
tar
|
||||
.x({
|
||||
file: zipLocation,
|
||||
cwd: backupPath,
|
||||
})
|
||||
.then(async () => {
|
||||
replaceLudusaviBackupWithCurrentUser(
|
||||
backupPath,
|
||||
objectId,
|
||||
normalizePath(homeDir)
|
||||
);
|
||||
writer.on("close", async () => {
|
||||
await tar.x({
|
||||
file: zipLocation,
|
||||
cwd: backupPath,
|
||||
});
|
||||
|
||||
Ludusavi.restoreBackup(backupPath).then(() => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-backup-download-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
restoreLudusaviBackup(
|
||||
backupPath,
|
||||
objectId,
|
||||
normalizePath(homeDir),
|
||||
game?.winePrefixPath
|
||||
);
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-backup-download-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Failed to download game artifact", err);
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-backup-download-complete-${objectId}-${shop}`,
|
||||
false
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { levelKeys, gamesSublevel } from "@main/level";
|
||||
import { Wine } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const selectGameWinePrefix = async (
|
||||
@@ -8,6 +9,10 @@ const selectGameWinePrefix = async (
|
||||
objectId: string,
|
||||
winePrefixPath: string | null
|
||||
) => {
|
||||
if (winePrefixPath && !Wine.validatePrefix(winePrefixPath)) {
|
||||
throw new Error("Invalid wine prefix path");
|
||||
}
|
||||
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
@@ -32,3 +32,5 @@ export const isPortableVersion = () => {
|
||||
|
||||
export const normalizePath = (str: string) =>
|
||||
path.posix.normalize(str).replace(/\\/g, "/");
|
||||
|
||||
export * from "./reg-parser";
|
||||
|
||||
58
src/main/helpers/reg-parser.ts
Normal file
58
src/main/helpers/reg-parser.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
type RegValue = string | number | null;
|
||||
|
||||
interface RegEntry {
|
||||
path: string;
|
||||
timestamp?: string;
|
||||
values: Record<string, RegValue>;
|
||||
}
|
||||
|
||||
export function parseRegFile(content: string): RegEntry[] {
|
||||
const lines = content.split(/\r?\n/);
|
||||
const entries: RegEntry[] = [];
|
||||
|
||||
let currentPath: string | null = null;
|
||||
let currentEntry: RegEntry | null = null;
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith(";") || line.startsWith(";;")) continue;
|
||||
|
||||
if (line.startsWith("#")) {
|
||||
const match = line.match(/^#time=(\w+)/);
|
||||
if (match && currentEntry) {
|
||||
currentEntry.timestamp = match[1];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("[")) {
|
||||
const match = line.match(/^\[(.+?)\](?:\s+\d+)?/);
|
||||
if (match) {
|
||||
if (currentEntry) entries.push(currentEntry);
|
||||
currentPath = match[1];
|
||||
currentEntry = { path: currentPath, values: {} };
|
||||
}
|
||||
} else if (currentEntry) {
|
||||
const kvMatch = line.match(/^"?(.*?)"?=(.*)$/);
|
||||
if (kvMatch) {
|
||||
const [, key, rawValue] = kvMatch;
|
||||
let value: RegValue;
|
||||
|
||||
if (rawValue === '""') {
|
||||
value = "";
|
||||
} else if (rawValue.startsWith("dword:")) {
|
||||
value = parseInt(rawValue.slice(6), 16);
|
||||
} else if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
|
||||
value = rawValue.slice(1, -1);
|
||||
} else {
|
||||
value = rawValue;
|
||||
}
|
||||
|
||||
currentEntry.values[key || "@"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentEntry) entries.push(currentEntry);
|
||||
return entries;
|
||||
}
|
||||
@@ -23,7 +23,9 @@ autoUpdater.logger = logger;
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.quit();
|
||||
|
||||
app.commandLine.appendSwitch("--no-sandbox");
|
||||
if (process.platform !== "linux") {
|
||||
app.commandLine.appendSwitch("--no-sandbox");
|
||||
}
|
||||
|
||||
i18n.init({
|
||||
resources,
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
RealDebridClient,
|
||||
Aria2,
|
||||
DownloadManager,
|
||||
Ludusavi,
|
||||
HydraApi,
|
||||
uploadGamesBatch,
|
||||
startMainLoop,
|
||||
Ludusavi,
|
||||
} from "@main/services";
|
||||
|
||||
export const loadState = async () => {
|
||||
@@ -39,7 +39,7 @@ export const loadState = async () => {
|
||||
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||
}
|
||||
|
||||
Ludusavi.addManifestToLudusaviConfig();
|
||||
Ludusavi.copyConfigFileToUserData();
|
||||
|
||||
await HydraApi.setupApi().then(() => {
|
||||
uploadGamesBatch();
|
||||
|
||||
@@ -7,7 +7,7 @@ import os from "node:os";
|
||||
import type { GameShop, User } from "@types";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { normalizePath, parseRegFile } from "@main/helpers";
|
||||
import { logger } from "./logger";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import axios from "axios";
|
||||
@@ -17,6 +17,53 @@ import i18next, { t } from "i18next";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export class CloudSync {
|
||||
public static getProfilePaths(winePrefixPath?: string | null) {
|
||||
const currentHomeDir = normalizePath(SystemPath.getPath("home"));
|
||||
|
||||
if (process.platform === "linux") {
|
||||
if (!winePrefixPath) {
|
||||
throw new Error("Wine prefix path is required");
|
||||
}
|
||||
|
||||
const userReg = fs.readFileSync(
|
||||
path.join(winePrefixPath, "user.reg"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const entries = parseRegFile(userReg);
|
||||
const volatileEnvironment = entries.find(
|
||||
(entry) => entry.path === "Volatile Environment"
|
||||
);
|
||||
|
||||
if (!volatileEnvironment) {
|
||||
throw new Error("Volatile environment not found in user.reg");
|
||||
}
|
||||
|
||||
const { values } = volatileEnvironment;
|
||||
const userProfile = String(values["USERPROFILE"]);
|
||||
|
||||
if (userProfile) {
|
||||
return {
|
||||
userProfilePath: path.join(
|
||||
winePrefixPath,
|
||||
normalizePath(userProfile.replace("C:", "drive_c"))
|
||||
),
|
||||
publicProfilePath: path.join(
|
||||
winePrefixPath,
|
||||
"drive_c",
|
||||
"users",
|
||||
"Public"
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userProfilePath: currentHomeDir,
|
||||
publicProfilePath: path.join("C:", "Users", "Public"),
|
||||
};
|
||||
}
|
||||
|
||||
public static getBackupLabel(automatic: boolean) {
|
||||
const language = i18next.language;
|
||||
|
||||
@@ -102,7 +149,9 @@ export class CloudSync {
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: normalizePath(SystemPath.getPath("home")),
|
||||
winePrefixPath: game?.winePrefixPath ?? null,
|
||||
homeDir: this.getProfilePaths(game?.winePrefixPath ?? null)
|
||||
.userProfilePath,
|
||||
downloadOptionTitle,
|
||||
platform: os.platform(),
|
||||
label,
|
||||
|
||||
@@ -15,3 +15,4 @@ export * from "./aria2";
|
||||
export * from "./ws";
|
||||
export * from "./system-path";
|
||||
export * from "./library-sync";
|
||||
export * from "./wine";
|
||||
|
||||
@@ -1,70 +1,83 @@
|
||||
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
|
||||
import Piscina from "piscina";
|
||||
|
||||
import { app } from "electron";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import YAML from "yaml";
|
||||
|
||||
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
|
||||
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
|
||||
import cp from "node:child_process";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export class Ludusavi {
|
||||
private static ludusaviPath = path.join(
|
||||
SystemPath.getPath("appData"),
|
||||
"ludusavi"
|
||||
);
|
||||
private static ludusaviConfigPath = path.join(
|
||||
this.ludusaviPath,
|
||||
private static ludusaviPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "ludusavi")
|
||||
: path.join(__dirname, "..", "..", "ludusavi");
|
||||
|
||||
private static binaryPath = path.join(this.ludusaviPath, "ludusavi");
|
||||
private static configPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
"config.yaml"
|
||||
);
|
||||
private static binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "ludusavi", "ludusavi")
|
||||
: path.join(__dirname, "..", "..", "ludusavi", "ludusavi");
|
||||
|
||||
private static worker = new Piscina({
|
||||
filename: ludusaviWorkerPath,
|
||||
workerData: {
|
||||
binaryPath: this.binaryPath,
|
||||
},
|
||||
maxThreads: 1,
|
||||
});
|
||||
|
||||
static async getConfig() {
|
||||
if (!fs.existsSync(this.ludusaviConfigPath)) {
|
||||
await this.worker.run(undefined, { name: "generateConfig" });
|
||||
}
|
||||
|
||||
public static async getConfig() {
|
||||
const config = YAML.parse(
|
||||
fs.readFileSync(this.ludusaviConfigPath, "utf-8")
|
||||
fs.readFileSync(this.configPath, "utf-8")
|
||||
) as LudusaviConfig;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
static async backupGame(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
backupPath: string,
|
||||
winePrefix?: string | null
|
||||
): Promise<LudusaviBackup> {
|
||||
return this.worker.run(
|
||||
{ title: objectId, backupPath, winePrefix },
|
||||
{ name: "backupGame" }
|
||||
);
|
||||
public static async copyConfigFileToUserData() {
|
||||
fs.cpSync(path.join(this.ludusaviPath, "config.yaml"), this.configPath);
|
||||
}
|
||||
|
||||
static async getBackupPreview(
|
||||
public static async backupGame(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
backupPath?: string | null,
|
||||
winePrefix?: string | null,
|
||||
preview?: boolean
|
||||
): Promise<LudusaviBackup> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
"--config",
|
||||
this.ludusaviPath,
|
||||
"backup",
|
||||
objectId,
|
||||
"--api",
|
||||
"--force",
|
||||
];
|
||||
|
||||
if (preview) args.push("--preview");
|
||||
if (backupPath) args.push("--path", backupPath);
|
||||
if (winePrefix) args.push("--wine-prefix", winePrefix);
|
||||
|
||||
cp.execFile(
|
||||
this.binaryPath,
|
||||
args,
|
||||
(err: cp.ExecFileException | null, stdout: string) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(stdout) as LudusaviBackup);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getBackupPreview(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefix?: string | null
|
||||
): Promise<LudusaviBackup | null> {
|
||||
const config = await this.getConfig();
|
||||
|
||||
const backupData = await this.worker.run(
|
||||
{ title: objectId, winePrefix, preview: true },
|
||||
{ name: "backupGame" }
|
||||
const backupData = await this.backupGame(
|
||||
_shop,
|
||||
objectId,
|
||||
null,
|
||||
winePrefix,
|
||||
true
|
||||
);
|
||||
|
||||
const customGame = config.customGames.find(
|
||||
@@ -77,19 +90,6 @@ export class Ludusavi {
|
||||
};
|
||||
}
|
||||
|
||||
static async restoreBackup(backupPath: string) {
|
||||
return this.worker.run(backupPath, { name: "restoreBackup" });
|
||||
}
|
||||
|
||||
static async addManifestToLudusaviConfig() {
|
||||
const config = await this.getConfig();
|
||||
|
||||
config.manifest.enable = false;
|
||||
config.manifest.secondary = [{ url: LUDUSAVI_MANIFEST_URL, enable: true }];
|
||||
|
||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||
}
|
||||
|
||||
static async addCustomGame(title: string, savePath: string | null) {
|
||||
const config = await this.getConfig();
|
||||
const filteredGames = config.customGames.filter(
|
||||
@@ -105,6 +105,10 @@ export class Ludusavi {
|
||||
}
|
||||
|
||||
config.customGames = filteredGames;
|
||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(this.ludusaviPath, "config.yaml"),
|
||||
YAML.stringify(config)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
23
src/main/services/wine.ts
Normal file
23
src/main/services/wine.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export class Wine {
|
||||
public static validatePrefix(winePrefixPath: string) {
|
||||
const requiredFiles = [
|
||||
"system.reg",
|
||||
"user.reg",
|
||||
"userdef.reg",
|
||||
"dosdevices",
|
||||
"drive_c",
|
||||
];
|
||||
|
||||
for (const file of requiredFiles) {
|
||||
const filePath = path.join(winePrefixPath, file);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { LudusaviBackup } from "@types";
|
||||
import cp from "node:child_process";
|
||||
|
||||
import { workerData } from "node:worker_threads";
|
||||
|
||||
const { binaryPath } = workerData;
|
||||
|
||||
export const backupGame = ({
|
||||
title,
|
||||
backupPath,
|
||||
preview = false,
|
||||
winePrefix,
|
||||
}: {
|
||||
title: string;
|
||||
backupPath: string;
|
||||
preview?: boolean;
|
||||
winePrefix?: string;
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = ["backup", title, "--api", "--force"];
|
||||
|
||||
if (preview) args.push("--preview");
|
||||
if (backupPath) args.push("--path", backupPath);
|
||||
if (winePrefix) args.push("--wine-prefix", winePrefix);
|
||||
|
||||
cp.execFile(
|
||||
binaryPath,
|
||||
args,
|
||||
(err: cp.ExecFileException | null, stdout: string) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(stdout) as LudusaviBackup);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const restoreBackup = (backupPath: string) => {
|
||||
const result = cp.execFileSync(binaryPath, [
|
||||
"restore",
|
||||
"--path",
|
||||
backupPath,
|
||||
"--api",
|
||||
"--force",
|
||||
]);
|
||||
|
||||
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
||||
};
|
||||
|
||||
export const generateConfig = () => {
|
||||
const result = cp.execFileSync(binaryPath, ["schema", "config"]);
|
||||
|
||||
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
||||
};
|
||||
@@ -160,7 +160,6 @@ export function GameDetailsContextProvider({
|
||||
|
||||
setShopDetails((prev) => {
|
||||
if (!prev) return null;
|
||||
console.log("assets", assets);
|
||||
return {
|
||||
...prev,
|
||||
assets,
|
||||
|
||||
@@ -147,12 +147,16 @@ export function GameOptionsModal({
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
await window.electron.selectGameWinePrefix(
|
||||
game.shop,
|
||||
game.objectId,
|
||||
filePaths[0]
|
||||
);
|
||||
await updateGame();
|
||||
try {
|
||||
await window.electron.selectGameWinePrefix(
|
||||
game.shop,
|
||||
game.objectId,
|
||||
filePaths[0]
|
||||
);
|
||||
await updateGame();
|
||||
} catch (error) {
|
||||
showErrorToast(t("invalid_wine_prefix_path"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -40,3 +40,12 @@ export interface LudusaviConfig {
|
||||
registry: [];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface LudusaviBackupMapping {
|
||||
files: {
|
||||
[key: string]: {
|
||||
hash: string;
|
||||
size: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
109
yarn.lock
109
yarn.lock
@@ -1850,108 +1850,6 @@
|
||||
dependencies:
|
||||
"@monaco-editor/loader" "^1.4.0"
|
||||
|
||||
"@napi-rs/nice-android-arm-eabi@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936"
|
||||
integrity sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==
|
||||
|
||||
"@napi-rs/nice-android-arm64@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz#32fc32e9649bd759d2a39ad745e95766f6759d2f"
|
||||
integrity sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==
|
||||
|
||||
"@napi-rs/nice-darwin-arm64@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz#d3c44c51b94b25a82d45803e2255891e833e787b"
|
||||
integrity sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==
|
||||
|
||||
"@napi-rs/nice-darwin-x64@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz#f1b1365a8370c6a6957e90085a9b4873d0e6a957"
|
||||
integrity sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==
|
||||
|
||||
"@napi-rs/nice-freebsd-x64@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz#4280f081efbe0b46c5165fdaea8b286e55a8f89e"
|
||||
integrity sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==
|
||||
|
||||
"@napi-rs/nice-linux-arm-gnueabihf@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz#07aec23a9467ed35eb7602af5e63d42c5d7bd473"
|
||||
integrity sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==
|
||||
|
||||
"@napi-rs/nice-linux-arm64-gnu@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz#038a77134cc6df3c48059d5a5e199d6f50fb9a90"
|
||||
integrity sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==
|
||||
|
||||
"@napi-rs/nice-linux-arm64-musl@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz#715d0906582ba0cff025109f42e5b84ea68c2bcc"
|
||||
integrity sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==
|
||||
|
||||
"@napi-rs/nice-linux-ppc64-gnu@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz#ac1c8f781c67b0559fa7a1cd4ae3ca2299dc3d06"
|
||||
integrity sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==
|
||||
|
||||
"@napi-rs/nice-linux-riscv64-gnu@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz#b0a430549acfd3920ffd28ce544e2fe17833d263"
|
||||
integrity sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==
|
||||
|
||||
"@napi-rs/nice-linux-s390x-gnu@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz#5b95caf411ad72a965885217db378c4d09733e97"
|
||||
integrity sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==
|
||||
|
||||
"@napi-rs/nice-linux-x64-gnu@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz#a98cdef517549f8c17a83f0236a69418a90e77b7"
|
||||
integrity sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==
|
||||
|
||||
"@napi-rs/nice-linux-x64-musl@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz#5e26843eafa940138aed437c870cca751c8a8957"
|
||||
integrity sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==
|
||||
|
||||
"@napi-rs/nice-win32-arm64-msvc@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz#bd62617d02f04aa30ab1e9081363856715f84cd8"
|
||||
integrity sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==
|
||||
|
||||
"@napi-rs/nice-win32-ia32-msvc@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz#b8b7aad552a24836027473d9b9f16edaeabecf18"
|
||||
integrity sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==
|
||||
|
||||
"@napi-rs/nice-win32-x64-msvc@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz#37d8718b8f722f49067713e9f1e85540c9a3dd09"
|
||||
integrity sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==
|
||||
|
||||
"@napi-rs/nice@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice/-/nice-1.0.1.tgz#483d3ff31e5661829a1efb4825591a135c3bfa7d"
|
||||
integrity sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==
|
||||
optionalDependencies:
|
||||
"@napi-rs/nice-android-arm-eabi" "1.0.1"
|
||||
"@napi-rs/nice-android-arm64" "1.0.1"
|
||||
"@napi-rs/nice-darwin-arm64" "1.0.1"
|
||||
"@napi-rs/nice-darwin-x64" "1.0.1"
|
||||
"@napi-rs/nice-freebsd-x64" "1.0.1"
|
||||
"@napi-rs/nice-linux-arm-gnueabihf" "1.0.1"
|
||||
"@napi-rs/nice-linux-arm64-gnu" "1.0.1"
|
||||
"@napi-rs/nice-linux-arm64-musl" "1.0.1"
|
||||
"@napi-rs/nice-linux-ppc64-gnu" "1.0.1"
|
||||
"@napi-rs/nice-linux-riscv64-gnu" "1.0.1"
|
||||
"@napi-rs/nice-linux-s390x-gnu" "1.0.1"
|
||||
"@napi-rs/nice-linux-x64-gnu" "1.0.1"
|
||||
"@napi-rs/nice-linux-x64-musl" "1.0.1"
|
||||
"@napi-rs/nice-win32-arm64-msvc" "1.0.1"
|
||||
"@napi-rs/nice-win32-ia32-msvc" "1.0.1"
|
||||
"@napi-rs/nice-win32-x64-msvc" "1.0.1"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
@@ -7722,13 +7620,6 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
piscina@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.7.0.tgz#68936fc77128db00541366531330138e366dc851"
|
||||
integrity sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==
|
||||
optionalDependencies:
|
||||
"@napi-rs/nice" "^1.0.1"
|
||||
|
||||
plist@3.1.0, plist@^3.0.4, plist@^3.0.5, plist@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9"
|
||||
|
||||
Reference in New Issue
Block a user