diff --git a/src/main/events/cloud-save/download-game-artifact.ts b/src/main/events/cloud-save/download-game-artifact.ts index 2d716c18..52444f60 100644 --- a/src/main/events/cloud-save/download-game-artifact.ts +++ b/src/main/events/cloud-save/download-game-artifact.ts @@ -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( diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 171d9891..acc589f9 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -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"; diff --git a/src/main/events/library/get-default-wine-prefix-selection-path.ts b/src/main/events/library/get-default-wine-prefix-selection-path.ts new file mode 100644 index 00000000..27ea03bf --- /dev/null +++ b/src/main/events/library/get-default-wine-prefix-selection-path.ts @@ -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 +); diff --git a/src/main/services/ludusavi.ts b/src/main/services/ludusavi.ts index 1a07a495..0f060ccb 100644 --- a/src/main/services/ludusavi.ts +++ b/src/main/services/ludusavi.ts @@ -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) + ); } } diff --git a/src/preload/index.ts b/src/preload/index.ts index bcaf6510..981901d3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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[] diff --git a/src/renderer/src/components/button/button.tsx b/src/renderer/src/components/button/button.tsx index fd86d4b2..8d83265d 100644 --- a/src/renderer/src/components/button/button.tsx +++ b/src/renderer/src/components/button/button.tsx @@ -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 > { + 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) { + const id = useId(); + + const tooltipProps = tooltip + ? { + "data-tooltip-id": id, + "data-tooltip-place": tooltipPlace, + "data-tooltip-content": tooltip, + } + : {}; + return ( - + <> + + + {tooltip && } + ); } diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 0f16ae2b..0dee5767 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -179,6 +179,7 @@ declare global { onExtractionComplete: ( cb: (shop: GameShop, objectId: string) => void ) => () => Electron.IpcRenderer; + getDefaultWinePrefixSelectionPath: () => Promise; /* Download sources */ putDownloadSource: ( diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index 3dee5087..bd70aec1 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -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 ( 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 ? ( diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index 7267c1fc..991f6a9d 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -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") + ); } } };