diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6f9c413..b0c92fa1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,20 +47,8 @@ jobs: - name: Install dependencies run: pip install -r requirements.txt - - name: Build with Nuitka - uses: Nuitka/Nuitka-Action@main - with: - nuitka-version: main - working-directory: torrent-client - script-name: main.py - standalone: true - enable-plugins: libtorrent - output-dir: resources/dist - output-file: hydra-download-manager - windows-icon-from-ico: images/icon.ico - - # - name: Build with pyinstaller - # run: pyinstaller torrent-client/main.py --distpath resources/dist --icon=images/icon.ico -n hydra-download-manager + - name: Build with cx_Freeze + run: python torrent-client/setup.py build - name: Publish run: yarn run publish diff --git a/README.md b/README.md index 8c2a5d9b..a737756e 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ yarn start Build the bittorrent client by using this command: ```bash -pyinstaller torrent-client/main.py --distpath resources/dist --icon=images/icon.ico -n hydra-download-manager +python torrent-client/setup.py build ``` ### Build the Electron application diff --git a/forge.config.ts b/forge.config.ts index 3bc8bf23..21718764 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -13,6 +13,16 @@ import { ElectronegativityPlugin } from "@electron-forge/plugin-electronegativit import { mainConfig } from "./webpack.main.config"; import { rendererConfig } from "./webpack.renderer.config"; +const linuxPkgConfig = { + mimeType: ["x-scheme-handler/hydralauncher"], + bin: "./Hydra", + desktopTemplate: "./hydra-launcher.desktop", + icon: "images/icon.png", + genericName: "Games Launcher", + name: "hydra-launcher", + productName: "Hydra" +}; + const config: ForgeConfig = { packagerConfig: { asar: true, @@ -40,16 +50,10 @@ const config: ForgeConfig = { }), new MakerZIP({}, ["darwin", "linux"]), new MakerRpm({ - options: { - mimeType: ["x-scheme-handler/hydralauncher"], - bin: "./Hydra", - }, + options: linuxPkgConfig }), new MakerDeb({ - options: { - mimeType: ["x-scheme-handler/hydralauncher"], - bin: "./Hydra", - }, + options: linuxPkgConfig }), ], publishers: [ diff --git a/hydra-launcher.desktop b/hydra-launcher.desktop new file mode 100644 index 00000000..e9e10e91 --- /dev/null +++ b/hydra-launcher.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Hydra +Comment=No bullshit. Just play. +GenericName=Games Launcher +Exec=hydra-launcher %U +Icon=hydra-launcher +Type=Application +StartupNotify=true +Categories=GNOME;GTK;Utility; +MimeType=x-scheme-handler/hydralauncher; +StartupWMClass=Hydra diff --git a/requirements.txt b/requirements.txt index ab929a73..6cee730a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ libtorrent +cx_Freeze +cx_Logging; sys_platform == 'win32' +lief; sys_platform == 'win32' pywin32; sys_platform == 'win32' diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 6c5aa32c..f59f1e87 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -7,7 +7,6 @@ "no_results": "No results found" }, "sidebar": { - "home": "Home", "catalogue": "Catalogue", "downloads": "Downloads", "settings": "Settings", @@ -16,7 +15,9 @@ "checking_files": "{{title}} ({{percentage}} - Checking files…)", "paused": "{{title}} (Paused)", "downloading": "{{title}} ({{percentage}} - Downloading…)", - "filter": "Filter library" + "filter": "Filter library", + "follow_us": "Follow us", + "home": "Home" }, "header": { "search": "Search", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 4cc90d15..9ac401bf 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -16,7 +16,8 @@ "paused": "{{title}} (Pausado)", "downloading": "{{title}} ({{percentage}} - Descargando…)", "filter": "Filtrar biblioteca", - "home": "Hogar" + "home": "Hogar", + "follow_us": "Síganos" }, "header": { "search": "Buscar", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index f4250be2..c83a825b 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -16,7 +16,8 @@ "paused": "{{title}} (En pause)", "downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)", "filter": "Filtrer la bibliothèque", - "home": "Maison" + "home": "Maison", + "follow_us": "Suivez-nous" }, "header": { "search": "Recherche", diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index a19ca24a..145163b4 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -16,7 +16,8 @@ "paused": "{{title}} (Pausado)", "downloading": "{{title}} ({{percentage}} - Baixando…)", "filter": "Filtrar biblioteca", - "home": "Início" + "home": "Início", + "follow_us": "Acompanhe-nos" }, "header": { "search": "Buscar", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 866f8317..5eaceff5 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -1,30 +1,31 @@ -import { app, ipcMain } from "electron"; import { defaultDownloadsPath } from "@main/constants"; +import { app, ipcMain } from "electron"; -import "./library/add-game-to-library"; -import "./catalogue/search-games"; -import "./catalogue/get-game-shop-details"; import "./catalogue/get-catalogue"; -import "./library/get-library"; +import "./catalogue/get-game-shop-details"; +import "./catalogue/get-games"; +import "./catalogue/get-how-long-to-beat"; +import "./catalogue/get-random-game"; +import "./catalogue/search-games"; import "./hardware/get-disk-free-space"; +import "./library/add-game-to-library"; +import "./library/close-game"; +import "./library/delete-game-folder"; +import "./library/get-game-by-object-id"; +import "./library/get-library"; +import "./library/get-repackers-friendly-names"; +import "./library/open-game"; +import "./library/open-game-installer"; +import "./library/remove-game"; +import "./misc/get-or-cache-image"; +import "./misc/open-external"; +import "./misc/show-open-dialog"; import "./torrenting/cancel-game-download"; import "./torrenting/pause-game-download"; import "./torrenting/resume-game-download"; import "./torrenting/start-game-download"; -import "./misc/get-or-cache-image"; -import "./user-preferences/update-user-preferences"; import "./user-preferences/get-user-preferences"; -import "./library/get-repackers-friendly-names"; -import "./library/get-game-by-object-id"; -import "./library/open-game-installer"; -import "./library/open-game"; -import "./library/close-game"; -import "./misc/show-open-dialog"; -import "./library/remove-game"; -import "./library/delete-game-folder"; -import "./catalogue/get-random-game"; -import "./catalogue/get-how-long-to-beat"; -import "./catalogue/get-games"; +import "./user-preferences/update-user-preferences"; ipcMain.handle("ping", () => "pong"); ipcMain.handle("getVersion", () => app.getVersion()); diff --git a/src/main/events/misc/open-external.ts b/src/main/events/misc/open-external.ts new file mode 100644 index 00000000..3d693c23 --- /dev/null +++ b/src/main/events/misc/open-external.ts @@ -0,0 +1,9 @@ +import { shell } from "electron"; +import { registerEvent } from "../register-event"; + +const openExternal = async (_event: Electron.IpcMainInvokeEvent, src: string) => + shell.openExternal(src); + +registerEvent(openExternal, { + name: "openExternal", +}); diff --git a/src/main/services/torrent-client.ts b/src/main/services/torrent-client.ts index a79f3f9c..fa1cd59d 100644 --- a/src/main/services/torrent-client.ts +++ b/src/main/services/torrent-client.ts @@ -1,7 +1,8 @@ import path from "node:path"; import cp from "node:child_process"; +import fs from "node:fs"; import * as Sentry from "@sentry/electron/main"; -import { Notification, app } from "electron"; +import { Notification, app, dialog } from "electron"; import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { Game } from "@main/entity"; @@ -54,6 +55,15 @@ export class TorrentClient { binaryName ); + if (!fs.existsSync(binaryPath)) { + dialog.showErrorBox( + "Fatal", + "Hydra download manager binary not found. Please check if it has been removed by Windows Defender." + ); + + app.quit(); + } + cp.spawn(binaryPath, commonArgs, { stdio: "inherit", windowsHide: true, @@ -155,7 +165,6 @@ export class TorrentClient { } } catch (err) { Sentry.captureException(err); - Sentry.captureMessage(message, "error"); } } } diff --git a/src/preload.ts b/src/preload.ts index db765453..93acde24 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -86,6 +86,7 @@ contextBridge.exposeInMainWorld("electron", { ping: () => ipcRenderer.invoke("ping"), getVersion: () => ipcRenderer.invoke("getVersion"), getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"), + openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), showOpenDialog: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke("showOpenDialog", options), platform: process.platform, diff --git a/src/renderer/assets/discord-icon.svg b/src/renderer/assets/discord-icon.svg new file mode 100644 index 00000000..2fba46cd --- /dev/null +++ b/src/renderer/assets/discord-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/assets/x-icon.svg b/src/renderer/assets/x-icon.svg new file mode 100644 index 00000000..f594427b --- /dev/null +++ b/src/renderer/assets/x-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/components/bottom-panel/bottom-panel.css.ts b/src/renderer/components/bottom-panel/bottom-panel.css.ts index 632e1bff..f251d1c0 100644 --- a/src/renderer/components/bottom-panel/bottom-panel.css.ts +++ b/src/renderer/components/bottom-panel/bottom-panel.css.ts @@ -1,5 +1,5 @@ -import { SPACING_UNIT, vars } from "../../theme.css"; import { style } from "@vanilla-extract/css"; +import { SPACING_UNIT, vars } from "../../theme.css"; export const bottomPanel = style({ width: "100%", @@ -14,9 +14,10 @@ export const bottomPanel = style({ }); export const downloadsButton = style({ - cursor: "pointer", color: vars.color.bodyText, + borderBottom: "1px solid transparent", ":hover": { - textDecoration: "underline", + borderBottom: `1px solid ${vars.color.bodyText}`, + cursor: "pointer", }, }); diff --git a/src/renderer/components/sidebar/sidebar.css.ts b/src/renderer/components/sidebar/sidebar.css.ts index 8bf62781..e4ff22f8 100644 --- a/src/renderer/components/sidebar/sidebar.css.ts +++ b/src/renderer/components/sidebar/sidebar.css.ts @@ -6,6 +6,7 @@ export const sidebar = recipe({ base: { backgroundColor: vars.color.darkBackground, color: "#c0c1c7", + flexDirection: "column", display: "flex", transition: "opacity ease 0.2s", borderRight: `solid 1px ${vars.color.borderColor}`, @@ -134,3 +135,36 @@ export const section = recipe({ }, }, }); + +export const sidebarFooter = style({ + marginTop: "auto", + padding: `${SPACING_UNIT * 2}px`, + display: "flex", + alignItems: "center", + justifyContent: "space-between", +}); + +export const footerSocialsContainer = style({ + display: "flex", + alignItems: "center", + gap: `${SPACING_UNIT * 1.5}px`, +}); + +export const footerSocialsItem = style({ + color: vars.color.bodyText, + backgroundColor: vars.color.darkBackground, + width: "16px", + height: "16px", + display: "flex", + alignItems: "center", + transition: "all ease 0.15s", + ":hover": { + opacity: 0.75, + cursor: "pointer", + }, +}); + +export const footerText = style({ + color: vars.color.bodyText, + fontSize: "12px", +}); diff --git a/src/renderer/components/sidebar/sidebar.tsx b/src/renderer/components/sidebar/sidebar.tsx index 962ecdfe..ad1bcdf3 100644 --- a/src/renderer/components/sidebar/sidebar.tsx +++ b/src/renderer/components/sidebar/sidebar.tsx @@ -4,13 +4,32 @@ import { useLocation, useNavigate } from "react-router-dom"; import type { Game } from "@types"; -import { useDownload, useLibrary } from "@renderer/hooks"; import { AsyncImage, TextField } from "@renderer/components"; +import { useDownload, useLibrary } from "@renderer/hooks"; import { SPACING_UNIT } from "@renderer/theme.css"; -import * as styles from "./sidebar.css"; import { routes } from "./routes"; +import { MarkGithubIcon } from "@primer/octicons-react"; +import DiscordLogo from "@renderer/assets/discord-icon.svg"; +import XLogo from "@renderer/assets/x-icon.svg"; +import * as styles from "./sidebar.css"; + +const socials = [ + { + url: "https://discord.gg/hydralauncher", + icon: , + }, + { + url: "https://twitter.com/hydralauncher", + icon: , + }, + { + url: "https://github.com/hydralauncher/hydra", + icon: , + }, +]; + const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_MAX_WIDTH = 450; @@ -212,6 +231,24 @@ export function Sidebar() { className={styles.handle} onMouseDown={handleMouseDown} /> + +
+
{t("follow_us")}
+ + + {socials.map((item) => { + return ( + + ); + })} + +
); } diff --git a/src/renderer/declaration.d.ts b/src/renderer/declaration.d.ts index 3db1a7c0..cceed299 100644 --- a/src/renderer/declaration.d.ts +++ b/src/renderer/declaration.d.ts @@ -1,12 +1,12 @@ import type { - CatalogueEntry, - GameShop, - Game, CatalogueCategory, - TorrentProgress, - ShopDetails, - UserPreferences, + CatalogueEntry, + Game, + GameShop, HowLongToBeatCategory, + ShopDetails, + TorrentProgress, + UserPreferences, } from "@types"; import type { DiskSpace } from "check-disk-space"; @@ -78,6 +78,7 @@ declare global { /* Misc */ getOrCacheImage: (url: string) => Promise; + openExternal: (src: string) => Promise; getVersion: () => Promise; ping: () => string; getDefaultDownloadsPath: () => Promise; diff --git a/src/renderer/pages/patch-notes/patch-notes-skeleton.tsx b/src/renderer/pages/patch-notes/patch-notes-skeleton.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/utils/format-bytes.ts b/src/renderer/utils/format-bytes.ts index 5aa39072..b052b43b 100644 --- a/src/renderer/utils/format-bytes.ts +++ b/src/renderer/utils/format-bytes.ts @@ -1,8 +1,8 @@ const FORMAT = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; export const formatBytes = (bytes: number): string => { - if (!Number.isFinite(bytes) || isNaN(bytes) || bytes < 0) { - return `N/A ${FORMAT[0]}`; + if (!Number.isFinite(bytes) || isNaN(bytes) || bytes <= 0) { + return `0 ${FORMAT[0]}`; } const byteKBase = 1024; diff --git a/torrent-client/setup.py b/torrent-client/setup.py new file mode 100644 index 00000000..e098ec80 --- /dev/null +++ b/torrent-client/setup.py @@ -0,0 +1,16 @@ +from cx_Freeze import setup, Executable + +# Dependencies are automatically detected, but it might need fine tuning. +build_exe_options = { + "packages": ["libtorrent"], + "build_exe": "resources/dist/hydra-download-manager", + "include_msvcr": True +} + +setup( + name="hydra-download-manager", + version="0.1", + description="Hydra Torrent Client", + options={"build_exe": build_exe_options}, + executables=[Executable("torrent-client/main.py", target_name="hydra-download-manager")] +)