mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-22 10:23:56 +00:00
Merge branch 'main' into feature/seed-completed-downloads
This commit is contained in:
@@ -19,6 +19,10 @@ export const seedsPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "seeds")
|
||||
: path.join(__dirname, "..", "..", "seeds");
|
||||
|
||||
export const achievementSoundPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "achievement.wav")
|
||||
: path.join(__dirname, "..", "..", "resources", "achievement.wav");
|
||||
|
||||
export const backupsPath = path.join(app.getPath("userData"), "Backups");
|
||||
|
||||
export const appVersion = app.getVersion();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
@@ -10,8 +9,6 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
if (!auth) return null;
|
||||
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||
|
||||
Sentry.setContext("sessionId", payload.sessionId);
|
||||
|
||||
return payload.sessionId;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
import {
|
||||
DownloadManager,
|
||||
HydraApi,
|
||||
@@ -29,9 +28,6 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
gamesPlaytime.clear();
|
||||
});
|
||||
|
||||
/* Removes user from Sentry */
|
||||
Sentry.setUser(null);
|
||||
|
||||
/* Cancels any ongoing downloads */
|
||||
DownloadManager.cancelDownload();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import parseTorrent from "parse-torrent";
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { HydraAnalytics } from "@main/services/hydra-analytics";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -90,6 +91,11 @@ const startGameDownload = async (
|
||||
logger.error("Failed to create game download", err);
|
||||
});
|
||||
|
||||
const { infoHash } = await parseTorrent(payload.uri);
|
||||
if (infoHash) {
|
||||
HydraAnalytics.postDownload(infoHash).catch(() => {});
|
||||
}
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { app, BrowserWindow, net, protocol } from "electron";
|
||||
import { init } from "@sentry/electron/main";
|
||||
import updater from "electron-updater";
|
||||
import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
@@ -26,12 +25,6 @@ autoUpdater.logger = logger;
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.quit();
|
||||
|
||||
if (import.meta.env.MAIN_VITE_SENTRY_DSN) {
|
||||
init({
|
||||
dsn: import.meta.env.MAIN_VITE_SENTRY_DSN,
|
||||
});
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch("--no-sandbox");
|
||||
|
||||
i18n.init({
|
||||
@@ -105,7 +98,6 @@ app.whenReady().then(async () => {
|
||||
WindowManager.createMainWindow();
|
||||
}
|
||||
|
||||
WindowManager.createNotificationWindow();
|
||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { AchievementFile, UnlockedAchievement } from "@types";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { Cracker } from "@shared";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||
|
||||
const fileStats: Map<string, number> = new Map();
|
||||
const fltFiles: Map<string, Set<string>> = new Map();
|
||||
@@ -249,11 +249,12 @@ export class AchievementWatcherManager {
|
||||
0
|
||||
);
|
||||
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-combined-achievements-unlocked",
|
||||
totalNewGamesWithAchievements,
|
||||
totalNewAchievements
|
||||
);
|
||||
if (totalNewAchievements > 0) {
|
||||
publishCombinedNewAchievementNotification(
|
||||
totalNewAchievements,
|
||||
totalNewGamesWithAchievements
|
||||
);
|
||||
}
|
||||
|
||||
this.hasFinishedMergingWithRemote = true;
|
||||
};
|
||||
|
||||
@@ -8,11 +8,12 @@ import { HydraApi } from "../hydra-api";
|
||||
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
||||
import { Game } from "@main/entity";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { publishNewAchievementNotification } from "../notifications";
|
||||
|
||||
const saveAchievementsOnLocal = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
achievements: any[],
|
||||
achievements: UnlockedAchievement[],
|
||||
sendUpdateEvent: boolean
|
||||
) => {
|
||||
return gameAchievementRepository
|
||||
@@ -82,6 +83,8 @@ export const mergeAchievements = async (
|
||||
};
|
||||
});
|
||||
|
||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||
|
||||
if (
|
||||
newAchievements.length &&
|
||||
publishNotification &&
|
||||
@@ -107,16 +110,15 @@ export const mergeAchievements = async (
|
||||
};
|
||||
});
|
||||
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
game.objectID,
|
||||
game.shop,
|
||||
achievementsInfo
|
||||
);
|
||||
publishNewAchievementNotification({
|
||||
achievements: achievementsInfo,
|
||||
unlockedAchievementCount: mergedLocalAchievements.length,
|
||||
totalAchievementCount: achievementsData.length,
|
||||
gameTitle: game.title,
|
||||
gameIcon: game.iconUrl,
|
||||
});
|
||||
}
|
||||
|
||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||
|
||||
if (game.remoteId) {
|
||||
await HydraApi.put("/profile/games/achievements", {
|
||||
id: game.remoteId,
|
||||
|
||||
34
src/main/services/hydra-analytics.ts
Normal file
34
src/main/services/hydra-analytics.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { userSubscriptionRepository } from "@main/repository";
|
||||
import axios from "axios";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
export class HydraAnalytics {
|
||||
private static instance = axios.create({
|
||||
baseURL: import.meta.env.MAIN_VITE_ANALYTICS_API_URL,
|
||||
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
|
||||
});
|
||||
|
||||
private static async hasActiveSubscription() {
|
||||
const userSubscription = await userSubscriptionRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
return (
|
||||
userSubscription?.expiresAt && userSubscription.expiresAt > new Date()
|
||||
);
|
||||
}
|
||||
|
||||
static async postDownload(hash: string) {
|
||||
const hasSubscription = await this.hasActiveSubscription();
|
||||
|
||||
return this.instance
|
||||
.post("/track", {
|
||||
event: "download",
|
||||
attributes: {
|
||||
hash,
|
||||
hasSubscription,
|
||||
},
|
||||
})
|
||||
.then((response) => response.data);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export class Ludusavi {
|
||||
workerData: {
|
||||
binaryPath: this.binaryPath,
|
||||
},
|
||||
maxThreads: 1,
|
||||
});
|
||||
|
||||
static async getConfig() {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Notification, nativeImage } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { parseICO } from "icojs";
|
||||
import trayIcon from "@resources/tray-icon.png?asset";
|
||||
import { Game } from "@main/entity";
|
||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
|
||||
const getGameIconNativeImage = async (gameId: number) => {
|
||||
try {
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
id: gameId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!game?.iconUrl) return undefined;
|
||||
|
||||
const images = await parseICO(
|
||||
Buffer.from(game.iconUrl.split("base64,")[1], "base64")
|
||||
);
|
||||
|
||||
const highResIcon = images.find((image) => image.width >= 128);
|
||||
if (!highResIcon) return undefined;
|
||||
|
||||
return nativeImage.createFromBuffer(Buffer.from(highResIcon.buffer));
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const publishDownloadCompleteNotification = async (game: Game) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
const icon = await getGameIconNativeImage(game.id);
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled) {
|
||||
new Notification({
|
||||
title: t("download_complete", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
body: t("game_ready_to_install", {
|
||||
ns: "notifications",
|
||||
title: game.title,
|
||||
}),
|
||||
icon,
|
||||
}).show();
|
||||
}
|
||||
};
|
||||
|
||||
export const publishNotificationUpdateReadyToInstall = async (
|
||||
version: string
|
||||
) => {
|
||||
new Notification({
|
||||
title: t("new_update_available", {
|
||||
ns: "notifications",
|
||||
version,
|
||||
}),
|
||||
body: t("restart_to_install_update", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
};
|
||||
|
||||
export const publishNewFriendRequestNotification = async () => {};
|
||||
146
src/main/services/notifications/index.ts
Normal file
146
src/main/services/notifications/index.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Notification, app } from "electron";
|
||||
import { t } from "i18next";
|
||||
import trayIcon from "@resources/tray-icon.png?asset";
|
||||
import { Game } from "@main/entity";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import fs from "node:fs";
|
||||
import axios from "axios";
|
||||
import path from "node:path";
|
||||
import sound from "sound-play";
|
||||
import { achievementSoundPath } from "@main/constants";
|
||||
import icon from "@resources/icon.png?asset";
|
||||
import { NotificationOptions, toXmlString } from "./xml";
|
||||
import { logger } from "../logger";
|
||||
|
||||
async function downloadImage(url: string | null) {
|
||||
if (!url) return undefined;
|
||||
if (!url.startsWith("http")) return undefined;
|
||||
|
||||
const fileName = url.split("/").pop()!;
|
||||
const outputPath = path.join(app.getPath("temp"), fileName);
|
||||
const writer = fs.createWriteStream(outputPath);
|
||||
|
||||
const response = await axios.get(url, {
|
||||
responseType: "stream",
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise<string | undefined>((resolve) => {
|
||||
writer.on("finish", () => {
|
||||
resolve(outputPath);
|
||||
});
|
||||
writer.on("error", () => {
|
||||
logger.error("Failed to download image", { url });
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const publishDownloadCompleteNotification = async (game: Game) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled) {
|
||||
new Notification({
|
||||
title: t("download_complete", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
body: t("game_ready_to_install", {
|
||||
ns: "notifications",
|
||||
title: game.title,
|
||||
}),
|
||||
icon: await downloadImage(game.iconUrl),
|
||||
}).show();
|
||||
}
|
||||
};
|
||||
|
||||
export const publishNotificationUpdateReadyToInstall = async (
|
||||
version: string
|
||||
) => {
|
||||
new Notification({
|
||||
title: t("new_update_available", {
|
||||
ns: "notifications",
|
||||
version,
|
||||
}),
|
||||
body: t("restart_to_install_update", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
};
|
||||
|
||||
export const publishNewFriendRequestNotification = async () => {};
|
||||
|
||||
export const publishCombinedNewAchievementNotification = async (
|
||||
achievementCount,
|
||||
gameCount
|
||||
) => {
|
||||
const options: NotificationOptions = {
|
||||
title: t("achievement_unlocked", { ns: "achievement" }),
|
||||
body: t("new_achievements_unlocked", {
|
||||
ns: "achievement",
|
||||
gameCount,
|
||||
achievementCount,
|
||||
}),
|
||||
icon,
|
||||
silent: true,
|
||||
};
|
||||
|
||||
new Notification({
|
||||
...options,
|
||||
toastXml: toXmlString(options),
|
||||
}).show();
|
||||
|
||||
if (process.platform !== "linux") {
|
||||
sound.play(achievementSoundPath);
|
||||
}
|
||||
};
|
||||
|
||||
export const publishNewAchievementNotification = async (info: {
|
||||
achievements: { displayName: string; iconUrl: string }[];
|
||||
unlockedAchievementCount: number;
|
||||
totalAchievementCount: number;
|
||||
gameTitle: string;
|
||||
gameIcon: string | null;
|
||||
}) => {
|
||||
const partialOptions =
|
||||
info.achievements.length > 1
|
||||
? {
|
||||
title: t("achievements_unlocked_for_game", {
|
||||
ns: "achievement",
|
||||
gameTitle: info.gameTitle,
|
||||
achievementCount: info.achievements.length,
|
||||
}),
|
||||
body: info.achievements.map((a) => a.displayName).join(", "),
|
||||
icon: (await downloadImage(info.gameIcon)) ?? icon,
|
||||
}
|
||||
: {
|
||||
title: t("achievement_unlocked", { ns: "achievement" }),
|
||||
body: info.achievements[0].displayName,
|
||||
icon: (await downloadImage(info.achievements[0].iconUrl)) ?? icon,
|
||||
};
|
||||
|
||||
const options: NotificationOptions = {
|
||||
...partialOptions,
|
||||
silent: true,
|
||||
progress: {
|
||||
value: info.unlockedAchievementCount / info.totalAchievementCount,
|
||||
valueOverride: t("achievement_progress", {
|
||||
ns: "achievement",
|
||||
unlockedCount: info.unlockedAchievementCount,
|
||||
totalCount: info.totalAchievementCount,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
new Notification({
|
||||
...options,
|
||||
toastXml: toXmlString(options),
|
||||
}).show();
|
||||
|
||||
if (process.platform !== "linux") {
|
||||
sound.play(achievementSoundPath);
|
||||
}
|
||||
};
|
||||
79
src/main/services/notifications/xml.ts
Normal file
79
src/main/services/notifications/xml.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export interface NotificationOptions {
|
||||
title: string;
|
||||
body?: string;
|
||||
icon: string;
|
||||
duration?: "short" | "long";
|
||||
silent?: boolean;
|
||||
progress?: {
|
||||
status?: string;
|
||||
value: number;
|
||||
valueOverride: string;
|
||||
};
|
||||
}
|
||||
|
||||
function escape(string: string) {
|
||||
return string.replace(/[<>&'"]/g, (match) => {
|
||||
switch (match) {
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "&":
|
||||
return "&";
|
||||
case "'":
|
||||
return "'";
|
||||
case '"':
|
||||
return """;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addAttributeOrTrim(name: string, value: string) {
|
||||
return value ? `${name}="${value}" ` : "";
|
||||
}
|
||||
|
||||
export function toXmlString(options: NotificationOptions) {
|
||||
let template =
|
||||
"<toast " +
|
||||
`displayTimestamp="${new Date().toISOString()}" ` +
|
||||
`scenario="default" ` +
|
||||
`duration="${options.duration ?? "short"}" ` +
|
||||
`activationType="protocol" ` +
|
||||
">";
|
||||
|
||||
//Visual
|
||||
template += `<visual><binding template="ToastGeneric">`;
|
||||
if (options.icon)
|
||||
template += `<image placement="appLogoOverride" src="${options.icon}" hint-crop="none"/>`;
|
||||
template +=
|
||||
`<text><![CDATA[${options.title}]]></text>` +
|
||||
`<text><![CDATA[${options.body}]]></text>`;
|
||||
|
||||
//Progress bar
|
||||
if (options.progress) {
|
||||
template +=
|
||||
"<progress " +
|
||||
`value="${options.progress.value}" ` +
|
||||
`status="" ` +
|
||||
addAttributeOrTrim(
|
||||
"valueStringOverride",
|
||||
escape(options.progress.valueOverride)
|
||||
) +
|
||||
"/>";
|
||||
}
|
||||
template += "</binding></visual>";
|
||||
|
||||
//Actions
|
||||
template += "<actions>";
|
||||
template += "</actions>";
|
||||
|
||||
//Audio
|
||||
template += "<audio " + `silent="true" ` + `loop="false" ` + "/>";
|
||||
|
||||
//EOF
|
||||
template += "</toast>";
|
||||
|
||||
return template;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
userAuthRepository,
|
||||
userSubscriptionRepository,
|
||||
} from "@main/repository";
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { logger } from "../logger";
|
||||
|
||||
@@ -39,8 +38,6 @@ export const getUserData = () => {
|
||||
await userSubscriptionRepository.delete({ id: 1 });
|
||||
}
|
||||
|
||||
Sentry.setUser({ id: me.id, username: me.username });
|
||||
|
||||
return me;
|
||||
})
|
||||
.catch(async (err) => {
|
||||
|
||||
@@ -20,7 +20,6 @@ import UserAgent from "user-agents";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
public static notificationWindow: Electron.BrowserWindow | null = null;
|
||||
|
||||
private static loadMainWindowURL(hash = "") {
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
@@ -39,21 +38,6 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static loadNotificationWindowURL() {
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.notificationWindow?.loadURL(
|
||||
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
|
||||
);
|
||||
} else {
|
||||
this.notificationWindow?.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash: "achievement-notification",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static createMainWindow() {
|
||||
if (this.mainWindow) return;
|
||||
|
||||
@@ -63,7 +47,7 @@ export class WindowManager {
|
||||
minWidth: 1024,
|
||||
minHeight: 540,
|
||||
backgroundColor: "#1c1c1c",
|
||||
titleBarStyle: process.platform === "win32" ? "hidden" : "default",
|
||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||
...(process.platform === "linux" ? { icon } : {}),
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
titleBarOverlay: {
|
||||
@@ -151,32 +135,6 @@ export class WindowManager {
|
||||
});
|
||||
}
|
||||
|
||||
public static createNotificationWindow() {
|
||||
this.notificationWindow = new BrowserWindow({
|
||||
transparent: true,
|
||||
maximizable: false,
|
||||
autoHideMenuBar: true,
|
||||
minimizable: false,
|
||||
focusable: false,
|
||||
skipTaskbar: true,
|
||||
frame: false,
|
||||
width: 350,
|
||||
height: 104,
|
||||
x: 0,
|
||||
y: 0,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
||||
// visibleOnFullScreen: true,
|
||||
// });
|
||||
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
this.loadNotificationWindowURL();
|
||||
}
|
||||
|
||||
public static openAuthWindow() {
|
||||
if (this.mainWindow) {
|
||||
const authWindow = new BrowserWindow({
|
||||
|
||||
2
src/main/vite-env.d.ts
vendored
2
src/main/vite-env.d.ts
vendored
@@ -3,8 +3,8 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
|
||||
readonly MAIN_VITE_API_URL: string;
|
||||
readonly MAIN_VITE_ANALYTICS_API_URL: string;
|
||||
readonly MAIN_VITE_AUTH_URL: string;
|
||||
readonly MAIN_VITE_SENTRY_DSN: string;
|
||||
readonly MAIN_VITE_CHECKOUT_URL: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,25 @@ export const backupGame = ({
|
||||
preview?: boolean;
|
||||
winePrefix?: string;
|
||||
}) => {
|
||||
const args = ["backup", title, "--api", "--force"];
|
||||
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);
|
||||
if (preview) args.push("--preview");
|
||||
if (backupPath) args.push("--path", backupPath);
|
||||
if (winePrefix) args.push("--wine-prefix", winePrefix);
|
||||
|
||||
const result = cp.execFileSync(binaryPath, args);
|
||||
cp.execFile(
|
||||
binaryPath,
|
||||
args,
|
||||
(err: cp.ExecFileException | null, stdout: string) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
||||
return resolve(JSON.parse(stdout) as LudusaviBackup);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const restoreBackup = (backupPath: string) => {
|
||||
|
||||
Reference in New Issue
Block a user