diff --git a/.gitignore b/.gitignore index dadf36d3a..85a90269c 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,8 @@ out/ .vscode/ +.venv + dev.db __pycache__ diff --git a/forge.config.ts b/forge.config.ts index de42f3ead..b8bf46d3d 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -17,6 +17,7 @@ const config: ForgeConfig = { packagerConfig: { asar: true, icon: "./images/icon.png", + executableName: "Hydra", extraResource: [ "./resources/hydra.db", "./resources/icon_tray.png", @@ -34,11 +35,17 @@ const config: ForgeConfig = { new MakerSquirrel({ setupIcon: "./images/icon.ico", }), - new MakerZIP({}, ["darwin"]), - new MakerRpm({}), + new MakerZIP({}, ["darwin", "linux"]), + new MakerRpm({ + options: { + mimeType: ["x-scheme-handler/hydralauncher"], + bin: './Hydra' + }, + }), new MakerDeb({ options: { mimeType: ["x-scheme-handler/hydralauncher"], + bin: './Hydra' }, }), ], diff --git a/package.json b/package.json index cea50da9d..a08033eb0 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "uuid": "^9.0.1", "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^4.3.2", - "winston": "^3.12.0" + "winston": "^3.12.0", + "yaml": "^2.4.1" } } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index cb0ac574d..ade0d8579 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -112,5 +112,10 @@ }, "game_card": { "no_downloads": "No downloads available" + }, + "binary_not_found_modal": { + "title": "Programs not installed", + "description": "Wine or Lutris executables were not found on your system", + "instructions": "Check the correct way to install any of them on your Linux distro so that the game can run normally" } } diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index cbeebcc0a..91b811754 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -112,5 +112,10 @@ }, "game_card": { "no_downloads": "No hay descargas disponibles" + }, + "binary_not_found_modal": { + "title": "Programas no instalados", + "description": "Los ejecutables de Wine o Lutris no se encontraron en su sistema", + "instructions": "Comprueba la forma correcta de instalar cualquiera de ellos en tu distro Linux para que el juego pueda ejecutarse con normalidad" } } diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index 106824b4e..c77ebf3fa 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -112,5 +112,10 @@ }, "game_card": { "no_downloads": "Sem downloads disponíveis" + }, + "binary_not_found_modal": { + "title": "Programas não instalados", + "description": "Não foram encontrados no seu sistema os executáveis do Wine ou Lutris", + "instructions": "Verifique a forma correta de instalar algum deles na sua distro Linux para que o jogo possa ser executado normalmente" } } diff --git a/src/main/events/library/open-game.ts b/src/main/events/library/open-game.ts index 82c097bdb..9b55348c8 100644 --- a/src/main/events/library/open-game.ts +++ b/src/main/events/library/open-game.ts @@ -1,6 +1,9 @@ import { gameRepository } from "@main/repository"; +import { generateYML } from "../misc/generate-lutris-yaml"; import path from "node:path"; import fs from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { spawnSync, exec } from "node:child_process"; import { registerEvent } from "../register-event"; import { shell } from "electron"; @@ -12,25 +15,42 @@ const openGame = async ( ) => { const game = await gameRepository.findOne({ where: { id: gameId } }); - if (!game) return; + if (!game) return true; const gamePath = path.join( game.downloadPath ?? (await getDownloadsPath()), game.folderName ); - if (fs.existsSync(gamePath)) { - const setupPath = path.join(gamePath, "setup.exe"); - if (fs.existsSync(setupPath)) { - shell.openExternal(setupPath); - } else { - shell.openPath(gamePath); - } - } else { - await gameRepository.delete({ - id: gameId, - }); + if (!fs.existsSync(gamePath)) { + await gameRepository.delete({ id: gameId, }); + return true; } + + const setupPath = path.join(gamePath, "setup.exe"); + if (!fs.existsSync(setupPath)) { + shell.openPath(gamePath); + return true; + } + + if (process.platform === "win32") { + shell.openExternal(setupPath); + return true; + } + + if (spawnSync("which", ["lutris"]).status === 0) { + const ymlPath = path.join(gamePath, "setup.yml"); + await writeFile(ymlPath, generateYML(game)); + exec(`lutris --install "${ymlPath}"`); + return true; + } + + if (spawnSync("which", ["wine"]).status === 0) { + exec(`wine "${setupPath}"`); + return true; + } + + return false; }; registerEvent(openGame, { diff --git a/src/main/events/misc/generate-lutris-yaml.ts b/src/main/events/misc/generate-lutris-yaml.ts new file mode 100644 index 000000000..2b6bfed6b --- /dev/null +++ b/src/main/events/misc/generate-lutris-yaml.ts @@ -0,0 +1,37 @@ +import { Document as YMLDocument } from "yaml"; +import { Game } from "@main/entity"; +import path from "node:path"; + +export const generateYML = (game: Game) => { + const slugifiedGameTitle = game.title.replace(/\s/g, "-").toLocaleLowerCase(); + + const doc = new YMLDocument({ + name: game.title, + game_slug: slugifiedGameTitle, + slug: `${slugifiedGameTitle}-installer`, + version: "Installer", + runner: "wine", + script: { + game: { + prefix: "$GAMEDIR", + arch: "win64", + working_dir: "$GAMEDIR" + }, + installer: [{ + task: { + name: "create_prefix", + arch: "win64", + prefix: "$GAMEDIR" + } + }, { + task: { + executable: path.join(game.downloadPath, game.folderName, "setup.exe"), + name: "wineexec", + prefix: "$GAMEDIR" + } + }] + } + }); + + return doc.toString(); +} diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 4e06ae8fa..5e6b11d2e 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -16,6 +16,8 @@ export class WindowManager { this.mainWindow = new BrowserWindow({ width: 1200, height: 720, + minWidth: 1024, + minHeight: 540, titleBarStyle: "hidden", icon: path.join(__dirname, "..", "..", "images", "icon.png"), trafficLightPosition: { x: 16, y: 16 }, diff --git a/src/renderer/declaration.d.ts b/src/renderer/declaration.d.ts index 2f2acf9e3..fb609c549 100644 --- a/src/renderer/declaration.d.ts +++ b/src/renderer/declaration.d.ts @@ -54,7 +54,7 @@ declare global { ) => Promise; getLibrary: () => Promise; getRepackersFriendlyNames: () => Promise>; - openGame: (gameId: number) => Promise; + openGame: (gameId: number) => Promise; removeGame: (gameId: number) => Promise; deleteGameFolder: (gameId: number) => Promise; getGameByObjectID: (objectID: string) => Promise; diff --git a/src/renderer/pages/downloads/downloads.tsx b/src/renderer/pages/downloads/downloads.tsx index f0fd80381..196eb1769 100644 --- a/src/renderer/pages/downloads/downloads.tsx +++ b/src/renderer/pages/downloads/downloads.tsx @@ -9,6 +9,7 @@ import type { Game } from "@types"; import * as styles from "./downloads.css"; import { useEffect, useState } from "react"; +import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal"; export function Downloads() { const { library, updateLibrary } = useLibrary(); @@ -18,6 +19,7 @@ export function Downloads() { const navigate = useNavigate(); const [filteredLibrary, setFilteredLibrary] = useState([]); + const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false); const { game: gameDownloading, @@ -37,7 +39,8 @@ export function Downloads() { }, [library]); const openGame = (gameId: number) => - window.electron.openGame(gameId).then(() => { + window.electron.openGame(gameId).then(isBinaryInPath => { + if (!isBinaryInPath) setShowBinaryNotFoundModal(true); updateLibrary(); }); @@ -202,6 +205,7 @@ export function Downloads() { return (
+ setShowBinaryNotFoundModal(false)} />
    diff --git a/src/renderer/pages/game-details/hero-panel.tsx b/src/renderer/pages/game-details/hero-panel.tsx index 8af4a365f..2d4588a39 100644 --- a/src/renderer/pages/game-details/hero-panel.tsx +++ b/src/renderer/pages/game-details/hero-panel.tsx @@ -10,6 +10,7 @@ import type { Game, ShopDetails } from "@types"; import * as styles from "./hero-panel.css"; import { formatDownloadProgress } from "@renderer/helpers"; import { HeartFillIcon, HeartIcon } from "@primer/octicons-react"; +import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal"; export interface HeroPanelProps { game: Game | null; @@ -28,6 +29,8 @@ export function HeroPanel({ }: HeroPanelProps) { const { t } = useTranslation("game_details"); + const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false); + const { game: gameDownloading, isDownloading, @@ -54,7 +57,8 @@ export function HeroPanel({ const isGameDownloading = isDownloading && gameDownloading?.id === game?.id; const openGame = (gameId: number) => - window.electron.openGame(gameId).then(() => { + window.electron.openGame(gameId).then((isBinaryInPath) => { + if (!isBinaryInPath) setShowBinaryNotFoundModal(true); updateLibrary(); }); @@ -240,9 +244,15 @@ export function HeroPanel({ }; return ( -
    -
    {getInfo()}
    -
    {getActions()}
    -
    + <> + setShowBinaryNotFoundModal(false)} + /> +
    +
    {getInfo()}
    +
    {getActions()}
    +
    + ); } diff --git a/src/renderer/pages/shared-modals/binary-not-found-modal.tsx b/src/renderer/pages/shared-modals/binary-not-found-modal.tsx new file mode 100644 index 000000000..ddbde5b61 --- /dev/null +++ b/src/renderer/pages/shared-modals/binary-not-found-modal.tsx @@ -0,0 +1,25 @@ +import { Modal } from "@renderer/components" +import { useTranslation } from "react-i18next"; + +interface BinaryNotFoundModalProps { + visible: boolean; + onClose: () => void; +} + +export const BinaryNotFoundModal = ({ + visible, + onClose +}: BinaryNotFoundModalProps) => { + const { t } = useTranslation("binary_not_found_modal"); + + return ( + + {t("instructions")} + + ) +} diff --git a/yarn.lock b/yarn.lock index d382d5025..79ab849c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10460,6 +10460,11 @@ yaml@^1.10.0: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" + integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz"