mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
feat: adding wine prefix to backup creation on linux
This commit is contained in:
@@ -23,7 +23,8 @@ const restoreLudusaviBackup = (
|
||||
backupPath: string,
|
||||
title: string,
|
||||
homeDir: string,
|
||||
winePrefixPath?: string | null
|
||||
winePrefixPath?: string | null,
|
||||
artifactWinePrefixPath?: string | null
|
||||
) => {
|
||||
const gameBackupPath = path.join(backupPath, title);
|
||||
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
|
||||
@@ -52,7 +53,7 @@ const restoreLudusaviBackup = (
|
||||
|
||||
const destinationPath = transformLudusaviBackupPathIntoWindowsPath(
|
||||
key,
|
||||
null
|
||||
artifactWinePrefixPath
|
||||
)
|
||||
.replace(homeDir, userProfilePath)
|
||||
.replace("C:/Users/Public", publicProfilePath);
|
||||
@@ -79,10 +80,16 @@ const downloadGameArtifact = async (
|
||||
try {
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
|
||||
const {
|
||||
downloadUrl,
|
||||
objectKey,
|
||||
homeDir,
|
||||
winePrefixPath: artifactWinePrefixPath,
|
||||
} = await HydraApi.post<{
|
||||
downloadUrl: string;
|
||||
objectKey: string;
|
||||
homeDir: string;
|
||||
winePrefixPath: string | null;
|
||||
}>(`/profile/games/artifacts/${gameArtifactId}/download`);
|
||||
|
||||
const zipLocation = path.join(SystemPath.getPath("userData"), objectKey);
|
||||
@@ -126,7 +133,8 @@ const downloadGameArtifact = async (
|
||||
backupPath,
|
||||
objectId,
|
||||
normalizePath(homeDir),
|
||||
game?.winePrefixPath
|
||||
game?.winePrefixPath,
|
||||
artifactWinePrefixPath
|
||||
);
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
|
||||
@@ -34,6 +34,7 @@ import "./library/remove-game-from-library";
|
||||
import "./library/select-game-wine-prefix";
|
||||
import "./library/reset-game-achievements";
|
||||
import "./library/toggle-automatic-cloud-sync";
|
||||
import "./library/get-default-wine-prefix-selection-path";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SystemPath } from "@main/services";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDefaultWinePrefixSelectionPath = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => {
|
||||
const steamWinePrefixes = path.join(
|
||||
SystemPath.getPath("home"),
|
||||
".local",
|
||||
"share",
|
||||
"Steam",
|
||||
"steamapps",
|
||||
"compatdata"
|
||||
);
|
||||
|
||||
if (fs.existsSync(steamWinePrefixes)) {
|
||||
return steamWinePrefixes;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
registerEvent(
|
||||
"getDefaultWinePrefixSelectionPath",
|
||||
getDefaultWinePrefixSelectionPath
|
||||
);
|
||||
@@ -15,19 +15,25 @@ export class Ludusavi {
|
||||
private static binaryPath = path.join(this.ludusaviPath, "ludusavi");
|
||||
private static configPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
"config.yaml"
|
||||
"ludusavi"
|
||||
);
|
||||
|
||||
public static async getConfig() {
|
||||
const config = YAML.parse(
|
||||
fs.readFileSync(this.configPath, "utf-8")
|
||||
fs.readFileSync(path.join(this.ludusaviPath, "config.yaml"), "utf-8")
|
||||
) as LudusaviConfig;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public static async copyConfigFileToUserData() {
|
||||
fs.cpSync(path.join(this.ludusaviPath, "config.yaml"), this.configPath);
|
||||
if (!fs.existsSync(this.configPath)) {
|
||||
fs.mkdirSync(this.configPath, { recursive: true });
|
||||
fs.cpSync(
|
||||
path.join(this.ludusaviPath, "config.yaml"),
|
||||
path.join(this.configPath, "config.yaml")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async backupGame(
|
||||
@@ -40,7 +46,7 @@ export class Ludusavi {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
"--config",
|
||||
this.ludusaviPath,
|
||||
this.configPath,
|
||||
"backup",
|
||||
objectId,
|
||||
"--api",
|
||||
@@ -106,6 +112,9 @@ export class Ludusavi {
|
||||
|
||||
config.customGames = filteredGames;
|
||||
|
||||
fs.writeFileSync(this.configPath, YAML.stringify(config));
|
||||
fs.writeFileSync(
|
||||
path.join(this.configPath, "config.yaml"),
|
||||
YAML.stringify(config)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||
extractGameDownload: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
||||
getDefaultWinePrefixSelectionPath: () =>
|
||||
ipcRenderer.invoke("getDefaultWinePrefixSelectionPath"),
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import cn from "classnames";
|
||||
import { PlacesType, Tooltip } from "react-tooltip";
|
||||
|
||||
import "./button.scss";
|
||||
import { useId } from "react";
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
> {
|
||||
tooltip?: string;
|
||||
tooltipPlace?: PlacesType;
|
||||
theme?: "primary" | "outline" | "dark" | "danger";
|
||||
}
|
||||
|
||||
@@ -14,15 +18,32 @@ export function Button({
|
||||
children,
|
||||
theme = "primary",
|
||||
className,
|
||||
tooltip,
|
||||
tooltipPlace = "top",
|
||||
...props
|
||||
}: Readonly<ButtonProps>) {
|
||||
const id = useId();
|
||||
|
||||
const tooltipProps = tooltip
|
||||
? {
|
||||
"data-tooltip-id": id,
|
||||
"data-tooltip-place": tooltipPlace,
|
||||
"data-tooltip-content": tooltip,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn("button", `button--${theme}`, className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("button", `button--${theme}`, className)}
|
||||
{...props}
|
||||
{...tooltipProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
{tooltip && <Tooltip id={id} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
@@ -179,6 +179,7 @@ declare global {
|
||||
onExtractionComplete: (
|
||||
cb: (shop: GameShop, objectId: string) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
getDefaultWinePrefixSelectionPath: () => Promise<string | null>;
|
||||
|
||||
/* Download sources */
|
||||
putDownloadSource: (
|
||||
|
||||
@@ -43,7 +43,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
getGameBackupPreview,
|
||||
} = useContext(cloudSyncContext);
|
||||
|
||||
const { objectId, shop, gameTitle, lastDownloadedOption } =
|
||||
const { objectId, shop, gameTitle, game, lastDownloadedOption } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
@@ -148,6 +148,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
]);
|
||||
|
||||
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
||||
const isMissingWinePrefix =
|
||||
window.electron.platform === "linux" && !game?.winePrefixPath;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -175,10 +177,13 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => uploadSaveGame(lastDownloadedOption?.title ?? null)}
|
||||
tooltip={isMissingWinePrefix ? t("missing_wine_prefix") : undefined}
|
||||
tooltipPlace="left"
|
||||
disabled={
|
||||
disableActions ||
|
||||
!backupPreview?.overall.totalGames ||
|
||||
artifacts.length >= backupsPerGameLimit
|
||||
artifacts.length >= backupsPerGameLimit ||
|
||||
isMissingWinePrefix
|
||||
}
|
||||
>
|
||||
{uploadingBackup ? (
|
||||
|
||||
@@ -144,6 +144,7 @@ export function GameOptionsModal({
|
||||
const handleChangeWinePrefixPath = async () => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
properties: ["openDirectory"],
|
||||
defaultPath: await window.electron.getDefaultWinePrefixSelectionPath(),
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
@@ -155,7 +156,10 @@ export function GameOptionsModal({
|
||||
);
|
||||
await updateGame();
|
||||
} catch (error) {
|
||||
showErrorToast(t("invalid_wine_prefix_path"));
|
||||
showErrorToast(
|
||||
t("invalid_wine_prefix_path"),
|
||||
t("invalid_wine_prefix_path_description")
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user