feat: adding wine prefix to backup creation on linux

This commit is contained in:
Chubby Granny Chaser
2025-05-12 10:55:44 +01:00
parent 427b77c597
commit 749a88b2b6
9 changed files with 98 additions and 19 deletions

View File

@@ -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(

View File

@@ -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";

View File

@@ -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
);

View File

@@ -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)
);
}
}

View File

@@ -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">[]

View File

@@ -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} />}
</>
);
}

View File

@@ -179,6 +179,7 @@ declare global {
onExtractionComplete: (
cb: (shop: GameShop, objectId: string) => void
) => () => Electron.IpcRenderer;
getDefaultWinePrefixSelectionPath: () => Promise<string | null>;
/* Download sources */
putDownloadSource: (

View File

@@ -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 ? (

View File

@@ -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")
);
}
}
};