chore: merge with main

This commit is contained in:
Hydra 2024-04-25 05:52:19 +01:00
commit 498a889f1d
47 changed files with 1337 additions and 451 deletions

71
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,71 @@
name: Build
on:
push:
branches: "**"
jobs:
build:
strategy:
matrix:
os:
[
{
name: windows-latest,
build_path: out/Hydra-win32-x64,
artifact: Hydra-win32-x64,
},
{
name: ubuntu-latest,
build_path: out/Hydra-linux-x64,
artifact: Hydra-linux-x64,
},
]
runs-on: ${{ matrix.os.name }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.11.1
- name: Install dependencies
run: yarn
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install dependencies
run: pip install -r requirements.txt
- name: Build with cx_Freeze
run: python torrent-client/setup.py build
- name: VirusTotal Scan
uses: crazy-max/ghaction-virustotal@v4
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
files: |
.exe$
# - name: Publish
# run: yarn run publish
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }}
# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# SENTRY_DSN: ${{ vars.SENTRY_DSN }}
# ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
# ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
- name: Create artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os.artifact }}
path: ${{ matrix.os.build_path }}

24
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Lint
on:
push:
branches: "**"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.11.1
- name: Install dependencies
run: yarn
- name: Lint
run: yarn lint

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ out
.DS_Store
*.log*
.env
.vite

3
.npmrc
View File

@ -1,3 +0,0 @@
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
shamefully-hoist=true

6
.prettierrc.cjs Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
semi: true,
trailingComma: "es5",
singleQuote: false,
tabWidth: 2,
};

View File

@ -1,4 +0,0 @@
singleQuote: false
semi: true
tabWidth: 2
trailingComma: es5

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Los Broxas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -4,6 +4,7 @@ directories:
buildResources: build
extraResources:
- hydra-download-manager
- resources/hydra.db
files:
- "!**/.vscode/*"
- "!src/*"
@ -42,7 +43,6 @@ appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
provider: github
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

View File

@ -10,7 +10,7 @@ import react from "@vitejs/plugin-react";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import svgr from "vite-plugin-svgr";
export default defineConfig(({ command, mode }) => {
export default defineConfig(({ mode }) => {
loadEnv(mode);
return {

View File

@ -3,8 +3,12 @@
"version": "1.0.0",
"description": "An Electron application with React and TypeScript",
"main": "./out/main/index.js",
"author": "Hydra Launcher",
"author": "Los Broxas",
"homepage": "https://electron-vite.org",
"repository": {
"type": "git",
"url": "https://github.com/hydralauncher/hydra.git"
},
"type": "module",
"scripts": {
"format": "prettier --write .",
@ -61,7 +65,7 @@
"@electron-toolkit/tsconfig": "^1.0.1",
"@swc/core": "^1.4.16",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.9",
"@types/node": "^20.12.7",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vanilla-extract/vite-plugin": "^4.0.7",

BIN
resources/hydra.db Normal file

Binary file not shown.

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -77,7 +77,13 @@
"play": "Play",
"deleting": "Deleting installer…",
"close": "Close",
"playing_now": "Playing now"
"playing_now": "Playing now",
"change": "Change",
"repacks_modal_description": "Choose the repack you want to download",
"downloads_path": "Downloads path",
"select_folder_hint": "To change the default folder, access the",
"settings": "Hydra settings",
"download_now": "Download now"
},
"activation": {
"title": "Activate Hydra",

View File

@ -0,0 +1,147 @@
{
"home": {
"featured": "Featured",
"recently_added": "Nemrég hozzáadott",
"trending": "Népszerű",
"surprise_me": "Lepj meg",
"no_results": "Nem található"
},
"sidebar": {
"catalogue": "Katalógus",
"downloads": "Letöltések",
"settings": "Beállítások",
"my_library": "Könyvtáram",
"downloading_metadata": "{{title}} (Metadata letöltése…)",
"checking_files": "{{title}} ({{percentage}} - Fájlok ellenőrzése…)",
"paused": "{{title}} (Szünet)",
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
"filter": "Könyvtár szűrése",
"follow_us": "Kövess minket",
"home": "Főoldal"
},
"header": {
"search": "Keresés",
"home": "Főoldal",
"catalogue": "Katalógus",
"downloads": "Letöltések",
"search_results": "Keresési eredmények",
"settings": "Beállítások"
},
"bottom_panel": {
"no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések",
"downloading_metadata": "{{title}} metaadatainak letöltése…",
"checking_files": "{{title}} fájlok ellenőrzése… ({{percentage}} kész)",
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Következő olda",
"previous_page": "Előző olda"
},
"game_details": {
"open_download_options": "Letöltési lehetőségek",
"download_options_zero": "Nincs letöltési lehetőség",
"download_options_one": "{{count}} letöltési lehetőség",
"download_options_other": "{{count}} letöltési lehetőség",
"updated_at": "Frissítve: {{updated_at}}",
"install": "Letöltés",
"resume": "Folytatás",
"pause": "Szüneteltetés",
"cancel": "Mégse",
"remove": "Eltávolítás",
"remove_from_list": "Eltávolítás",
"space_left_on_disk": "{{space}} szabad hely a lemezen",
"eta": "Befejezés {{eta}}",
"downloading_metadata": "Metaadatok letöltése…",
"checking_files": "Fájlok ellenőrzése…",
"filter": "Repackek szűrése",
"requirements": "Rendszerkövetelmények",
"minimum": "Minimális",
"recommended": "Ajánlott",
"no_minimum_requirements": "{{title}} nem tartalmaz információt a minimális követelményekről",
"no_recommended_requirements": "{{title}} nem tartalmaz információt az ajánlott követelményekről",
"paused_progress": "{{progress}} (Szünetel)",
"release_date": "Megjelenés: {{date}}",
"publisher": "Kiadta: {{publisher}}",
"copy_link_to_clipboard": "Link másolása",
"copied_link_to_clipboard": "Link másolva",
"hours": "óra",
"minutes": "perc",
"accuracy": "{{accuracy}}% pontosság",
"add_to_library": "Hozzáadás a könyvtárhoz",
"remove_from_library": "Eltávolítás a könyvtárból",
"no_downloads": "Nincs elérhető letöltés",
"play_time": "Játszva: {{amount}}",
"last_time_played": "Utoljára játszva {{period}}",
"not_played_yet": "{{title}} még nem játszottál",
"next_suggestion": "Következő javaslat",
"play": "Játék",
"deleting": "Telepítő törlése…",
"close": "Bezárás",
"playing_now": "Jelenleg játszva",
"change": "Változtatás",
"repacks_modal_description": "Choose the repack you want to download",
"downloads_path": "Letöltések helye",
"select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a",
"hydra_settings": "Hydra beállítások",
"download_now": "Töltsd le most"
},
"activation": {
"title": "Hydra Aktiválása",
"installation_id": "Telepítési ID:",
"enter_activation_code": "Add meg az aktiválási kódodat",
"message": "Ha nem tudod, hol kérdezd meg ezt, akkor nem is kellene, hogy legyen ilyened.",
"activate": "Aktiválás",
"loading": "Betöltés…"
},
"downloads": {
"resume": "Folytatás",
"pause": "Szüneteltetés",
"eta": "Befejezés {{eta}}",
"paused": "Szüneteltetve",
"verifying": "Ellenőrzés…",
"completed_at": "Befejezve {{date}}-kor",
"completed": "Befejezve",
"cancelled": "Megszakítva",
"download_again": "Újra letöltés",
"cancel": "Mégse",
"filter": "Letöltött játékok szűrése",
"remove": "Eltávolítás",
"downloading_metadata": "Metaadatok letöltése…",
"checking_files": "Fájlok ellenőrzése…",
"starting_download": "Letöltés indítása…",
"deleting": "Telepítő törlése…",
"delete": "Telepítő eltávolítása",
"remove_from_list": "Eltávolítás",
"delete_modal_title": "Biztos vagy benne?",
"delete_modal_description": "Ez eltávolít minden telepítési fájlt a számítógépedről",
"install": "Telepítés"
},
"settings": {
"downloads_path": "Letöltések helye",
"change": "Frissítés",
"notifications": "Értesítések",
"enable_download_notifications": "Amikor egy letöltés befejeződik",
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül",
"telemetry": "Telemetria",
"telemetry_description": "Névtelen felhasználási statisztikák engedélyezése"
},
"notifications": {
"download_complete": "Letöltés befejeződött",
"game_ready_to_install": "{{title}} telepítésre kész",
"repack_list_updated": "Repack lista frissítve",
"repack_count_one": "{{count}} repack hozzáadva",
"repack_count_other": "{{count}} repack hozzáadva"
},
"system_tray": {
"open": "Hydra megnyitása",
"quit": "Kilépés"
},
"game_card": {
"no_downloads": "Nincs elérhető letöltés"
},
"binary_not_found_modal": {
"title": "A programok nincsenek telepítve",
"description": "A Wine vagy a Lutris végrehajtható fájljai nem találhatók a rendszereden",
"instructions": "Ellenőrizd a megfelelő telepítési módot bármelyiküknek a Linux disztribúciódon, hogy a játék normálisan fusson"
}
}

View File

@ -2,3 +2,5 @@ export { default as en } from "./en/translation.json";
export { default as pt } from "./pt/translation.json";
export { default as es } from "./es/translation.json";
export { default as fr } from "./fr/translation.json";
export { default as hu } from "./hu/translation.json";
export { default as it } from "./it/translation.json";

View File

@ -0,0 +1,141 @@
{
"home": {
"featured": "In primo piano",
"recently_added": "Aggiunti di recente",
"trending": "Di tendenza",
"surprise_me": "Sorprendimi",
"no_results": "Nessun risultato trovato"
},
"sidebar": {
"catalogue": "Catalogo",
"downloads": "Download",
"settings": "Impostazioni",
"my_library": "La mia libreria",
"downloading_metadata": "{{title}} (Scaricamento metadati…)",
"checking_files": "{{title}} ({{percentage}} - Verifica file…)",
"paused": "{{title}} (In pausa)",
"downloading": "{{title}} ({{percentage}} - Download…)",
"filter": "Filtra libreria",
"follow_us": "Seguici",
"home": "Home"
},
"header": {
"search": "Cerca",
"home": "Home",
"catalogue": "Catalogo",
"downloads": "Download",
"search_results": "Risultati della ricerca",
"settings": "Impostazioni"
},
"bottom_panel": {
"no_downloads_in_progress": "Nessun download in corso",
"downloading_metadata": "Scaricamento metadati di {{title}}…",
"checking_files": "Verifica file di {{title}}… ({{percentage}} completato)",
"downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Pagina successiva",
"previous_page": "Pagina precedente"
},
"game_details": {
"open_download_options": "Apri opzioni di download",
"download_options_zero": "Nessuna opzione di download",
"download_options_one": "{{count}} opzione di download",
"download_options_other": "{{count}} opzioni di download",
"updated_at": "Aggiornato il {{updated_at}}",
"install": "Installa",
"resume": "Riprendi",
"pause": "Metti in pausa",
"cancel": "Annulla",
"remove": "Rimuovi",
"remove_from_list": "Rimuovi",
"space_left_on_disk": "{{space}} rimasto sul disco",
"eta": "Conclusione {{eta}}",
"downloading_metadata": "Scaricamento metadati…",
"checking_files": "Verifica file…",
"filter": "Filtra repack",
"requirements": "Requisiti di sistema",
"minimum": "Minimi",
"recommended": "Consigliati",
"no_minimum_requirements": "{{title}} non fornisce informazioni sui requisiti minimi",
"no_recommended_requirements": "{{title}} non fornisce informazioni sui requisiti consigliati",
"paused_progress": "{{progress}} (In pausa)",
"release_date": "Rilasciato il {{date}}",
"publisher": "Pubblicato da {{publisher}}",
"copy_link_to_clipboard": "Copia link",
"copied_link_to_clipboard": "Link copiato",
"hours": "ore",
"minutes": "minuti",
"accuracy": "{{accuratezza}}% di accuratezza",
"add_to_library": "Aggiungi alla libreria",
"remove_from_library": "Rimuovi dalla libreria",
"no_downloads": "Nessun download disponibile",
"play_time": "Giocato per {{amount}}",
"last_time_played": "Ultimo gioco giocato {{period}}",
"not_played_yet": "Non hai ancora giocato a {{title}}",
"next_suggestion": "Prossimo suggerimento",
"play": "Gioca",
"deleting": "Eliminazione dell'installer…",
"close": "Chiudi",
"playing_now": "Stai giocando adesso"
},
"activation": {
"title": "Attiva Hydra",
"installation_id": "ID installazione:",
"enter_activation_code": "Inserisci il tuo codice di attivazione",
"message": "Se non sai dove chiederlo, allora non dovresti averlo.",
"activate": "Attiva",
"loading": "Caricamento…"
},
"downloads": {
"resume": "Riprendi",
"pause": "Metti in pausa",
"eta": "Conclusione {{eta}}",
"paused": "In pausa",
"verifying": "Verifica…",
"completed_at": "Completato in {{date}}",
"completed": "Completato",
"cancelled": "Annullato",
"download_again": "Scarica di nuovo",
"cancel": "Annulla",
"filter": "Filtra giochi scaricati",
"remove": "Rimuovi",
"downloading_metadata": "Scaricamento metadati…",
"checking_files": "Verifica file…",
"starting_download": "Avvio download…",
"deleting": "Eliminazione dell'installer…",
"delete": "Rimuovi installer",
"remove_from_list": "Rimuovi",
"delete_modal_title": "Sei sicuro?",
"delete_modal_description": "Questo rimuoverà tutti i file di installazione dal tuo computer",
"install": "Installa"
},
"settings": {
"downloads_path": "Percorso dei download",
"change": "Aggiorna",
"notifications": "Notifiche",
"enable_download_notifications": "Quando un download è completo",
"enable_repack_list_notifications": "Quando viene aggiunto un nuovo repack",
"telemetry": "Telemetria",
"telemetry_description": "Abilita statistiche di utilizzo anonime"
},
"notifications": {
"download_complete": "Download completato",
"game_ready_to_install": "{{title}} è pronto per l'installazione",
"repack_list_updated": "Elenco repack aggiornato",
"repack_count_one": "{{count}} repack aggiunto",
"repack_count_other": "{{count}} repack aggiunti"
},
"system_tray": {
"open": "Apri Hydra",
"quit": "Esci"
},
"game_card": {
"no_downloads": "Nessun download disponibile"
},
"binary_not_found_modal": {
"title": "Programmi non installati",
"description": "Gli eseguibili di Wine o Lutris non sono stati trovati sul tuo sistema",
"instructions": "Verifica il modo corretto di installare uno di essi sulla tua distribuzione Linux in modo che il gioco possa funzionare normalmente"
}
}

View File

@ -73,7 +73,13 @@
"not_played_yet": "Você ainda não jogou {{title}}",
"close": "Fechar",
"deleting": "Excluindo instalador…",
"playing_now": "Jogando agora"
"playing_now": "Jogando agora",
"change": "Mudar",
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
"downloads_path": "Diretório do download",
"select_folder_hint": "Para trocar a pasta padrão, acesse as ",
"settings": "Configurações do Hydra",
"download_now": "Baixe agora"
},
"activation": {
"title": "Ativação",

View File

@ -40,7 +40,7 @@ export class Game {
shop: GameShop;
@Column("text", { nullable: true })
status: string;
status: string | null;
@Column("float", { default: 0 })
progress: number;
@ -61,6 +61,9 @@ export class Game {
@JoinColumn()
repack: Repack;
@Column("boolean", { default: false })
isDeleted: boolean;
@CreateDateColumn()
createdAt: Date;

View File

@ -16,8 +16,6 @@ const getGames = async (
let i = 0 + cursor;
if (!steamGames.length) return [];
while (results.length < take) {
const game = steamGames[i];
const repacks = searchRepacks(game.name);

View File

@ -1,10 +1,11 @@
import checkDiskSpace from "check-disk-space";
import { registerEvent } from "../register-event";
import { getDownloadsPath } from "../helpers/get-downloads-path";
const getDiskFreeSpace = async (_event: Electron.IpcMainInvokeEvent) =>
checkDiskSpace(await getDownloadsPath());
const getDiskFreeSpace = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) => checkDiskSpace(path);
registerEvent(getDiskFreeSpace, {
name: "getDiskFreeSpace",

View File

@ -7,8 +7,8 @@ import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
import { stateManager } from "@main/state-manager";
const { Index } = flexSearch;
const repacksIndex = new Index({ tokenize: "strict" });
const steamGamesIndex = new Index({ tokenize: "forward" });
const repacksIndex = new Index();
const steamGamesIndex = new Index({ tokenize: "reverse" });
const repacks = stateManager.getValue("repacks");
const steamGames = stateManager.getValue("steamGames");
@ -21,8 +21,6 @@ for (let i = 0; i < repacks.length; i++) {
repacksIndex.add(i, formatName(formatter(repack.title)));
}
console.log(true);
for (let i = 0; i < steamGames.length; i++) {
const steamGame = steamGames[i];
steamGamesIndex.add(i, formatName(steamGame.name));

View File

@ -17,6 +17,7 @@ import "./library/get-repackers-friendly-names";
import "./library/open-game";
import "./library/open-game-installer";
import "./library/remove-game";
import "./library/remove-game-from-library";
import "./misc/get-or-cache-image";
import "./misc/open-external";
import "./misc/show-open-dialog";
@ -24,6 +25,7 @@ import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download";
import "./torrenting/start-game-download";
import "./torrenting/remove-game-from-download";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";

View File

@ -13,6 +13,24 @@ const addGameToLibrary = async (
gameShop: GameShop,
executablePath: string
) => {
const game = await gameRepository.findOne({
where: {
objectID,
},
});
if (game) {
return gameRepository.update(
{
id: game.id,
},
{
shop: gameShop,
executablePath,
isDeleted: false,
}
);
} else {
const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID));
return gameRepository.insert({
@ -22,6 +40,7 @@ const addGameToLibrary = async (
shop: gameShop,
executablePath,
});
}
};
registerEvent(addGameToLibrary, {

View File

@ -22,7 +22,10 @@ const deleteGameFolder = async (
if (!game) return;
if (game.folderName) {
const folderPath = path.join(await getDownloadsPath(), game.folderName);
const folderPath = path.join(
game.downloadPath ?? (await getDownloadsPath()),
game.folderName
);
if (fs.existsSync(folderPath)) {
return new Promise((resolve, reject) => {

View File

@ -9,6 +9,7 @@ const getGameByObjectID = async (
gameRepository.findOne({
where: {
objectID,
isDeleted: false,
},
relations: {
repack: true,

View File

@ -5,9 +5,12 @@ import { searchRepacks } from "../helpers/search-games";
import { registerEvent } from "../register-event";
import { sortBy } from "lodash-es";
const getLibrary = async (_event: Electron.IpcMainInvokeEvent) =>
const getLibrary = async () =>
gameRepository
.find({
where: {
isDeleted: false,
},
order: {
createdAt: "desc",
},

View File

@ -1,7 +1,7 @@
import { registerEvent } from "../register-event";
import { stateManager } from "@main/state-manager";
const getRepackersFriendlyNames = async (_event: Electron.IpcMainInvokeEvent) =>
const getRepackersFriendlyNames = async () =>
stateManager.getValue("repackersFriendlyNames").reduce((prev, next) => {
return { ...prev, [next.name]: next.friendlyName };
}, {});

View File

@ -23,7 +23,7 @@ const openGameInstaller = async (
);
if (!fs.existsSync(gamePath)) {
await gameRepository.delete({ id: gameId });
await gameRepository.update({ id: gameId }, { status: null });
return true;
}

View File

@ -0,0 +1,13 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
const removeGameFromLibrary = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
gameRepository.update({ id: gameId }, { isDeleted: true });
};
registerEvent(removeGameFromLibrary, {
name: "removeGameFromLibrary",
});

View File

@ -25,14 +25,13 @@ const cancelGameDownload = async (
if (!game) return;
gameRepository
await gameRepository
.update(
{
id: game.id,
},
{
status: GameStatus.Cancelled,
downloadPath: null,
bytesDownloaded: 0,
progress: 0,
}

View File

@ -0,0 +1,34 @@
import { GameStatus } from "@main/constants";
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
const removeGameFromDownload = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
const game = await gameRepository.findOne({
where: {
id: gameId,
status: GameStatus.Cancelled,
},
});
if (!game) return;
gameRepository.update(
{
id: game.id,
},
{
status: null,
downloadPath: null,
bytesDownloaded: 0,
progress: 0,
}
);
};
registerEvent(removeGameFromDownload, {
name: "removeGameFromDownload",
});

View File

@ -5,7 +5,6 @@ import { GameStatus } from "@main/constants";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { getDownloadsPath } from "../helpers/get-downloads-path";
import { getImageBase64 } from "@main/helpers";
import { In } from "typeorm";
@ -14,7 +13,8 @@ const startGameDownload = async (
repackId: number,
objectID: string,
title: string,
gameShop: GameShop
gameShop: GameShop,
downloadPath: string
) => {
const [game, repack] = await Promise.all([
gameRepository.findOne({
@ -37,8 +37,6 @@ const startGameDownload = async (
writePipe.write({ action: "pause" });
const downloadsPath = game?.downloadPath ?? (await getDownloadsPath());
await gameRepository.update(
{
status: In([
@ -57,8 +55,9 @@ const startGameDownload = async (
},
{
status: GameStatus.DownloadingMetadata,
downloadPath: downloadsPath,
downloadPath: downloadPath,
repack: { id: repackId },
isDeleted: false,
}
);
@ -66,18 +65,11 @@ const startGameDownload = async (
action: "start",
game_id: game.id,
magnet: repack.magnet,
save_path: downloadsPath,
save_path: downloadPath,
});
game.status = GameStatus.DownloadingMetadata;
writePipe.write({
action: "start",
game_id: game.id,
magnet: repack.magnet,
save_path: downloadsPath,
});
return game;
} else {
const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID));
@ -88,7 +80,7 @@ const startGameDownload = async (
objectID,
shop: gameShop,
status: GameStatus.DownloadingMetadata,
downloadPath: downloadsPath,
downloadPath: downloadPath,
repack: { id: repackId },
});
@ -96,7 +88,7 @@ const startGameDownload = async (
action: "start",
game_id: createdGame.id,
magnet: repack.magnet,
save_path: downloadsPath,
save_path: downloadPath,
});
const { repack: _, ...rest } = createdGame;

View File

@ -2,6 +2,7 @@ import { app, BrowserWindow } from "electron";
import { init } from "@sentry/electron/main";
import i18n from "i18next";
import path from "node:path";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { resolveDatabaseUpdates, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source";
import * as resources from "@locales";
@ -49,8 +50,10 @@ if (process.defaultApp) {
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
electronApp.setAppUserModelId("site.hydralauncher.hydra");
dataSource.initialize().then(async () => {
// await resolveDatabaseUpdates();
await resolveDatabaseUpdates();
await import("./main");
@ -59,10 +62,14 @@ app.whenReady().then(() => {
});
WindowManager.createMainWindow();
// WindowManager.createSystemTray(userPreferences?.language || "en");
WindowManager.createSystemTray(userPreferences?.language || "en");
});
});
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window);
});
app.on("second-instance", (_event, commandLine) => {
// Someone tried to run a second instance, we should focus our window.
if (WindowManager.mainWindow) {

View File

@ -4,7 +4,7 @@ import {
getNewGOGGames,
getNewRepacksFromCPG,
getNewRepacksFromUser,
getNewRepacksFromXatab,
// getNewRepacksFromXatab,
// getNewRepacksFromOnlineFix,
readPipe,
startProcessWatcher,
@ -73,9 +73,9 @@ const checkForNewRepacks = async () => {
getNewGOGGames(
existingRepacks.filter((repack) => repack.repacker === "GOG")
),
getNewRepacksFromXatab(
existingRepacks.filter((repack) => repack.repacker === "Xatab")
),
// getNewRepacksFromXatab(
// existingRepacks.filter((repack) => repack.repacker === "Xatab")
// ),
getNewRepacksFromCPG(
existingRepacks.filter((repack) => repack.repacker === "CPG")
),

View File

@ -1,11 +1,27 @@
import { BrowserWindow, Menu, Tray, app } from "electron";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import { is } from "@electron-toolkit/utils";
import { t } from "i18next";
import path from "node:path";
import icon from "../../../resources/icon.png?asset";
import trayIcon from "../../../resources/icon.png?asset";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
private static loadURL(hash = "") {
// 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.mainWindow.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`
);
} else {
this.mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
hash,
});
}
}
public static createMainWindow() {
// Create the browser window.
this.mainWindow = new BrowserWindow({
@ -14,7 +30,7 @@ export class WindowManager {
minWidth: 1024,
minHeight: 540,
titleBarStyle: "hidden",
// icon: path.join(__dirname, "..", "..", "images", "icon.png"),
...(process.platform === "linux" ? { icon } : {}),
trafficLightPosition: { x: 16, y: 16 },
titleBarOverlay: {
symbolColor: "#DADBE1",
@ -27,42 +43,24 @@ export class WindowManager {
},
});
this.loadURL();
this.mainWindow.removeMenu();
// 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.mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
} else {
this.mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
}
this.mainWindow.webContents.on("did-finish-load", () => {
if (!app.isPackaged) {
// Open the DevTools.
this.mainWindow.webContents.openDevTools();
}
});
this.mainWindow.on("close", () => {
WindowManager.mainWindow.setProgressBar(-1);
});
}
public static redirect(path: string) {
public static redirect(hash: string) {
if (!this.mainWindow) this.createMainWindow();
this.mainWindow.loadURL(`${MAIN_WINDOW_WEBPACK_ENTRY}#${path}`);
this.loadURL(hash);
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
this.mainWindow.focus();
}
public static createSystemTray(language: string) {
const tray = new Tray(
app.isPackaged
? path.join(process.resourcesPath, "icon_tray.png")
: path.join(__dirname, "..", "..", "resources", "icon_tray.png")
);
const tray = new Tray(trayIcon);
const contextMenu = Menu.buildFromTemplate([
{

View File

@ -15,8 +15,17 @@ contextBridge.exposeInMainWorld("electron", {
repackId: number,
objectID: string,
title: string,
shop: GameShop
) => ipcRenderer.invoke("startGameDownload", repackId, objectID, title, shop),
shop: GameShop,
downloadPath: string
) =>
ipcRenderer.invoke(
"startGameDownload",
repackId,
objectID,
title,
shop,
downloadPath
),
cancelGameDownload: (gameId: number) =>
ipcRenderer.invoke("cancelGameDownload", gameId),
pauseGameDownload: (gameId: number) =>
@ -90,7 +99,8 @@ contextBridge.exposeInMainWorld("electron", {
},
/* Hardware */
getDiskFreeSpace: () => ipcRenderer.invoke("getDiskFreeSpace"),
getDiskFreeSpace: (path: string) =>
ipcRenderer.invoke("getDiskFreeSpace", path),
/* Misc */
getOrCacheImage: (url: string) => ipcRenderer.invoke("getOrCacheImage", url),

View File

@ -4,9 +4,14 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hydra</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
/>
</head>
<body>
<div id="root"></div>
<h1>hello</h1>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { XIcon } from "@primer/octicons-react";
@ -23,8 +23,9 @@ export function Modal({
}: ModalProps) {
const [isClosing, setIsClosing] = useState(false);
const dispatch = useAppDispatch();
const modalContentRef = useRef<HTMLDivElement | null>(null);
const handleCloseClick = () => {
const handleCloseClick = useCallback(() => {
setIsClosing(true);
const zero = performance.now();
@ -36,8 +37,44 @@ export function Modal({
setIsClosing(false);
}
});
}, [onClose]);
const isTopMostModal = () => {
const openModals = document.querySelectorAll("[role=modal]");
return (
openModals.length &&
openModals[openModals.length - 1] === modalContentRef.current
);
};
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isTopMostModal()) {
handleCloseClick();
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [handleCloseClick]);
useEffect(() => {
const onMouseDown = (e: MouseEvent) => {
if (!isTopMostModal()) return;
const clickedOutsideContent = !modalContentRef.current.contains(
e.target as Node
);
if (clickedOutsideContent) {
handleCloseClick();
}
};
window.addEventListener("mousedown", onMouseDown);
return () => window.removeEventListener("mousedown", onMouseDown);
}, [handleCloseClick]);
useEffect(() => {
dispatch(toggleDragging(visible));
}, [dispatch, visible]);
@ -46,7 +83,11 @@ export function Modal({
return createPortal(
<div className={styles.backdrop({ closing: isClosing })}>
<div className={styles.modal({ closing: isClosing })}>
<div
className={styles.modal({ closing: isClosing })}
role="modal"
ref={modalContentRef}
>
<div className={styles.modalHeader}>
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
<h3>{title}</h3>

View File

@ -21,8 +21,8 @@ declare global {
startGameDownload: (
repackId: number,
objectID: string,
title: string,
shop: GameShop
shop: GameShop,
downloadPath: string
) => Promise<Game>;
cancelGameDownload: (gameId: number) => Promise<void>;
pauseGameDownload: (gameId: number) => Promise<void>;
@ -75,7 +75,7 @@ declare global {
) => Promise<void>;
/* Hardware */
getDiskFreeSpace: () => Promise<DiskSpace>;
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
/* Misc */
getOrCacheImage: (url: string) => Promise<string>;

View File

@ -28,10 +28,11 @@ export function useDownload() {
repackId: number,
objectID: string,
title: string,
shop: GameShop
shop: GameShop,
downloadPath: string
) =>
window.electron
.startGameDownload(repackId, objectID, title, shop)
.startGameDownload(repackId, objectID, title, shop, downloadPath)
.then((game) => {
dispatch(clearDownload());
updateLibrary();

View File

@ -19,15 +19,15 @@ import { useAppDispatch, useDownload } from "@renderer/hooks";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import { vars } from "../../theme.css";
import Lottie from "lottie-react";
import { useTranslation } from "react-i18next";
import { SkeletonTheme } from "react-loading-skeleton";
import { DescriptionHeader } from "./description-header";
import { GameDetailsSkeleton } from "./game-details-skeleton";
import * as styles from "./game-details.css";
import { HeroPanel } from "./hero-panel";
import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { RepacksModal } from "./repacks-modal";
import Lottie from "lottie-react";
import { DescriptionHeader } from "./description-header";
export function GameDetails() {
const { objectID, shop } = useParams();
@ -51,6 +51,7 @@ export function GameDetails() {
const { t, i18n } = useTranslation("game_details");
const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const randomGameObjectID = useRef<string | null>(null);
@ -140,15 +141,20 @@ export function GameDetails() {
};
}, [game?.id, isGamePlaying, getGame]);
const handleStartDownload = async (repackId: number) => {
const handleStartDownload = async (
repackId: number,
downloadPath: string
) => {
return startDownload(
repackId,
gameDetails.objectID,
gameDetails.name,
shop as GameShop
shop as GameShop,
downloadPath
).then(() => {
getGame();
setShowRepacksModal(false);
setShowSelectFolderModal(false);
});
};
@ -173,6 +179,8 @@ export function GameDetails() {
visible={showRepacksModal}
gameDetails={gameDetails}
startDownload={handleStartDownload}
showSelectFolderModal={showSelectFolderModal}
setShowSelectFolderModal={setShowSelectFolderModal}
onClose={() => setShowRepacksModal(false)}
/>
)}

View File

@ -6,28 +6,30 @@ import type { GameRepack, ShopDetails } from "@types";
import * as styles from "./repacks-modal.css";
import type { DiskSpace } from "check-disk-space";
import { format } from "date-fns";
import { SPACING_UNIT } from "../../theme.css";
import { formatBytes } from "@renderer/utils";
import { useAppSelector } from "@renderer/hooks";
import { SPACING_UNIT } from "../../theme.css";
import { format } from "date-fns";
import { SelectFolderModal } from "./select-folder-modal";
export interface RepacksModalProps {
visible: boolean;
gameDetails: ShopDetails;
startDownload: (repackId: number) => Promise<void>;
showSelectFolderModal: boolean;
setShowSelectFolderModal: (value: boolean) => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
onClose: () => void;
}
export function RepacksModal({
visible,
gameDetails,
showSelectFolderModal,
setShowSelectFolderModal,
startDownload,
onClose,
}: RepacksModalProps) {
const [downloadStarting, setDownloadStarting] = useState(false);
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace>(null);
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack>(null);
const repackersFriendlyNames = useAppSelector(
(state) => state.repackersFriendlyNames.value
@ -39,21 +41,9 @@ export function RepacksModal({
setFilteredRepacks(gameDetails.repacks);
}, [gameDetails.repacks]);
const getDiskFreeSpace = () => {
window.electron.getDiskFreeSpace().then((result) => {
setDiskFreeSpace(result);
});
};
useEffect(() => {
getDiskFreeSpace();
}, [visible]);
const handleRepackClick = (repack: GameRepack) => {
setDownloadStarting(true);
startDownload(repack.id).finally(() => {
setDownloadStarting(false);
});
setRepack(repack);
setShowSelectFolderModal(true);
};
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
@ -70,11 +60,16 @@ export function RepacksModal({
<Modal
visible={visible}
title={`${gameDetails.name} Repacks`}
description={t("space_left_on_disk", {
space: formatBytes(diskFreeSpace?.free ?? 0),
})}
description={t("repacks_modal_description")}
onClose={onClose}
>
<SelectFolderModal
visible={showSelectFolderModal}
onClose={() => setShowSelectFolderModal(false)}
gameDetails={gameDetails}
startDownload={startDownload}
repack={repack}
/>
<div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}>
<TextField placeholder={t("filter")} onChange={handleFilter} />
</div>
@ -85,7 +80,6 @@ export function RepacksModal({
key={repack.id}
theme="dark"
onClick={() => handleRepackClick(repack)}
disabled={downloadStarting}
className={styles.repackButton}
>
<p style={{ color: "#DADBE1" }}>{repack.title}</p>

View File

@ -0,0 +1,19 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const container = style({
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
width: "100%",
});
export const downloadsPathField = style({
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
});
export const hintText = style({
fontSize: "12px",
color: vars.color.bodyText,
});

View File

@ -0,0 +1,115 @@
import { Button, Modal, TextField } from "@renderer/components";
import { GameRepack, ShopDetails } from "@types";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { formatBytes } from "@renderer/utils";
import { DiskSpace } from "check-disk-space";
import { Link } from "react-router-dom";
import * as styles from "./select-folder-modal.css";
export interface SelectFolderModalProps {
visible: boolean;
gameDetails: ShopDetails;
onClose: () => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
repack: GameRepack;
}
export function SelectFolderModal({
visible,
gameDetails,
onClose,
startDownload,
repack,
}: SelectFolderModalProps) {
const { t } = useTranslation("game_details");
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace>(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
useEffect(() => {
visible && getDiskFreeSpace(selectedPath);
}, [visible, selectedPath]);
useEffect(() => {
Promise.all([
window.electron.getDefaultDownloadsPath(),
window.electron.getUserPreferences(),
]).then(([path, userPreferences]) => {
setSelectedPath(userPreferences?.downloadsPath || path);
});
}, []);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
setDiskFreeSpace(result);
});
};
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
defaultPath: selectedPath,
properties: ["openDirectory"],
});
if (filePaths && filePaths.length > 0) {
const path = filePaths[0];
setSelectedPath(path);
}
};
const handleStartClick = () => {
setDownloadStarting(true);
startDownload(repack.id, selectedPath).finally(() => {
setDownloadStarting(false);
});
};
return (
<Modal
visible={visible}
title={`${gameDetails.name} Installation folder`}
description={t("space_left_on_disk", {
space: formatBytes(diskFreeSpace?.free ?? 0),
})}
onClose={onClose}
>
<div className={styles.container}>
<div className={styles.downloadsPathField}>
<TextField
label={t("downloads_path")}
value={selectedPath}
readOnly
disabled
/>
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
disabled={downloadStarting}
>
{t("change")}
</Button>
</div>
<p className={styles.hintText}>
{t("select_folder_hint")}{" "}
<Link
to="/settings"
style={{
textDecoration: "none",
color: "#C0C1C7",
}}
>
{t("settings")}
</Link>
</p>
<Button onClick={handleStartClick} disabled={downloadStarting}>
{t("download_now")}
</Button>
</div>
</Modal>
);
}

856
yarn.lock

File diff suppressed because it is too large Load Diff