diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..c06126d3 --- /dev/null +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..2f25da53 --- /dev/null +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.gitignore b/.gitignore index 73b023c7..3cb8d1ca 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ out .DS_Store *.log* .env +.vite diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 2ced9527..00000000 --- a/.npmrc +++ /dev/null @@ -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 diff --git a/.prettierrc.cjs b/.prettierrc.cjs new file mode 100644 index 00000000..5e16a55f --- /dev/null +++ b/.prettierrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + semi: true, + trailingComma: "es5", + singleQuote: false, + tabWidth: 2, +}; diff --git a/.prettierrc.yaml b/.prettierrc.yaml deleted file mode 100644 index f9711fb4..00000000 --- a/.prettierrc.yaml +++ /dev/null @@ -1,4 +0,0 @@ -singleQuote: false -semi: true -tabWidth: 2 -trailingComma: es5 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ec3459a4 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/build/icon.png b/build/icon.png index cf9e8b2c..865a96a2 100644 Binary files a/build/icon.png and b/build/icon.png differ diff --git a/electron-builder.yml b/electron-builder.yml index 74d51e0d..14df7838 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -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/ diff --git a/electron.vite.config.ts b/electron.vite.config.ts index e94af154..d3265042 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -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 { diff --git a/package.json b/package.json index 78b0b6a9..07e3e883 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/resources/hydra.db b/resources/hydra.db new file mode 100644 index 00000000..7db97249 Binary files /dev/null and b/resources/hydra.db differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 00000000..865a96a2 Binary files /dev/null and b/resources/icon.png differ diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 2f967b76..03f00a0d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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", diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json new file mode 100644 index 00000000..087068ac --- /dev/null +++ b/src/locales/hu/translation.json @@ -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" + } +} diff --git a/src/locales/index.ts b/src/locales/index.ts index 98a93c78..4653bcb5 100644 --- a/src/locales/index.ts +++ b/src/locales/index.ts @@ -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"; diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json new file mode 100644 index 00000000..01b34c1e --- /dev/null +++ b/src/locales/it/translation.json @@ -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" + } +} diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index 3498ad83..c6b393bb 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -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", diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index b1598b8b..25ca7495 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -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; diff --git a/src/main/events/catalogue/get-games.ts b/src/main/events/catalogue/get-games.ts index 75c6b62d..64c491ae 100644 --- a/src/main/events/catalogue/get-games.ts +++ b/src/main/events/catalogue/get-games.ts @@ -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); diff --git a/src/main/events/hardware/get-disk-free-space.ts b/src/main/events/hardware/get-disk-free-space.ts index 28dcafa2..0c87ed8b 100644 --- a/src/main/events/hardware/get-disk-free-space.ts +++ b/src/main/events/hardware/get-disk-free-space.ts @@ -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", diff --git a/src/main/events/helpers/search-games.ts b/src/main/events/helpers/search-games.ts index 5b217b28..b23bcd67 100644 --- a/src/main/events/helpers/search-games.ts +++ b/src/main/events/helpers/search-games.ts @@ -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)); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 5eaceff5..e62720fb 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -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"; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index abbba592..ba528dbc 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -13,15 +13,34 @@ const addGameToLibrary = async ( gameShop: GameShop, executablePath: string ) => { - const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID)); - - return gameRepository.insert({ - title, - iconUrl, - objectID, - shop: gameShop, - executablePath, + 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({ + title, + iconUrl, + objectID, + shop: gameShop, + executablePath, + }); + } }; registerEvent(addGameToLibrary, { diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts index eaf791ed..c8821415 100644 --- a/src/main/events/library/delete-game-folder.ts +++ b/src/main/events/library/delete-game-folder.ts @@ -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) => { diff --git a/src/main/events/library/get-game-by-object-id.ts b/src/main/events/library/get-game-by-object-id.ts index 522d3bbb..0aa5976a 100644 --- a/src/main/events/library/get-game-by-object-id.ts +++ b/src/main/events/library/get-game-by-object-id.ts @@ -9,6 +9,7 @@ const getGameByObjectID = async ( gameRepository.findOne({ where: { objectID, + isDeleted: false, }, relations: { repack: true, diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index a38b0cc9..be7eb84f 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -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", }, diff --git a/src/main/events/library/get-repackers-friendly-names.ts b/src/main/events/library/get-repackers-friendly-names.ts index 22481d82..4dcd5c68 100644 --- a/src/main/events/library/get-repackers-friendly-names.ts +++ b/src/main/events/library/get-repackers-friendly-names.ts @@ -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 }; }, {}); diff --git a/src/main/events/library/open-game-installer.ts b/src/main/events/library/open-game-installer.ts index acd3e343..796e063b 100644 --- a/src/main/events/library/open-game-installer.ts +++ b/src/main/events/library/open-game-installer.ts @@ -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; } diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts new file mode 100644 index 00000000..8767ee08 --- /dev/null +++ b/src/main/events/library/remove-game-from-library.ts @@ -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", +}); diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index 686033cc..a1a2e6b7 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -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, } diff --git a/src/main/events/torrenting/remove-game-from-download.ts b/src/main/events/torrenting/remove-game-from-download.ts new file mode 100644 index 00000000..47c1ebe6 --- /dev/null +++ b/src/main/events/torrenting/remove-game-from-download.ts @@ -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", +}); diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 570fd2ec..1bdb1a6b 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -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; diff --git a/src/main/index.ts b/src/main/index.ts index a48db177..df529366 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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) { diff --git a/src/main/main.ts b/src/main/main.ts index 5637bbd3..a98b804c 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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") ), diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index d8b174a2..0eea8e96 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -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([ { diff --git a/src/preload/index.ts b/src/preload/index.ts index 97dcca0b..66fff80f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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), diff --git a/src/renderer/index.html b/src/renderer/index.html index a2f6763a..abb1eaae 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -4,9 +4,14 @@