Merge branch 'main' of https://github.com/hydralauncher/hydra
9
.editorconfig
Normal file
@ -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
|
@ -1,4 +1,5 @@
|
|||||||
STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
||||||
ONLINEFIX_USERNAME=YOUR_ONLINEFIX_USERNAME
|
MAIN_VITE_ONLINEFIX_USERNAME=YOUR_USERNAME
|
||||||
ONLINEFIX_PASSWORD=YOUR_ONLINEFIX_PASSWORD
|
MAIN_VITE_ONLINEFIX_PASSWORD=YOUR_PASSWORD
|
||||||
SENTRY_DSN=
|
MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN
|
||||||
|
RENDERER_VITE_SENTRY_DSN=YOUR_SENTRY_DSN
|
||||||
|
@ -1 +1,4 @@
|
|||||||
postinstall.js
|
node_modules
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
.gitignore
|
||||||
|
22
.eslintrc.cjs
Normal file
@ -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",
|
||||||
|
},
|
||||||
|
};
|
46
.eslintrc.js
@ -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",
|
|
||||||
},
|
|
||||||
};
|
|
71
.github/workflows/build.yml
vendored
@ -2,27 +2,15 @@ name: Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: "**"
|
branches: main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os: [windows-latest, ubuntu-latest]
|
||||||
[
|
|
||||||
{
|
|
||||||
name: windows-latest,
|
|
||||||
build_path: out/Hydra-win32-x64,
|
|
||||||
artifact: Hydra-win32-x64,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: ubuntu-latest,
|
|
||||||
build_path: out/Hydra-linux-x64,
|
|
||||||
artifact: Hydra-linux-x64,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os.name }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
@ -36,9 +24,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
run: yarn lint
|
|
||||||
|
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@ -50,18 +35,50 @@ jobs:
|
|||||||
- name: Build with cx_Freeze
|
- name: Build with cx_Freeze
|
||||||
run: python torrent-client/setup.py build
|
run: python torrent-client/setup.py build
|
||||||
|
|
||||||
- name: Publish
|
- name: Build Linux
|
||||||
run: yarn run publish
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: yarn build:linux
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
MAIN_VITE_STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }}
|
||||||
STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }}
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||||
ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
|
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||||
ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
|
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
|
- name: Create artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os.artifact }}
|
name: Build-${{ matrix.os }}
|
||||||
path: ${{ matrix.os.build_path }}
|
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
|
||||||
|
14
.github/workflows/contributors.yml
vendored
Normal file
@ -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 }}
|
7
.github/workflows/lint.yml
vendored
@ -2,9 +2,7 @@ name: Lint
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: "**"
|
||||||
- "**"
|
|
||||||
- "!main"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@ -22,5 +20,8 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: yarn typecheck
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
113
.gitignore
vendored
@ -1,108 +1,13 @@
|
|||||||
# Logs
|
.vscode
|
||||||
logs
|
node_modules
|
||||||
*.log
|
hydra-download-manager
|
||||||
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
|
|
||||||
|
|
||||||
__pycache__
|
__pycache__
|
||||||
|
dist
|
||||||
# pyinstaller
|
out
|
||||||
build/
|
.DS_Store
|
||||||
resources/dist/
|
*.log*
|
||||||
*.spec
|
.env
|
||||||
|
.vite
|
||||||
|
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.env.sentry-build-plugin
|
.env.sentry-build-plugin
|
||||||
|
6
.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
out
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
LICENSE.md
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.*.json
|
88
README.md
@ -81,11 +81,89 @@ yarn make
|
|||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
|
<!-- readme: contributors -start -->
|
||||||
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
|
<table>
|
||||||
</a>
|
<tr>
|
||||||
|
<td align="center">
|
||||||
Made with [contrib.rocks](https://contrib.rocks).
|
<a href="https://github.com/hydralauncher">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/164102380?v=4" width="100;" alt="hydralauncher"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Hydra</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/zamitto">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/167933696?v=4" width="100;" alt="zamitto"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Null</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/fzanutto">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/15229294?v=4" width="100;" alt="fzanutto"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Null</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/JackEnx">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/167036558?v=4" width="100;" alt="JackEnx"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Null</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/fhilipecrash">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/36455575?v=4" width="100;" alt="fhilipecrash"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Fhilipe Coelho</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Magrid0">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/73496008?v=4" width="100;" alt="Magrid0"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Magrid</b></sub>
|
||||||
|
</a>
|
||||||
|
</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ferivoq">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/36544651?v=4" width="100;" alt="ferivoq"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>FeriVOQ</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/xbozo">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/119091492?v=4" width="100;" alt="xbozo"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Guilherme Viana</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/pmenta">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/71457671?v=4" width="100;" alt="pmenta"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>João Martins</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/eltociear">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Ikko Eltociear Ashimine</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Netflixyapp">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/91623880?v=4" width="100;" alt="Netflixyapp"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Netflixy</b></sub>
|
||||||
|
</a>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
<!-- readme: contributors -end -->
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
12
build/entitlements.mac.plist
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
BIN
build/icon.icns
Normal file
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
48
electron-builder.yml
Normal file
@ -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/
|
66
electron.vite.config.ts
Normal file
@ -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,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
103
forge.config.ts
@ -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;
|
|
@ -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
|
|
147
package.json
@ -1,112 +1,95 @@
|
|||||||
{
|
{
|
||||||
"name": "hydra",
|
"name": "hydra",
|
||||||
"productName": "Hydra",
|
"version": "1.1.1",
|
||||||
"version": "1.1.0",
|
"description": "Hydra",
|
||||||
"description": "No bullshit. Just play.",
|
"main": "./out/main/index.js",
|
||||||
"main": ".webpack/main",
|
"author": "Los Broxas",
|
||||||
|
"homepage": "https://hydralauncher.site",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://github.com/hydralauncher/hydra"
|
"type": "git",
|
||||||
},
|
"url": "https://github.com/hydralauncher/hydra.git"
|
||||||
"author": {
|
|
||||||
"name": "Hydra",
|
|
||||||
"email": "hydra@hydralauncher.site"
|
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"format": "prettier --write .",
|
||||||
"package": "electron-forge package",
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||||
"make": "electron-forge make",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
"publish": "electron-forge publish",
|
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
"lint": "eslint .",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"format": "prettier . --write",
|
"start": "electron-vite preview",
|
||||||
"postinstall": "node ./postinstall.js"
|
"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": {
|
"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",
|
"@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",
|
"@vanilla-extract/recipes": "^0.5.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"better-sqlite3": "^9.5.0",
|
||||||
"check-disk-space": "^3.4.0",
|
"check-disk-space": "^3.4.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"color": "^4.2.3",
|
|
||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
"date-fns": "^3.5.0",
|
|
||||||
"electron-dl-manager": "^3.0.0",
|
"electron-dl-manager": "^3.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
"date-fns": "^3.6.0",
|
||||||
"flexsearch": "^0.7.43",
|
"flexsearch": "^0.7.43",
|
||||||
"got-scraping": "^4.0.5",
|
"got-scraping": "^4.0.5",
|
||||||
"i18next": "^23.10.1",
|
"i18next": "^23.11.2",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-unrar-js": "^2.0.2",
|
"node-unrar-js": "^2.0.2",
|
||||||
"parse-torrent": "9.1.5",
|
"parse-torrent": "^11.0.16",
|
||||||
"ps-list": "^8.1.1",
|
"ps-list": "^8.1.1",
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"sqlite3": "^5.1.7",
|
|
||||||
"tough-cookie": "^4.1.3",
|
"tough-cookie": "^4.1.3",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"update-electron-app": "^3.0.0",
|
|
||||||
"uuid": "^9.0.1",
|
|
||||||
"windows-1251": "^3.0.4",
|
"windows-1251": "^3.0.4",
|
||||||
"winston": "^3.12.0",
|
"winston": "^3.13.0",
|
||||||
"yaml": "^2.4.1"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
postinstall.cjs
Normal file
@ -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"
|
||||||
|
);
|
||||||
|
}
|
@ -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")
|
|
||||||
}
|
|
BIN
resources/icon.png
Normal file
After Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 2.6 KiB |
BIN
resources/tray-icon.png
Normal file
After Width: | Height: | Size: 59 KiB |
@ -1,11 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Hydra</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
110
src/index.ts
@ -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.
|
|
@ -82,7 +82,7 @@
|
|||||||
"repacks_modal_description": "Choose the repack you want to download",
|
"repacks_modal_description": "Choose the repack you want to download",
|
||||||
"downloads_path": "Downloads path",
|
"downloads_path": "Downloads path",
|
||||||
"select_folder_hint": "To change the default folder, access the",
|
"select_folder_hint": "To change the default folder, access the",
|
||||||
"hydra_settings": "Hydra settings",
|
"settings": "Hydra settings",
|
||||||
"download_now": "Download now"
|
"download_now": "Download now"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
"paused": "{{title}} (Pausado)",
|
"paused": "{{title}} (Pausado)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
||||||
"filter": "Filtrar biblioteca",
|
"filter": "Filtrar biblioteca",
|
||||||
"home": "Hogar",
|
"home": "Inicio",
|
||||||
"follow_us": "Síganos"
|
"follow_us": "Síguenos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
@ -25,7 +25,7 @@
|
|||||||
"downloads": "Descargas",
|
"downloads": "Descargas",
|
||||||
"search_results": "Resultados de búsqueda",
|
"search_results": "Resultados de búsqueda",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"home": "Início"
|
"home": "Inicio"
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Sin descargas en progreso",
|
"no_downloads_in_progress": "Sin descargas en progreso",
|
||||||
@ -100,7 +100,7 @@
|
|||||||
"checking_files": "Verificando archivos…",
|
"checking_files": "Verificando archivos…",
|
||||||
"starting_download": "Iniciando descarga…",
|
"starting_download": "Iniciando descarga…",
|
||||||
"remove_from_list": "Eliminar",
|
"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_description": "Esto eliminará todos los archivos de instalación de su computadora.",
|
||||||
"delete_modal_title": "¿Está seguro?",
|
"delete_modal_title": "¿Está seguro?",
|
||||||
"deleting": "Eliminando instalador…",
|
"deleting": "Eliminando instalador…",
|
||||||
@ -112,8 +112,8 @@
|
|||||||
"notifications": "Notificaciones",
|
"notifications": "Notificaciones",
|
||||||
"enable_download_notifications": "Cuando se completa una descarga",
|
"enable_download_notifications": "Cuando se completa una descarga",
|
||||||
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
|
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
|
||||||
"telemetry": "Telemetria",
|
"telemetry": "Telemetría",
|
||||||
"telemetry_description": "Habilitar estadísticas de uso anónimas"
|
"telemetry_description": "Habilitar recopilación de datos de manera anónima"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
@ -132,7 +132,7 @@
|
|||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Programas no instalados",
|
"title": "Programas no instalados",
|
||||||
"description": "Los ejecutables de Wine o Lutris no se encontraron en su sistema",
|
"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": {
|
"catalogue": {
|
||||||
"next_page": "Siguiente página",
|
"next_page": "Siguiente página",
|
||||||
|
@ -1,147 +1,147 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"recently_added": "Nemrég hozzáadott",
|
"recently_added": "Nemrég hozzáadott",
|
||||||
"trending": "Népszerű",
|
"trending": "Népszerű",
|
||||||
"surprise_me": "Lepj meg",
|
"surprise_me": "Lepj meg",
|
||||||
"no_results": "Nem található"
|
"no_results": "Nem található"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Katalógus",
|
"catalogue": "Katalógus",
|
||||||
"downloads": "Letöltések",
|
"downloads": "Letöltések",
|
||||||
"settings": "Beállítások",
|
"settings": "Beállítások",
|
||||||
"my_library": "Könyvtáram",
|
"my_library": "Könyvtáram",
|
||||||
"downloading_metadata": "{{title}} (Metadata letöltése…)",
|
"downloading_metadata": "{{title}} (Metadata letöltése…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Fájlok ellenőrzése…)",
|
"checking_files": "{{title}} ({{percentage}} - Fájlok ellenőrzése…)",
|
||||||
"paused": "{{title}} (Szünet)",
|
"paused": "{{title}} (Szünet)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||||
"filter": "Könyvtár szűrése",
|
"filter": "Könyvtár szűrése",
|
||||||
"follow_us": "Kövess minket",
|
"follow_us": "Kövess minket",
|
||||||
"home": "Főoldal"
|
"home": "Főoldal"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Keresés",
|
"search": "Keresés",
|
||||||
"home": "Főoldal",
|
"home": "Főoldal",
|
||||||
"catalogue": "Katalógus",
|
"catalogue": "Katalógus",
|
||||||
"downloads": "Letöltések",
|
"downloads": "Letöltések",
|
||||||
"search_results": "Keresési eredmények",
|
"search_results": "Keresési eredmények",
|
||||||
"settings": "Beállítások"
|
"settings": "Beállítások"
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések",
|
"no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések",
|
||||||
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
||||||
"checking_files": "{{title}} fájlok ellenőrzése… ({{percentage}} kész)",
|
"checking_files": "{{title}} fájlok ellenőrzése… ({{percentage}} kész)",
|
||||||
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
|
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "Következő olda",
|
"next_page": "Következő olda",
|
||||||
"previous_page": "Előző olda"
|
"previous_page": "Előző olda"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Letöltési lehetőségek",
|
"open_download_options": "Letöltési lehetőségek",
|
||||||
"download_options_zero": "Nincs letöltési lehetőség",
|
"download_options_zero": "Nincs letöltési lehetőség",
|
||||||
"download_options_one": "{{count}} 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",
|
"download_options_other": "{{count}} letöltési lehetőség",
|
||||||
"updated_at": "Frissítve: {{updated_at}}",
|
"updated_at": "Frissítve: {{updated_at}}",
|
||||||
"install": "Letöltés",
|
"install": "Letöltés",
|
||||||
"resume": "Folytatás",
|
"resume": "Folytatás",
|
||||||
"pause": "Szüneteltetés",
|
"pause": "Szüneteltetés",
|
||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolítás",
|
||||||
"remove_from_list": "Eltávolítás",
|
"remove_from_list": "Eltávolítás",
|
||||||
"space_left_on_disk": "{{space}} szabad hely a lemezen",
|
"space_left_on_disk": "{{space}} szabad hely a lemezen",
|
||||||
"eta": "Befejezés {{eta}}",
|
"eta": "Befejezés {{eta}}",
|
||||||
"downloading_metadata": "Metaadatok letöltése…",
|
"downloading_metadata": "Metaadatok letöltése…",
|
||||||
"checking_files": "Fájlok ellenőrzése…",
|
"checking_files": "Fájlok ellenőrzése…",
|
||||||
"filter": "Repackek szűrése",
|
"filter": "Repackek szűrése",
|
||||||
"requirements": "Rendszerkövetelmények",
|
"requirements": "Rendszerkövetelmények",
|
||||||
"minimum": "Minimális",
|
"minimum": "Minimális",
|
||||||
"recommended": "Ajánlott",
|
"recommended": "Ajánlott",
|
||||||
"no_minimum_requirements": "{{title}} nem tartalmaz információt a minimális követelményekről",
|
"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",
|
"no_recommended_requirements": "{{title}} nem tartalmaz információt az ajánlott követelményekről",
|
||||||
"paused_progress": "{{progress}} (Szünetel)",
|
"paused_progress": "{{progress}} (Szünetel)",
|
||||||
"release_date": "Megjelenés: {{date}}",
|
"release_date": "Megjelenés: {{date}}",
|
||||||
"publisher": "Kiadta: {{publisher}}",
|
"publisher": "Kiadta: {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Link másolása",
|
"copy_link_to_clipboard": "Link másolása",
|
||||||
"copied_link_to_clipboard": "Link másolva",
|
"copied_link_to_clipboard": "Link másolva",
|
||||||
"hours": "óra",
|
"hours": "óra",
|
||||||
"minutes": "perc",
|
"minutes": "perc",
|
||||||
"accuracy": "{{accuracy}}% pontosság",
|
"accuracy": "{{accuracy}}% pontosság",
|
||||||
"add_to_library": "Hozzáadás a könyvtárhoz",
|
"add_to_library": "Hozzáadás a könyvtárhoz",
|
||||||
"remove_from_library": "Eltávolítás a könyvtárból",
|
"remove_from_library": "Eltávolítás a könyvtárból",
|
||||||
"no_downloads": "Nincs elérhető letöltés",
|
"no_downloads": "Nincs elérhető letöltés",
|
||||||
"play_time": "Játszva: {{amount}}",
|
"play_time": "Játszva: {{amount}}",
|
||||||
"last_time_played": "Utoljára játszva {{period}}",
|
"last_time_played": "Utoljára játszva {{period}}",
|
||||||
"not_played_yet": "{{title}} még nem játszottál",
|
"not_played_yet": "{{title}} még nem játszottál",
|
||||||
"next_suggestion": "Következő javaslat",
|
"next_suggestion": "Következő javaslat",
|
||||||
"play": "Játék",
|
"play": "Játék",
|
||||||
"deleting": "Telepítő törlése…",
|
"deleting": "Telepítő törlése…",
|
||||||
"close": "Bezárás",
|
"close": "Bezárás",
|
||||||
"playing_now": "Jelenleg játszva",
|
"playing_now": "Jelenleg játszva",
|
||||||
"change": "Változtatás",
|
"change": "Változtatás",
|
||||||
"repacks_modal_description": "Choose the repack you want to download",
|
"repacks_modal_description": "Choose the repack you want to download",
|
||||||
"downloads_path": "Letöltések helye",
|
"downloads_path": "Letöltések helye",
|
||||||
"select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a",
|
"select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a",
|
||||||
"hydra_settings": "Hydra beállítások",
|
"hydra_settings": "Hydra beállítások",
|
||||||
"download_now": "Töltsd le most"
|
"download_now": "Töltsd le most"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Hydra Aktiválása",
|
"title": "Hydra Aktiválása",
|
||||||
"installation_id": "Telepítési ID:",
|
"installation_id": "Telepítési ID:",
|
||||||
"enter_activation_code": "Add meg az aktiválási kódodat",
|
"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.",
|
"message": "Ha nem tudod, hol kérdezd meg ezt, akkor nem is kellene, hogy legyen ilyened.",
|
||||||
"activate": "Aktiválás",
|
"activate": "Aktiválás",
|
||||||
"loading": "Betöltés…"
|
"loading": "Betöltés…"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "Folytatás",
|
"resume": "Folytatás",
|
||||||
"pause": "Szüneteltetés",
|
"pause": "Szüneteltetés",
|
||||||
"eta": "Befejezés {{eta}}",
|
"eta": "Befejezés {{eta}}",
|
||||||
"paused": "Szüneteltetve",
|
"paused": "Szüneteltetve",
|
||||||
"verifying": "Ellenőrzés…",
|
"verifying": "Ellenőrzés…",
|
||||||
"completed_at": "Befejezve {{date}}-kor",
|
"completed_at": "Befejezve {{date}}-kor",
|
||||||
"completed": "Befejezve",
|
"completed": "Befejezve",
|
||||||
"cancelled": "Megszakítva",
|
"cancelled": "Megszakítva",
|
||||||
"download_again": "Újra letöltés",
|
"download_again": "Újra letöltés",
|
||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"filter": "Letöltött játékok szűrése",
|
"filter": "Letöltött játékok szűrése",
|
||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolítás",
|
||||||
"downloading_metadata": "Metaadatok letöltése…",
|
"downloading_metadata": "Metaadatok letöltése…",
|
||||||
"checking_files": "Fájlok ellenőrzése…",
|
"checking_files": "Fájlok ellenőrzése…",
|
||||||
"starting_download": "Letöltés indítása…",
|
"starting_download": "Letöltés indítása…",
|
||||||
"deleting": "Telepítő törlése…",
|
"deleting": "Telepítő törlése…",
|
||||||
"delete": "Telepítő eltávolítása",
|
"delete": "Telepítő eltávolítása",
|
||||||
"remove_from_list": "Eltávolítás",
|
"remove_from_list": "Eltávolítás",
|
||||||
"delete_modal_title": "Biztos vagy benne?",
|
"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",
|
"delete_modal_description": "Ez eltávolít minden telepítési fájlt a számítógépedről",
|
||||||
"install": "Telepítés"
|
"install": "Telepítés"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Letöltések helye",
|
"downloads_path": "Letöltések helye",
|
||||||
"change": "Frissítés",
|
"change": "Frissítés",
|
||||||
"notifications": "Értesítések",
|
"notifications": "Értesítések",
|
||||||
"enable_download_notifications": "Amikor egy letöltés befejeződik",
|
"enable_download_notifications": "Amikor egy letöltés befejeződik",
|
||||||
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül",
|
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül",
|
||||||
"telemetry": "Telemetria",
|
"telemetry": "Telemetria",
|
||||||
"telemetry_description": "Névtelen felhasználási statisztikák engedélyezése"
|
"telemetry_description": "Névtelen felhasználási statisztikák engedélyezése"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Letöltés befejeződött",
|
"download_complete": "Letöltés befejeződött",
|
||||||
"game_ready_to_install": "{{title}} telepítésre kész",
|
"game_ready_to_install": "{{title}} telepítésre kész",
|
||||||
"repack_list_updated": "Repack lista frissítve",
|
"repack_list_updated": "Repack lista frissítve",
|
||||||
"repack_count_one": "{{count}} repack hozzáadva",
|
"repack_count_one": "{{count}} repack hozzáadva",
|
||||||
"repack_count_other": "{{count}} repack hozzáadva"
|
"repack_count_other": "{{count}} repack hozzáadva"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Hydra megnyitása",
|
"open": "Hydra megnyitása",
|
||||||
"quit": "Kilépés"
|
"quit": "Kilépés"
|
||||||
},
|
},
|
||||||
"game_card": {
|
"game_card": {
|
||||||
"no_downloads": "Nincs elérhető letöltés"
|
"no_downloads": "Nincs elérhető letöltés"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "A programok nincsenek telepítve",
|
"title": "A programok nincsenek telepítve",
|
||||||
"description": "A Wine vagy a Lutris végrehajtható fájljai nem találhatók a rendszereden",
|
"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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"featured": "Destaque",
|
"featured": "Destaque",
|
||||||
"recently_added": "Novidades",
|
"recently_added": "Novidades",
|
||||||
"trending": "Populares",
|
"trending": "Populares",
|
||||||
"surprise_me": "Me surpreenda",
|
"surprise_me": "Surpreenda-me",
|
||||||
"no_results": "Nenhum resultado encontrado"
|
"no_results": "Nenhum resultado encontrado"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
@ -78,7 +78,7 @@
|
|||||||
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
|
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
|
||||||
"downloads_path": "Diretório do download",
|
"downloads_path": "Diretório do download",
|
||||||
"select_folder_hint": "Para trocar a pasta padrão, acesse as ",
|
"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"
|
"download_now": "Baixe agora"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
|
@ -15,7 +15,7 @@ import { databasePath } from "./constants";
|
|||||||
|
|
||||||
export const createDataSource = (options: Partial<SqliteConnectionOptions>) =>
|
export const createDataSource = (options: Partial<SqliteConnectionOptions>) =>
|
||||||
new DataSource({
|
new DataSource({
|
||||||
type: "sqlite",
|
type: "better-sqlite3",
|
||||||
database: databasePath,
|
database: databasePath,
|
||||||
entities: [
|
entities: [
|
||||||
Game,
|
Game,
|
||||||
|
@ -44,7 +44,7 @@ export class Game {
|
|||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
status: GameStatus | "";
|
status: GameStatus | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Progress is a float between 0 and 1
|
* Progress is a float between 0 and 1
|
||||||
|
@ -9,7 +9,7 @@ const steamGames = stateManager.getValue("steamGames");
|
|||||||
|
|
||||||
const getGames = async (
|
const getGames = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
take?: number,
|
take = 12,
|
||||||
cursor = 0
|
cursor = 0
|
||||||
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
|
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
|
||||||
const results: CatalogueEntry[] = [];
|
const results: CatalogueEntry[] = [];
|
||||||
|
@ -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 { registerEvent } from "../register-event";
|
||||||
import { searchGames, searchRepacks } from "../helpers/search-games";
|
import { searchGames, searchRepacks } from "../helpers/search-games";
|
||||||
import { formatName } from "@main/helpers";
|
|
||||||
|
const state = { games: Array<Steam250Game>(), index: 0 };
|
||||||
|
|
||||||
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
return getRandomSteam250List().then(async (games) => {
|
if (state.games.length == 0) {
|
||||||
const shuffledList = shuffle(games);
|
const steam250List = await getSteam250List();
|
||||||
|
|
||||||
for (const game of shuffledList) {
|
const filteredSteam250List = steam250List.filter((game) => {
|
||||||
const repacks = searchRepacks(formatName(game.title));
|
const repacks = searchRepacks(game.title);
|
||||||
|
const catalogue = searchGames({ query: game.title });
|
||||||
|
|
||||||
if (repacks.length) {
|
return repacks.length && catalogue.length;
|
||||||
const results = await searchGames({ query: game.title });
|
});
|
||||||
|
|
||||||
if (results.length) {
|
state.games = shuffle(filteredSteam250List);
|
||||||
return results[0].objectID;
|
}
|
||||||
}
|
|
||||||
}
|
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, {
|
registerEvent(getRandomGame, {
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { searchGames } from "../helpers/search-games";
|
import { searchGames } from "../helpers/search-games";
|
||||||
|
import { CatalogueEntry } from "@types";
|
||||||
|
|
||||||
registerEvent(
|
const searchGamesEvent = async (
|
||||||
(_event: Electron.IpcMainInvokeEvent, query: string) =>
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
searchGames({ query, take: 12 }),
|
query: string
|
||||||
{
|
): Promise<CatalogueEntry[]> => {
|
||||||
name: "searchGames",
|
return searchGames({ query, take: 12 });
|
||||||
memoize: true,
|
};
|
||||||
}
|
|
||||||
);
|
registerEvent(searchGamesEvent, {
|
||||||
|
name: "searchGames",
|
||||||
|
memoize: true,
|
||||||
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import flexSearch from "flexsearch";
|
import flexSearch from "flexsearch";
|
||||||
import orderBy from "lodash/orderBy";
|
import { orderBy } from "lodash-es";
|
||||||
|
|
||||||
import type { GameRepack, GameShop, CatalogueEntry } from "@types";
|
import type { GameRepack, GameShop, CatalogueEntry } from "@types";
|
||||||
|
|
||||||
@ -42,11 +42,11 @@ export interface SearchGamesArgs {
|
|||||||
skip?: number;
|
skip?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchGames = async ({
|
export const searchGames = ({
|
||||||
query,
|
query,
|
||||||
take,
|
take,
|
||||||
skip,
|
skip,
|
||||||
}: SearchGamesArgs): Promise<CatalogueEntry[]> => {
|
}: SearchGamesArgs): CatalogueEntry[] => {
|
||||||
const results = steamGamesIndex
|
const results = steamGamesIndex
|
||||||
.search(formatName(query || ""), { limit: take, offset: skip })
|
.search(formatName(query || ""), { limit: take, offset: skip })
|
||||||
.map((index) => {
|
.map((index) => {
|
||||||
@ -61,11 +61,9 @@ export const searchGames = async ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(results).then((resultsWithRepacks) =>
|
return orderBy(
|
||||||
orderBy(
|
results,
|
||||||
resultsWithRepacks,
|
[({ repacks }) => repacks.length, "repacks"],
|
||||||
[({ repacks }) => repacks.length, "repacks"],
|
["desc"]
|
||||||
["desc"]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,12 +10,13 @@ import "./catalogue/search-games";
|
|||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
import "./library/close-game";
|
import "./library/close-game";
|
||||||
import "./torrenting/delete-game-folder";
|
import "./library/delete-game-folder";
|
||||||
import "./library/get-game-by-object-id";
|
import "./library/get-game-by-object-id";
|
||||||
import "./library/get-library";
|
import "./library/get-library";
|
||||||
import "./library/get-repackers-friendly-names";
|
import "./library/get-repackers-friendly-names";
|
||||||
import "./library/open-game";
|
import "./library/open-game";
|
||||||
import "./library/open-game-installer";
|
import "./library/open-game-installer";
|
||||||
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
import "./misc/get-or-cache-image";
|
import "./misc/get-or-cache-image";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
@ -24,7 +25,6 @@ import "./torrenting/cancel-game-download";
|
|||||||
import "./torrenting/pause-game-download";
|
import "./torrenting/pause-game-download";
|
||||||
import "./torrenting/resume-game-download";
|
import "./torrenting/resume-game-download";
|
||||||
import "./torrenting/start-game-download";
|
import "./torrenting/start-game-download";
|
||||||
import "./torrenting/remove-game-from-download";
|
|
||||||
import "./user-preferences/get-user-preferences";
|
import "./user-preferences/get-user-preferences";
|
||||||
import "./user-preferences/update-user-preferences";
|
import "./user-preferences/update-user-preferences";
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { getProcesses } from "@main/helpers";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { getProcesses } from "@main/helpers";
|
|
||||||
|
|
||||||
const closeGame = async (
|
const closeGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -12,13 +12,17 @@ const closeGame = async (
|
|||||||
const processes = await getProcesses();
|
const processes = await getProcesses();
|
||||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
const game = await gameRepository.findOne({ where: { id: gameId } });
|
||||||
|
|
||||||
const gameProcess = processes.find((runningProcess) => {
|
if (!game) return false;
|
||||||
const basename = path.win32.basename(game.executablePath);
|
|
||||||
const basenameWithoutExtension = path.win32.basename(
|
|
||||||
game.executablePath,
|
|
||||||
path.extname(game.executablePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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") {
|
if (process.platform === "win32") {
|
||||||
return runningProcess.name === basename;
|
return runningProcess.name === basename;
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@ import { gameRepository } from "@main/repository";
|
|||||||
|
|
||||||
import { searchRepacks } from "../helpers/search-games";
|
import { searchRepacks } from "../helpers/search-games";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import sortBy from "lodash/sortBy";
|
|
||||||
import { GameStatus } from "@globals";
|
import { GameStatus } from "@globals";
|
||||||
|
import { sortBy } from "lodash-es";
|
||||||
|
|
||||||
const getLibrary = async (_event: Electron.IpcMainInvokeEvent) =>
|
const getLibrary = async () =>
|
||||||
gameRepository
|
gameRepository
|
||||||
.find({
|
.find({
|
||||||
where: {
|
where: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { stateManager } from "@main/state-manager";
|
import { stateManager } from "@main/state-manager";
|
||||||
|
|
||||||
const getRepackersFriendlyNames = async (_event: Electron.IpcMainInvokeEvent) =>
|
const getRepackersFriendlyNames = async () =>
|
||||||
stateManager.getValue("repackersFriendlyNames").reduce((prev, next) => {
|
stateManager.getValue("repackersFriendlyNames").reduce((prev, next) => {
|
||||||
return { ...prev, [next.name]: next.friendlyName };
|
return { ...prev, [next.name]: next.friendlyName };
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -1,25 +1,16 @@
|
|||||||
import { GameStatus } from "@globals";
|
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gameRepository } from "../../repository";
|
||||||
|
import { GameStatus } from "@globals";
|
||||||
|
|
||||||
const removeGameFromDownload = async (
|
const removeGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
gameId: number
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
await gameRepository.update(
|
||||||
where: {
|
{
|
||||||
id: gameId,
|
id: gameId,
|
||||||
status: GameStatus.Cancelled,
|
status: GameStatus.Cancelled,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
|
||||||
|
|
||||||
gameRepository.update(
|
|
||||||
{
|
|
||||||
id: game.id,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
status: null,
|
status: null,
|
||||||
downloadPath: null,
|
downloadPath: null,
|
||||||
@ -29,6 +20,6 @@ const removeGameFromDownload = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent(removeGameFromDownload, {
|
registerEvent(removeGame, {
|
||||||
name: "removeGameFromDownload",
|
name: "removeGame",
|
||||||
});
|
});
|
@ -25,7 +25,7 @@ const pauseGameDownload = async (
|
|||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.affected) {
|
if (result.affected) {
|
||||||
Downloader.pauseDownload();
|
Downloader.pauseDownload();
|
||||||
WindowManager.mainWindow.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { userPreferencesRepository } from "@main/repository";
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const getUserPreferences = async (_event: Electron.IpcMainInvokeEvent) =>
|
const getUserPreferences = async () =>
|
||||||
userPreferencesRepository.findOne({
|
userPreferencesRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
});
|
});
|
||||||
|
@ -1,52 +1,68 @@
|
|||||||
import { stateManager } from "./state-manager";
|
import { app, BrowserWindow } from "electron";
|
||||||
import { repackers } from "./constants";
|
import { init } from "@sentry/electron/main";
|
||||||
import {
|
import i18n from "i18next";
|
||||||
getNewGOGGames,
|
import path from "node:path";
|
||||||
getNewRepacksFromCPG,
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
getNewRepacksFromUser,
|
import { resolveDatabaseUpdates, WindowManager } from "@main/services";
|
||||||
getNewRepacksFromXatab,
|
import { dataSource } from "@main/data-source";
|
||||||
// getNewRepacksFromOnlineFix,
|
import * as resources from "@locales";
|
||||||
readPipe,
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
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";
|
|
||||||
|
|
||||||
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 () => {
|
if (userPreferences?.telemetryEnabled) return event;
|
||||||
const game = await gameRepository.findOne({
|
return null;
|
||||||
where: {
|
|
||||||
status: In([
|
|
||||||
GameStatus.Downloading,
|
|
||||||
GameStatus.DownloadingMetadata,
|
|
||||||
GameStatus.CheckingFiles,
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
relations: { repack: true },
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (game) {
|
i18n.init({
|
||||||
Downloader.downloadGame(game, game.repack);
|
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) => {
|
// This method will be called when Electron has finished
|
||||||
TorrentClient.onSocketData(data);
|
// 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 },
|
where: { id: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingRepacks = stateManager.getValue("repacks");
|
WindowManager.createMainWindow();
|
||||||
|
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||||
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 () => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
const [friendlyNames, repacks, steamGames] = await Promise.all([
|
optimizer.watchWindowShortcuts(window);
|
||||||
repackerFriendlyNameRepository.find(),
|
});
|
||||||
repackRepository.find({
|
|
||||||
order: {
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
steamGameRepository.find({
|
|
||||||
order: {
|
|
||||||
name: "asc",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
stateManager.setValue("repackersFriendlyNames", friendlyNames);
|
app.on("second-instance", (_event, commandLine) => {
|
||||||
stateManager.setValue("repacks", repacks);
|
// Someone tried to run a second instance, we should focus our window.
|
||||||
stateManager.setValue("steamGames", steamGames);
|
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.
|
||||||
|
129
src/main/main.ts
Normal file
@ -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());
|
@ -46,10 +46,9 @@ export class TorrentClient {
|
|||||||
const commonArgs = [BITTORRENT_PORT, writePipePath, readPipePath];
|
const commonArgs = [BITTORRENT_PORT, writePipePath, readPipePath];
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
const binaryName = binaryNameByPlatform[process.platform];
|
const binaryName = binaryNameByPlatform[process.platform]!;
|
||||||
const binaryPath = path.join(
|
const binaryPath = path.join(
|
||||||
process.resourcesPath,
|
process.resourcesPath,
|
||||||
"dist",
|
|
||||||
"hydra-download-manager",
|
"hydra-download-manager",
|
||||||
binaryName
|
binaryName
|
||||||
);
|
);
|
||||||
|
@ -28,10 +28,11 @@ export const startProcessWatcher = async () => {
|
|||||||
const processes = await getProcesses();
|
const processes = await getProcesses();
|
||||||
|
|
||||||
for (const game of games) {
|
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(
|
const basenameWithoutExtension = path.win32.basename(
|
||||||
game.executablePath,
|
executablePath,
|
||||||
path.extname(game.executablePath)
|
path.extname(executablePath)
|
||||||
);
|
);
|
||||||
|
|
||||||
const gameProcess = processes.find((runningProcess) => {
|
const gameProcess = processes.find((runningProcess) => {
|
||||||
@ -46,7 +47,7 @@ export const startProcessWatcher = async () => {
|
|||||||
|
|
||||||
if (gameProcess) {
|
if (gameProcess) {
|
||||||
if (gamesPlaytime.has(game.id)) {
|
if (gamesPlaytime.has(game.id)) {
|
||||||
const zero = gamesPlaytime.get(game.id);
|
const zero = gamesPlaytime.get(game.id) ?? 0;
|
||||||
const delta = performance.now() - zero;
|
const delta = performance.now() - zero;
|
||||||
|
|
||||||
if (WindowManager.mainWindow) {
|
if (WindowManager.mainWindow) {
|
||||||
|
@ -4,7 +4,6 @@ import { formatUploadDate } from "@main/helpers";
|
|||||||
|
|
||||||
import { Repack } from "@main/entity";
|
import { Repack } from "@main/entity";
|
||||||
import { requestWebPage, savePage } from "./helpers";
|
import { requestWebPage, savePage } from "./helpers";
|
||||||
import type { GameRepackInput } from "./helpers";
|
|
||||||
|
|
||||||
export const request1337x = async (path: string) =>
|
export const request1337x = async (path: string) =>
|
||||||
requestWebPage(`https://1337xx.to${path}`);
|
requestWebPage(`https://1337xx.to${path}`);
|
||||||
@ -68,7 +67,7 @@ export const extractTorrentsFromDocument = async (
|
|||||||
user: string,
|
user: string,
|
||||||
document: Document,
|
document: Document,
|
||||||
existingRepacks: Repack[] = []
|
existingRepacks: Repack[] = []
|
||||||
): Promise<GameRepackInput[]> => {
|
) => {
|
||||||
const $trs = Array.from(document.querySelectorAll("tbody tr"));
|
const $trs = Array.from(document.querySelectorAll("tbody tr"));
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
@ -108,7 +107,7 @@ export const getNewRepacksFromUser = async (
|
|||||||
user: string,
|
user: string,
|
||||||
existingRepacks: Repack[],
|
existingRepacks: Repack[],
|
||||||
page = 1
|
page = 1
|
||||||
): Promise<Repack[]> => {
|
) => {
|
||||||
const response = await request1337x(`/user/${user}/${page}`);
|
const response = await request1337x(`/user/${user}/${page}`);
|
||||||
const { window } = new JSDOM(response);
|
const { window } = new JSDOM(response);
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { JSDOM } from "jsdom";
|
|||||||
import { Repack } from "@main/entity";
|
import { Repack } from "@main/entity";
|
||||||
|
|
||||||
import { requestWebPage, savePage } from "./helpers";
|
import { requestWebPage, savePage } from "./helpers";
|
||||||
import type { GameRepackInput } from "./helpers";
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
|
||||||
export const getNewRepacksFromCPG = async (
|
export const getNewRepacksFromCPG = async (
|
||||||
@ -14,22 +13,22 @@ export const getNewRepacksFromCPG = async (
|
|||||||
|
|
||||||
const { window } = new JSDOM(data);
|
const { window } = new JSDOM(data);
|
||||||
|
|
||||||
const repacks: GameRepackInput[] = [];
|
const repacks = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Array.from(window.document.querySelectorAll(".post")).forEach(($post) => {
|
Array.from(window.document.querySelectorAll(".post")).forEach(($post) => {
|
||||||
const $title = $post.querySelector(".entry-title");
|
const $title = $post.querySelector(".entry-title");
|
||||||
const uploadDate = $post.querySelector("time").getAttribute("datetime");
|
const uploadDate = $post.querySelector("time")?.getAttribute("datetime");
|
||||||
|
|
||||||
const $downloadInfo = Array.from(
|
const $downloadInfo = Array.from(
|
||||||
$post.querySelectorAll(".wp-block-heading")
|
$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" */
|
/* Side note: CPG often misspells "Magnet" as "Magent" */
|
||||||
const $magnet = Array.from($post.querySelectorAll("a")).find(
|
const $magnet = Array.from($post.querySelectorAll("a")).find(
|
||||||
($a) =>
|
($a) =>
|
||||||
$a.textContent.startsWith("Magnet") ||
|
$a.textContent?.startsWith("Magnet") ||
|
||||||
$a.textContent.startsWith("Magent")
|
$a.textContent?.startsWith("Magent")
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileSize = $downloadInfo.textContent
|
const fileSize = $downloadInfo.textContent
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { JSDOM, VirtualConsole } from "jsdom";
|
import { JSDOM, VirtualConsole } from "jsdom";
|
||||||
import { GameRepackInput, requestWebPage, savePage } from "./helpers";
|
import { requestWebPage, savePage } from "./helpers";
|
||||||
import { Repack } from "@main/entity";
|
import { Repack } from "@main/entity";
|
||||||
import { logger } from "../logger";
|
|
||||||
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||||
|
|
||||||
const virtualConsole = new VirtualConsole();
|
const virtualConsole = new VirtualConsole();
|
||||||
|
|
||||||
@ -36,43 +37,35 @@ const getGOGGame = async (url: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getNewGOGGames = async (existingRepacks: Repack[] = []) => {
|
export const getNewGOGGames = async (existingRepacks: Repack[] = []) => {
|
||||||
try {
|
const data = await requestWebPage(
|
||||||
const data = await requestWebPage(
|
"https://freegogpcgames.com/a-z-games-list/"
|
||||||
"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) {
|
for (const $ul of $uls) {
|
||||||
const repacks: GameRepackInput[] = [];
|
const repacks: QueryDeepPartialEntity<Repack>[] = [];
|
||||||
const $lis = Array.from($ul.querySelectorAll("li"));
|
const $lis = Array.from($ul.querySelectorAll("li"));
|
||||||
|
|
||||||
for (const $li of $lis) {
|
for (const $li of $lis) {
|
||||||
const $a = $li.querySelector("a");
|
const $a = $li.querySelector("a");
|
||||||
const href = $a.href;
|
const href = $a.href;
|
||||||
|
|
||||||
const title = $a.textContent.trim();
|
const title = $a.textContent.trim();
|
||||||
|
|
||||||
const gameExists = existingRepacks.some(
|
const gameExists = existingRepacks.some(
|
||||||
(existingRepack) => existingRepack.title === title
|
(existingRepack) => existingRepack.title === title
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!gameExists) {
|
if (!gameExists) {
|
||||||
try {
|
const game = await getGOGGame(href);
|
||||||
const game = await getGOGGame(href);
|
|
||||||
|
|
||||||
repacks.push({ ...game, title });
|
repacks.push({ ...game, title });
|
||||||
} catch (err) {
|
|
||||||
logger.error(err.message, { method: "getGOGGame", url: href });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repacks.length) await savePage(repacks);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
logger.error(err.message, { method: "getNewGOGGames" });
|
if (repacks.length) await savePage(repacks);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
|
import type { Repack } from "@main/entity";
|
||||||
import { repackRepository } from "@main/repository";
|
import { repackRepository } from "@main/repository";
|
||||||
|
|
||||||
import type { GameRepack } from "@types";
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||||
|
|
||||||
export type GameRepackInput = Omit<
|
export const savePage = async (repacks: QueryDeepPartialEntity<Repack>[]) =>
|
||||||
GameRepack,
|
|
||||||
"id" | "repackerFriendlyName" | "createdAt" | "updatedAt"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const savePage = async (repacks: GameRepackInput[]) =>
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
repacks.map((repack) => repackRepository.insert(repack).catch(() => {}))
|
repacks.map((repack) => repackRepository.insert(repack).catch(() => {}))
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Repack } from "@main/entity";
|
import { Repack } from "@main/entity";
|
||||||
import { savePage } from "./helpers";
|
import { savePage } from "./helpers";
|
||||||
import type { GameRepackInput } from "./helpers";
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import parseTorrent, {
|
import parseTorrent, {
|
||||||
toMagnetURI,
|
toMagnetURI,
|
||||||
@ -21,7 +20,8 @@ export const getNewRepacksFromOnlineFix = async (
|
|||||||
cookieJar = new CookieJar()
|
cookieJar = new CookieJar()
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const hasCredentials =
|
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;
|
if (!hasCredentials) return;
|
||||||
|
|
||||||
const http = gotScraping.extend({
|
const http = gotScraping.extend({
|
||||||
@ -58,8 +58,8 @@ export const getNewRepacksFromOnlineFix = async (
|
|||||||
if (!preLogin.field || !preLogin.value) return;
|
if (!preLogin.field || !preLogin.value) return;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
login_name: process.env.ONLINEFIX_USERNAME,
|
login_name: import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME,
|
||||||
login_password: process.env.ONLINEFIX_PASSWORD,
|
login_password: import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD,
|
||||||
login: "submit",
|
login: "submit",
|
||||||
[preLogin.field]: preLogin.value,
|
[preLogin.field]: preLogin.value,
|
||||||
});
|
});
|
||||||
@ -84,10 +84,10 @@ export const getNewRepacksFromOnlineFix = async (
|
|||||||
});
|
});
|
||||||
const document = new JSDOM(home.body).window.document;
|
const document = new JSDOM(home.body).window.document;
|
||||||
|
|
||||||
const repacks: GameRepackInput[] = [];
|
const repacks = [];
|
||||||
const articles = Array.from(document.querySelectorAll(".news"));
|
const articles = Array.from(document.querySelectorAll(".news"));
|
||||||
const totalPages = Number(
|
const totalPages = Number(
|
||||||
document.querySelector("nav > a:nth-child(13)").textContent
|
document.querySelector("nav > a:nth-child(13)")?.textContent
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -185,8 +185,10 @@ export const getNewRepacksFromOnlineFix = async (
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
logger.error(err.message, { method: "getNewRepacksFromOnlineFix" });
|
logger.error((err as Error).message, {
|
||||||
|
method: "getNewRepacksFromOnlineFix",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRepacks = repacks.filter(
|
const newRepacks = repacks.filter(
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
import parseTorrent, { toMagnetURI } from "parse-torrent";
|
|
||||||
|
|
||||||
import { Repack } from "@main/entity";
|
import { Repack } from "@main/entity";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { requestWebPage, savePage } from "./helpers";
|
import { requestWebPage, savePage } from "./helpers";
|
||||||
import type { GameRepackInput } from "./helpers";
|
|
||||||
|
|
||||||
const getTorrentBuffer = (url: string) =>
|
import createWorker from "@main/workers/torrent-parser.worker?nodeWorker";
|
||||||
fetch(url, { method: "GET" }).then((response) =>
|
import { toMagnetURI } from "parse-torrent";
|
||||||
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
import type { Instance } from "parse-torrent";
|
||||||
);
|
|
||||||
|
const worker = createWorker({});
|
||||||
|
|
||||||
const formatXatabDate = (str: string) => {
|
const formatXatabDate = (str: string) => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
@ -28,28 +26,36 @@ const formatXatabDate = (str: string) => {
|
|||||||
const formatXatabDownloadSize = (str: string) =>
|
const formatXatabDownloadSize = (str: string) =>
|
||||||
str.replace(",", ".").replace(/Гб/g, "GB").replace(/Мб/g, "MB");
|
str.replace(",", ".").replace(/Гб/g, "GB").replace(/Мб/g, "MB");
|
||||||
|
|
||||||
const getXatabRepack = async (url: string) => {
|
const getXatabRepack = (url: string) => {
|
||||||
const data = await requestWebPage(url);
|
return new Promise((resolve) => {
|
||||||
const { window } = new JSDOM(data);
|
(async () => {
|
||||||
|
const data = await requestWebPage(url);
|
||||||
|
const { window } = new JSDOM(data);
|
||||||
|
const { document } = window;
|
||||||
|
|
||||||
const $uploadDate = window.document.querySelector(".entry__date");
|
const $uploadDate = document.querySelector(".entry__date");
|
||||||
const $size = window.document.querySelector(".entry__info-size");
|
const $size = document.querySelector(".entry__info-size");
|
||||||
|
|
||||||
const $downloadButton = window.document.querySelector(
|
const $downloadButton = document.querySelector(
|
||||||
".download-torrent"
|
".download-torrent"
|
||||||
) as HTMLAnchorElement;
|
) 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 {
|
worker.removeListener("message", onMessage);
|
||||||
fileSize: formatXatabDownloadSize($size.textContent).toUpperCase(),
|
};
|
||||||
magnet: toMagnetURI({
|
|
||||||
infoHash: parseTorrent(torrentBuffer).infoHash,
|
worker.on("message", onMessage);
|
||||||
}),
|
worker.postMessage($downloadButton.href);
|
||||||
uploadDate: formatXatabDate($uploadDate.textContent),
|
})();
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNewRepacksFromXatab = async (
|
export const getNewRepacksFromXatab = async (
|
||||||
@ -60,7 +66,7 @@ export const getNewRepacksFromXatab = async (
|
|||||||
|
|
||||||
const { window } = new JSDOM(data);
|
const { window } = new JSDOM(data);
|
||||||
|
|
||||||
const repacks: GameRepackInput[] = [];
|
const repacks = [];
|
||||||
|
|
||||||
for (const $a of Array.from(
|
for (const $a of Array.from(
|
||||||
window.document.querySelectorAll(".entry__title a")
|
window.document.querySelectorAll(".entry__title a")
|
||||||
@ -74,14 +80,15 @@ export const getNewRepacksFromXatab = async (
|
|||||||
...repack,
|
...repack,
|
||||||
page,
|
page,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
logger.error(err.message, { method: "getNewRepacksFromXatab" });
|
logger.error((err as Error).message, {
|
||||||
|
method: "getNewRepacksFromXatab",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRepacks = repacks.filter(
|
const newRepacks = repacks.filter(
|
||||||
(repack) =>
|
(repack) =>
|
||||||
repack.uploadDate &&
|
|
||||||
!existingRepacks.some(
|
!existingRepacks.some(
|
||||||
(existingRepack) => existingRepack.title === repack.title
|
(existingRepack) => existingRepack.title === repack.title
|
||||||
)
|
)
|
||||||
|
@ -1,24 +1,31 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import shuffle from "lodash/shuffle";
|
|
||||||
|
export interface Steam250Game {
|
||||||
|
title: string;
|
||||||
|
objectID: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const requestSteam250 = async (path: string) => {
|
export const requestSteam250 = async (path: string) => {
|
||||||
return axios.get(`https://steam250.com${path}`).then((response) => {
|
return axios
|
||||||
const { window } = new JSDOM(response.data);
|
.get(`https://steam250.com${path}`)
|
||||||
const { document } = window;
|
.then((response) => {
|
||||||
|
const { window } = new JSDOM(response.data);
|
||||||
|
const { document } = window;
|
||||||
|
|
||||||
return Array.from(document.querySelectorAll(".appline .title a")).map(
|
return Array.from(document.querySelectorAll(".appline .title a"))
|
||||||
($title: HTMLAnchorElement) => {
|
.map(($title) => {
|
||||||
const steamGameUrl = $title.href;
|
const steamGameUrl = ($title as HTMLAnchorElement).href;
|
||||||
if (!steamGameUrl) return null;
|
if (!steamGameUrl) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: $title.textContent,
|
title: $title.textContent,
|
||||||
objectID: steamGameUrl.split("/").pop(),
|
objectID: steamGameUrl.split("/").pop(),
|
||||||
};
|
} as Steam250Game;
|
||||||
}
|
})
|
||||||
);
|
.filter((game) => game != null);
|
||||||
});
|
})
|
||||||
|
.catch((_) => [] as Steam250Game[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const steam250Paths = [
|
const steam250Paths = [
|
||||||
@ -28,7 +35,15 @@ const steam250Paths = [
|
|||||||
"/most_played",
|
"/most_played",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getRandomSteam250List = async () => {
|
export const getSteam250List = async () => {
|
||||||
const [path] = shuffle(steam250Paths);
|
const gamesList = (
|
||||||
return requestSteam250(path);
|
await Promise.all(steam250Paths.map((path) => requestSteam250(path)))
|
||||||
|
).flat();
|
||||||
|
|
||||||
|
const gamesMap: Map<string, Steam250Game> = gamesList.reduce((map, item) => {
|
||||||
|
map.set(item.objectID, item);
|
||||||
|
return map;
|
||||||
|
}, new Map());
|
||||||
|
|
||||||
|
return [...gamesMap.values()];
|
||||||
};
|
};
|
||||||
|
@ -32,7 +32,7 @@ export const getSteamGridData = async (
|
|||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.STEAMGRIDDB_API_KEY}`,
|
Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
|
||||||
import chunk from "lodash/chunk";
|
import { chunk } from "lodash-es";
|
||||||
|
|
||||||
import { createDataSource, dataSource } from "@main/data-source";
|
import { createDataSource, dataSource } from "@main/data-source";
|
||||||
import { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
|
import { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
|
||||||
@ -109,7 +109,7 @@ export const resolveDatabaseUpdates = async () => {
|
|||||||
const updateDataSource = createDataSource({
|
const updateDataSource = createDataSource({
|
||||||
database: app.isPackaged
|
database: app.isPackaged
|
||||||
? path.join(process.resourcesPath, "hydra.db")
|
? path.join(process.resourcesPath, "hydra.db")
|
||||||
: path.join(__dirname, "..", "..", "resources", "hydra.db"),
|
: path.join(__dirname, "..", "..", "hydra.db"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return updateDataSource.initialize().then(async () => {
|
return updateDataSource.initialize().then(async () => {
|
||||||
|
@ -1,16 +1,30 @@
|
|||||||
import { BrowserWindow, Menu, Tray, app } from "electron";
|
import { BrowserWindow, Menu, Tray, app } from "electron";
|
||||||
|
import { is } from "@electron-toolkit/utils";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import icon from "@resources/icon.png?asset";
|
||||||
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
|
import trayIcon from "@resources/tray-icon.png?asset";
|
||||||
// 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;
|
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
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() {
|
public static createMainWindow() {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
this.mainWindow = new BrowserWindow({
|
this.mainWindow = new BrowserWindow({
|
||||||
@ -19,7 +33,7 @@ export class WindowManager {
|
|||||||
minWidth: 1024,
|
minWidth: 1024,
|
||||||
minHeight: 540,
|
minHeight: 540,
|
||||||
titleBarStyle: "hidden",
|
titleBarStyle: "hidden",
|
||||||
icon: path.join(__dirname, "..", "..", "images", "icon.png"),
|
...(process.platform === "linux" ? { icon } : {}),
|
||||||
trafficLightPosition: { x: 16, y: 16 },
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
titleBarOverlay: {
|
titleBarOverlay: {
|
||||||
symbolColor: "#DADBE1",
|
symbolColor: "#DADBE1",
|
||||||
@ -27,40 +41,29 @@ export class WindowManager {
|
|||||||
height: 34,
|
height: 34,
|
||||||
},
|
},
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
|
sandbox: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.loadURL();
|
||||||
this.mainWindow.removeMenu();
|
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", () => {
|
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();
|
if (!this.mainWindow) this.createMainWindow();
|
||||||
this.mainWindow.loadURL(`${MAIN_WINDOW_WEBPACK_ENTRY}#${path}`);
|
this.loadURL(hash);
|
||||||
|
|
||||||
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
if (this.mainWindow?.isMinimized()) this.mainWindow.restore();
|
||||||
this.mainWindow.focus();
|
this.mainWindow?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createSystemTray(language: string) {
|
public static createSystemTray(language: string) {
|
||||||
const tray = new Tray(
|
const tray = new Tray(trayIcon);
|
||||||
app.isPackaged
|
|
||||||
? path.join(process.resourcesPath, "icon_tray.png")
|
|
||||||
: path.join(__dirname, "..", "..", "resources", "icon_tray.png")
|
|
||||||
);
|
|
||||||
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
@ -93,10 +96,10 @@ export class WindowManager {
|
|||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
tray.addListener("click", () => {
|
tray.addListener("click", () => {
|
||||||
if (this.mainWindow) {
|
if (this.mainWindow) {
|
||||||
if (WindowManager.mainWindow.isMinimized())
|
if (WindowManager.mainWindow?.isMinimized())
|
||||||
WindowManager.mainWindow.restore();
|
WindowManager.mainWindow.restore();
|
||||||
|
|
||||||
WindowManager.mainWindow.focus();
|
WindowManager.mainWindow?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
src/main/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
17
src/main/workers/torrent-parser.worker.ts
Normal file
@ -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);
|
||||||
|
});
|
105
src/preload/index.d.ts
vendored
Normal file
@ -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,
|
||||||
|
});
|
@ -82,8 +82,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
||||||
removeGameFromLibrary: (gameId: number) =>
|
removeGameFromLibrary: (gameId: number) =>
|
||||||
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
||||||
removeGameFromDownload: (gameId: number) =>
|
|
||||||
ipcRenderer.invoke("removeGameFromDownload", gameId),
|
|
||||||
deleteGameFolder: (gameId: number) =>
|
deleteGameFolder: (gameId: number) =>
|
||||||
ipcRenderer.invoke("deleteGameFolder", gameId),
|
ipcRenderer.invoke("deleteGameFolder", gameId),
|
||||||
getGameByObjectID: (objectID: string) =>
|
getGameByObjectID: (objectID: string) =>
|
@ -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";
|
|
16
src/renderer/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Hydra</title>
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body style="background-color: #1c1c1">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -12,7 +12,7 @@ import {
|
|||||||
import * as styles from "./app.css";
|
import * as styles from "./app.css";
|
||||||
import { themeClass } from "./theme.css";
|
import { themeClass } from "./theme.css";
|
||||||
|
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
setSearch,
|
setSearch,
|
||||||
clearSearch,
|
clearSearch,
|
||||||
@ -22,7 +22,7 @@ import {
|
|||||||
|
|
||||||
document.body.classList.add(themeClass);
|
document.body.classList.add(themeClass);
|
||||||
|
|
||||||
export function App() {
|
export function App({ children }: any) {
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const { updateLibrary } = useLibrary();
|
const { updateLibrary } = useLibrary();
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<section ref={contentRef} className={styles.content}>
|
<section ref={contentRef} className={styles.content}>
|
||||||
<Outlet />
|
{children}
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 828 B |
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 697 B |
@ -25,3 +25,5 @@ export const AsyncImage = forwardRef<HTMLImageElement, AsyncImageProps>(
|
|||||||
return <img ref={ref} {...props} src={source ?? props.src} />;
|
return <img ref={ref} {...props} src={source ?? props.src} />;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
AsyncImage.displayName = "AsyncImage";
|
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useDownload } from "@renderer/hooks";
|
import { useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./bottom-panel.css";
|
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 { useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { VERSION_CODENAME } from "@renderer/constants";
|
import { VERSION_CODENAME } from "@renderer/constants";
|
||||||
@ -23,7 +23,7 @@ export function BottomPanel() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
if (isDownloading) {
|
if (isDownloading && game) {
|
||||||
if (game.status === GameStatus.DownloadingMetadata)
|
if (game.status === GameStatus.DownloadingMetadata)
|
||||||
return t("downloading_metadata", { title: game.title });
|
return t("downloading_metadata", { title: game.title });
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ export function BottomPanel() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<small>
|
<small>
|
||||||
v{version} "{VERSION_CODENAME}"
|
v{version} "{VERSION_CODENAME}"
|
||||||
</small>
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
@ -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 { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const checkboxField = style({
|
export const checkboxField = style({
|
@ -1,8 +1,8 @@
|
|||||||
import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||||
import type { CatalogueEntry } from "@types";
|
import type { CatalogueEntry } from "@types";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg";
|
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg?react";
|
||||||
|
|
||||||
import { AsyncImage } from "../async-image/async-image";
|
import { AsyncImage } from "../async-image/async-image";
|
||||||
|
|
@ -2,7 +2,7 @@ import type { ComplexStyleRule } from "@vanilla-extract/css";
|
|||||||
import { keyframes, style } from "@vanilla-extract/css";
|
import { keyframes, style } from "@vanilla-extract/css";
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
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({
|
export const slideIn = keyframes({
|
||||||
"0%": { transform: "translateX(20px)", opacity: "0" },
|
"0%": { transform: "translateX(20px)", opacity: "0" },
|
@ -1,5 +1,5 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
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({
|
export const hero = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -13,11 +13,6 @@ export const hero = style({
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
border: `solid 1px ${vars.color.borderColor}`,
|
border: `solid 1px ${vars.color.borderColor}`,
|
||||||
zIndex: "1",
|
zIndex: "1",
|
||||||
"@media": {
|
|
||||||
"(min-width: 1250px)": {
|
|
||||||
backgroundPosition: "center",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const heroMedia = style({
|
export const heroMedia = style({
|
@ -6,7 +6,7 @@ import { ShopDetails } from "@types";
|
|||||||
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
|
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const FEATURED_GAME_ID = "377160";
|
const FEATURED_GAME_ID = "253230";
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
const [featuredGameDetails, setFeaturedGameDetails] =
|
const [featuredGameDetails, setFeaturedGameDetails] =
|
||||||
@ -36,7 +36,7 @@ export function Hero() {
|
|||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className={styles.backdrop}>
|
||||||
<AsyncImage
|
<AsyncImage
|
||||||
src="https://cdn2.steamgriddb.com/hero/e7a7ba56b1be30e178cd52820e063396.png"
|
src="https://cdn2.steamgriddb.com/hero/a6115ed32394915aac1e5502382eaaea.jpg"
|
||||||
alt={featuredGameDetails?.name}
|
alt={featuredGameDetails?.name}
|
||||||
className={styles.heroMedia}
|
className={styles.heroMedia}
|
||||||
/>
|
/>
|
@ -41,6 +41,7 @@ export function Modal({
|
|||||||
|
|
||||||
const isTopMostModal = () => {
|
const isTopMostModal = () => {
|
||||||
const openModals = document.querySelectorAll("[role=modal]");
|
const openModals = document.querySelectorAll("[role=modal]");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
openModals.length &&
|
openModals.length &&
|
||||||
openModals[openModals.length - 1] === modalContentRef.current
|
openModals[openModals.length - 1] === modalContentRef.current
|
||||||
@ -48,32 +49,37 @@ export function Modal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
if (visible) {
|
||||||
if (e.key === "Escape" && isTopMostModal()) {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
handleCloseClick();
|
if (e.key === "Escape" && isTopMostModal()) {
|
||||||
}
|
handleCloseClick();
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
if (!isTopMostModal()) return;
|
||||||
}, [handleCloseClick]);
|
if (modalContentRef.current) {
|
||||||
|
const clickedWithinModal = modalContentRef.current.contains(
|
||||||
|
e.target as Node
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
if (!clickedWithinModal) {
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
handleCloseClick();
|
||||||
if (!isTopMostModal()) return;
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const clickedOutsideContent = !modalContentRef.current.contains(
|
window.addEventListener("keydown", onKeyDown);
|
||||||
e.target as Node
|
window.addEventListener("mousedown", onMouseDown);
|
||||||
);
|
|
||||||
|
|
||||||
if (clickedOutsideContent) {
|
return () => {
|
||||||
handleCloseClick();
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
}
|
window.removeEventListener("mousedown", onMouseDown);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("mousedown", onMouseDown);
|
return () => {};
|
||||||
return () => window.removeEventListener("mousedown", onMouseDown);
|
}, [handleCloseClick, visible]);
|
||||||
}, [handleCloseClick]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(toggleDragging(visible));
|
dispatch(toggleDragging(visible));
|
@ -6,13 +6,14 @@ import type { Game } from "@types";
|
|||||||
|
|
||||||
import { AsyncImage, TextField } from "@renderer/components";
|
import { AsyncImage, TextField } from "@renderer/components";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
|
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
|
|
||||||
import { MarkGithubIcon } from "@primer/octicons-react";
|
import { MarkGithubIcon } from "@primer/octicons-react";
|
||||||
import DiscordLogo from "@renderer/assets/discord-icon.svg";
|
import DiscordLogo from "@renderer/assets/discord-icon.svg?react";
|
||||||
import XLogo from "@renderer/assets/x-icon.svg";
|
import XLogo from "@renderer/assets/x-icon.svg?react";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar.css";
|
||||||
import { GameStatus } from "@globals";
|
import { GameStatus } from "@globals";
|
||||||
|
|
@ -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 { style } from "@vanilla-extract/css";
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
@ -7,7 +7,7 @@ export interface TextFieldProps
|
|||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
theme?: RecipeVariants<typeof styles.textField>["theme"];
|
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|