mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
feat: adding wine prefix
This commit is contained in:
parent
a498f9dd80
commit
0e5d37a3a0
@ -39,6 +39,9 @@ export class Game {
|
|||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
executablePath: string | null;
|
executablePath: string | null;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true })
|
||||||
|
winePrefixPath: string | null;
|
||||||
|
|
||||||
@Column("int", { default: 0 })
|
@Column("int", { default: 0 })
|
||||||
playTimeInMilliseconds: number;
|
playTimeInMilliseconds: number;
|
||||||
|
|
||||||
|
@ -10,8 +10,13 @@ import os from "node:os";
|
|||||||
import { backupsPath } from "@main/constants";
|
import { backupsPath } from "@main/constants";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { normalizePath } from "@main/helpers";
|
import { normalizePath } from "@main/helpers";
|
||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
const bundleBackup = async (shop: GameShop, objectId: string) => {
|
const bundleBackup = async (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
winePrefix: string | null
|
||||||
|
) => {
|
||||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||||
|
|
||||||
// Remove existing backup
|
// Remove existing backup
|
||||||
@ -19,7 +24,7 @@ const bundleBackup = async (shop: GameShop, objectId: string) => {
|
|||||||
fs.rmSync(backupPath, { recursive: true });
|
fs.rmSync(backupPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await Ludusavi.backupGame(shop, objectId, backupPath);
|
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||||
|
|
||||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
||||||
|
|
||||||
@ -38,9 +43,21 @@ const bundleBackup = async (shop: GameShop, objectId: string) => {
|
|||||||
const uploadSaveGame = async (
|
const uploadSaveGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop,
|
||||||
|
downloadOptionTitle: string | null
|
||||||
) => {
|
) => {
|
||||||
const bundleLocation = await bundleBackup(shop, objectId);
|
const game = await gameRepository.findOne({
|
||||||
|
where: {
|
||||||
|
objectID: objectId,
|
||||||
|
shop,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bundleLocation = await bundleBackup(
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
game?.winePrefixPath ?? null
|
||||||
|
);
|
||||||
|
|
||||||
fs.stat(bundleLocation, async (err, stat) => {
|
fs.stat(bundleLocation, async (err, stat) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -57,6 +74,7 @@ const uploadSaveGame = async (
|
|||||||
objectId,
|
objectId,
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
homeDir: normalizePath(app.getPath("home")),
|
homeDir: normalizePath(app.getPath("home")),
|
||||||
|
downloadOptionTitle,
|
||||||
platform: os.platform(),
|
platform: os.platform(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import "./library/update-executable-path";
|
|||||||
import "./library/verify-executable-path";
|
import "./library/verify-executable-path";
|
||||||
import "./library/remove-game";
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
|
import "./library/select-game-wine-prefix";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
|
13
src/main/events/library/select-game-wine-prefix.ts
Normal file
13
src/main/events/library/select-game-wine-prefix.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const selectGameWinePrefix = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
id: number,
|
||||||
|
winePrefixPath: string
|
||||||
|
) => {
|
||||||
|
return gameRepository.update({ id }, { winePrefixPath });
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("selectGameWinePrefix", selectGameWinePrefix);
|
@ -10,6 +10,7 @@ import { CreateGameAchievement } from "./migrations/20240919030940_create_game_a
|
|||||||
import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference";
|
import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference";
|
||||||
import { CreateUserSubscription } from "./migrations/20241015235142_create_user_subscription";
|
import { CreateUserSubscription } from "./migrations/20241015235142_create_user_subscription";
|
||||||
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
||||||
|
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
|
||||||
|
|
||||||
export type HydraMigration = Knex.Migration & { name: string };
|
export type HydraMigration = Knex.Migration & { name: string };
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
AddAchievementNotificationPreference,
|
AddAchievementNotificationPreference,
|
||||||
CreateUserSubscription,
|
CreateUserSubscription,
|
||||||
AddBackgroundImageUrl,
|
AddBackgroundImageUrl,
|
||||||
|
AddWinePrefixToGame,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
getMigrationName(migration: HydraMigration): string {
|
getMigrationName(migration: HydraMigration): string {
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { DownloadManager, PythonInstance, startMainLoop } from "./services";
|
import {
|
||||||
|
DownloadManager,
|
||||||
|
Ludusavi,
|
||||||
|
PythonInstance,
|
||||||
|
startMainLoop,
|
||||||
|
} from "./services";
|
||||||
import {
|
import {
|
||||||
downloadQueueRepository,
|
downloadQueueRepository,
|
||||||
userPreferencesRepository,
|
userPreferencesRepository,
|
||||||
@ -15,6 +20,8 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
|
||||||
HydraApi.setupApi().then(() => {
|
HydraApi.setupApi().then(() => {
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const AddWinePrefixToGame: HydraMigration = {
|
||||||
|
name: "AddWinePrefixToGame",
|
||||||
|
up: (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.text("winePrefixPath").nullable();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.dropColumn("winePrefixPath");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
@ -50,7 +50,7 @@ export const searchHowLongToBeat = async (gameName: string) => {
|
|||||||
|
|
||||||
const response = await axios
|
const response = await axios
|
||||||
.post(
|
.post(
|
||||||
"https://howlongtobeat.com/api/search/8fbd64723a8204dd",
|
`https://howlongtobeat.com/api/search/${state.apiKey}`,
|
||||||
{
|
{
|
||||||
searchType: "games",
|
searchType: "games",
|
||||||
searchTerms: formatName(gameName).split(" "),
|
searchTerms: formatName(gameName).split(" "),
|
||||||
|
@ -1,20 +1,27 @@
|
|||||||
import { GameShop, LudusaviBackup } from "@types";
|
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
|
||||||
import Piscina from "piscina";
|
import Piscina from "piscina";
|
||||||
|
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import YAML from "yaml";
|
||||||
|
|
||||||
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
|
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
|
||||||
|
|
||||||
const binaryPath = app.isPackaged
|
export class Ludusavi {
|
||||||
|
private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi");
|
||||||
|
private static ludusaviConfigPath = path.join(
|
||||||
|
this.ludusaviPath,
|
||||||
|
"config.yaml"
|
||||||
|
);
|
||||||
|
private static binaryPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, "ludusavi", "ludusavi")
|
? path.join(process.resourcesPath, "ludusavi", "ludusavi")
|
||||||
: path.join(__dirname, "..", "..", "ludusavi", "ludusavi");
|
: path.join(__dirname, "..", "..", "ludusavi", "ludusavi");
|
||||||
|
|
||||||
export class Ludusavi {
|
|
||||||
private static worker = new Piscina({
|
private static worker = new Piscina({
|
||||||
filename: ludusaviWorkerPath,
|
filename: ludusaviWorkerPath,
|
||||||
workerData: {
|
workerData: {
|
||||||
binaryPath,
|
binaryPath: this.binaryPath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -27,16 +34,29 @@ export class Ludusavi {
|
|||||||
return games;
|
return games;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getConfig() {
|
||||||
|
if (!fs.existsSync(this.ludusaviConfigPath)) {
|
||||||
|
await this.worker.run(undefined, { name: "generateConfig" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = YAML.parse(
|
||||||
|
fs.readFileSync(this.ludusaviConfigPath, "utf-8")
|
||||||
|
) as LudusaviConfig;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
static async backupGame(
|
static async backupGame(
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
backupPath: string
|
backupPath: string,
|
||||||
|
winePrefix?: string | null
|
||||||
): Promise<LudusaviBackup> {
|
): Promise<LudusaviBackup> {
|
||||||
const games = await this.findGames(shop, objectId);
|
const games = await this.findGames(shop, objectId);
|
||||||
if (!games.length) throw new Error("Game not found");
|
if (!games.length) throw new Error("Game not found");
|
||||||
|
|
||||||
return this.worker.run(
|
return this.worker.run(
|
||||||
{ title: games[0], backupPath },
|
{ title: games[0], backupPath, winePrefix },
|
||||||
{ name: "backupGame" }
|
{ name: "backupGame" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -60,4 +80,31 @@ export class Ludusavi {
|
|||||||
static async restoreBackup(backupPath: string) {
|
static async restoreBackup(backupPath: string) {
|
||||||
return this.worker.run(backupPath, { name: "restoreBackup" });
|
return this.worker.run(backupPath, { name: "restoreBackup" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async addManifestToLudusaviConfig() {
|
||||||
|
const config = await this.getConfig();
|
||||||
|
|
||||||
|
config.manifest.enable = false;
|
||||||
|
config.manifest.secondary = [
|
||||||
|
{ url: "https://cdn.losbroxas.org/manifest.yaml", enable: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addCustomGame(title: string, savePath: string) {
|
||||||
|
const config = await this.getConfig();
|
||||||
|
const filteredGames = config.customGames.filter(
|
||||||
|
(game) => game.name !== title
|
||||||
|
);
|
||||||
|
|
||||||
|
filteredGames.push({
|
||||||
|
name: title,
|
||||||
|
files: [savePath],
|
||||||
|
registry: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
config.customGames = filteredGames;
|
||||||
|
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,4 +58,8 @@ export const restoreBackup = (backupPath: string) => {
|
|||||||
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --wine-prefix
|
export const generateConfig = () => {
|
||||||
|
const result = cp.execFileSync(binaryPath, ["schema", "config"]);
|
||||||
|
|
||||||
|
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
||||||
|
};
|
||||||
|
@ -107,6 +107,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("createGameShortcut", id),
|
ipcRenderer.invoke("createGameShortcut", id),
|
||||||
updateExecutablePath: (id: number, executablePath: string) =>
|
updateExecutablePath: (id: number, executablePath: string) =>
|
||||||
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
||||||
|
selectGameWinePrefix: (id: number, winePrefixPath: string) =>
|
||||||
|
ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath),
|
||||||
verifyExecutablePathInUse: (executablePath: string) =>
|
verifyExecutablePathInUse: (executablePath: string) =>
|
||||||
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
|
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
|
||||||
getLibrary: () => ipcRenderer.invoke("getLibrary"),
|
getLibrary: () => ipcRenderer.invoke("getLibrary"),
|
||||||
@ -148,8 +150,12 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("getDiskFreeSpace", path),
|
ipcRenderer.invoke("getDiskFreeSpace", path),
|
||||||
|
|
||||||
/* Cloud save */
|
/* Cloud save */
|
||||||
uploadSaveGame: (objectId: string, shop: GameShop) =>
|
uploadSaveGame: (
|
||||||
ipcRenderer.invoke("uploadSaveGame", objectId, shop),
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
downloadOptionTitle: string | null
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke("uploadSaveGame", objectId, shop, downloadOptionTitle),
|
||||||
downloadGameArtifact: (
|
downloadGameArtifact: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
@ -183,7 +189,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.on(`on-backup-download-progress-${objectId}-${shop}`, listener);
|
ipcRenderer.on(`on-backup-download-progress-${objectId}-${shop}`, listener);
|
||||||
return () =>
|
return () =>
|
||||||
ipcRenderer.removeListener(
|
ipcRenderer.removeListener(
|
||||||
`on-backup-download-complete-${objectId}-${shop}`,
|
`on-backup-download-progress-${objectId}-${shop}`,
|
||||||
listener
|
listener
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -26,7 +26,7 @@ export interface CloudSyncContext {
|
|||||||
backupState: CloudSyncState;
|
backupState: CloudSyncState;
|
||||||
setShowCloudSyncModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowCloudSyncModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
downloadGameArtifact: (gameArtifactId: string) => Promise<void>;
|
downloadGameArtifact: (gameArtifactId: string) => Promise<void>;
|
||||||
uploadSaveGame: () => Promise<void>;
|
uploadSaveGame: (downloadOptionTitle: string | null) => Promise<void>;
|
||||||
deleteGameArtifact: (gameArtifactId: string) => Promise<void>;
|
deleteGameArtifact: (gameArtifactId: string) => Promise<void>;
|
||||||
setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
getGameBackupPreview: () => Promise<void>;
|
getGameBackupPreview: () => Promise<void>;
|
||||||
@ -108,10 +108,13 @@ export function CloudSyncContextProvider({
|
|||||||
]);
|
]);
|
||||||
}, [objectId, shop]);
|
}, [objectId, shop]);
|
||||||
|
|
||||||
const uploadSaveGame = useCallback(async () => {
|
const uploadSaveGame = useCallback(
|
||||||
|
async (downloadOptionTitle: string | null) => {
|
||||||
setUploadingBackup(true);
|
setUploadingBackup(true);
|
||||||
window.electron.uploadSaveGame(objectId, shop);
|
window.electron.uploadSaveGame(objectId, shop, downloadOptionTitle);
|
||||||
}, [objectId, shop]);
|
},
|
||||||
|
[objectId, shop]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeUploadCompleteListener = window.electron.onUploadComplete(
|
const removeUploadCompleteListener = window.electron.onUploadComplete(
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
@ -45,6 +46,7 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
|||||||
stats: null,
|
stats: null,
|
||||||
achievements: null,
|
achievements: null,
|
||||||
hasNSFWContentBlocked: false,
|
hasNSFWContentBlocked: false,
|
||||||
|
lastDownloadedOption: null,
|
||||||
setGameColor: () => {},
|
setGameColor: () => {},
|
||||||
selectGameExecutable: async () => null,
|
selectGameExecutable: async () => null,
|
||||||
updateGame: async () => {},
|
updateGame: async () => {},
|
||||||
@ -199,6 +201,19 @@ export function GameDetailsContextProvider({
|
|||||||
};
|
};
|
||||||
}, [game?.id, isGameRunning, updateGame]);
|
}, [game?.id, isGameRunning, updateGame]);
|
||||||
|
|
||||||
|
const lastDownloadedOption = useMemo(() => {
|
||||||
|
if (game?.uri) {
|
||||||
|
const repack = repacks.find((repack) =>
|
||||||
|
repack.uris.some((uri) => uri.includes(game.uri!))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!repack) return null;
|
||||||
|
return repack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [game?.uri, repacks]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onUpdateAchievements(
|
const unsubscribe = window.electron.onUpdateAchievements(
|
||||||
objectId,
|
objectId,
|
||||||
@ -259,6 +274,7 @@ export function GameDetailsContextProvider({
|
|||||||
stats,
|
stats,
|
||||||
achievements,
|
achievements,
|
||||||
hasNSFWContentBlocked,
|
hasNSFWContentBlocked,
|
||||||
|
lastDownloadedOption,
|
||||||
setHasNSFWContentBlocked,
|
setHasNSFWContentBlocked,
|
||||||
setGameColor,
|
setGameColor,
|
||||||
selectGameExecutable,
|
selectGameExecutable,
|
||||||
|
@ -22,6 +22,7 @@ export interface GameDetailsContext {
|
|||||||
stats: GameStats | null;
|
stats: GameStats | null;
|
||||||
achievements: UserAchievement[] | null;
|
achievements: UserAchievement[] | null;
|
||||||
hasNSFWContentBlocked: boolean;
|
hasNSFWContentBlocked: boolean;
|
||||||
|
lastDownloadedOption: GameRepack | null;
|
||||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||||
selectGameExecutable: () => Promise<string | null>;
|
selectGameExecutable: () => Promise<string | null>;
|
||||||
updateGame: () => Promise<void>;
|
updateGame: () => Promise<void>;
|
||||||
|
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@ -91,6 +91,7 @@ declare global {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
createGameShortcut: (id: number) => Promise<boolean>;
|
createGameShortcut: (id: number) => Promise<boolean>;
|
||||||
updateExecutablePath: (id: number, executablePath: string) => Promise<void>;
|
updateExecutablePath: (id: number, executablePath: string) => Promise<void>;
|
||||||
|
selectGameWinePrefix: (id: number, winePrefixPath: string) => Promise<void>;
|
||||||
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
||||||
getLibrary: () => Promise<LibraryGame[]>;
|
getLibrary: () => Promise<LibraryGame[]>;
|
||||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||||
@ -125,7 +126,11 @@ declare global {
|
|||||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||||
|
|
||||||
/* Cloud save */
|
/* Cloud save */
|
||||||
uploadSaveGame: (objectId: string, shop: GameShop) => Promise<void>;
|
uploadSaveGame: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
downloadOptionTitle: string | null
|
||||||
|
) => Promise<void>;
|
||||||
downloadGameArtifact: (
|
downloadGameArtifact: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
@ -134,9 +134,11 @@ export const achievementsProgressBar = style({
|
|||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
"::-webkit-progress-bar": {
|
"::-webkit-progress-bar": {
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
borderRadius: "4px",
|
||||||
},
|
},
|
||||||
"::-webkit-progress-value": {
|
"::-webkit-progress-value": {
|
||||||
backgroundColor: vars.color.muted,
|
backgroundColor: vars.color.muted,
|
||||||
|
borderRadius: "4px",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Modal, ModalProps } from "@renderer/components";
|
import { Button, Modal, ModalProps, TextField } from "@renderer/components";
|
||||||
import { useContext, useMemo } from "react";
|
import { useContext, useMemo } from "react";
|
||||||
import { cloudSyncContext } from "@renderer/context";
|
import { cloudSyncContext } from "@renderer/context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface CloudSyncFilesModalProps
|
export interface CloudSyncFilesModalProps
|
||||||
extends Omit<ModalProps, "children" | "title"> {}
|
extends Omit<ModalProps, "children" | "title"> {}
|
||||||
@ -11,6 +12,8 @@ export function CloudSyncFilesModal({
|
|||||||
}: CloudSyncFilesModalProps) {
|
}: CloudSyncFilesModalProps) {
|
||||||
const { backupPreview } = useContext(cloudSyncContext);
|
const { backupPreview } = useContext(cloudSyncContext);
|
||||||
|
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const files = useMemo(() => {
|
const files = useMemo(() => {
|
||||||
if (!backupPreview) {
|
if (!backupPreview) {
|
||||||
return [];
|
return [];
|
||||||
@ -24,6 +27,24 @@ export function CloudSyncFilesModal({
|
|||||||
});
|
});
|
||||||
}, [backupPreview]);
|
}, [backupPreview]);
|
||||||
|
|
||||||
|
const handleChangeExecutableLocation = async () => {
|
||||||
|
const path = await selectGameExecutable();
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
const gameUsingPath =
|
||||||
|
await window.electron.verifyExecutablePathInUse(path);
|
||||||
|
|
||||||
|
if (gameUsingPath) {
|
||||||
|
showErrorToast(
|
||||||
|
t("executable_path_in_use", { game: gameUsingPath.title })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electron.updateExecutablePath(game.id, path).then(updateGame);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@ -31,11 +52,11 @@ export function CloudSyncFilesModal({
|
|||||||
description="Escolha quais diretórios serão sincronizados"
|
description="Escolha quais diretórios serão sincronizados"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
{/* <div className={styles.downloaders}>
|
{/* <div>
|
||||||
{["AUTOMATIC", "CUSTOM"].map((downloader) => (
|
{["AUTOMATIC", "CUSTOM"].map((downloader) => (
|
||||||
<Button
|
<Button
|
||||||
key={downloader}
|
key={downloader}
|
||||||
className={styles.downloaderOption}
|
// className={styles.downloaderOption}
|
||||||
theme={selectedDownloader === downloader ? "primary" : "outline"}
|
theme={selectedDownloader === downloader ? "primary" : "outline"}
|
||||||
disabled={
|
disabled={
|
||||||
downloader === Downloader.RealDebrid &&
|
downloader === Downloader.RealDebrid &&
|
||||||
@ -51,6 +72,23 @@ export function CloudSyncFilesModal({
|
|||||||
))}
|
))}
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
// value={game.executablePath || ""}
|
||||||
|
readOnly
|
||||||
|
theme="dark"
|
||||||
|
disabled
|
||||||
|
placeholder={t("no_directory_selected")}
|
||||||
|
rightContent={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleChangeExecutableLocation}
|
||||||
|
>
|
||||||
|
{t("select_directory")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -38,3 +38,14 @@ export const syncIcon = style({
|
|||||||
animationIterationCount: "infinite",
|
animationIterationCount: "infinite",
|
||||||
animationTimingFunction: "linear",
|
animationTimingFunction: "linear",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const progress = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "5px",
|
||||||
|
"::-webkit-progress-bar": {
|
||||||
|
backgroundColor: vars.color.darkBackground,
|
||||||
|
},
|
||||||
|
"::-webkit-progress-value": {
|
||||||
|
backgroundColor: vars.color.muted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { Button, Modal, ModalProps } from "@renderer/components";
|
import {
|
||||||
|
Button,
|
||||||
|
ConfirmationModal,
|
||||||
|
Modal,
|
||||||
|
ModalProps,
|
||||||
|
} from "@renderer/components";
|
||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
@ -10,6 +15,7 @@ import {
|
|||||||
ClockIcon,
|
ClockIcon,
|
||||||
DeviceDesktopIcon,
|
DeviceDesktopIcon,
|
||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
|
InfoIcon,
|
||||||
SyncIcon,
|
SyncIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
@ -43,7 +49,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
setShowCloudSyncFilesModal,
|
setShowCloudSyncFilesModal,
|
||||||
} = useContext(cloudSyncContext);
|
} = useContext(cloudSyncContext);
|
||||||
|
|
||||||
const { objectId, shop, gameTitle } = useContext(gameDetailsContext);
|
const { objectId, shop, gameTitle, lastDownloadedOption } =
|
||||||
|
useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { showSuccessToast, showErrorToast } = useToast();
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
@ -140,6 +147,17 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{/* <ConfirmationModal
|
||||||
|
confirmButtonLabel="confirm"
|
||||||
|
cancelButtonLabel="cancel"
|
||||||
|
descriptionText="description"
|
||||||
|
title="title"
|
||||||
|
onConfirm={() => {}}
|
||||||
|
onClose={() => {}}
|
||||||
|
visible
|
||||||
|
/> */}
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
title={t("cloud_save")}
|
title={t("cloud_save")}
|
||||||
@ -178,7 +196,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={uploadSaveGame}
|
onClick={() => uploadSaveGame(lastDownloadedOption?.title ?? null)}
|
||||||
disabled={disableActions || !backupPreview}
|
disabled={disableActions || !backupPreview}
|
||||||
>
|
>
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
@ -186,6 +204,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
@ -198,6 +217,12 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
<small>{artifacts.length} / 2</small>
|
<small>{artifacts.length} / 2</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<small>Espaço usado</small>
|
||||||
|
<progress className={styles.progress} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul className={styles.artifacts}>
|
<ul className={styles.artifacts}>
|
||||||
{artifacts.map((artifact) => (
|
{artifacts.map((artifact) => (
|
||||||
<li key={artifact.id} className={styles.artifactButton}>
|
<li key={artifact.id} className={styles.artifactButton}>
|
||||||
@ -207,6 +232,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
marginBottom: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3>Backup do dia {format(artifact.createdAt, "dd/MM")}</h3>
|
<h3>Backup do dia {format(artifact.createdAt, "dd/MM")}</h3>
|
||||||
@ -218,6 +244,11 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
{artifact.hostname}
|
{artifact.hostname}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<InfoIcon size={14} />
|
||||||
|
{artifact.downloadOptionTitle ?? t("no_download_option_info")}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<ClockIcon size={14} />
|
<ClockIcon size={14} />
|
||||||
{format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")}
|
{format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")}
|
||||||
@ -247,5 +278,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { gameDetailsContext } from "@renderer/context";
|
|||||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||||
import { useDownload, useToast } from "@renderer/hooks";
|
import { useDownload, useToast } from "@renderer/hooks";
|
||||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||||
|
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export interface GameOptionsModalProps {
|
export interface GameOptionsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -94,6 +95,20 @@ export function GameOptionsModal({
|
|||||||
await window.electron.openGameExecutablePath(game.id);
|
await window.electron.openGameExecutablePath(game.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeWinePrefixPath = async () => {
|
||||||
|
const { filePaths } = await window.electron.showOpenDialog({
|
||||||
|
properties: ["openDirectory"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths && filePaths.length > 0) {
|
||||||
|
await window.electron.selectGameWinePrefix(game.id, filePaths[0]);
|
||||||
|
await updateGame();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldShowWinePrefixConfiguration =
|
||||||
|
window.electron.platform === "darwin";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteGameModal
|
<DeleteGameModal
|
||||||
@ -135,6 +150,7 @@ export function GameOptionsModal({
|
|||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={handleChangeExecutableLocation}
|
onClick={handleChangeExecutableLocation}
|
||||||
>
|
>
|
||||||
|
<FileIcon />
|
||||||
{t("select_executable")}
|
{t("select_executable")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@ -155,12 +171,41 @@ export function GameOptionsModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldShowWinePrefixConfiguration && (
|
||||||
|
<div className={styles.optionsContainer}>
|
||||||
|
<div className={styles.gameOptionHeader}>
|
||||||
|
<h2>{t("wine_prefix")}</h2>
|
||||||
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
{t("wine_prefix_description")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<TextField
|
||||||
|
value={game.winePrefixPath || ""}
|
||||||
|
readOnly
|
||||||
|
theme="dark"
|
||||||
|
disabled
|
||||||
|
placeholder={t("no_directory_selected")}
|
||||||
|
rightContent={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleChangeWinePrefixPath}
|
||||||
|
>
|
||||||
|
<FileDirectoryIcon />
|
||||||
|
{t("select_executable")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className={styles.gameOptionHeader}>
|
||||||
<h2>{t("downloads_secion_title")}</h2>
|
<h2>{t("downloads_secion_title")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
{t("downloads_section_description")}
|
{t("downloads_section_description")}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gameOptionRow}>
|
<div className={styles.gameOptionRow}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowRepacksModal(true)}
|
onClick={() => setShowRepacksModal(true)}
|
||||||
@ -179,12 +224,14 @@ export function GameOptionsModal({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className={styles.gameOptionHeader}>
|
||||||
<h2>{t("danger_zone_section_title")}</h2>
|
<h2>{t("danger_zone_section_title")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
{t("danger_zone_section_description")}
|
{t("danger_zone_section_description")}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gameOptionRow}>
|
<div className={styles.gameOptionRow}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowRemoveGameModal(true)}
|
onClick={() => setShowRemoveGameModal(true)}
|
||||||
|
@ -196,8 +196,10 @@ export const achievementsProgressBar = style({
|
|||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
"::-webkit-progress-bar": {
|
"::-webkit-progress-bar": {
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
borderRadius: "4px",
|
||||||
},
|
},
|
||||||
"::-webkit-progress-value": {
|
"::-webkit-progress-value": {
|
||||||
backgroundColor: vars.color.muted,
|
backgroundColor: vars.color.muted,
|
||||||
|
borderRadius: "4px",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -70,7 +70,7 @@ export const heroPanel = style({
|
|||||||
|
|
||||||
export const userInformation = style({
|
export const userInformation = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
padding: `${SPACING_UNIT * 6}px ${SPACING_UNIT * 3}px`,
|
padding: `${SPACING_UNIT * 7}px ${SPACING_UNIT * 3}px`,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
});
|
});
|
||||||
|
@ -33,7 +33,7 @@ type FriendAction =
|
|||||||
| ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND");
|
| ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND");
|
||||||
|
|
||||||
const backgroundImageLayer =
|
const backgroundImageLayer =
|
||||||
"linear-gradient(135deg, rgb(0 0 0 / 50%), rgb(0 0 0 / 60%))";
|
"linear-gradient(135deg, rgb(0 0 0 / 40%), rgb(0 0 0 / 30%))";
|
||||||
|
|
||||||
export function ProfileHero() {
|
export function ProfileHero() {
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
|
@ -12,7 +12,7 @@ export function UploadBackgroundImageButton() {
|
|||||||
const { hasActiveSubscription } = useUserDetails();
|
const { hasActiveSubscription } = useUserDetails();
|
||||||
|
|
||||||
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
|
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
|
||||||
const { patchUser } = useUserDetails();
|
const { patchUser, fetchUserDetails } = useUserDetails();
|
||||||
|
|
||||||
const { showSuccessToast } = useToast();
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ export function UploadBackgroundImageButton() {
|
|||||||
await patchUser({ backgroundImageUrl: path });
|
await patchUser({ backgroundImageUrl: path });
|
||||||
|
|
||||||
showSuccessToast("Background image updated");
|
showSuccessToast("Background image updated");
|
||||||
|
await fetchUserDetails();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingBackgorundImage(false);
|
setIsUploadingBackgorundImage(false);
|
||||||
|
@ -115,6 +115,7 @@ export interface Game {
|
|||||||
bytesDownloaded: number;
|
bytesDownloaded: number;
|
||||||
playTimeInMilliseconds: number;
|
playTimeInMilliseconds: number;
|
||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
|
winePrefixPath: string | null;
|
||||||
executablePath: string | null;
|
executablePath: string | null;
|
||||||
lastTimePlayed: Date | null;
|
lastTimePlayed: Date | null;
|
||||||
uri: string | null;
|
uri: string | null;
|
||||||
@ -333,6 +334,7 @@ export type GameAchievementFiles = {
|
|||||||
export interface GameArtifact {
|
export interface GameArtifact {
|
||||||
id: string;
|
id: string;
|
||||||
artifactLengthInBytes: number;
|
artifactLengthInBytes: number;
|
||||||
|
downloadOptionTitle: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
@ -25,3 +25,18 @@ export interface LudusaviBackup {
|
|||||||
export interface LudusaviFindResult {
|
export interface LudusaviFindResult {
|
||||||
games: Record<string, unknown>;
|
games: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LudusaviConfig {
|
||||||
|
manifest: {
|
||||||
|
enable: boolean;
|
||||||
|
secondary: {
|
||||||
|
url: string;
|
||||||
|
enable: boolean;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
customGames: {
|
||||||
|
name: string;
|
||||||
|
files: string[];
|
||||||
|
registry: [];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user