mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge pull request #14 from fhilipecrash/feat/add-wine-lutris-integration
Added modal to warning user if Wine or Lutris is not installed
This commit is contained in:
commit
66a115371a
13 changed files with 140 additions and 19 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -93,6 +93,8 @@ out/
|
|||
|
||||
.vscode/
|
||||
|
||||
.venv
|
||||
|
||||
dev.db
|
||||
|
||||
__pycache__
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,5 +107,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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,5 +107,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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,5 +107,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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
37
src/main/events/misc/generate-lutris-yaml.ts
Normal file
37
src/main/events/misc/generate-lutris-yaml.ts
Normal file
|
@ -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();
|
||||
}
|
2
src/renderer/declaration.d.ts
vendored
2
src/renderer/declaration.d.ts
vendored
|
@ -43,7 +43,7 @@ declare global {
|
|||
/* Library */
|
||||
getLibrary: () => Promise<Game[]>;
|
||||
getRepackersFriendlyNames: () => Promise<Record<string, string>>;
|
||||
openGame: (gameId: number) => Promise<void>;
|
||||
openGame: (gameId: number) => Promise<boolean>;
|
||||
removeGame: (gameId: number) => Promise<void>;
|
||||
deleteGameFolder: (gameId: number) => Promise<unknown>;
|
||||
getGameByObjectID: (objectID: string) => Promise<Game | null>;
|
||||
|
|
|
@ -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<Game[]>([]);
|
||||
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 (
|
||||
<section className={styles.downloadsContainer}>
|
||||
<BinaryNotFoundModal visible={showBinaryNotFoundModal} onClose={() => setShowBinaryNotFoundModal(false)} />
|
||||
<TextField placeholder={t("filter")} onChange={handleFilter} />
|
||||
|
||||
<ul className={styles.downloads}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { format } from "date-fns";
|
||||
|
@ -9,6 +9,7 @@ import type { Game, ShopDetails } from "@types";
|
|||
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
|
||||
export interface HeroPanelProps {
|
||||
game: Game | null;
|
||||
|
@ -27,6 +28,8 @@ export function HeroPanel({
|
|||
}: HeroPanelProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||
|
||||
const {
|
||||
game: gameDownloading,
|
||||
isDownloading,
|
||||
|
@ -46,7 +49,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();
|
||||
});
|
||||
|
||||
|
@ -202,6 +206,7 @@ export function HeroPanel({
|
|||
|
||||
return (
|
||||
<div style={{ backgroundColor: color }} className={styles.panel}>
|
||||
<BinaryNotFoundModal visible={showBinaryNotFoundModal} onClose={() => setShowBinaryNotFoundModal(false)} />
|
||||
<div className={styles.content}>{getInfo()}</div>
|
||||
<div className={styles.actions}>{getActions()}</div>
|
||||
</div>
|
||||
|
|
25
src/renderer/pages/shared-modals/binary-not-found-modal.tsx
Normal file
25
src/renderer/pages/shared-modals/binary-not-found-modal.tsx
Normal file
|
@ -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 (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("title")}
|
||||
description={t("description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
{t("instructions")}
|
||||
</Modal>
|
||||
)
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue