diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cf640d53 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.env.example b/.env.example index 006859ba..7946dbc1 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ -STEAMGRIDDB_API_KEY=YOUR_API_KEY -ONLINEFIX_USERNAME=YOUR_ONLINEFIX_USERNAME -ONLINEFIX_PASSWORD=YOUR_ONLINEFIX_PASSWORD -SENTRY_DSN= +MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY +MAIN_VITE_ONLINEFIX_USERNAME=YOUR_USERNAME +MAIN_VITE_ONLINEFIX_PASSWORD=YOUR_PASSWORD +MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN +RENDERER_VITE_SENTRY_DSN=YOUR_SENTRY_DSN diff --git a/.eslintignore b/.eslintignore index 289e7a42..a6f34fea 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,4 @@ -postinstall.js +node_modules +dist +out +.gitignore diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..49aa25e2 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,22 @@ +module.exports = { + extends: [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "@electron-toolkit/eslint-config-ts/recommended", + "prettier", + ], + rules: { + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "warn", + }, +}; diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 442c001f..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - }, - settings: { - react: { - version: "detect", - }, - }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "prettier", - ], - overrides: [ - { - env: { - node: true, - }, - files: [".eslintrc.{js,cjs}"], - parserOptions: { - sourceType: "script", - }, - }, - ], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - }, - plugins: ["@typescript-eslint", "react"], - rules: { - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - "@typescript-eslint/no-explicit-any": "warn", - }, -}; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d26f3641..ea9180bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,27 +2,15 @@ name: Build on: push: - branches: "**" + branches: main 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, - }, - ] + os: [windows-latest, ubuntu-latest] - runs-on: ${{ matrix.os.name }} + runs-on: ${{ matrix.os }} steps: - name: Check out Git repository @@ -36,9 +24,6 @@ jobs: - name: Install dependencies run: yarn - - name: Lint - run: yarn lint - - name: Install Python uses: actions/setup-python@v5 with: @@ -50,18 +35,50 @@ jobs: - name: Build with cx_Freeze run: python torrent-client/setup.py build - - name: Publish - run: yarn run publish + - name: Build Linux + if: matrix.os == 'ubuntu-latest' + run: yarn build:linux env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }} + MAIN_VITE_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 }} + MAIN_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} + RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} + MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} + MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Windows + if: matrix.os == 'windows-latest' + run: yarn build:win + env: + MAIN_VITE_STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + MAIN_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} + RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} + MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} + MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create artifact uses: actions/upload-artifact@v4 with: - name: ${{ matrix.os.artifact }} - path: ${{ matrix.os.build_path }} + name: Build-${{ matrix.os }} + path: | + dist/*.exe + dist/*.zip + dist/*.dmg + dist/*.AppImage + dist/*.snap + dist/*.deb + dist/*.rpm + dist/*.tar.gz + dist/*.yml + dist/*.blockmap + + - name: VirusTotal Scan + uses: crazy-max/ghaction-virustotal@v4 + if: matrix.os == 'windows-latest' + with: + vt_api_key: ${{ secrets.VT_API_KEY }} + files: | + ./dist/*.exe diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 00000000..921eed6c --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,14 @@ +name: Contributors + +on: + push: + branches: main + +jobs: + contributors: + runs-on: ubuntu-latest + + steps: + - uses: akhilmhdh/contributors-readme-action@v2.3.8 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7b1c6317..743c12dc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,9 +2,7 @@ name: Lint on: push: - branches: - - "**" - - "!main" + branches: "**" jobs: lint: @@ -22,5 +20,8 @@ jobs: - name: Install dependencies run: yarn + - name: Typecheck + run: yarn typecheck + - name: Lint run: yarn lint diff --git a/.gitignore b/.gitignore index 0d0363c8..0767dd62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,108 +1,13 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock -.DS_Store - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# next.js build output -.next - -# nuxt.js build output -.nuxt - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# Webpack -.webpack/ - -# Vite -.vite/ - -# Electron-Forge -out/ - -.vscode/ - -.venv - -dev.db - +.vscode +node_modules +hydra-download-manager __pycache__ - -# pyinstaller -build/ -resources/dist/ -*.spec +dist +out +.DS_Store +*.log* +.env +.vite # Sentry Config File .env.sentry-build-plugin diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..9c6b791d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +out +dist +pnpm-lock.yaml +LICENSE.md +tsconfig.json +tsconfig.*.json diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 100% rename from .prettierrc.js rename to .prettierrc.cjs diff --git a/README.md b/README.md index 2ba817f3..ea6256e6 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,89 @@ yarn make ## Contributors - - - - -Made with [contrib.rocks](https://contrib.rocks). + + + + + + + + + + + + + + + +
+ + hydralauncher +
+ Hydra +
+
+ + zamitto +
+ Null +
+
+ + fzanutto +
+ Null +
+
+ + JackEnx +
+ Null +
+
+ + fhilipecrash +
+ Fhilipe Coelho +
+
+ + Magrid0 +
+ Magrid +
+
+ + ferivoq +
+ FeriVOQ +
+
+ + xbozo +
+ Guilherme Viana +
+
+ + pmenta +
+ João Martins +
+
+ + eltociear +
+ Ikko Eltociear Ashimine +
+
+ + Netflixyapp +
+ Netflixy +
+
+ ## License diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist new file mode 100644 index 00000000..38c887b2 --- /dev/null +++ b/build/entitlements.mac.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/build/icon.icns b/build/icon.icns new file mode 100644 index 00000000..28644aa9 Binary files /dev/null and b/build/icon.icns differ diff --git a/images/icon.ico b/build/icon.ico similarity index 100% rename from images/icon.ico rename to build/icon.ico diff --git a/images/icon.png b/build/icon.png similarity index 99% rename from images/icon.png rename to build/icon.png index ed07e153..865a96a2 100644 Binary files a/images/icon.png and b/build/icon.png differ diff --git a/electron-builder.yml b/electron-builder.yml new file mode 100644 index 00000000..9101b028 --- /dev/null +++ b/electron-builder.yml @@ -0,0 +1,48 @@ +appId: site.hydralauncher.hydra +productName: Hydra +directories: + buildResources: build +extraResources: + - hydra-download-manager + - hydra.db +files: + - "!**/.vscode/*" + - "!src/*" + - "!electron.vite.config.{js,ts,mjs,cjs}" + - "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}" + - "!{.env,.env.*,.npmrc,pnpm-lock.yaml}" + - "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}" +asarUnpack: + - resources/** +win: + executableName: Hydra + requestedExecutionLevel: requireAdministrator +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always +mac: + entitlementsInherit: build/entitlements.mac.plist + extendInfo: + - NSCameraUsageDescription: Application requests access to the device's camera. + - NSMicrophoneUsageDescription: Application requests access to the device's microphone. + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. + notarize: false +dmg: + artifactName: ${name}-${version}.${ext} +linux: + target: + - AppImage + - snap + - deb + maintainer: electronjs.org + category: Utility +appImage: + artifactName: ${name}-${version}.${ext} +npmRebuild: false +publish: + provider: github +electronDownload: + mirror: https://npmmirror.com/mirrors/electron/ diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 00000000..d98d0238 --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,66 @@ +import { resolve } from "path"; +import { + defineConfig, + loadEnv, + swcPlugin, + externalizeDepsPlugin, + bytecodePlugin, +} from "electron-vite"; +import react from "@vitejs/plugin-react"; +import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; +import { sentryVitePlugin } from "@sentry/vite-plugin"; +import svgr from "vite-plugin-svgr"; +export default defineConfig(({ mode }) => { + loadEnv(mode); + + const sentryPlugin = sentryVitePlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: "hydra-launcher", + project: "hydra-launcher", + }); + + return { + main: { + build: { + sourcemap: true, + rollupOptions: { + external: ["better-sqlite3"], + }, + }, + resolve: { + alias: { + "@main": resolve("src/main"), + "@locales": resolve("src/locales"), + "@resources": resolve("resources"), + }, + }, + plugins: [ + externalizeDepsPlugin(), + swcPlugin(), + bytecodePlugin(), + sentryPlugin, + ], + }, + preload: { + plugins: [externalizeDepsPlugin()], + }, + renderer: { + build: { + sourcemap: true, + }, + resolve: { + alias: { + "@renderer": resolve("src/renderer/src"), + "@locales": resolve("src/locales"), + }, + }, + plugins: [ + svgr(), + react(), + vanillaExtractPlugin(), + bytecodePlugin(), + sentryPlugin, + ], + }, + }; +}); diff --git a/forge.config.ts b/forge.config.ts deleted file mode 100644 index 94cfac57..00000000 --- a/forge.config.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ForgeConfig } from "@electron-forge/shared-types"; -import { MakerSquirrel } from "@electron-forge/maker-squirrel"; -import { MakerZIP } from "@electron-forge/maker-zip"; -import { MakerDeb } from "@electron-forge/maker-deb"; -import { MakerRpm } from "@electron-forge/maker-rpm"; -import { AutoUnpackNativesPlugin } from "@electron-forge/plugin-auto-unpack-natives"; -import { WebpackPlugin } from "@electron-forge/plugin-webpack"; -import { FusesPlugin } from "@electron-forge/plugin-fuses"; -import { PublisherGithub } from "@electron-forge/publisher-github"; -import { FuseV1Options, FuseVersion } from "@electron/fuses"; -import { ElectronegativityPlugin } from "@electron-forge/plugin-electronegativity"; - -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, - icon: "./images/icon.png", - executableName: "Hydra", - extraResource: [ - "./resources/hydra.db", - "./resources/icon_tray.png", - "./resources/dist", - ], - protocols: [ - { - name: "Hydra", - schemes: ["hydralauncher"], - }, - ], - win32metadata: { - "requested-execution-level": "requireAdministrator", - }, - }, - rebuildConfig: {}, - makers: [ - new MakerSquirrel({ - setupIcon: "./images/icon.ico", - }), - new MakerZIP({}, ["darwin", "linux"]), - new MakerRpm({ - options: linuxPkgConfig, - }), - new MakerDeb({ - options: linuxPkgConfig, - }), - ], - publishers: [ - new PublisherGithub({ - repository: { - owner: "hydralauncher", - name: "hydra", - }, - }), - ], - plugins: [ - new AutoUnpackNativesPlugin({}), - new WebpackPlugin({ - mainConfig, - devContentSecurityPolicy: "connect-src 'self' * 'unsafe-eval'", - renderer: { - config: rendererConfig, - entryPoints: [ - { - html: "./src/index.html", - js: "./src/renderer.ts", - name: "main_window", - preload: { - js: "./src/preload.ts", - }, - }, - ], - }, - }), - // Fuses are used to enable/disable various Electron functionality - // at package time, before code signing the application - new FusesPlugin({ - version: FuseVersion.V1, - [FuseV1Options.RunAsNode]: false, - [FuseV1Options.EnableCookieEncryption]: true, - [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, - [FuseV1Options.EnableNodeCliInspectArguments]: false, - [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, - [FuseV1Options.OnlyLoadAppFromAsar]: true, - }), - new ElectronegativityPlugin({ - isSarif: true, - }), - ], -}; - -export default config; diff --git a/hydra-launcher.desktop b/hydra-launcher.desktop deleted file mode 100644 index e9e10e91..00000000 --- a/hydra-launcher.desktop +++ /dev/null @@ -1,11 +0,0 @@ -[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/hydra.db b/hydra.db new file mode 100644 index 00000000..7db97249 Binary files /dev/null and b/hydra.db differ diff --git a/package.json b/package.json index 64021f00..84c47e42 100644 --- a/package.json +++ b/package.json @@ -1,112 +1,95 @@ { "name": "hydra", - "productName": "Hydra", - "version": "1.1.0", - "description": "No bullshit. Just play.", - "main": ".webpack/main", + "version": "1.1.1", + "description": "Hydra", + "main": "./out/main/index.js", + "author": "Los Broxas", + "homepage": "https://hydralauncher.site", "repository": { - "url": "https://github.com/hydralauncher/hydra" - }, - "author": { - "name": "Hydra", - "email": "hydra@hydralauncher.site" + "type": "git", + "url": "https://github.com/hydralauncher/hydra.git" }, + "type": "module", "scripts": { - "start": "electron-forge start", - "package": "electron-forge package", - "make": "electron-forge make", - "publish": "electron-forge publish", - "lint": "eslint .", - "format": "prettier . --write", - "postinstall": "node ./postinstall.js" + "format": "prettier --write .", + "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", + "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", + "typecheck": "npm run typecheck:node && npm run typecheck:web", + "start": "electron-vite preview", + "dev": "electron-vite dev", + "build": "npm run typecheck && electron-vite build", + "postinstall": "electron-builder install-app-deps && node ./postinstall.cjs", + "build:unpack": "npm run build && electron-builder --dir", + "build:win": "electron-vite build && electron-builder --win", + "build:mac": "electron-vite build && electron-builder --mac", + "build:linux": "electron-vite build && electron-builder --linux" }, - "devDependencies": { - "@electron-forge/cli": "^7.3.0", - "@electron-forge/maker-deb": "^7.3.0", - "@electron-forge/maker-rpm": "^7.3.0", - "@electron-forge/maker-squirrel": "^7.3.0", - "@electron-forge/maker-zip": "^7.3.0", - "@electron-forge/plugin-auto-unpack-natives": "^7.3.0", - "@electron-forge/plugin-electronegativity": "^7.3.0", - "@electron-forge/plugin-fuses": "^7.3.0", - "@electron-forge/plugin-webpack": "^7.3.0", - "@electron-forge/publisher-github": "^7.3.0", - "@electron/fuses": "^1.7.0", - "@sentry/webpack-plugin": "^2.16.1", - "@svgr/webpack": "^8.1.0", - "@types/color": "^3.0.6", - "@types/dotenv-webpack": "^7.0.7", - "@types/jsdom": "^21.1.6", - "@types/lodash": "^4.17.0", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@types/uuid": "^9.0.8", - "@types/webtorrent": "^0.109.8", - "@types/windows-1251": "^0.1.22", - "@typescript-eslint/eslint-plugin": "^7.3.1", - "@typescript-eslint/parser": "^7.3.1", - "@vanilla-extract/webpack-plugin": "^2.3.7", - "@vercel/webpack-asset-relocator-loader": "1.7.3", - "css-loader": "^6.0.0", - "dotenv-webpack": "^8.1.0", - "electron": "29.1.4", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^4.6.0", - "fork-ts-checker-webpack-plugin": "^7.2.13", - "node-loader": "^2.0.0", - "prettier": "^3.2.5", - "style-loader": "^3.0.0", - "ts-loader": "^9.2.2", - "ts-node": "^10.0.0", - "tsconfig-paths-webpack-plugin": "^4.1.0", - "typescript": "^5.4.3" - }, - "keywords": [], - "license": "MIT", "dependencies": { - "@fontsource/fira-mono": "^5.0.12", - "@fontsource/fira-sans": "^5.0.19", - "@primer/octicons-react": "^19.8.0", - "@reduxjs/toolkit": "^2.2.2", - "@sentry/electron": "^4.22.0", - "@sentry/react": "^7.110.1", "@types/node-fetch": "^2.6.11", - "@vanilla-extract/css": "^1.14.1", + "@electron-toolkit/preload": "^3.0.0", + "@electron-toolkit/utils": "^3.0.0", + "@fontsource/fira-mono": "^5.0.13", + "@fontsource/fira-sans": "^5.0.20", + "@primer/octicons-react": "^19.9.0", + "@reduxjs/toolkit": "^2.2.3", + "@sentry/electron": "^4.23.0", + "@sentry/react": "^7.111.0", + "@sentry/vite-plugin": "^2.16.1", + "@vanilla-extract/css": "^1.14.2", "@vanilla-extract/recipes": "^0.5.2", "axios": "^1.6.8", + "better-sqlite3": "^9.5.0", "check-disk-space": "^3.4.0", "classnames": "^2.5.1", - "color": "^4.2.3", "color.js": "^1.2.0", - "date-fns": "^3.5.0", "electron-dl-manager": "^3.0.0", - "electron-squirrel-startup": "^1.0.0", + "date-fns": "^3.6.0", "flexsearch": "^0.7.43", "got-scraping": "^4.0.5", - "i18next": "^23.10.1", - "i18next-browser-languagedetector": "^7.2.0", + "i18next": "^23.11.2", + "i18next-browser-languagedetector": "^7.2.1", "jsdom": "^24.0.0", - "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "lottie-react": "^2.4.0", "node-fetch": "^2.6.1", "node-unrar-js": "^2.0.2", - "parse-torrent": "9.1.5", + "parse-torrent": "^11.0.16", "ps-list": "^8.1.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", "react-i18next": "^14.1.0", "react-loading-skeleton": "^3.4.0", - "react-redux": "^9.1.0", + "react-redux": "^9.1.1", "react-router-dom": "^6.22.3", - "sqlite3": "^5.1.7", "tough-cookie": "^4.1.3", "typeorm": "^0.3.20", - "update-electron-app": "^3.0.0", - "uuid": "^9.0.1", "windows-1251": "^3.0.4", - "winston": "^3.12.0", + "winston": "^3.13.0", "yaml": "^2.4.1" + }, + "devDependencies": { + "@electron-toolkit/eslint-config-prettier": "^2.0.0", + "@electron-toolkit/eslint-config-ts": "^1.0.1", + "@electron-toolkit/tsconfig": "^1.0.1", + "@swc/core": "^1.4.16", + "@types/jsdom": "^21.1.6", + "@types/lodash-es": "^4.17.12", + "@types/node": "^20.12.7", + "@types/parse-torrent": "^5.8.7", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vanilla-extract/vite-plugin": "^4.0.7", + "@vitejs/plugin-react": "^4.2.1", + "electron": "^28.2.0", + "electron-builder": "^24.9.1", + "electron-vite": "^2.0.0", + "eslint": "^8.56.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^3.2.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.3.3", + "vite": "^5.0.12", + "vite-plugin-svgr": "^4.2.0" } } diff --git a/postinstall.cjs b/postinstall.cjs new file mode 100644 index 00000000..0b47b380 --- /dev/null +++ b/postinstall.cjs @@ -0,0 +1,12 @@ +const fs = require("fs"); + +if (process.platform === "win32") { + if (!fs.existsSync("resources/dist")) { + fs.mkdirSync("resources/dist"); + } + + fs.copyFileSync( + "node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe", + "resources/dist/fastlist.exe" + ); +} diff --git a/postinstall.js b/postinstall.js deleted file mode 100644 index 63b6399e..00000000 --- a/postinstall.js +++ /dev/null @@ -1,9 +0,0 @@ -const fs = require("fs") - -if (process.platform === "win32"){ - if (!fs.existsSync("resources/dist")) { - fs.mkdirSync("resources/dist") - } - - fs.copyFileSync("node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe", "resources/dist/fastlist.exe") -} diff --git a/resources/hydra.db b/resources/hydra.db index 7db97249..e69de29b 100644 Binary files a/resources/hydra.db 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/resources/icon_tray.png b/resources/icon_tray.png deleted file mode 100644 index d020c638..00000000 Binary files a/resources/icon_tray.png and /dev/null differ diff --git a/resources/tray-icon.png b/resources/tray-icon.png new file mode 100644 index 00000000..9254a8fb Binary files /dev/null and b/resources/tray-icon.png differ diff --git a/src/index.html b/src/index.html deleted file mode 100644 index 8e5e1a11..00000000 --- a/src/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Hydra - - -
- - diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 87e599b9..00000000 --- a/src/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { app, BrowserWindow } from "electron"; -import { init } from "@sentry/electron/main"; -import i18n from "i18next"; -import path from "node:path"; -import { resolveDatabaseUpdates, WindowManager } from "@main/services"; -import { updateElectronApp } from "update-electron-app"; -import { dataSource } from "@main/data-source"; -import * as resources from "@locales"; -import { userPreferencesRepository } from "@main/repository"; - -const gotTheLock = app.requestSingleInstanceLock(); -if (!gotTheLock) app.quit(); - -// Handle creating/removing shortcuts on Windows when installing/uninstalling. -if (require("electron-squirrel-startup")) app.quit(); - -if (process.platform !== "darwin") { - updateElectronApp(); -} - -if (process.env.SENTRY_DSN) { - init({ - dsn: process.env.SENTRY_DSN, - beforeSend: async (event) => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - - if (userPreferences?.telemetryEnabled) return event; - return null; - }, - }); -} - -i18n.init({ - resources, - lng: "en", - fallbackLng: "en", - interpolation: { - escapeValue: false, - }, -}); - -const PROTOCOL = "hydralauncher"; - -if (process.defaultApp) { - if (process.argv.length >= 2) { - app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [ - path.resolve(process.argv[1]), - ]); - } -} else { - app.setAsDefaultProtocolClient(PROTOCOL); -} - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.on("ready", () => { - dataSource.initialize().then(async () => { - await resolveDatabaseUpdates(); - - await import("./main"); - - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - - WindowManager.createMainWindow(); - WindowManager.createSystemTray(userPreferences?.language || "en"); - }); -}); - -app.on("second-instance", (_event, commandLine) => { - // Someone tried to run a second instance, we should focus our window. - if (WindowManager.mainWindow) { - if (WindowManager.mainWindow.isMinimized()) - WindowManager.mainWindow.restore(); - - WindowManager.mainWindow.focus(); - } else { - WindowManager.createMainWindow(); - } - - const [, path] = commandLine.pop().split("://"); - if (path) WindowManager.redirect(path); -}); - -app.on("open-url", (_event, url) => { - const [, path] = url.split("://"); - WindowManager.redirect(path); -}); - -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. -app.on("window-all-closed", () => { - WindowManager.mainWindow = null; -}); - -app.on("activate", () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) { - WindowManager.createMainWindow(); - } -}); - -// In this file you can include the rest of your app's specific main process -// code. You can also put them in separate files and import them here. diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 20043f54..03f00a0d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -82,7 +82,7 @@ "repacks_modal_description": "Choose the repack you want to download", "downloads_path": "Downloads path", "select_folder_hint": "To change the default folder, access the", - "hydra_settings": "Hydra settings", + "settings": "Hydra settings", "download_now": "Download now" }, "activation": { diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index c8501138..ad484825 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -16,8 +16,8 @@ "paused": "{{title}} (Pausado)", "downloading": "{{title}} ({{percentage}} - Descargando…)", "filter": "Filtrar biblioteca", - "home": "Hogar", - "follow_us": "Síganos" + "home": "Inicio", + "follow_us": "Síguenos" }, "header": { "search": "Buscar", @@ -25,7 +25,7 @@ "downloads": "Descargas", "search_results": "Resultados de búsqueda", "settings": "Ajustes", - "home": "Início" + "home": "Inicio" }, "bottom_panel": { "no_downloads_in_progress": "Sin descargas en progreso", @@ -100,7 +100,7 @@ "checking_files": "Verificando archivos…", "starting_download": "Iniciando descarga…", "remove_from_list": "Eliminar", - "delete": "Quitar instalador", + "delete": "Eliminar instalador", "delete_modal_description": "Esto eliminará todos los archivos de instalación de su computadora.", "delete_modal_title": "¿Está seguro?", "deleting": "Eliminando instalador…", @@ -112,8 +112,8 @@ "notifications": "Notificaciones", "enable_download_notifications": "Cuando se completa una descarga", "enable_repack_list_notifications": "Cuando se añade un repack nuevo", - "telemetry": "Telemetria", - "telemetry_description": "Habilitar estadísticas de uso anónimas" + "telemetry": "Telemetría", + "telemetry_description": "Habilitar recopilación de datos de manera anónima" }, "notifications": { "download_complete": "Descarga completada", @@ -132,7 +132,7 @@ "binary_not_found_modal": { "title": "Programas no instalados", "description": "Los ejecutables de Wine o Lutris no se encontraron en su sistema", - "instructions": "Comprueba la forma correcta de instalar cualquiera de ellos en tu distro Linux para que el juego pueda ejecutarse con normalidad" + "instructions": "Comprueba como instalar de forma correcta uno de los dos en tu distro de Linux para ejecutar el juego con normalidad" }, "catalogue": { "next_page": "Siguiente página", diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 087068ac..901574a1 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -1,147 +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" - } -} +{ + "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/pt/translation.json b/src/locales/pt/translation.json index 5733059d..270e0451 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -3,7 +3,7 @@ "featured": "Destaque", "recently_added": "Novidades", "trending": "Populares", - "surprise_me": "Me surpreenda", + "surprise_me": "Surpreenda-me", "no_results": "Nenhum resultado encontrado" }, "sidebar": { @@ -78,7 +78,7 @@ "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 ", - "hydra_settings": "Configurações do Hydra", + "settings": "Configurações do Hydra", "download_now": "Baixe agora" }, "activation": { diff --git a/src/main/data-source.ts b/src/main/data-source.ts index d1bfe65f..a2ca976c 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -15,7 +15,7 @@ import { databasePath } from "./constants"; export const createDataSource = (options: Partial) => new DataSource({ - type: "sqlite", + type: "better-sqlite3", database: databasePath, entities: [ Game, diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 60daeb64..c3621d5d 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -44,7 +44,7 @@ export class Game { shop: GameShop; @Column("text", { nullable: true }) - status: GameStatus | ""; + status: GameStatus | null; /** * Progress is a float between 0 and 1 diff --git a/src/main/events/catalogue/get-games.ts b/src/main/events/catalogue/get-games.ts index 64c491ae..b2766928 100644 --- a/src/main/events/catalogue/get-games.ts +++ b/src/main/events/catalogue/get-games.ts @@ -9,7 +9,7 @@ const steamGames = stateManager.getValue("steamGames"); const getGames = async ( _event: Electron.IpcMainInvokeEvent, - take?: number, + take = 12, cursor = 0 ): Promise<{ results: CatalogueEntry[]; cursor: number }> => { const results: CatalogueEntry[] = []; diff --git a/src/main/events/catalogue/get-random-game.ts b/src/main/events/catalogue/get-random-game.ts index 07a827dd..72f9cd90 100644 --- a/src/main/events/catalogue/get-random-game.ts +++ b/src/main/events/catalogue/get-random-game.ts @@ -1,27 +1,40 @@ -import shuffle from "lodash/shuffle"; +import { shuffle } from "lodash-es"; -import { getRandomSteam250List } from "@main/services"; +import { Steam250Game, getSteam250List } from "@main/services"; import { registerEvent } from "../register-event"; import { searchGames, searchRepacks } from "../helpers/search-games"; -import { formatName } from "@main/helpers"; + +const state = { games: Array(), index: 0 }; const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => { - return getRandomSteam250List().then(async (games) => { - const shuffledList = shuffle(games); + if (state.games.length == 0) { + const steam250List = await getSteam250List(); - for (const game of shuffledList) { - const repacks = searchRepacks(formatName(game.title)); + const filteredSteam250List = steam250List.filter((game) => { + const repacks = searchRepacks(game.title); + const catalogue = searchGames({ query: game.title }); - if (repacks.length) { - const results = await searchGames({ query: game.title }); + return repacks.length && catalogue.length; + }); - if (results.length) { - return results[0].objectID; - } - } - } - }); + state.games = shuffle(filteredSteam250List); + } + + if (state.games.length == 0) { + return ""; + } + + const resultObjectId = state.games[state.index].objectID; + + state.index += 1; + + if (state.index == state.games.length) { + state.index = 0; + state.games = shuffle(state.games); + } + + return resultObjectId; }; registerEvent(getRandomGame, { diff --git a/src/main/events/catalogue/search-games.ts b/src/main/events/catalogue/search-games.ts index eb9c0640..f0539562 100644 --- a/src/main/events/catalogue/search-games.ts +++ b/src/main/events/catalogue/search-games.ts @@ -1,11 +1,15 @@ import { registerEvent } from "../register-event"; import { searchGames } from "../helpers/search-games"; +import { CatalogueEntry } from "@types"; -registerEvent( - (_event: Electron.IpcMainInvokeEvent, query: string) => - searchGames({ query, take: 12 }), - { - name: "searchGames", - memoize: true, - } -); +const searchGamesEvent = async ( + _event: Electron.IpcMainInvokeEvent, + query: string +): Promise => { + return searchGames({ query, take: 12 }); +}; + +registerEvent(searchGamesEvent, { + name: "searchGames", + memoize: true, +}); diff --git a/src/main/events/helpers/search-games.ts b/src/main/events/helpers/search-games.ts index 5fb3cea0..f931b1d9 100644 --- a/src/main/events/helpers/search-games.ts +++ b/src/main/events/helpers/search-games.ts @@ -1,5 +1,5 @@ import flexSearch from "flexsearch"; -import orderBy from "lodash/orderBy"; +import { orderBy } from "lodash-es"; import type { GameRepack, GameShop, CatalogueEntry } from "@types"; @@ -42,11 +42,11 @@ export interface SearchGamesArgs { skip?: number; } -export const searchGames = async ({ +export const searchGames = ({ query, take, skip, -}: SearchGamesArgs): Promise => { +}: SearchGamesArgs): CatalogueEntry[] => { const results = steamGamesIndex .search(formatName(query || ""), { limit: take, offset: skip }) .map((index) => { @@ -61,11 +61,9 @@ export const searchGames = async ({ }; }); - return Promise.all(results).then((resultsWithRepacks) => - orderBy( - resultsWithRepacks, - [({ repacks }) => repacks.length, "repacks"], - ["desc"] - ) + return orderBy( + results, + [({ repacks }) => repacks.length, "repacks"], + ["desc"] ); }; diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 6148bd8a..4ce550c6 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -10,12 +10,13 @@ import "./catalogue/search-games"; import "./hardware/get-disk-free-space"; import "./library/add-game-to-library"; import "./library/close-game"; -import "./torrenting/delete-game-folder"; +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 "./library/remove-game-from-library"; import "./misc/get-or-cache-image"; import "./misc/open-external"; @@ -24,7 +25,6 @@ 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/close-game.ts b/src/main/events/library/close-game.ts index 0d556925..d549f3b7 100644 --- a/src/main/events/library/close-game.ts +++ b/src/main/events/library/close-game.ts @@ -1,9 +1,9 @@ import path from "node:path"; import { gameRepository } from "@main/repository"; +import { getProcesses } from "@main/helpers"; import { registerEvent } from "../register-event"; -import { getProcesses } from "@main/helpers"; const closeGame = async ( _event: Electron.IpcMainInvokeEvent, @@ -12,13 +12,17 @@ const closeGame = async ( const processes = await getProcesses(); const game = await gameRepository.findOne({ where: { id: gameId } }); - const gameProcess = processes.find((runningProcess) => { - const basename = path.win32.basename(game.executablePath); - const basenameWithoutExtension = path.win32.basename( - game.executablePath, - path.extname(game.executablePath) - ); + if (!game) return false; + const executablePath = game.executablePath!; + + const basename = path.win32.basename(executablePath); + const basenameWithoutExtension = path.win32.basename( + executablePath, + path.extname(executablePath) + ); + + const gameProcess = processes.find((runningProcess) => { if (process.platform === "win32") { return runningProcess.name === basename; } diff --git a/src/main/events/torrenting/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts similarity index 100% rename from src/main/events/torrenting/delete-game-folder.ts rename to src/main/events/library/delete-game-folder.ts diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index 047d848b..1e74ad81 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -2,10 +2,10 @@ import { gameRepository } from "@main/repository"; import { searchRepacks } from "../helpers/search-games"; import { registerEvent } from "../register-event"; -import sortBy from "lodash/sortBy"; import { GameStatus } from "@globals"; +import { sortBy } from "lodash-es"; -const getLibrary = async (_event: Electron.IpcMainInvokeEvent) => +const getLibrary = async () => gameRepository .find({ where: { 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/torrenting/remove-game-from-download.ts b/src/main/events/library/remove-game.ts similarity index 52% rename from src/main/events/torrenting/remove-game-from-download.ts rename to src/main/events/library/remove-game.ts index 2ca608ef..6e2ee785 100644 --- a/src/main/events/torrenting/remove-game-from-download.ts +++ b/src/main/events/library/remove-game.ts @@ -1,25 +1,16 @@ -import { GameStatus } from "@globals"; -import { gameRepository } from "@main/repository"; - import { registerEvent } from "../register-event"; +import { gameRepository } from "../../repository"; +import { GameStatus } from "@globals"; -const removeGameFromDownload = async ( +const removeGame = async ( _event: Electron.IpcMainInvokeEvent, gameId: number ) => { - const game = await gameRepository.findOne({ - where: { + await gameRepository.update( + { id: gameId, status: GameStatus.Cancelled, }, - }); - - if (!game) return; - - gameRepository.update( - { - id: game.id, - }, { status: null, downloadPath: null, @@ -29,6 +20,6 @@ const removeGameFromDownload = async ( ); }; -registerEvent(removeGameFromDownload, { - name: "removeGameFromDownload", +registerEvent(removeGame, { + name: "removeGame", }); diff --git a/src/main/events/torrenting/pause-game-download.ts b/src/main/events/torrenting/pause-game-download.ts index 6e728ede..263ec9be 100644 --- a/src/main/events/torrenting/pause-game-download.ts +++ b/src/main/events/torrenting/pause-game-download.ts @@ -25,7 +25,7 @@ const pauseGameDownload = async ( .then((result) => { if (result.affected) { Downloader.pauseDownload(); - WindowManager.mainWindow.setProgressBar(-1); + WindowManager.mainWindow?.setProgressBar(-1); } }); }; diff --git a/src/main/events/user-preferences/get-user-preferences.ts b/src/main/events/user-preferences/get-user-preferences.ts index 219713eb..03b4ae9d 100644 --- a/src/main/events/user-preferences/get-user-preferences.ts +++ b/src/main/events/user-preferences/get-user-preferences.ts @@ -1,7 +1,7 @@ import { userPreferencesRepository } from "@main/repository"; import { registerEvent } from "../register-event"; -const getUserPreferences = async (_event: Electron.IpcMainInvokeEvent) => +const getUserPreferences = async () => userPreferencesRepository.findOne({ where: { id: 1 }, }); diff --git a/src/main/index.ts b/src/main/index.ts index cc89a58e..d9aa1a43 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,52 +1,68 @@ -import { stateManager } from "./state-manager"; -import { repackers } from "./constants"; -import { - getNewGOGGames, - getNewRepacksFromCPG, - getNewRepacksFromUser, - getNewRepacksFromXatab, - // getNewRepacksFromOnlineFix, - readPipe, - startProcessWatcher, - writePipe, -} from "./services"; -import { - gameRepository, - repackRepository, - repackerFriendlyNameRepository, - steamGameRepository, - userPreferencesRepository, -} from "./repository"; -import { TorrentClient } from "./services/donwloaders/torrent-client"; -import { Repack } from "./entity"; -import { Notification } from "electron"; -import { t } from "i18next"; -import { In } from "typeorm"; -import { Downloader } from "./services/donwloaders/downloader"; -import { GameStatus } from "@globals"; +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"; +import { userPreferencesRepository } from "@main/repository"; -startProcessWatcher(); +const gotTheLock = app.requestSingleInstanceLock(); +if (!gotTheLock) app.quit(); -TorrentClient.startTorrentClient(writePipe.socketPath, readPipe.socketPath); +if (import.meta.env.MAIN_VITE_SENTRY_DSN) { + init({ + dsn: import.meta.env.MAIN_VITE_SENTRY_DSN, + beforeSend: async (event) => { + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); -Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then(async () => { - const game = await gameRepository.findOne({ - where: { - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), + if (userPreferences?.telemetryEnabled) return event; + return null; }, - relations: { repack: true }, }); +} - if (game) { - Downloader.downloadGame(game, game.repack); +i18n.init({ + resources, + lng: "en", + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, +}); + +const PROTOCOL = "hydralauncher"; + +if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [ + path.resolve(process.argv[1]), + ]); } +} else { + app.setAsDefaultProtocolClient(PROTOCOL); +} - readPipe.socket.on("data", (data) => { - TorrentClient.onSocketData(data); +// This method will be called when Electron has finished +// 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 import("./main"); + + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + WindowManager.createMainWindow(); + WindowManager.createSystemTray(userPreferences?.language || "en"); }); }); @@ -64,63 +80,49 @@ const checkForNewRepacks = async () => { where: { id: 1 }, }); - const existingRepacks = stateManager.getValue("repacks"); - - Promise.allSettled([ - getNewGOGGames( - existingRepacks.filter((repack) => repack.repacker === "GOG") - ), - getNewRepacksFromXatab( - existingRepacks.filter((repack) => repack.repacker === "Xatab") - ), - getNewRepacksFromCPG( - existingRepacks.filter((repack) => repack.repacker === "CPG") - ), - // getNewRepacksFromOnlineFix( - // existingRepacks.filter((repack) => repack.repacker === "onlinefix") - // ), - track1337xUsers(existingRepacks), - ]).then(() => { - repackRepository.count().then((count) => { - const total = count - stateManager.getValue("repacks").length; - - if (total > 0 && userPreferences?.repackUpdatesNotificationsEnabled) { - new Notification({ - title: t("repack_list_updated", { - ns: "notifications", - lng: userPreferences?.language || "en", - }), - body: t("repack_count", { - ns: "notifications", - lng: userPreferences?.language || "en", - count: total, - }), - }).show(); - } - }); + WindowManager.createMainWindow(); + WindowManager.createSystemTray(userPreferences?.language || "en"); }); -}; +}); -const loadState = async () => { - const [friendlyNames, repacks, steamGames] = await Promise.all([ - repackerFriendlyNameRepository.find(), - repackRepository.find({ - order: { - createdAt: "desc", - }, - }), - steamGameRepository.find({ - order: { - name: "asc", - }, - }), - ]); +app.on("browser-window-created", (_, window) => { + optimizer.watchWindowShortcuts(window); +}); - stateManager.setValue("repackersFriendlyNames", friendlyNames); - stateManager.setValue("repacks", repacks); - stateManager.setValue("steamGames", steamGames); +app.on("second-instance", (_event, commandLine) => { + // Someone tried to run a second instance, we should focus our window. + if (WindowManager.mainWindow) { + if (WindowManager.mainWindow.isMinimized()) + WindowManager.mainWindow.restore(); - import("./events"); -}; + WindowManager.mainWindow.focus(); + } else { + WindowManager.createMainWindow(); + } -loadState().then(() => checkForNewRepacks()); + const [, path] = commandLine.pop().split("://"); + if (path) WindowManager.redirect(path); +}); + +app.on("open-url", (_event, url) => { + const [, path] = url.split("://"); + WindowManager.redirect(path); +}); + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on("window-all-closed", () => { + WindowManager.mainWindow = null; +}); + +app.on("activate", () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) { + WindowManager.createMainWindow(); + } +}); + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and import them here. diff --git a/src/main/main.ts b/src/main/main.ts new file mode 100644 index 00000000..c95be117 --- /dev/null +++ b/src/main/main.ts @@ -0,0 +1,129 @@ +import { stateManager } from "./state-manager"; +import { GameStatus, repackers } from "./constants"; +import { + getNewGOGGames, + getNewRepacksFromCPG, + getNewRepacksFromUser, + getNewRepacksFromXatab, + // getNewRepacksFromOnlineFix, + readPipe, + startProcessWatcher, + writePipe, +} from "./services"; +import { + gameRepository, + repackRepository, + repackerFriendlyNameRepository, + steamGameRepository, + userPreferencesRepository, +} from "./repository"; +import { TorrentClient } from "./services/torrent-client"; +import { Repack } from "./entity"; +import { Notification } from "electron"; +import { t } from "i18next"; +import { In } from "typeorm"; + +startProcessWatcher(); + +TorrentClient.startTorrentClient(writePipe.socketPath, readPipe.socketPath); + +Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then(async () => { + const game = await gameRepository.findOne({ + where: { + status: In([ + GameStatus.Downloading, + GameStatus.DownloadingMetadata, + GameStatus.CheckingFiles, + ]), + }, + relations: { repack: true }, + }); + + if (game) { + writePipe.write({ + action: "start", + game_id: game.id, + magnet: game.repack.magnet, + save_path: game.downloadPath, + }); + } + + readPipe.socket?.on("data", (data) => { + TorrentClient.onSocketData(data); + }); +}); + +const track1337xUsers = async (existingRepacks: Repack[]) => { + for (const repacker of repackers) { + await getNewRepacksFromUser( + repacker, + existingRepacks.filter((repack) => repack.repacker === repacker) + ); + } +}; + +const checkForNewRepacks = async () => { + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + const existingRepacks = stateManager.getValue("repacks"); + + Promise.allSettled([ + getNewGOGGames( + existingRepacks.filter((repack) => repack.repacker === "GOG") + ), + getNewRepacksFromXatab( + existingRepacks.filter((repack) => repack.repacker === "Xatab") + ), + getNewRepacksFromCPG( + existingRepacks.filter((repack) => repack.repacker === "CPG") + ), + // getNewRepacksFromOnlineFix( + // existingRepacks.filter((repack) => repack.repacker === "onlinefix") + // ), + track1337xUsers(existingRepacks), + ]).then(() => { + repackRepository.count().then((count) => { + const total = count - stateManager.getValue("repacks").length; + + if (total > 0 && userPreferences?.repackUpdatesNotificationsEnabled) { + new Notification({ + title: t("repack_list_updated", { + ns: "notifications", + lng: userPreferences?.language || "en", + }), + body: t("repack_count", { + ns: "notifications", + lng: userPreferences?.language || "en", + count: total, + }), + }).show(); + } + }); + }); +}; + +const loadState = async () => { + const [friendlyNames, repacks, steamGames] = await Promise.all([ + repackerFriendlyNameRepository.find(), + repackRepository.find({ + order: { + createdAt: "desc", + }, + }), + steamGameRepository.find({ + order: { + name: "asc", + }, + }), + ]); + + stateManager.setValue("repackersFriendlyNames", friendlyNames); + stateManager.setValue("repacks", repacks); + stateManager.setValue("steamGames", steamGames); + + import("./events"); +}; + +loadState().then(() => checkForNewRepacks()); diff --git a/src/main/services/donwloaders/torrent-client.ts b/src/main/services/donwloaders/torrent-client.ts index 8e48bbbd..d76564d0 100644 --- a/src/main/services/donwloaders/torrent-client.ts +++ b/src/main/services/donwloaders/torrent-client.ts @@ -46,10 +46,9 @@ export class TorrentClient { const commonArgs = [BITTORRENT_PORT, writePipePath, readPipePath]; if (app.isPackaged) { - const binaryName = binaryNameByPlatform[process.platform]; + const binaryName = binaryNameByPlatform[process.platform]!; const binaryPath = path.join( process.resourcesPath, - "dist", "hydra-download-manager", binaryName ); diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 0d699165..fc81e8d3 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -28,10 +28,11 @@ export const startProcessWatcher = async () => { const processes = await getProcesses(); for (const game of games) { - const basename = path.win32.basename(game.executablePath); + const executablePath = game.executablePath!; + const basename = path.win32.basename(executablePath); const basenameWithoutExtension = path.win32.basename( - game.executablePath, - path.extname(game.executablePath) + executablePath, + path.extname(executablePath) ); const gameProcess = processes.find((runningProcess) => { @@ -46,7 +47,7 @@ export const startProcessWatcher = async () => { if (gameProcess) { if (gamesPlaytime.has(game.id)) { - const zero = gamesPlaytime.get(game.id); + const zero = gamesPlaytime.get(game.id) ?? 0; const delta = performance.now() - zero; if (WindowManager.mainWindow) { diff --git a/src/main/services/repack-tracker/1337x.ts b/src/main/services/repack-tracker/1337x.ts index 1cbafa3b..8573079b 100644 --- a/src/main/services/repack-tracker/1337x.ts +++ b/src/main/services/repack-tracker/1337x.ts @@ -4,7 +4,6 @@ import { formatUploadDate } from "@main/helpers"; import { Repack } from "@main/entity"; import { requestWebPage, savePage } from "./helpers"; -import type { GameRepackInput } from "./helpers"; export const request1337x = async (path: string) => requestWebPage(`https://1337xx.to${path}`); @@ -68,7 +67,7 @@ export const extractTorrentsFromDocument = async ( user: string, document: Document, existingRepacks: Repack[] = [] -): Promise => { +) => { const $trs = Array.from(document.querySelectorAll("tbody tr")); return Promise.all( @@ -108,7 +107,7 @@ export const getNewRepacksFromUser = async ( user: string, existingRepacks: Repack[], page = 1 -): Promise => { +) => { const response = await request1337x(`/user/${user}/${page}`); const { window } = new JSDOM(response); diff --git a/src/main/services/repack-tracker/cpg-repacks.ts b/src/main/services/repack-tracker/cpg-repacks.ts index 0d7c172b..2b939d08 100644 --- a/src/main/services/repack-tracker/cpg-repacks.ts +++ b/src/main/services/repack-tracker/cpg-repacks.ts @@ -3,7 +3,6 @@ import { JSDOM } from "jsdom"; import { Repack } from "@main/entity"; import { requestWebPage, savePage } from "./helpers"; -import type { GameRepackInput } from "./helpers"; import { logger } from "../logger"; export const getNewRepacksFromCPG = async ( @@ -14,22 +13,22 @@ export const getNewRepacksFromCPG = async ( const { window } = new JSDOM(data); - const repacks: GameRepackInput[] = []; + const repacks = []; try { Array.from(window.document.querySelectorAll(".post")).forEach(($post) => { const $title = $post.querySelector(".entry-title"); - const uploadDate = $post.querySelector("time").getAttribute("datetime"); + const uploadDate = $post.querySelector("time")?.getAttribute("datetime"); const $downloadInfo = Array.from( $post.querySelectorAll(".wp-block-heading") - ).find(($heading) => $heading.textContent.startsWith("Download")); + ).find(($heading) => $heading.textContent?.startsWith("Download")); /* Side note: CPG often misspells "Magnet" as "Magent" */ const $magnet = Array.from($post.querySelectorAll("a")).find( ($a) => - $a.textContent.startsWith("Magnet") || - $a.textContent.startsWith("Magent") + $a.textContent?.startsWith("Magnet") || + $a.textContent?.startsWith("Magent") ); const fileSize = $downloadInfo.textContent diff --git a/src/main/services/repack-tracker/gog.ts b/src/main/services/repack-tracker/gog.ts index 73daebf7..00c78e36 100644 --- a/src/main/services/repack-tracker/gog.ts +++ b/src/main/services/repack-tracker/gog.ts @@ -1,7 +1,8 @@ import { JSDOM, VirtualConsole } from "jsdom"; -import { GameRepackInput, requestWebPage, savePage } from "./helpers"; +import { requestWebPage, savePage } from "./helpers"; import { Repack } from "@main/entity"; -import { logger } from "../logger"; + +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; const virtualConsole = new VirtualConsole(); @@ -36,43 +37,35 @@ const getGOGGame = async (url: string) => { }; export const getNewGOGGames = async (existingRepacks: Repack[] = []) => { - try { - const data = await requestWebPage( - "https://freegogpcgames.com/a-z-games-list/" - ); + const data = await requestWebPage( + "https://freegogpcgames.com/a-z-games-list/" + ); - const { window } = new JSDOM(data, { virtualConsole }); + const { window } = new JSDOM(data, { virtualConsole }); - const $uls = Array.from(window.document.querySelectorAll(".az-columns")); + const $uls = Array.from(window.document.querySelectorAll(".az-columns")); - for (const $ul of $uls) { - const repacks: GameRepackInput[] = []; - const $lis = Array.from($ul.querySelectorAll("li")); + for (const $ul of $uls) { + const repacks: QueryDeepPartialEntity[] = []; + const $lis = Array.from($ul.querySelectorAll("li")); - for (const $li of $lis) { - const $a = $li.querySelector("a"); - const href = $a.href; + for (const $li of $lis) { + const $a = $li.querySelector("a"); + const href = $a.href; - const title = $a.textContent.trim(); + const title = $a.textContent.trim(); - const gameExists = existingRepacks.some( - (existingRepack) => existingRepack.title === title - ); + const gameExists = existingRepacks.some( + (existingRepack) => existingRepack.title === title + ); - if (!gameExists) { - try { - const game = await getGOGGame(href); + if (!gameExists) { + const game = await getGOGGame(href); - repacks.push({ ...game, title }); - } catch (err) { - logger.error(err.message, { method: "getGOGGame", url: href }); - } - } + repacks.push({ ...game, title }); } - - if (repacks.length) await savePage(repacks); } - } catch (err) { - logger.error(err.message, { method: "getNewGOGGames" }); + + if (repacks.length) await savePage(repacks); } }; diff --git a/src/main/services/repack-tracker/helpers.ts b/src/main/services/repack-tracker/helpers.ts index f30bf355..fd2dd7c5 100644 --- a/src/main/services/repack-tracker/helpers.ts +++ b/src/main/services/repack-tracker/helpers.ts @@ -1,13 +1,9 @@ +import type { Repack } from "@main/entity"; import { repackRepository } from "@main/repository"; -import type { GameRepack } from "@types"; +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; -export type GameRepackInput = Omit< - GameRepack, - "id" | "repackerFriendlyName" | "createdAt" | "updatedAt" ->; - -export const savePage = async (repacks: GameRepackInput[]) => +export const savePage = async (repacks: QueryDeepPartialEntity[]) => Promise.all( repacks.map((repack) => repackRepository.insert(repack).catch(() => {})) ); diff --git a/src/main/services/repack-tracker/online-fix.ts b/src/main/services/repack-tracker/online-fix.ts index fdcd1513..c627eccb 100644 --- a/src/main/services/repack-tracker/online-fix.ts +++ b/src/main/services/repack-tracker/online-fix.ts @@ -1,6 +1,5 @@ import { Repack } from "@main/entity"; import { savePage } from "./helpers"; -import type { GameRepackInput } from "./helpers"; import { logger } from "../logger"; import parseTorrent, { toMagnetURI, @@ -21,7 +20,8 @@ export const getNewRepacksFromOnlineFix = async ( cookieJar = new CookieJar() ): Promise => { const hasCredentials = - process.env.ONLINEFIX_USERNAME && process.env.ONLINEFIX_PASSWORD; + import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME && + import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD; if (!hasCredentials) return; const http = gotScraping.extend({ @@ -58,8 +58,8 @@ export const getNewRepacksFromOnlineFix = async ( if (!preLogin.field || !preLogin.value) return; const params = new URLSearchParams({ - login_name: process.env.ONLINEFIX_USERNAME, - login_password: process.env.ONLINEFIX_PASSWORD, + login_name: import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME, + login_password: import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD, login: "submit", [preLogin.field]: preLogin.value, }); @@ -84,10 +84,10 @@ export const getNewRepacksFromOnlineFix = async ( }); const document = new JSDOM(home.body).window.document; - const repacks: GameRepackInput[] = []; + const repacks = []; const articles = Array.from(document.querySelectorAll(".news")); const totalPages = Number( - document.querySelector("nav > a:nth-child(13)").textContent + document.querySelector("nav > a:nth-child(13)")?.textContent ); try { @@ -185,8 +185,10 @@ export const getNewRepacksFromOnlineFix = async ( }); }) ); - } catch (err) { - logger.error(err.message, { method: "getNewRepacksFromOnlineFix" }); + } catch (err: unknown) { + logger.error((err as Error).message, { + method: "getNewRepacksFromOnlineFix", + }); } const newRepacks = repacks.filter( diff --git a/src/main/services/repack-tracker/xatab.ts b/src/main/services/repack-tracker/xatab.ts index 91a0a4c4..de9f5285 100644 --- a/src/main/services/repack-tracker/xatab.ts +++ b/src/main/services/repack-tracker/xatab.ts @@ -1,16 +1,14 @@ import { JSDOM } from "jsdom"; -import parseTorrent, { toMagnetURI } from "parse-torrent"; - import { Repack } from "@main/entity"; import { logger } from "../logger"; import { requestWebPage, savePage } from "./helpers"; -import type { GameRepackInput } from "./helpers"; -const getTorrentBuffer = (url: string) => - fetch(url, { method: "GET" }).then((response) => - response.arrayBuffer().then((buffer) => Buffer.from(buffer)) - ); +import createWorker from "@main/workers/torrent-parser.worker?nodeWorker"; +import { toMagnetURI } from "parse-torrent"; +import type { Instance } from "parse-torrent"; + +const worker = createWorker({}); const formatXatabDate = (str: string) => { const date = new Date(); @@ -28,28 +26,36 @@ const formatXatabDate = (str: string) => { const formatXatabDownloadSize = (str: string) => str.replace(",", ".").replace(/Гб/g, "GB").replace(/Мб/g, "MB"); -const getXatabRepack = async (url: string) => { - const data = await requestWebPage(url); - const { window } = new JSDOM(data); +const getXatabRepack = (url: string) => { + return new Promise((resolve) => { + (async () => { + const data = await requestWebPage(url); + const { window } = new JSDOM(data); + const { document } = window; - const $uploadDate = window.document.querySelector(".entry__date"); - const $size = window.document.querySelector(".entry__info-size"); + const $uploadDate = document.querySelector(".entry__date"); + const $size = document.querySelector(".entry__info-size"); - const $downloadButton = window.document.querySelector( - ".download-torrent" - ) as HTMLAnchorElement; + const $downloadButton = document.querySelector( + ".download-torrent" + ) as HTMLAnchorElement; - if (!$downloadButton) throw new Error("Download button not found"); + if (!$downloadButton) throw new Error("Download button not found"); - const torrentBuffer = await getTorrentBuffer($downloadButton.href); + const onMessage = (torrent: Instance) => { + resolve({ + fileSize: formatXatabDownloadSize($size.textContent).toUpperCase(), + magnet: toMagnetURI(torrent), + uploadDate: formatXatabDate($uploadDate.textContent), + }); - return { - fileSize: formatXatabDownloadSize($size.textContent).toUpperCase(), - magnet: toMagnetURI({ - infoHash: parseTorrent(torrentBuffer).infoHash, - }), - uploadDate: formatXatabDate($uploadDate.textContent), - }; + worker.removeListener("message", onMessage); + }; + + worker.on("message", onMessage); + worker.postMessage($downloadButton.href); + })(); + }); }; export const getNewRepacksFromXatab = async ( @@ -60,7 +66,7 @@ export const getNewRepacksFromXatab = async ( const { window } = new JSDOM(data); - const repacks: GameRepackInput[] = []; + const repacks = []; for (const $a of Array.from( window.document.querySelectorAll(".entry__title a") @@ -74,14 +80,15 @@ export const getNewRepacksFromXatab = async ( ...repack, page, }); - } catch (err) { - logger.error(err.message, { method: "getNewRepacksFromXatab" }); + } catch (err: unknown) { + logger.error((err as Error).message, { + method: "getNewRepacksFromXatab", + }); } } const newRepacks = repacks.filter( (repack) => - repack.uploadDate && !existingRepacks.some( (existingRepack) => existingRepack.title === repack.title ) diff --git a/src/main/services/steam-250.ts b/src/main/services/steam-250.ts index 6447c226..0ece8ae5 100644 --- a/src/main/services/steam-250.ts +++ b/src/main/services/steam-250.ts @@ -1,24 +1,31 @@ import axios from "axios"; import { JSDOM } from "jsdom"; -import shuffle from "lodash/shuffle"; + +export interface Steam250Game { + title: string; + objectID: string; +} export const requestSteam250 = async (path: string) => { - return axios.get(`https://steam250.com${path}`).then((response) => { - const { window } = new JSDOM(response.data); - const { document } = window; + return axios + .get(`https://steam250.com${path}`) + .then((response) => { + const { window } = new JSDOM(response.data); + const { document } = window; - return Array.from(document.querySelectorAll(".appline .title a")).map( - ($title: HTMLAnchorElement) => { - const steamGameUrl = $title.href; - if (!steamGameUrl) return null; + return Array.from(document.querySelectorAll(".appline .title a")) + .map(($title) => { + const steamGameUrl = ($title as HTMLAnchorElement).href; + if (!steamGameUrl) return null; - return { - title: $title.textContent, - objectID: steamGameUrl.split("/").pop(), - }; - } - ); - }); + return { + title: $title.textContent, + objectID: steamGameUrl.split("/").pop(), + } as Steam250Game; + }) + .filter((game) => game != null); + }) + .catch((_) => [] as Steam250Game[]); }; const steam250Paths = [ @@ -28,7 +35,15 @@ const steam250Paths = [ "/most_played", ]; -export const getRandomSteam250List = async () => { - const [path] = shuffle(steam250Paths); - return requestSteam250(path); +export const getSteam250List = async () => { + const gamesList = ( + await Promise.all(steam250Paths.map((path) => requestSteam250(path))) + ).flat(); + + const gamesMap: Map = gamesList.reduce((map, item) => { + map.set(item.objectID, item); + return map; + }, new Map()); + + return [...gamesMap.values()]; }; diff --git a/src/main/services/steam-grid.ts b/src/main/services/steam-grid.ts index 5b7d2c3f..9e2ce9d8 100644 --- a/src/main/services/steam-grid.ts +++ b/src/main/services/steam-grid.ts @@ -32,7 +32,7 @@ export const getSteamGridData = async ( { method: "GET", headers: { - Authorization: `Bearer ${process.env.STEAMGRIDDB_API_KEY}`, + Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`, }, } ); diff --git a/src/main/services/update-resolver.ts b/src/main/services/update-resolver.ts index 535659df..ef9a5ade 100644 --- a/src/main/services/update-resolver.ts +++ b/src/main/services/update-resolver.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { app } from "electron"; -import chunk from "lodash/chunk"; +import { chunk } from "lodash-es"; import { createDataSource, dataSource } from "@main/data-source"; import { Repack, RepackerFriendlyName, SteamGame } from "@main/entity"; @@ -109,7 +109,7 @@ export const resolveDatabaseUpdates = async () => { const updateDataSource = createDataSource({ database: app.isPackaged ? path.join(process.resourcesPath, "hydra.db") - : path.join(__dirname, "..", "..", "resources", "hydra.db"), + : path.join(__dirname, "..", "..", "hydra.db"), }); return updateDataSource.initialize().then(async () => { diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 5e6b11d2..05cb95d6 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -1,16 +1,30 @@ import { BrowserWindow, Menu, Tray, app } from "electron"; +import { is } from "@electron-toolkit/utils"; import { t } from "i18next"; import path from "node:path"; - -// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack -// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on -// whether you're running in development or production). -declare const MAIN_WINDOW_WEBPACK_ENTRY: string; -declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; +import icon from "@resources/icon.png?asset"; +import trayIcon from "@resources/tray-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({ @@ -19,7 +33,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,40 +41,29 @@ export class WindowManager { height: 34, }, webPreferences: { - preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, + preload: path.join(__dirname, "../preload/index.mjs"), + sandbox: false, }, }); + this.loadURL(); this.mainWindow.removeMenu(); - this.mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); - - 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); + 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(); + 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([ { @@ -93,10 +96,10 @@ export class WindowManager { if (process.platform === "win32") { tray.addListener("click", () => { if (this.mainWindow) { - if (WindowManager.mainWindow.isMinimized()) + if (WindowManager.mainWindow?.isMinimized()) WindowManager.mainWindow.restore(); - WindowManager.mainWindow.focus(); + WindowManager.mainWindow?.focus(); return; } diff --git a/src/main/vite-env.d.ts b/src/main/vite-env.d.ts new file mode 100644 index 00000000..2c277d37 --- /dev/null +++ b/src/main/vite-env.d.ts @@ -0,0 +1,12 @@ +/// + +interface ImportMetaEnv { + readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string; + readonly MAIN_VITE_ONLINEFIX_USERNAME: string; + readonly MAIN_VITE_ONLINEFIX_PASSWORD: string; + readonly MAIN_VITE_SENTRY_DSN: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/src/main/workers/torrent-parser.worker.ts b/src/main/workers/torrent-parser.worker.ts new file mode 100644 index 00000000..7502fd5f --- /dev/null +++ b/src/main/workers/torrent-parser.worker.ts @@ -0,0 +1,17 @@ +import { parentPort } from "worker_threads"; +import parseTorrent from "parse-torrent"; + +const port = parentPort; +if (!port) throw new Error("IllegalState"); + +const getTorrentBuffer = (url: string) => + fetch(url, { method: "GET" }).then((response) => + response.arrayBuffer().then((buffer) => Buffer.from(buffer)) + ); + +port.on("message", async (url: string) => { + const buffer = await getTorrentBuffer(url); + const torrent = await parseTorrent(buffer); + + port.postMessage(torrent); +}); diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts new file mode 100644 index 00000000..4ca7b2fb --- /dev/null +++ b/src/preload/index.d.ts @@ -0,0 +1,105 @@ +// See the Electron documentation for details on how to use preload scripts: +// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts +import { contextBridge, ipcRenderer } from "electron"; + +import type { + CatalogueCategory, + GameShop, + TorrentProgress, + UserPreferences, +} from "@types"; + +contextBridge.exposeInMainWorld("electron", { + /* Torrenting */ + startGameDownload: ( + repackId: number, + objectID: string, + title: string, + shop: GameShop + ) => ipcRenderer.invoke("startGameDownload", repackId, objectID, title, shop), + cancelGameDownload: (gameId: number) => + ipcRenderer.invoke("cancelGameDownload", gameId), + pauseGameDownload: (gameId: number) => + ipcRenderer.invoke("pauseGameDownload", gameId), + resumeGameDownload: (gameId: number) => + ipcRenderer.invoke("resumeGameDownload", gameId), + onDownloadProgress: (cb: (value: TorrentProgress) => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + value: TorrentProgress + ) => cb(value); + ipcRenderer.on("on-download-progress", listener); + return () => ipcRenderer.removeListener("on-download-progress", listener); + }, + + /* Catalogue */ + searchGames: (query: string) => ipcRenderer.invoke("searchGames", query), + getCatalogue: (category: CatalogueCategory) => + ipcRenderer.invoke("getCatalogue", category), + getGameShopDetails: (objectID: string, shop: GameShop, language: string) => + ipcRenderer.invoke("getGameShopDetails", objectID, shop, language), + getRandomGame: () => ipcRenderer.invoke("getRandomGame"), + getHowLongToBeat: (objectID: string, shop: GameShop, title: string) => + ipcRenderer.invoke("getHowLongToBeat", objectID, shop, title), + getGames: (take?: number, prevCursor?: number) => + ipcRenderer.invoke("getGames", take, prevCursor), + + /* User preferences */ + getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"), + updateUserPreferences: (preferences: UserPreferences) => + ipcRenderer.invoke("updateUserPreferences", preferences), + + /* Library */ + addGameToLibrary: ( + objectID: string, + title: string, + shop: GameShop, + executablePath: string + ) => + ipcRenderer.invoke( + "addGameToLibrary", + objectID, + title, + shop, + executablePath + ), + getLibrary: () => ipcRenderer.invoke("getLibrary"), + getRepackersFriendlyNames: () => + ipcRenderer.invoke("getRepackersFriendlyNames"), + openGameInstaller: (gameId: number) => + ipcRenderer.invoke("openGameInstaller", gameId), + openGame: (gameId: number, executablePath: string) => + ipcRenderer.invoke("openGame", gameId, executablePath), + closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId), + removeGameFromLibrary: (gameId: number) => + ipcRenderer.invoke("removeGameFromLibrary", gameId), + deleteGameFolder: (gameId: number) => + ipcRenderer.invoke("deleteGameFolder", gameId), + getGameByObjectID: (objectID: string) => + ipcRenderer.invoke("getGameByObjectID", objectID), + onPlaytime: (cb: (gameId: number) => void) => { + const listener = (_event: Electron.IpcRendererEvent, gameId: number) => + cb(gameId); + ipcRenderer.on("on-playtime", listener); + return () => ipcRenderer.removeListener("on-playtime", listener); + }, + onGameClose: (cb: (gameId: number) => void) => { + const listener = (_event: Electron.IpcRendererEvent, gameId: number) => + cb(gameId); + ipcRenderer.on("on-game-close", listener); + return () => ipcRenderer.removeListener("on-game-close", listener); + }, + + /* Hardware */ + getDiskFreeSpace: () => ipcRenderer.invoke("getDiskFreeSpace"), + + /* Misc */ + getOrCacheImage: (url: string) => ipcRenderer.invoke("getOrCacheImage", url), + 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/preload.ts b/src/preload/index.ts similarity index 97% rename from src/preload.ts rename to src/preload/index.ts index 6250083f..c4f8ca96 100644 --- a/src/preload.ts +++ b/src/preload/index.ts @@ -82,8 +82,6 @@ contextBridge.exposeInMainWorld("electron", { closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId), removeGameFromLibrary: (gameId: number) => ipcRenderer.invoke("removeGameFromLibrary", gameId), - removeGameFromDownload: (gameId: number) => - ipcRenderer.invoke("removeGameFromDownload", gameId), deleteGameFolder: (gameId: number) => ipcRenderer.invoke("deleteGameFolder", gameId), getGameByObjectID: (objectID: string) => diff --git a/src/renderer.ts b/src/renderer.ts deleted file mode 100644 index e259266e..00000000 --- a/src/renderer.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This file will automatically be loaded by vite and run in the "renderer" context. - * To learn more about the differences between the "main" and the "renderer" context in - * Electron, visit: - * - * https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes - * - * By default, Node.js integration in this file is disabled. When enabling Node.js integration - * in a renderer process, please be aware of potential security implications. You can read - * more about security risks here: - * - * https://electronjs.org/docs/tutorial/security - * - * To enable Node.js integration in this file, open up `main.ts` and enable the `nodeIntegration` - * flag: - * - * ``` - * // Create the browser window. - * mainWindow = new BrowserWindow({ - * width: 800, - * height: 600, - * webPreferences: { - * nodeIntegration: true - * } - * }); - * ``` - */ - -import "./renderer/main"; diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 00000000..1917de45 --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,16 @@ + + + + + + Hydra + + + +
+ + + diff --git a/src/renderer/pages/patch-notes/patch-notes-skeleton.tsx b/src/renderer/pages/patch-notes/patch-notes-skeleton.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/renderer/app.css.ts b/src/renderer/src/app.css.ts similarity index 100% rename from src/renderer/app.css.ts rename to src/renderer/src/app.css.ts diff --git a/src/renderer/app.tsx b/src/renderer/src/app.tsx similarity index 95% rename from src/renderer/app.tsx rename to src/renderer/src/app.tsx index 4e633bcf..6461c0d0 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/src/app.tsx @@ -12,7 +12,7 @@ import { import * as styles from "./app.css"; import { themeClass } from "./theme.css"; -import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { setSearch, clearSearch, @@ -22,7 +22,7 @@ import { document.body.classList.add(themeClass); -export function App() { +export function App({ children }: any) { const contentRef = useRef(null); const { updateLibrary } = useLibrary(); @@ -112,7 +112,7 @@ export function App() { />
- + {children}
diff --git a/src/renderer/assets/discord-icon.svg b/src/renderer/src/assets/discord-icon.svg similarity index 100% rename from src/renderer/assets/discord-icon.svg rename to src/renderer/src/assets/discord-icon.svg diff --git a/src/renderer/assets/epic-games-logo.svg b/src/renderer/src/assets/epic-games-logo.svg similarity index 100% rename from src/renderer/assets/epic-games-logo.svg rename to src/renderer/src/assets/epic-games-logo.svg diff --git a/src/renderer/assets/lottie/downloading.json b/src/renderer/src/assets/lottie/downloading.json similarity index 100% rename from src/renderer/assets/lottie/downloading.json rename to src/renderer/src/assets/lottie/downloading.json diff --git a/src/renderer/assets/lottie/settings.json b/src/renderer/src/assets/lottie/settings.json similarity index 100% rename from src/renderer/assets/lottie/settings.json rename to src/renderer/src/assets/lottie/settings.json diff --git a/src/renderer/assets/lottie/stars.json b/src/renderer/src/assets/lottie/stars.json similarity index 100% rename from src/renderer/assets/lottie/stars.json rename to src/renderer/src/assets/lottie/stars.json diff --git a/src/renderer/assets/steam-logo.svg b/src/renderer/src/assets/steam-logo.svg similarity index 100% rename from src/renderer/assets/steam-logo.svg rename to src/renderer/src/assets/steam-logo.svg diff --git a/src/renderer/assets/x-icon.svg b/src/renderer/src/assets/x-icon.svg similarity index 100% rename from src/renderer/assets/x-icon.svg rename to src/renderer/src/assets/x-icon.svg diff --git a/src/renderer/components/async-image/async-image.tsx b/src/renderer/src/components/async-image/async-image.tsx similarity index 94% rename from src/renderer/components/async-image/async-image.tsx rename to src/renderer/src/components/async-image/async-image.tsx index 857a9942..e3c0ee45 100644 --- a/src/renderer/components/async-image/async-image.tsx +++ b/src/renderer/src/components/async-image/async-image.tsx @@ -25,3 +25,5 @@ export const AsyncImage = forwardRef( return ; } ); + +AsyncImage.displayName = "AsyncImage"; diff --git a/src/renderer/components/bottom-panel/bottom-panel.css.ts b/src/renderer/src/components/bottom-panel/bottom-panel.css.ts similarity index 100% rename from src/renderer/components/bottom-panel/bottom-panel.css.ts rename to src/renderer/src/components/bottom-panel/bottom-panel.css.ts diff --git a/src/renderer/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx similarity index 93% rename from src/renderer/components/bottom-panel/bottom-panel.tsx rename to src/renderer/src/components/bottom-panel/bottom-panel.tsx index 46e278ca..8a9cc090 100644 --- a/src/renderer/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { useDownload } from "@renderer/hooks"; import * as styles from "./bottom-panel.css"; -import { vars } from "@renderer/theme.css"; +import { vars } from "../../theme.css"; import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { VERSION_CODENAME } from "@renderer/constants"; @@ -23,7 +23,7 @@ export function BottomPanel() { }, []); const status = useMemo(() => { - if (isDownloading) { + if (isDownloading && game) { if (game.status === GameStatus.DownloadingMetadata) return t("downloading_metadata", { title: game.title }); @@ -62,7 +62,7 @@ export function BottomPanel() { - v{version} "{VERSION_CODENAME}" + v{version} "{VERSION_CODENAME}" ); diff --git a/src/renderer/components/button/button.css.ts b/src/renderer/src/components/button/button.css.ts similarity index 100% rename from src/renderer/components/button/button.css.ts rename to src/renderer/src/components/button/button.css.ts diff --git a/src/renderer/components/button/button.tsx b/src/renderer/src/components/button/button.tsx similarity index 100% rename from src/renderer/components/button/button.tsx rename to src/renderer/src/components/button/button.tsx diff --git a/src/renderer/components/checkbox-field/checkbox-field.css.ts b/src/renderer/src/components/checkbox-field/checkbox-field.css.ts similarity index 93% rename from src/renderer/components/checkbox-field/checkbox-field.css.ts rename to src/renderer/src/components/checkbox-field/checkbox-field.css.ts index 951d9e90..2b7cb77c 100644 --- a/src/renderer/components/checkbox-field/checkbox-field.css.ts +++ b/src/renderer/src/components/checkbox-field/checkbox-field.css.ts @@ -1,4 +1,4 @@ -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "../../theme.css"; import { style } from "@vanilla-extract/css"; export const checkboxField = style({ diff --git a/src/renderer/components/checkbox-field/checkbox-field.tsx b/src/renderer/src/components/checkbox-field/checkbox-field.tsx similarity index 100% rename from src/renderer/components/checkbox-field/checkbox-field.tsx rename to src/renderer/src/components/checkbox-field/checkbox-field.tsx diff --git a/src/renderer/components/game-card/game-card.css.ts b/src/renderer/src/components/game-card/game-card.css.ts similarity index 100% rename from src/renderer/components/game-card/game-card.css.ts rename to src/renderer/src/components/game-card/game-card.css.ts diff --git a/src/renderer/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx similarity index 94% rename from src/renderer/components/game-card/game-card.tsx rename to src/renderer/src/components/game-card/game-card.tsx index d3e9cb8d..ce9789c8 100644 --- a/src/renderer/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -1,8 +1,8 @@ import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react"; import type { CatalogueEntry } from "@types"; -import SteamLogo from "@renderer/assets/steam-logo.svg"; -import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg"; +import SteamLogo from "@renderer/assets/steam-logo.svg?react"; +import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg?react"; import { AsyncImage } from "../async-image/async-image"; diff --git a/src/renderer/components/header/header.css.ts b/src/renderer/src/components/header/header.css.ts similarity index 98% rename from src/renderer/components/header/header.css.ts rename to src/renderer/src/components/header/header.css.ts index 1a9d5073..eb95dc6e 100644 --- a/src/renderer/components/header/header.css.ts +++ b/src/renderer/src/components/header/header.css.ts @@ -2,7 +2,7 @@ import type { ComplexStyleRule } from "@vanilla-extract/css"; import { keyframes, style } from "@vanilla-extract/css"; import { recipe } from "@vanilla-extract/recipes"; -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "../../theme.css"; export const slideIn = keyframes({ "0%": { transform: "translateX(20px)", opacity: "0" }, diff --git a/src/renderer/components/header/header.tsx b/src/renderer/src/components/header/header.tsx similarity index 100% rename from src/renderer/components/header/header.tsx rename to src/renderer/src/components/header/header.tsx diff --git a/src/renderer/components/hero/hero.css.ts b/src/renderer/src/components/hero/hero.css.ts similarity index 89% rename from src/renderer/components/hero/hero.css.ts rename to src/renderer/src/components/hero/hero.css.ts index 34d3fa76..3c9ec81c 100644 --- a/src/renderer/components/hero/hero.css.ts +++ b/src/renderer/src/components/hero/hero.css.ts @@ -1,5 +1,5 @@ import { style } from "@vanilla-extract/css"; -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "../../theme.css"; export const hero = style({ width: "100%", @@ -13,11 +13,6 @@ export const hero = style({ cursor: "pointer", border: `solid 1px ${vars.color.borderColor}`, zIndex: "1", - "@media": { - "(min-width: 1250px)": { - backgroundPosition: "center", - }, - }, }); export const heroMedia = style({ diff --git a/src/renderer/components/hero/hero.tsx b/src/renderer/src/components/hero/hero.tsx similarity index 92% rename from src/renderer/components/hero/hero.tsx rename to src/renderer/src/components/hero/hero.tsx index 4e766ca8..9eadbd74 100644 --- a/src/renderer/components/hero/hero.tsx +++ b/src/renderer/src/components/hero/hero.tsx @@ -6,7 +6,7 @@ import { ShopDetails } from "@types"; import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers"; import { useTranslation } from "react-i18next"; -const FEATURED_GAME_ID = "377160"; +const FEATURED_GAME_ID = "253230"; export function Hero() { const [featuredGameDetails, setFeaturedGameDetails] = @@ -36,7 +36,7 @@ export function Hero() { >
diff --git a/src/renderer/components/index.ts b/src/renderer/src/components/index.ts similarity index 100% rename from src/renderer/components/index.ts rename to src/renderer/src/components/index.ts diff --git a/src/renderer/components/modal/modal.css.ts b/src/renderer/src/components/modal/modal.css.ts similarity index 100% rename from src/renderer/components/modal/modal.css.ts rename to src/renderer/src/components/modal/modal.css.ts diff --git a/src/renderer/components/modal/modal.tsx b/src/renderer/src/components/modal/modal.tsx similarity index 72% rename from src/renderer/components/modal/modal.tsx rename to src/renderer/src/components/modal/modal.tsx index 9b5f8cf5..b8b4e7ef 100644 --- a/src/renderer/components/modal/modal.tsx +++ b/src/renderer/src/components/modal/modal.tsx @@ -41,6 +41,7 @@ export function Modal({ const isTopMostModal = () => { const openModals = document.querySelectorAll("[role=modal]"); + return ( openModals.length && openModals[openModals.length - 1] === modalContentRef.current @@ -48,32 +49,37 @@ export function Modal({ }; useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape" && isTopMostModal()) { - handleCloseClick(); - } - }; + if (visible) { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && isTopMostModal()) { + handleCloseClick(); + } + }; - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [handleCloseClick]); + const onMouseDown = (e: MouseEvent) => { + if (!isTopMostModal()) return; + if (modalContentRef.current) { + const clickedWithinModal = modalContentRef.current.contains( + e.target as Node + ); - useEffect(() => { - const onMouseDown = (e: MouseEvent) => { - if (!isTopMostModal()) return; + if (!clickedWithinModal) { + handleCloseClick(); + } + } + }; - const clickedOutsideContent = !modalContentRef.current.contains( - e.target as Node - ); + window.addEventListener("keydown", onKeyDown); + window.addEventListener("mousedown", onMouseDown); - if (clickedOutsideContent) { - handleCloseClick(); - } - }; + return () => { + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("mousedown", onMouseDown); + }; + } - window.addEventListener("mousedown", onMouseDown); - return () => window.removeEventListener("mousedown", onMouseDown); - }, [handleCloseClick]); + return () => {}; + }, [handleCloseClick, visible]); useEffect(() => { dispatch(toggleDragging(visible)); diff --git a/src/renderer/components/sidebar/download-icon.tsx b/src/renderer/src/components/sidebar/download-icon.tsx similarity index 100% rename from src/renderer/components/sidebar/download-icon.tsx rename to src/renderer/src/components/sidebar/download-icon.tsx diff --git a/src/renderer/components/sidebar/routes.tsx b/src/renderer/src/components/sidebar/routes.tsx similarity index 100% rename from src/renderer/components/sidebar/routes.tsx rename to src/renderer/src/components/sidebar/routes.tsx diff --git a/src/renderer/components/sidebar/sidebar.css.ts b/src/renderer/src/components/sidebar/sidebar.css.ts similarity index 100% rename from src/renderer/components/sidebar/sidebar.css.ts rename to src/renderer/src/components/sidebar/sidebar.css.ts diff --git a/src/renderer/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx similarity index 97% rename from src/renderer/components/sidebar/sidebar.tsx rename to src/renderer/src/components/sidebar/sidebar.tsx index cd4aff92..ab4ea26b 100644 --- a/src/renderer/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -6,13 +6,14 @@ import type { Game } from "@types"; import { AsyncImage, TextField } from "@renderer/components"; import { useDownload, useLibrary } from "@renderer/hooks"; -import { SPACING_UNIT } from "@renderer/theme.css"; +import { SPACING_UNIT } from "../../theme.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 DiscordLogo from "@renderer/assets/discord-icon.svg?react"; +import XLogo from "@renderer/assets/x-icon.svg?react"; + import * as styles from "./sidebar.css"; import { GameStatus } from "@globals"; diff --git a/src/renderer/components/text-field/text-field.css.ts b/src/renderer/src/components/text-field/text-field.css.ts similarity index 95% rename from src/renderer/components/text-field/text-field.css.ts rename to src/renderer/src/components/text-field/text-field.css.ts index 4b21b38d..d38230ef 100644 --- a/src/renderer/components/text-field/text-field.css.ts +++ b/src/renderer/src/components/text-field/text-field.css.ts @@ -1,4 +1,4 @@ -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "../../theme.css"; import { style } from "@vanilla-extract/css"; import { recipe } from "@vanilla-extract/recipes"; diff --git a/src/renderer/components/text-field/text-field.tsx b/src/renderer/src/components/text-field/text-field.tsx similarity index 92% rename from src/renderer/components/text-field/text-field.tsx rename to src/renderer/src/components/text-field/text-field.tsx index 62378615..3b86e290 100644 --- a/src/renderer/components/text-field/text-field.tsx +++ b/src/renderer/src/components/text-field/text-field.tsx @@ -7,7 +7,7 @@ export interface TextFieldProps React.InputHTMLAttributes, HTMLInputElement > { - theme?: RecipeVariants["theme"]; + theme?: NonNullable>["theme"]; label?: string; } diff --git a/src/renderer/constants.ts b/src/renderer/src/constants.ts similarity index 100% rename from src/renderer/constants.ts rename to src/renderer/src/constants.ts diff --git a/src/renderer/declaration.d.ts b/src/renderer/src/declaration.d.ts similarity index 97% rename from src/renderer/declaration.d.ts rename to src/renderer/src/declaration.d.ts index 541408bf..661253d0 100644 --- a/src/renderer/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -64,7 +64,6 @@ declare global { openGame: (gameId: number, executablePath: string) => Promise; closeGame: (gameId: number) => Promise; removeGameFromLibrary: (gameId: number) => Promise; - removeGameFromDownload: (gameId: number) => Promise; deleteGameFolder: (gameId: number) => Promise; getGameByObjectID: (objectID: string) => Promise; onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer; diff --git a/src/renderer/features/download-slice.ts b/src/renderer/src/features/download-slice.ts similarity index 100% rename from src/renderer/features/download-slice.ts rename to src/renderer/src/features/download-slice.ts diff --git a/src/renderer/features/index.ts b/src/renderer/src/features/index.ts similarity index 100% rename from src/renderer/features/index.ts rename to src/renderer/src/features/index.ts diff --git a/src/renderer/features/library-slice.ts b/src/renderer/src/features/library-slice.ts similarity index 100% rename from src/renderer/features/library-slice.ts rename to src/renderer/src/features/library-slice.ts diff --git a/src/renderer/features/repackers-friendly-names-slice.ts b/src/renderer/src/features/repackers-friendly-names-slice.ts similarity index 100% rename from src/renderer/features/repackers-friendly-names-slice.ts rename to src/renderer/src/features/repackers-friendly-names-slice.ts diff --git a/src/renderer/features/search-slice.ts b/src/renderer/src/features/search-slice.ts similarity index 100% rename from src/renderer/features/search-slice.ts rename to src/renderer/src/features/search-slice.ts diff --git a/src/renderer/features/use-preferences-slice.ts b/src/renderer/src/features/use-preferences-slice.ts similarity index 83% rename from src/renderer/features/use-preferences-slice.ts rename to src/renderer/src/features/use-preferences-slice.ts index f6a3cf65..d735e7a2 100644 --- a/src/renderer/features/use-preferences-slice.ts +++ b/src/renderer/src/features/use-preferences-slice.ts @@ -14,7 +14,10 @@ export const userPreferencesSlice = createSlice({ name: "userPreferences", initialState, reducers: { - setUserPreferences: (state, action: PayloadAction) => { + setUserPreferences: ( + state, + action: PayloadAction + ) => { state.value = action.payload; }, }, diff --git a/src/renderer/features/window-slice.ts b/src/renderer/src/features/window-slice.ts similarity index 100% rename from src/renderer/features/window-slice.ts rename to src/renderer/src/features/window-slice.ts diff --git a/src/renderer/helpers.ts b/src/renderer/src/helpers.ts similarity index 100% rename from src/renderer/helpers.ts rename to src/renderer/src/helpers.ts diff --git a/src/renderer/hooks/index.ts b/src/renderer/src/hooks/index.ts similarity index 100% rename from src/renderer/hooks/index.ts rename to src/renderer/src/hooks/index.ts diff --git a/src/renderer/hooks/redux.ts b/src/renderer/src/hooks/redux.ts similarity index 100% rename from src/renderer/hooks/redux.ts rename to src/renderer/src/hooks/redux.ts diff --git a/src/renderer/hooks/use-date.ts b/src/renderer/src/hooks/use-date.ts similarity index 100% rename from src/renderer/hooks/use-date.ts rename to src/renderer/src/hooks/use-date.ts diff --git a/src/renderer/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts similarity index 94% rename from src/renderer/hooks/use-download.ts rename to src/renderer/src/hooks/use-download.ts index e11a3fef..092b352f 100644 --- a/src/renderer/hooks/use-download.ts +++ b/src/renderer/src/hooks/use-download.ts @@ -59,15 +59,15 @@ export function useDownload() { deleteGame(gameId); }); - const removeGameFromDownload = (gameId: number) => - window.electron.removeGameFromDownload(gameId).then(() => { + const removeGameFromLibrary = (gameId: number) => + window.electron.removeGameFromLibrary(gameId).then(() => { updateLibrary(); }); const isVerifying = GameStatus.isVerifying(lastPacket?.game.status); const getETA = () => { - if (isVerifying || !isFinite(lastPacket?.timeRemaining)) { + if (isVerifying || !isFinite(lastPacket?.timeRemaining ?? 0)) { return ""; } @@ -125,7 +125,7 @@ export function useDownload() { pauseDownload, resumeDownload, cancelDownload, - removeGameFromDownload, + removeGameFromLibrary, deleteGame, isGameDeleting, clearDownload: () => dispatch(clearDownload()), diff --git a/src/renderer/hooks/use-library.ts b/src/renderer/src/hooks/use-library.ts similarity index 69% rename from src/renderer/hooks/use-library.ts rename to src/renderer/src/hooks/use-library.ts index 076b91a7..f7310df0 100644 --- a/src/renderer/hooks/use-library.ts +++ b/src/renderer/src/hooks/use-library.ts @@ -12,10 +12,5 @@ export function useLibrary() { .then((updatedLibrary) => dispatch(setLibrary(updatedLibrary))); }, [dispatch]); - const removeGameFromLibrary = (gameId: number) => - window.electron.removeGameFromLibrary(gameId).then(() => { - updateLibrary(); - }); - - return { library, updateLibrary, removeGameFromLibrary }; + return { library, updateLibrary }; } diff --git a/src/renderer/main.tsx b/src/renderer/src/main.tsx similarity index 67% rename from src/renderer/main.tsx rename to src/renderer/src/main.tsx index 956e9b6c..d9b7821e 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/src/main.tsx @@ -4,7 +4,7 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import { Provider } from "react-redux"; import LanguageDetector from "i18next-browser-languagedetector"; -import { createHashRouter, RouterProvider } from "react-router-dom"; +import { HashRouter, Route, Routes } from "react-router-dom"; import { init } from "@sentry/electron/renderer"; import { init as reactInit } from "@sentry/react"; @@ -31,10 +31,10 @@ import { store } from "./store"; import * as resources from "@locales"; -if (process.env.SENTRY_DSN) { +if (import.meta.env.RENDERER_VITE_SENTRY_DSN) { init( { - dsn: process.env.SENTRY_DSN, + dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN, beforeSend: async (event) => { const userPreferences = await window.electron.getUserPreferences(); @@ -46,39 +46,6 @@ if (process.env.SENTRY_DSN) { ); } -const router = createHashRouter([ - { - path: "/", - Component: App, - children: [ - { - path: "/", - Component: Home, - }, - { - path: "/catalogue", - Component: Catalogue, - }, - { - path: "/downloads", - Component: Downloads, - }, - { - path: "/game/:shop/:objectID", - Component: GameDetails, - }, - { - path: "/search", - Component: SearchResults, - }, - { - path: "/settings", - Component: Settings, - }, - ], - }, -]); - i18n .use(LanguageDetector) .use(initReactI18next) @@ -96,7 +63,18 @@ i18n ReactDOM.createRoot(document.getElementById("root")!).render( - + + + + + + + + + + + + ); diff --git a/src/renderer/pages/catalogue/catalogue.tsx b/src/renderer/src/pages/catalogue/catalogue.tsx similarity index 98% rename from src/renderer/pages/catalogue/catalogue.tsx rename to src/renderer/src/pages/catalogue/catalogue.tsx index fb3c620d..8dc5fc56 100644 --- a/src/renderer/pages/catalogue/catalogue.tsx +++ b/src/renderer/src/pages/catalogue/catalogue.tsx @@ -6,7 +6,7 @@ import type { CatalogueEntry } from "@types"; import { clearSearch } from "@renderer/features"; import { useAppDispatch } from "@renderer/hooks"; -import { vars } from "@renderer/theme.css"; +import { vars } from "../../theme.css"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import * as styles from "../home/home.css"; diff --git a/src/renderer/pages/downloads/delete-modal.css.ts b/src/renderer/src/pages/downloads/delete-modal.css.ts similarity index 80% rename from src/renderer/pages/downloads/delete-modal.css.ts rename to src/renderer/src/pages/downloads/delete-modal.css.ts index 4d04c7a2..ef0ba179 100644 --- a/src/renderer/pages/downloads/delete-modal.css.ts +++ b/src/renderer/src/pages/downloads/delete-modal.css.ts @@ -1,4 +1,4 @@ -import { SPACING_UNIT } from "@renderer/theme.css"; +import { SPACING_UNIT } from "../../theme.css"; import { style } from "@vanilla-extract/css"; export const deleteActionsButtonsCtn = style({ diff --git a/src/renderer/pages/downloads/delete-modal.tsx b/src/renderer/src/pages/downloads/delete-modal.tsx similarity index 100% rename from src/renderer/pages/downloads/delete-modal.tsx rename to src/renderer/src/pages/downloads/delete-modal.tsx diff --git a/src/renderer/pages/downloads/downloads.css.ts b/src/renderer/src/pages/downloads/downloads.css.ts similarity index 97% rename from src/renderer/pages/downloads/downloads.css.ts rename to src/renderer/src/pages/downloads/downloads.css.ts index 9026b16b..fd68de81 100644 --- a/src/renderer/pages/downloads/downloads.css.ts +++ b/src/renderer/src/pages/downloads/downloads.css.ts @@ -1,4 +1,4 @@ -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "../../theme.css"; import { style } from "@vanilla-extract/css"; import { recipe } from "@vanilla-extract/recipes"; diff --git a/src/renderer/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx similarity index 97% rename from src/renderer/pages/downloads/downloads.tsx rename to src/renderer/src/pages/downloads/downloads.tsx index 7c33178a..4300d23e 100644 --- a/src/renderer/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -34,6 +34,7 @@ export function Downloads() { numSeeds, pauseDownload, resumeDownload, + removeGameFromLibrary, cancelDownload, deleteGame, isGameDeleting, @@ -53,11 +54,6 @@ export function Downloads() { updateLibrary(); }); - const removeGameFromDownload = (gameId: number) => - window.electron.removeGameFromDownload(gameId).then(() => { - updateLibrary(); - }); - const getFinalDownloadSize = (game: Game) => { const isGameDownloading = isDownloading && gameDownloading?.id === game?.id; @@ -195,7 +191,7 @@ export function Downloads() {
@@ -281,6 +274,7 @@ export function GameDetails() { className={styles.randomizerButton} onClick={handleRandomizerClick} theme="outline" + disabled={isLoadingRandomGame} >