Merge branch 'main' into teste-locale2

This commit is contained in:
Zamitto 2024-05-22 20:44:31 -03:00 committed by GitHub
commit 8f63bc5f15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 516 additions and 23 deletions

View File

@ -1,4 +1,4 @@
name: Build
name: Release
on:
push:

182
README.es.md Normal file
View File

@ -0,0 +1,182 @@
<br>
<div align="center">
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra es un launcher de juegos con su propio cliente de bittorrent y gestor propio de repacks.</strong>
</p>
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![pl](https://img.shields.io/badge/lang-pl-white)](README.pl.md)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
![Hydra Catalogue](./docs/screenshot.png)
</div>
## Tabla de Contenidos
- [Acerca de](#acerca-de)
- [Características](#caracteristicas)
- [Instalación](#Instalacion)
- [Contribuir](#contribuir)
- [Únete a nuestro Telegram](#unete-a-nuestro-telegram)
- [Haz un fork y clona tu repositorio](#haz-un-fork-y-clona-tu-repositorio)
- [Maneras en las que puedes contribuir](#maneras-en-las-que-puedes-contribuir)
- [Estructura del proyecto](#estructura-del-proyecto)
- [Compilar desde el código fuente](#compilar-desde-el-código-fuente)
- [Instalar Node.js](#instalar-nodejs)
- [Instalar Yarn](#instalar-yarn)
- [Instalar Dependencias de Node](#instalar-dependencias-de-node)
- [Instalar Python 3.9](#instalar-python-39)
- [Instalar Dependencias de Python](#Instalar-dependencias-de-python)
- [Variables del Entorno](#variables-del-entorno)
- [Ejecución](#ejecucion)
- [Compilación](#compilacion)
- [Compilar el cliente de bittorrent](#compilar-el-cliente-de-bittorrent)
- [Compilar la aplicación Electron](#compilar-la-aplicacion-electron)
- [Colaboradores](#colaboradores)
## Acerca de
**Hydra** es un **Launcher de Juegos** con su propio **Cliente Bittorrent** y **autogestor de Repacks**.
<br>
El launcher está escrito en TypeScript (Electron) y Python, el cuál se encarga del sistema de torrent usando libtorrent.
## Caracteristicas
- Buscador e instalador autogestionado de repacks a través de las páginas más confiables en él [Megahilo](https://www.reddit.com/r/Piracy/wiki/megathread/)
- Cliente propio de bittorrent integrado
- Integración de How Long To Beat (HLTB) en la página del juego
- Customización de rutas de descargas
- Notificaciones en actualizaciones a listas de repacks
- Soporte a Windows y Linux
- En constante actualización
- Y mucho más ...
## Instalacion
Sigue los pasos de abajo para instalar:
1. Descarga la última versión de Hydra desde la página de [Releases](https://github.com/hydralauncher/hydra/releases/latest).
- Descarga solo el .exe si quieres instalar Hydra en Windows.
- Descarga el .deb o .rpm o .zip si quieres instalar Hydra en Linux. (Depende de tu distro de Linux)
2. Ejecuta el archivo descargado.
3. ¡Disfruta de Hydra!
## <a name="contribuir"> Contribuir
### <a name="unete-a-nuestro-telegram"></a> Unete a nuestro Telegram
Puedes unirte a nuestra conversación y discusiones en nuestro canal de [Telegram](https://t.me/hydralauncher).
### Haz un fork y clona tu repositorio
1. Rea;iza un fork del repositorio [(Haz click acá para hacer un fork ahora)](https://github.com/hydralauncher/hydra/fork)
2. Clona el código forkeado `git clone https://github.com/tu_nombredeusuario/hydra`
3. Crea una nueva rama
4. Sube tus commits
5. Envía nuevas solicitudes de pull
### Maneras en las que puedes contribuir
- Traducción: Queremos que Hydra esté disponible para todas las personas que sean posible. Siéntete libre de ayudarnos a traducirlo a nuevos lenguajes o actualizar y mejorar las ya disponibles en Hydra.
- Código: Hydra está hecho con Typescript, Electron y un poquito de Python. Si quieres contribuir, ¡únete a nuestro [Telegram](https://t.me/hydralauncher)!
### Estructura del proyecto
- torrent-client: Usamos libtorrent, una librería de Python que se encarga de manejar las descargas torrent
- src/renderer: El UI de la aplicación
- src/main: El resto de la lógica va acá.
## Compilar desde el código fuente
### Instalar Node.js
Asegúrate que tienes Node.js instalado en tú máquina. Si no es así, puedes descargarlo e instalarlo desde [nodejs.org](https://nodejs.org/).
### Instalar Yarn
Yarn es un gestor de paquetes para Node.js. Si no tienes aún instalado Yarn todavía, puedes hacerlo siguiendo las instrucciones en [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
### Instalar Dependencias de Node
Dirígete hasta el directorio del proyecto e instala las dependencias de Node usando Yarn:
```bash
cd hydra
yarn
```
### Instalar Python 3.9
Asegúrate que tienes Python 3.9 instalado en tu máquina. Puedes descargarlo e instalarlo desde [python.org](https://www.python.org/downloads/release/python-3913/).
### Instalar Dependencias de Python
Instala las dependencias de Python requeridas usando pip:
```bash
pip install -r requirements.txt
```
## Variables del Entorno
Necesitas una llave API de SteamGridDB para así poder obtener los íconos de los juegos en la instalación.
Si quieres también tener los repacks de onlinefix, necesitarás añadir tus credenciales al .env
Una vez que los tengas, puedes copiar o renombrar el archivo `.env.example` cómo `.env` y colocarlo en `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
## Ejecucion
Una vez que tengas todas las cosas listas, puedes ejecutar el siguiente comando para así iniciar el proceso de Electron y el cliente de bittorrent:
```bash
yarn dev
```
## Compilacion
### Compilar el cliente de bittorrent
Crea el cliente bittorrent usando este comando:
```bash
python torrent-client/setup.py build
```
### Compilar la aplicacion Electron
Crea la aplicación de Electron usando este comando:
En Windows:
```bash
yarn build:win
```
En Linux:
```bash
yarn build:linux
```
## Colaboradores
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## Licencia
Hydra está licenciado bajo la [MIT License](LICENSE).

BIN
hydra.db

Binary file not shown.

View File

@ -1,5 +1,5 @@
{
"name": "hydra",
"name": "hydralauncher",
"version": "1.2.3",
"description": "Hydra",
"main": "./out/main/index.js",

View File

@ -177,5 +177,11 @@
},
"modal": {
"close": "Close button"
},
"splash": {
"downloading_version": "Downloading version {{version}}",
"searching_updates": "Searching for updates",
"update_found": "Update {{version}} found",
"restarting_and_applying": "Restarting and applying update"
}
}

View File

@ -177,5 +177,11 @@
},
"modal": {
"close": "Botón de cierre"
},
"splash": {
"downloading_version": "Descargando versión {{version}}",
"searching_updates": "Buscando actualizaciones",
"update_found": "Actualización {{version}} encontrada",
"restarting_and_applying": "Reiniciando y aplicando actualización"
}
}

View File

@ -177,5 +177,11 @@
},
"modal": {
"close": "Botão de fechar"
},
"splash": {
"downloading_version": "Baixando versão {{version}}",
"searching_updates": "Buscando atualizações",
"update_found": "Versão {{version}} encontrada",
"restarting_and_applying": "Reiniciando e aplicando atualização"
}
}

View File

@ -22,7 +22,7 @@ export const defaultDownloadsPath = app.getPath("downloads");
export const databasePath = path.join(
app.getPath("appData"),
app.getName(),
"hydra",
"hydra.db"
);

View File

@ -0,0 +1,48 @@
import { AppUpdaterEvents } from "@types";
import { registerEvent } from "../register-event";
import updater, { ProgressInfo, UpdateInfo } from "electron-updater";
import { WindowManager } from "@main/services";
import { app } from "electron";
const { autoUpdater } = updater;
const sendEvent = (event: AppUpdaterEvents) => {
WindowManager.splashWindow?.webContents.send("autoUpdaterEvent", event);
};
const mockValuesForDebug = async () => {
sendEvent({ type: "update-downloaded" });
};
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater
.addListener("error", () => {
sendEvent({ type: "error" });
})
.addListener("checking-for-update", () => {
sendEvent({ type: "checking-for-updates" });
})
.addListener("update-not-available", () => {
sendEvent({ type: "update-not-available" });
})
.addListener("update-available", (info: UpdateInfo) => {
sendEvent({ type: "update-available", info });
})
.addListener("update-downloaded", () => {
sendEvent({ type: "update-downloaded" });
})
.addListener("download-progress", (info: ProgressInfo) => {
sendEvent({ type: "download-progress", info });
})
.addListener("update-cancelled", () => {
sendEvent({ type: "update-cancelled" });
});
if (app.isPackaged) {
autoUpdater.checkForUpdates();
} else {
await mockValuesForDebug();
}
};
registerEvent("checkForUpdates", checkForUpdates);

View File

@ -0,0 +1,12 @@
import { WindowManager } from "@main/services";
import { registerEvent } from "../register-event";
import updater from "electron-updater";
const { autoUpdater } = updater;
const continueToMainWindow = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater.removeAllListeners();
WindowManager.prepareMainWindowAndCloseSplash();
};
registerEvent("continueToMainWindow", continueToMainWindow);

View File

@ -0,0 +1,17 @@
import { app } from "electron";
import { registerEvent } from "../register-event";
import updater from "electron-updater";
import { WindowManager } from "@main/services";
const { autoUpdater } = updater;
const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => {
if (app.isPackaged) {
autoUpdater.quitAndInstall(true, true);
} else {
autoUpdater.removeAllListeners();
WindowManager.prepareMainWindowAndCloseSplash();
}
};
registerEvent("restartAndInstallUpdate", restartAndInstallUpdate);

View File

@ -27,6 +27,9 @@ import "./torrenting/start-game-download";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./autoupdater/continue-to-main-window";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());

View File

@ -3,11 +3,10 @@ import updater from "electron-updater";
import i18n from "i18next";
import path from "node:path";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { resolveDatabaseUpdates, WindowManager } from "@main/services";
import { logger, resolveDatabaseUpdates, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source";
import * as resources from "@locales";
import { userPreferencesRepository } from "@main/repository";
const { autoUpdater } = updater;
autoUpdater.setFeedURL({
@ -16,6 +15,8 @@ autoUpdater.setFeedURL({
repo: "hydra",
});
autoUpdater.logger = logger;
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
@ -63,12 +64,8 @@ app.whenReady().then(() => {
where: { id: 1 },
});
WindowManager.createMainWindow();
WindowManager.createSplashScreen();
WindowManager.createSystemTray(userPreferences?.language || "en");
WindowManager.mainWindow?.on("ready-to-show", () => {
autoUpdater.checkForUpdatesAndNotify();
});
});
});

View File

@ -148,9 +148,10 @@ export const getNewRepacksFromOnlineFix = async (
);
if (!newRepacks.length) return;
if (page === totalPages) return;
await savePage(newRepacks);
if (page === totalPages) return;
return getNewRepacksFromOnlineFix(existingRepacks, page + 1, cookieJar);
};

View File

@ -111,9 +111,10 @@ export const getNewRepacksFromXatab = async (
);
if (!newRepacks.length) return;
if (page === totalPages) return;
await savePage(newRepacks);
if (page === totalPages) return;
return getNewRepacksFromXatab(existingRepacks, page + 1);
};

View File

@ -17,6 +17,8 @@ import { IsNull, Not } from "typeorm";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
public static splashWindow: Electron.BrowserWindow | null = null;
public static isReadyToShowMainWindow = false;
private static loadURL(hash = "") {
// HMR for renderer base on electron-vite cli.
@ -35,13 +37,51 @@ export class WindowManager {
}
}
private static loadSplashURL() {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.splashWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/splash`
);
} else {
this.splashWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash: "splash",
}
);
}
}
public static createSplashScreen() {
if (this.splashWindow) return;
this.splashWindow = new BrowserWindow({
width: 380,
height: 380,
frame: false,
resizable: false,
backgroundColor: "#1c1c1c",
webPreferences: {
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
},
});
this.loadSplashURL();
this.splashWindow.removeMenu();
}
public static createMainWindow() {
// Create the browser window.
if (this.mainWindow || !this.isReadyToShowMainWindow) return;
this.mainWindow = new BrowserWindow({
width: 1200,
height: 720,
minWidth: 1024,
minHeight: 540,
backgroundColor: "#1c1c1c",
titleBarStyle: "hidden",
...(process.platform === "linux" ? { icon } : {}),
trafficLightPosition: { x: 16, y: 16 },
@ -75,6 +115,12 @@ export class WindowManager {
});
}
public static prepareMainWindowAndCloseSplash() {
this.isReadyToShowMainWindow = true;
this.splashWindow?.close();
this.createMainWindow();
}
public static redirect(hash: string) {
if (!this.mainWindow) this.createMainWindow();
this.loadURL(hash);

View File

@ -7,6 +7,7 @@ import type {
GameShop,
TorrentProgress,
UserPreferences,
AppUpdaterEvents,
} from "@types";
contextBridge.exposeInMainWorld("electron", {
@ -112,4 +113,21 @@ contextBridge.exposeInMainWorld("electron", {
showOpenDialog: (options: Electron.OpenDialogOptions) =>
ipcRenderer.invoke("showOpenDialog", options),
platform: process.platform,
/* Splash */
onAutoUpdaterEvent: (cb: (value: AppUpdaterEvents) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
value: AppUpdaterEvents
) => cb(value);
ipcRenderer.on("autoUpdaterEvent", listener);
return () => {
ipcRenderer.removeListener("autoUpdaterEvent", listener);
};
},
checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"),
restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"),
continueToMainWindow: () => ipcRenderer.invoke("continueToMainWindow"),
});

View File

@ -12,7 +12,7 @@ import {
import * as styles from "./app.css";
import { themeClass } from "./theme.css";
import { useLocation, useNavigate } from "react-router-dom";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import {
setSearch,
clearSearch,
@ -27,7 +27,7 @@ export interface AppProps {
children: React.ReactNode;
}
export function App({ children }: AppProps) {
export function App() {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary } = useLibrary();
@ -128,7 +128,7 @@ export function App({ children }: AppProps) {
/>
<section ref={contentRef} className={styles.content}>
{children}
<Outlet />
</section>
</article>
</main>

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -1,4 +1,5 @@
import type {
AppUpdaterEvents,
CatalogueCategory,
CatalogueEntry,
Game,
@ -90,6 +91,14 @@ declare global {
options: Electron.OpenDialogOptions
) => Promise<Electron.OpenDialogReturnValue>;
platform: NodeJS.Platform;
/* Splash */
onAutoUpdaterEvent: (
cb: (event: AppUpdaterEvents) => void
) => () => Electron.IpcRenderer;
checkForUpdates: () => Promise<void>;
restartAndInstallUpdate: () => Promise<void>;
continueToMainWindow: () => Promise<void>;
}
interface Window {

View File

@ -27,6 +27,7 @@ import {
import { store } from "./store";
import * as resources from "@locales";
import Splash from "./pages/splash/splash";
i18n
.use(LanguageDetector)
@ -46,16 +47,17 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Provider store={store}>
<HashRouter>
<App>
<Routes>
<Routes>
<Route path="/splash" Component={Splash} />
<Route element={<App />}>
<Route path="/" Component={Home} />
<Route path="/catalogue" Component={Catalogue} />
<Route path="/downloads" Component={Downloads} />
<Route path="/game/:shop/:objectID" Component={GameDetails} />
<Route path="/search" Component={SearchResults} />
<Route path="/settings" Component={Settings} />
</Routes>
</App>
</Route>
</Routes>
</HashRouter>
</Provider>
</React.StrictMode>

View File

@ -58,9 +58,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
if (hasMovies && mediaContainerRef.current) {
mediaContainerRef.current.childNodes.forEach((node, index) => {
if (node instanceof HTMLVideoElement) {
if (index == mediaIndex) {
node.play();
} else {
if (index !== mediaIndex) {
node.pause();
}
}

View File

@ -0,0 +1,49 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const main = style({
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
padding: `${SPACING_UNIT * 3}px`,
flex: "1",
overflowY: "auto",
alignItems: "center",
});
export const splashIcon = style({
width: "75%",
});
export const updateInfoSection = style({
width: "100%",
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
flex: "1",
overflowY: "auto",
alignItems: "center",
justifyContent: "center",
});
export const progressBar = style({
WebkitAppearance: "none",
appearance: "none",
borderRadius: "4px",
width: "100%",
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
height: "18px",
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
transition: "width 0.2s",
},
"::-webkit-progress-bar": {
backgroundColor: vars.color.darkBackground,
},
});
export const progressBarText = style({
zIndex: 2,
});

View File

@ -0,0 +1,82 @@
import icon from "@renderer/assets/icon.png";
import * as styles from "./splash.css";
import { themeClass } from "../../theme.css";
import "../../app.css";
import { useEffect, useState } from "react";
import { AppUpdaterEvents } from "@types";
import { useTranslation } from "react-i18next";
document.body.classList.add(themeClass);
export default function Splash() {
const [status, setStatus] = useState<AppUpdaterEvents | null>(null);
const [newVersion, setNewVersion] = useState("");
const { t } = useTranslation("splash");
useEffect(() => {
const unsubscribe = window.electron.onAutoUpdaterEvent(
(event: AppUpdaterEvents) => {
setStatus(event);
switch (event.type) {
case "error":
window.electron.continueToMainWindow();
break;
case "update-available":
setNewVersion(event.info.version);
break;
case "update-cancelled":
window.electron.continueToMainWindow();
break;
case "update-downloaded":
window.electron.restartAndInstallUpdate();
break;
case "update-not-available":
window.electron.continueToMainWindow();
break;
}
}
);
window.electron.checkForUpdates();
return () => {
unsubscribe();
};
}, []);
const renderUpdateInfo = () => {
switch (status?.type) {
case "download-progress":
return (
<>
<p>{t("downloading_version", { version: newVersion })}</p>
<progress
className={styles.progressBar}
max="100"
value={status.info.percent}
/>
</>
);
case "checking-for-updates":
return <p>{t("searching_updates")}</p>;
case "update-available":
return <p>{t("update_found", { version: newVersion })}</p>;
case "update-downloaded":
return <p>{t("restarting_and_applying")}</p>;
default:
return <></>;
}
};
return (
<main className={styles.main}>
<img src={icon} className={styles.splashIcon} alt="Hydra Launcher Logo" />
<section className={styles.updateInfoSection}>
{renderUpdateInfo()}
</section>
</main>
);
}

View File

@ -1,4 +1,5 @@
import type { Downloader, GameStatus } from "@shared";
import { ProgressInfo, UpdateInfo } from "electron-updater";
export type GameShop = "steam" | "epic";
export type CatalogueCategory = "recently_added" | "trending";
@ -143,3 +144,12 @@ export interface SteamGame {
name: string;
clientIcon: string | null;
}
export type AppUpdaterEvents =
| { type: "error" }
| { type: "checking-for-updates" }
| { type: "update-not-available" }
| { type: "update-available"; info: UpdateInfo }
| { type: "update-downloaded" }
| { type: "download-progress"; info: ProgressInfo }
| { type: "update-cancelled" };