mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 09:13:57 +00:00
Compare commits
176 Commits
chore/test
...
v3.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d79048a25 | ||
|
|
4a3ba43dae | ||
|
|
dc413736e8 | ||
|
|
2d98addd02 | ||
|
|
b85e712d8c | ||
|
|
582c276e95 | ||
|
|
430b07eb89 | ||
|
|
b46f46bc45 | ||
|
|
fd41ec5070 | ||
|
|
25d0f77c1d | ||
|
|
c5b2a8242c | ||
|
|
8b7ce6b062 | ||
|
|
db58ff0ba3 | ||
|
|
e951e11e62 | ||
|
|
0b854eda7c | ||
|
|
648083fbf4 | ||
|
|
09316dac9e | ||
|
|
670df826af | ||
|
|
912f9611ea | ||
|
|
d8254353b5 | ||
|
|
73185e7cbc | ||
|
|
c36c940a79 | ||
|
|
1d73b0a251 | ||
|
|
8e4e87840a | ||
|
|
cb8d9aeb82 | ||
|
|
1656ff1b0a | ||
|
|
b54cfbadc5 | ||
|
|
f02959f134 | ||
|
|
3bff55ef2c | ||
|
|
02d1f8ff47 | ||
|
|
f1cf4b683b | ||
|
|
a774b107f3 | ||
|
|
57179446ce | ||
|
|
034e71b3ee | ||
|
|
c1144d3891 | ||
|
|
2620f31989 | ||
|
|
3bca6eed2b | ||
|
|
d10fed6aeb | ||
|
|
ee73c04b12 | ||
|
|
73eb7ac637 | ||
|
|
144444ad10 | ||
|
|
8d892ab440 | ||
|
|
eb6c056713 | ||
|
|
4063568a42 | ||
|
|
7ebe42ebf1 | ||
|
|
21b6783624 | ||
|
|
1ab9564fac | ||
|
|
ab618cea90 | ||
|
|
202533c85c | ||
|
|
7a5cacf103 | ||
|
|
dfb6a324c3 | ||
|
|
6a26c6e5e8 | ||
|
|
5dac84051e | ||
|
|
57e4bd4d27 | ||
|
|
dec4254cb3 | ||
|
|
7d3d5d42d1 | ||
|
|
6bbd916e99 | ||
|
|
ceef316a19 | ||
|
|
fe056eaf85 | ||
|
|
bfcf8178d8 | ||
|
|
3ade87fe0b | ||
|
|
07205d043d | ||
|
|
7de6e96f63 | ||
|
|
9c595583cd | ||
|
|
1c63fc11ee | ||
|
|
49ee05770a | ||
|
|
44131fe831 | ||
|
|
a72eb768e7 | ||
|
|
955725b646 | ||
|
|
8a24fd8ef9 | ||
|
|
b7f717a6f4 | ||
|
|
6d1f290895 | ||
|
|
e86daacad4 | ||
|
|
f2d66df34f | ||
|
|
21fecb2c4e | ||
|
|
af8468066a | ||
|
|
34a33ccef3 | ||
|
|
6ef1135ba2 | ||
|
|
df5f82d47f | ||
|
|
d801bad49e | ||
|
|
9f76ae8c59 | ||
|
|
2f9fa6da48 | ||
|
|
d0f42e73ff | ||
|
|
0bcf005365 | ||
|
|
27e8a0820f | ||
|
|
fd5262cd6e | ||
|
|
36b98a7d73 | ||
|
|
c6fda9b4d8 | ||
|
|
bb65d77fc6 | ||
|
|
22fc95ff53 | ||
|
|
36e6a8cef7 | ||
|
|
33e91e2007 | ||
|
|
2599b332fd | ||
|
|
735b540af4 | ||
|
|
1d7858438d | ||
|
|
993b35cf3b | ||
|
|
63507f00f6 | ||
|
|
ded56c518d | ||
|
|
fbae552b1b | ||
|
|
0567674f2a | ||
|
|
b7c9b5ec54 | ||
|
|
f0a2bf2f48 | ||
|
|
89bb099caa | ||
|
|
f7b9a88219 | ||
|
|
7f8fd32cfe | ||
|
|
614c840aac | ||
|
|
bc6d038c58 | ||
|
|
0e5d37a3a0 | ||
|
|
1bdb80a92b | ||
|
|
584f725eda | ||
|
|
1176ddbe30 | ||
|
|
c7fe59c854 | ||
|
|
9a61a3b448 | ||
|
|
801658fb39 | ||
|
|
6c6d13e387 | ||
|
|
ab27fd21d7 | ||
|
|
2700f27d03 | ||
|
|
ea5b07a1ec | ||
|
|
108b2552b5 | ||
|
|
a498f9dd80 | ||
|
|
67109ff51a | ||
|
|
bdaf68ad23 | ||
|
|
05625e7594 | ||
|
|
4820109b8d | ||
|
|
5c4ddd9b7a | ||
|
|
fe681c3af9 | ||
|
|
8ff925fbb9 | ||
|
|
958a7d037f | ||
|
|
c5764a49e1 | ||
|
|
64c16e82b4 | ||
|
|
e9186e0a3c | ||
|
|
e7a4888f54 | ||
|
|
359733fa40 | ||
|
|
034e88e286 | ||
|
|
1d29bc3620 | ||
|
|
c24f6be1b7 | ||
|
|
694e0cd12c | ||
|
|
8cc8b5fe6f | ||
|
|
a4475d2145 | ||
|
|
a064958d4c | ||
|
|
7e25741657 | ||
|
|
0d909d6eeb | ||
|
|
9a699e082d | ||
|
|
bf416e47b3 | ||
|
|
0461aa2b71 | ||
|
|
b3aae3e8aa | ||
|
|
887ec3f8eb | ||
|
|
be08fb6d14 | ||
|
|
05653500b6 | ||
|
|
0241d8752b | ||
|
|
c8022896a6 | ||
|
|
3dcdcae9a7 | ||
|
|
fa026f82a6 | ||
|
|
e3f61bbaa8 | ||
|
|
8fb31e0e64 | ||
|
|
d93e234001 | ||
|
|
03413a9e6b | ||
|
|
ffac677e3f | ||
|
|
0fdd2797a5 | ||
|
|
4b763173f6 | ||
|
|
446b03eeff | ||
|
|
16cd5b43d8 | ||
|
|
bcca701dc9 | ||
|
|
365178b06d | ||
|
|
1bba8c84bd | ||
|
|
a557735545 | ||
|
|
1705b89355 | ||
|
|
fca585d062 | ||
|
|
63aee44982 | ||
|
|
f5445b00f4 | ||
|
|
e93088e8b9 | ||
|
|
0f1ed20bbb | ||
|
|
9405b0dcfc | ||
|
|
39af661720 | ||
|
|
75be6e5b47 | ||
|
|
baafc6c7d1 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -42,6 +42,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||||
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -52,6 +53,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||||
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -61,7 +63,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: Build-${{ matrix.os }}
|
name: Build-${{ matrix.os }}
|
||||||
path: |
|
path: |
|
||||||
dist/win-unpacked/**
|
|
||||||
dist/*-portable.exe
|
dist/*-portable.exe
|
||||||
dist/*.zip
|
dist/*.zip
|
||||||
dist/*.dmg
|
dist/*.dmg
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -44,6 +44,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||||
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -54,6 +55,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||||
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -25,8 +25,9 @@
|
|||||||
[](./README.cs.md)
|
[](./README.cs.md)
|
||||||
[](./README.da.md)
|
[](./README.da.md)
|
||||||
[](./README.nb.md)
|
[](./README.nb.md)
|
||||||
|
[](./README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
[](README.it.md)
|
[](README.it.md)
|
||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
186
docs/README.et.md
Normal file
186
docs/README.et.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[<img src="../resources/icon.png" width="144"/>](https://hydralauncher.site)
|
||||||
|
|
||||||
|
<h1 align="center">Hydra Launcher</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<strong>Hydra on mängulauncher oma sisseehitatud bittorrenti kliendiga.</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](https://github.com/hydralauncher/hydra/actions)
|
||||||
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
|
[](./README.pt-BR.md)
|
||||||
|
[](./README.md)
|
||||||
|
[](./README.ru.md)
|
||||||
|
[](./README.uk-UA.md)
|
||||||
|
[](./README.be.md)
|
||||||
|
[](./README.es.md)
|
||||||
|
[](./README.fr.md)
|
||||||
|
[](./README.de.md)
|
||||||
|
[](./README.it.md)
|
||||||
|
[](./README.cs.md)
|
||||||
|
[](./README.da.md)
|
||||||
|
[](./README.nb.md)
|
||||||
|
[](./README.et.md)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Sisukord
|
||||||
|
|
||||||
|
- [Sisukord](#sisukord)
|
||||||
|
- [Tutvustus](#tutvustus)
|
||||||
|
- [Funktsioonid](#funktsioonid)
|
||||||
|
- [Paigaldamine](#paigaldamine)
|
||||||
|
- [Panustamine](#panustamine)
|
||||||
|
- [Liitu meie Telegramiga](#liitu-meie-telegramiga)
|
||||||
|
- [Forki ja klooni oma repositoorium](#forki-ja-klooni-oma-repositoorium)
|
||||||
|
- [Viisid panustamiseks](#viisid-panustamiseks)
|
||||||
|
- [Projekti Struktuur](#projekti-struktuur)
|
||||||
|
- [Lähtekoodi kompileerimine](#lähtekoodi-kompileerimine)
|
||||||
|
- [Node.js paigaldamine](#nodejs-paigaldamine)
|
||||||
|
- [Yarn'i paigaldamine](#yarni-paigaldamine)
|
||||||
|
- [Node sõltuvuste paigaldamine](#node-sõltuvuste-paigaldamine)
|
||||||
|
- [Python 3.9 paigaldamine](#python-39-paigaldamine)
|
||||||
|
- [Python'i sõltuvuste paigaldamine](#pythoni-sõltuvuste-paigaldamine)
|
||||||
|
- [Keskkonna muutujad](#keskkonna-muutujad)
|
||||||
|
- [Käivitamine](#käivitamine)
|
||||||
|
- [Kompileerimine](#kompileerimine)
|
||||||
|
- [Bittorrenti kliendi kompileerimine](#bittorrenti-kliendi-kompileerimine)
|
||||||
|
- [Electron rakenduse kompileerimine](#electron-rakenduse-kompileerimine)
|
||||||
|
- [Panustajad](#panustajad)
|
||||||
|
- [Litsents](#litsents)
|
||||||
|
|
||||||
|
## Tutvustus
|
||||||
|
|
||||||
|
**Hydra** on **Mängulauncher** oma sisseehitatud **BitTorrent Kliendiga**.
|
||||||
|
<br>
|
||||||
|
Launcher on kirjutatud TypeScriptis (Electron) ja Pythonis, mis haldab torrentide süsteemi kasutades libtorrenti.
|
||||||
|
|
||||||
|
## Funktsioonid
|
||||||
|
|
||||||
|
- Sisseehitatud bittorrenti klient
|
||||||
|
- How Long To Beat (HLTB) integratsioon mängu lehel
|
||||||
|
- Allalaadimiste kausta kohandamine
|
||||||
|
- Windowsi ja Linuxi tugi
|
||||||
|
- Pidevad uuendused
|
||||||
|
- Ja palju muud ...
|
||||||
|
|
||||||
|
## Paigaldamine
|
||||||
|
|
||||||
|
Järgi paigaldamiseks järgmisi samme:
|
||||||
|
|
||||||
|
1. Lae alla Hydra uusim versioon [Releases](https://github.com/hydralauncher/hydra/releases/latest) lehelt.
|
||||||
|
- Lae alla ainult .exe fail, kui soovid paigaldada Hydrat Windowsile.
|
||||||
|
- Lae alla .deb või .rpm või .zip fail, kui soovid paigaldada Hydrat Linuxile. (sõltub sinu Linuxi distrost)
|
||||||
|
2. Käivita allalaaditud fail.
|
||||||
|
3. Naudi Hydrat!
|
||||||
|
|
||||||
|
## Panustamine
|
||||||
|
|
||||||
|
### Liitu meie Telegramiga
|
||||||
|
|
||||||
|
Me keskendume aruteludele meie [Telegrami](https://t.me/hydralauncher) kanalis.
|
||||||
|
|
||||||
|
### Forki ja klooni oma repositoorium
|
||||||
|
|
||||||
|
1. Forki repositoorium [(klõpsa siia forkimiseks)](https://github.com/hydralauncher/hydra/fork)
|
||||||
|
2. Klooni oma forkitud kood `git clone https://github.com/your_username/hydra`
|
||||||
|
3. Loo uus haru
|
||||||
|
4. Pushi oma commitid
|
||||||
|
5. Esita uus Pull Request
|
||||||
|
|
||||||
|
### Viisid panustamiseks
|
||||||
|
|
||||||
|
- Tõlkimine: Me soovime, et Hydra oleks kättesaadav võimalikult paljudele inimestele. Võid aidata tõlkida uutesse keeltesse või uuendada ja parandada juba olemasolevaid tõlkeid Hydras.
|
||||||
|
- Kood: Hydra on ehitatud kasutades TypeScripti, Electroni ja natuke Pythonit. Kui soovid panustada, liitu meie [Telegramiga](https://t.me/hydralauncher)!
|
||||||
|
|
||||||
|
### Projekti Struktuur
|
||||||
|
|
||||||
|
- torrent-client: Kasutame libtorrenti, Pythoni teeki, torrentide allalaadimiste haldamiseks
|
||||||
|
- src/renderer: rakenduse kasutajaliides
|
||||||
|
- src/main: kogu loogika asub siin.
|
||||||
|
|
||||||
|
## Lähtekoodi kompileerimine
|
||||||
|
|
||||||
|
### Node.js paigaldamine
|
||||||
|
|
||||||
|
Veendu, et Node.js on sinu arvutisse paigaldatud. Kui ei ole, lae alla ja paigalda see [nodejs.org](https://nodejs.org/) lehelt.
|
||||||
|
|
||||||
|
### Yarn'i paigaldamine
|
||||||
|
|
||||||
|
Yarn on Node.js paketihaldur. Kui sa pole Yarni veel paigaldanud, saad seda teha järgides juhiseid [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/) lehel.
|
||||||
|
|
||||||
|
### Node sõltuvuste paigaldamine
|
||||||
|
|
||||||
|
Liigu projekti kausta ja paigalda Node sõltuvused kasutades Yarni:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd hydra
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python 3.9 paigaldamine
|
||||||
|
|
||||||
|
Veendu, et Python 3.9 on sinu arvutisse paigaldatud. Saad selle alla laadida ja paigaldada [python.org](https://www.python.org/downloads/release/python-3913/) lehelt.
|
||||||
|
|
||||||
|
### Python'i sõltuvuste paigaldamine
|
||||||
|
|
||||||
|
Paigalda vajalikud Pythoni sõltuvused kasutades pip'i:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keskkonna muutujad
|
||||||
|
|
||||||
|
Sul on vaja SteamGridDB API võtit, et laadida alla mängude ikoone paigaldamisel.
|
||||||
|
|
||||||
|
Kui sul on see olemas, saad kopeerida või ümber nimetada `.env.example` faili `.env` failiks ja lisada sinna `STEAMGRIDDB_API_KEY`.
|
||||||
|
|
||||||
|
## Käivitamine
|
||||||
|
|
||||||
|
Kui kõik on seadistatud, saad käivitada järgmise käsu, et käivitada nii Electroni protsess kui ka bittorrenti klient:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kompileerimine
|
||||||
|
|
||||||
|
### Bittorrenti kliendi kompileerimine
|
||||||
|
|
||||||
|
Kompileeri bittorrenti klient kasutades järgmist käsku:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python torrent-client/setup.py build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Electron rakenduse kompileerimine
|
||||||
|
|
||||||
|
Kompileeri Electron rakendus kasutades järgmist käsku:
|
||||||
|
|
||||||
|
Windowsil:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
Linuxil:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build:linux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Panustajad
|
||||||
|
|
||||||
|
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Litsents
|
||||||
|
|
||||||
|
Hydra on litsentseeritud [MIT Litsentsi](LICENSE) all.
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
[](README.it.md)
|
[](README.it.md)
|
||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -50,9 +51,9 @@
|
|||||||
- [Environment variables](#-environment-variables)
|
- [Environment variables](#-environment-variables)
|
||||||
- [Running](#-running)
|
- [Running](#-running)
|
||||||
- [Build](#-build)
|
- [Build](#-build)
|
||||||
- [Build the bittorrent client](#-build-the-bittorrent-client)
|
- [ Criar o cliente bittorrent](#-build-the-bittorrent-client)
|
||||||
- [Build the Electron application](#-build-the-electron-application)
|
- [Criar a aplicação Electron](#-build-the-electron-application)
|
||||||
- [Contributors](#-contributors)
|
- [Contribuidores](#-contributors)
|
||||||
- [Licença](#-licença)
|
- [Licença](#-licença)
|
||||||
|
|
||||||
## <a name="about"> Sobre
|
## <a name="about"> Sobre
|
||||||
@@ -152,7 +153,7 @@ yarn dev
|
|||||||
|
|
||||||
## <a name="build"></a> Build
|
## <a name="build"></a> Build
|
||||||
|
|
||||||
### <a name="build-the-bittorrent-client"></a> Build the bittorrent client
|
### <a name="build-the-bittorrent-client"></a> Criar o cliente bittorrent
|
||||||
|
|
||||||
Compile o cliente BitTorrent usando este comando
|
Compile o cliente BitTorrent usando este comando
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ Compile o cliente BitTorrent usando este comando
|
|||||||
python torrent-client/setup.py build
|
python torrent-client/setup.py build
|
||||||
```
|
```
|
||||||
|
|
||||||
### <a name="build-the-electron-application"></a> Build the Electron application
|
### <a name="build-the-electron-application"></a> Criar a aplicação Electron
|
||||||
|
|
||||||
Compile a aplicação Electron usando este comando:
|
Compile a aplicação Electron usando este comando:
|
||||||
|
|
||||||
@@ -176,7 +177,7 @@ No Linux:
|
|||||||
yarn build:linux
|
yarn build:linux
|
||||||
```
|
```
|
||||||
|
|
||||||
## <a name="contributors"></a> Contributors
|
## <a name="contributors"></a> Contribuidores
|
||||||
|
|
||||||
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
|
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
|
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](README.cs.md)
|
[](README.cs.md)
|
||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "2.1.7-preview",
|
"version": "3.0.2",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
@@ -53,18 +53,17 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"electron-log": "^5.2.0",
|
"electron-log": "^5.2.0",
|
||||||
"electron-updater": "^6.3.4",
|
"electron-updater": "^6.3.9",
|
||||||
"fetch-cookie": "^3.0.1",
|
|
||||||
"flexsearch": "^0.7.43",
|
"flexsearch": "^0.7.43",
|
||||||
"i18next": "^23.11.2",
|
"i18next": "^23.11.2",
|
||||||
"i18next-browser-languagedetector": "^7.2.1",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
"icojs": "^0.19.3",
|
"icojs": "^0.19.4",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"parse-torrent": "^11.0.16",
|
"parse-torrent": "^11.0.17",
|
||||||
"piscina": "^4.5.1",
|
"piscina": "^4.5.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
@@ -101,7 +100,7 @@
|
|||||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"electron": "^30.3.0",
|
"electron": "^30.3.0",
|
||||||
"electron-builder": "^25.1.6",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.0.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ cx_Logging; sys_platform == 'win32'
|
|||||||
pywin32; sys_platform == 'win32'
|
pywin32; sys_platform == 'win32'
|
||||||
psutil
|
psutil
|
||||||
Pillow
|
Pillow
|
||||||
requests
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "اَلْعَرَبِيَّةُ",
|
"language_name": "اَلْعَرَبِيَّةُ",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "مميّز",
|
"featured": "مميّز",
|
||||||
"trending": "شائع",
|
|
||||||
"surprise_me": "فاجئني",
|
"surprise_me": "فاجئني",
|
||||||
"no_results": "لم يتم العثور على نتائج"
|
"no_results": "لم يتم العثور على نتائج"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "беларуская мова",
|
"language_name": "беларуская мова",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Рэкамэндаванае",
|
"featured": "Рэкамэндаванае",
|
||||||
"trending": "Актуальнае",
|
|
||||||
"surprise_me": "Здзіві мяне",
|
"surprise_me": "Здзіві мяне",
|
||||||
"no_results": "Няма вынікаў"
|
"no_results": "Няма вынікаў"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destacats",
|
"featured": "Destacats",
|
||||||
"trending": "Populars",
|
|
||||||
"surprise_me": "Sorprèn-me",
|
"surprise_me": "Sorprèn-me",
|
||||||
"no_results": "No s'ha trobat res"
|
"no_results": "No s'ha trobat res"
|
||||||
},
|
},
|
||||||
@@ -178,9 +177,6 @@
|
|||||||
"download_count_zero": "No hi ha baixades a la llista",
|
"download_count_zero": "No hi ha baixades a la llista",
|
||||||
"download_count_one": "{{countFormatted}} a la llista de baixades",
|
"download_count_one": "{{countFormatted}} a la llista de baixades",
|
||||||
"download_count_other": "{{countFormatted}} baixades a la llista",
|
"download_count_other": "{{countFormatted}} baixades a la llista",
|
||||||
"download_options_zero": "No hi ha cap descàrrega disponible",
|
|
||||||
"download_options_one": "{{countFormatted}} descàrrega disponible",
|
|
||||||
"download_options_other": "{{countFormatted}} baixades disponibles",
|
|
||||||
"download_source_url": "Descarrega l'URL de la font",
|
"download_source_url": "Descarrega l'URL de la font",
|
||||||
"add_download_source_description": "Inseriu la URL que conté el fitxer .json",
|
"add_download_source_description": "Inseriu la URL que conté el fitxer .json",
|
||||||
"download_source_up_to_date": "Actualitzat",
|
"download_source_up_to_date": "Actualitzat",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Doporučené",
|
"featured": "Doporučené",
|
||||||
"trending": "Trendy",
|
|
||||||
"surprise_me": "Překvap mě",
|
"surprise_me": "Překvap mě",
|
||||||
"no_results": "Výsledek nenalezen"
|
"no_results": "Výsledek nenalezen"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Anbefalet",
|
"featured": "Anbefalet",
|
||||||
"trending": "Trender",
|
|
||||||
"surprise_me": "Overrask mig",
|
"surprise_me": "Overrask mig",
|
||||||
"no_results": "Ingen resultater fundet",
|
"no_results": "Ingen resultater fundet",
|
||||||
"start_typing": "Begynd at skrive for at søge...",
|
"start_typing": "Begynd at skrive for at søge...",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Empfohlen",
|
"featured": "Empfohlen",
|
||||||
"trending": "Beliebt",
|
|
||||||
"surprise_me": "Überrasche mich",
|
"surprise_me": "Überrasche mich",
|
||||||
"no_results": "Keine Ergebnisse gefunden"
|
"no_results": "Keine Ergebnisse gefunden"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"trending": "Trending",
|
|
||||||
"surprise_me": "Surprise me",
|
"surprise_me": "Surprise me",
|
||||||
"no_results": "No results found",
|
"no_results": "No results found",
|
||||||
"start_typing": "Starting typing to search...",
|
"start_typing": "Starting typing to search...",
|
||||||
"hot": "Hot now",
|
"hot": "Hot now",
|
||||||
"weekly": "📅 Top games of the week"
|
"weekly": "📅 Top games of the week",
|
||||||
|
"achievements": "🏆 Games to beat"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Catalogue",
|
"catalogue": "Catalogue",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "No downloads in progress",
|
"no_downloads_in_progress": "No downloads in progress",
|
||||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
"downloading_metadata": "Downloading {{title}} metadata…",
|
||||||
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}",
|
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}",
|
||||||
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
|
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
|
||||||
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
|
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
|
||||||
},
|
},
|
||||||
@@ -132,6 +132,7 @@
|
|||||||
"warning": "Warning:",
|
"warning": "Warning:",
|
||||||
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util its conclusion. In case Hydra closes before the conclusion, you will lose your progress.",
|
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util its conclusion. In case Hydra closes before the conclusion, you will lose your progress.",
|
||||||
"achievements": "Achievements",
|
"achievements": "Achievements",
|
||||||
|
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "Cloud save",
|
"cloud_save": "Cloud save",
|
||||||
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
@@ -145,7 +146,26 @@
|
|||||||
"no_backups": "You haven't created any backups for this game yet",
|
"no_backups": "You haven't created any backups for this game yet",
|
||||||
"backup_uploaded": "Backup uploaded",
|
"backup_uploaded": "Backup uploaded",
|
||||||
"backup_deleted": "Backup deleted",
|
"backup_deleted": "Backup deleted",
|
||||||
"backup_restored": "Backup restored"
|
"backup_restored": "Backup restored",
|
||||||
|
"see_all_achievements": "See all achievements",
|
||||||
|
"sign_in_to_see_achievements": "Sign in to see achievements",
|
||||||
|
"mapping_method_automatic": "Automatic",
|
||||||
|
"mapping_method_manual": "Manual",
|
||||||
|
"mapping_method_label": "Mapping method",
|
||||||
|
"files_automatically_mapped": "Files automatically mapped",
|
||||||
|
"no_backups_created": "No backups created for this game",
|
||||||
|
"manage_files": "Manage files",
|
||||||
|
"loading_save_preview": "Searching for save games…",
|
||||||
|
"wine_prefix": "Wine Prefix",
|
||||||
|
"wine_prefix_description": "The Wine prefix used to run this game",
|
||||||
|
"no_download_option_info": "No information available",
|
||||||
|
"backup_deletion_failed": "Failed to delete backup",
|
||||||
|
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
|
||||||
|
"achievements_not_sync": "Your achievements are not synchronized",
|
||||||
|
"manage_files_description": "Manage which files will be backed up and restored",
|
||||||
|
"select_folder": "Select folder",
|
||||||
|
"backup_from": "Backup from {{date}}",
|
||||||
|
"custom_backup_location_set": "Custom backup location set"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
@@ -232,7 +252,8 @@
|
|||||||
"source_already_exists": "This source has been already added",
|
"source_already_exists": "This source has been already added",
|
||||||
"must_be_valid_url": "The source must be a valid URL",
|
"must_be_valid_url": "The source must be a valid URL",
|
||||||
"blocked_users": "Blocked users",
|
"blocked_users": "Blocked users",
|
||||||
"user_unblocked": "User has been unblocked"
|
"user_unblocked": "User has been unblocked",
|
||||||
|
"enable_achievement_notifications": "When an achievement in unlocked"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
@@ -329,9 +350,27 @@
|
|||||||
"report_reason_spam": "Spam",
|
"report_reason_spam": "Spam",
|
||||||
"report_reason_other": "Other",
|
"report_reason_other": "Other",
|
||||||
"profile_reported": "Profile reported",
|
"profile_reported": "Profile reported",
|
||||||
"your_friend_code": "Your friend code:"
|
"your_friend_code": "Your friend code:",
|
||||||
|
"upload_banner": "Upload banner",
|
||||||
|
"uploading_banner": "Uploading banner…",
|
||||||
|
"background_image_updated": "Background image updated"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Achievement unlocked"
|
"achievement_unlocked": "Achievement unlocked",
|
||||||
|
"user_achievements": "{{displayName}}'s Achievements",
|
||||||
|
"your_achievements": "Your Achievements",
|
||||||
|
"unlocked_at": "Unlocked at:",
|
||||||
|
"subscription_needed": "A Hydra Cloud subscription is required to see this content",
|
||||||
|
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"subscription_tour_title": "Hydra Cloud Subscription",
|
||||||
|
"subscribe_now": "Subscribe now",
|
||||||
|
"cloud_saving": "Cloud saving",
|
||||||
|
"cloud_achievements": "Save your achievements on the cloud",
|
||||||
|
"animated_profile_picture": "Animated profile pictures",
|
||||||
|
"premium_support": "Premium Support",
|
||||||
|
"show_and_compare_achievements": "Show and compare your achievements to other users",
|
||||||
|
"animated_profile_banner": "Animated profile banner"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Español",
|
"language_name": "Español",
|
||||||
"app": {
|
"app": {
|
||||||
"successfully_signed_in": "Sesión iniciada correctamente"
|
"successfully_signed_in": "Sesión iniciada exitosamente"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destacado",
|
"featured": "Destacado",
|
||||||
"trending": "Tendencias",
|
|
||||||
"surprise_me": "¡Sorpréndeme!",
|
"surprise_me": "¡Sorpréndeme!",
|
||||||
"no_results": "No se encontraron resultados",
|
"no_results": "Sin resultados encontrados",
|
||||||
"hot": "Caliente ahora",
|
"start_typing": "Empieza a escribir para buscar...",
|
||||||
"weekly": "📅 Los mejores juegos de la semana",
|
"hot": "Popular Ahora",
|
||||||
"start_typing": "Empieza a escribir para buscar..."
|
"weekly": "📅 Mejores juegos de la semana",
|
||||||
|
"achievements": "🏆 Juegos para completar"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
||||||
"filter": "Buscar en la biblioteca",
|
"filter": "Buscar en la biblioteca",
|
||||||
"home": "Inicio",
|
"home": "Inicio",
|
||||||
"queued": "{{title}} (En Cola)",
|
"queued": "{{title}} (En cola)",
|
||||||
"game_has_no_executable": "El juego no tiene un ejecutable",
|
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||||
"sign_in": "Iniciar sesión",
|
"sign_in": "Iniciar sesión",
|
||||||
"friends": "Amigos"
|
"friends": "Amigos"
|
||||||
},
|
},
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"downloads": "Descargas",
|
"downloads": "Descargas",
|
||||||
"search_results": "Resultados de búsqueda",
|
"search_results": "Resultados de búsqueda",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"version_available_install": "Version {{version}} disponible. Haz clic aquí para reiniciar e instalar.",
|
"version_available_install": "Versión {{version}} disponible. Presiona acá para descargar y reinstalar.",
|
||||||
"version_available_download": "Version {{version}} disponible. Haz clic aquí para descargar."
|
"version_available_download": "Versión {{version}} disponible. Presiona aquí para descargar."
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Sin descargas en progreso",
|
"no_downloads_in_progress": "Sin descargas en progreso",
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"open_screenshot": "Abrir captura {{number}}",
|
"open_screenshot": "Abrir captura {{number}}",
|
||||||
"download_settings": "Ajustes de descarga",
|
"download_settings": "Ajustes de descarga",
|
||||||
"downloader": "Método de descarga",
|
"downloader": "Método de descarga",
|
||||||
"select_executable": "Seleccionar",
|
"select_executable": "Seleccionar ejecutable",
|
||||||
"no_executable_selected": "No se seleccionó un ejecutable",
|
"no_executable_selected": "No se seleccionó un ejecutable",
|
||||||
"open_folder": "Abrir carpeta",
|
"open_folder": "Abrir carpeta",
|
||||||
"open_download_location": "Ver archivos descargados",
|
"open_download_location": "Ver archivos descargados",
|
||||||
@@ -119,16 +119,53 @@
|
|||||||
"last_downloaded_option": "Última opción descargada",
|
"last_downloaded_option": "Última opción descargada",
|
||||||
"create_shortcut_success": "Atajo creado con éxito",
|
"create_shortcut_success": "Atajo creado con éxito",
|
||||||
"create_shortcut_error": "Error al crear un atajo",
|
"create_shortcut_error": "Error al crear un atajo",
|
||||||
"allow_nsfw_content": "Continuar",
|
|
||||||
"download": "Descargar",
|
|
||||||
"download_count": "Descargas",
|
|
||||||
"download_error": "Esta opción de descarga no está disponible.",
|
|
||||||
"executable_path_in_use": "Ejecutable ya en uso por \"{{game}}\"",
|
|
||||||
"nsfw_content_description": "{{title}} incluye contenido que puede no ser adecuado para todas las edades. \n¿Estás seguro de que quieres continuar?",
|
|
||||||
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
||||||
|
"nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?",
|
||||||
|
"allow_nsfw_content": "Continuar",
|
||||||
|
"refuse_nsfw_content": "No, gracias",
|
||||||
|
"stats": "Estadísticas",
|
||||||
|
"download_count": "Downloads",
|
||||||
"player_count": "Jugadores activos",
|
"player_count": "Jugadores activos",
|
||||||
"refuse_nsfw_content": "Volver",
|
"download_error": "Esta opción de descarga no está disponible.",
|
||||||
"stats": "Estadísticas"
|
"download": "Descargar",
|
||||||
|
"executable_path_in_use": "El ejecutable se encuentra en uso por \"{{game}}\"",
|
||||||
|
"warning": "Advertencia:",
|
||||||
|
"hydra_needs_to_remain_open": "Para esta descarga, Hydra necesita mantenerse abierta hasta que concluya. En caso de que Hydra se cierre antes de que concluya, podrías perder todo el progreso.",
|
||||||
|
"achievements": "Logros",
|
||||||
|
"achievements_count": "Logros {{unlockedCount}}/{{achievementsCount}}",
|
||||||
|
"cloud_save": "Guardado en la nube",
|
||||||
|
"cloud_save_description": "Guarda tu progreso en la nube y continúa jugando en cualquier dispositivo",
|
||||||
|
"backups": "Copias de Seguridad",
|
||||||
|
"install_backup": "Instalar",
|
||||||
|
"delete_backup": "Eliminar",
|
||||||
|
"create_backup": "Nueva Copia de Seguridad",
|
||||||
|
"last_backup_date": "Última copia de seguridad el {{date}}",
|
||||||
|
"no_backup_preview": "No se encontraron datos de guardados para este juego",
|
||||||
|
"restoring_backup": "Restaurando copia de seguridad ({{progress}} completado)…",
|
||||||
|
"uploading_backup": "Subiendo copia de seguridad…",
|
||||||
|
"no_backups": "No has creado ninguna copia de seguridad para este juego aún",
|
||||||
|
"backup_uploaded": "Copia de seguridad subida",
|
||||||
|
"backup_deleted": "Copia de seguridad eliminada",
|
||||||
|
"backup_restored": "Copia de seguridad restaurada",
|
||||||
|
"see_all_achievements": "Ver todos los logros",
|
||||||
|
"sign_in_to_see_achievements": "Inicia sesión para ver los logros",
|
||||||
|
"mapping_method_automatic": "Automático",
|
||||||
|
"mapping_method_manual": "Manual",
|
||||||
|
"mapping_method_label": "Método de mapeo",
|
||||||
|
"files_automatically_mapped": "Archivos mapeados automáticamente",
|
||||||
|
"no_backups_created": "Sin copias de seguridad creadas para este juego",
|
||||||
|
"manage_files": "Gestionar archivos",
|
||||||
|
"loading_save_preview": "Buscando datos de guardados de juegos…",
|
||||||
|
"wine_prefix": "Prefijo de Wine",
|
||||||
|
"wine_prefix_description": "El prefijo de Wine usado para ejecutar este juego",
|
||||||
|
"no_download_option_info": "Sin información disponible",
|
||||||
|
"backup_deletion_failed": "La eliminación de la copia de seguridad falló",
|
||||||
|
"max_number_of_artifacts_reached": "Número máximo de copias de seguridad de este juego alcanzadas",
|
||||||
|
"achievements_not_sync": "Tus logros no están sincronizados",
|
||||||
|
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
||||||
|
"select_folder": "Seleccionar carpeta",
|
||||||
|
"backup_from": "Copia de seguridad de {{date}}",
|
||||||
|
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
@@ -205,20 +242,18 @@
|
|||||||
"found_download_option_one": "Se encontró {{countFormatted}} opción de descarga",
|
"found_download_option_one": "Se encontró {{countFormatted}} opción de descarga",
|
||||||
"found_download_option_other": "Se encontraron {{countFormatted}} opciones de descarga",
|
"found_download_option_other": "Se encontraron {{countFormatted}} opciones de descarga",
|
||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
"blocked_users": "Usuarios bloqueados",
|
"public": "Público",
|
||||||
"download_options_one": "",
|
|
||||||
"download_options_other": "",
|
|
||||||
"download_options_zero": "",
|
|
||||||
"friends_only": "solo amigos",
|
|
||||||
"must_be_valid_url": "La fuente debe ser una URL válida.",
|
|
||||||
"privacy": "Privacidad",
|
|
||||||
"private": "Privado",
|
"private": "Privado",
|
||||||
|
"friends_only": "Solo amigos",
|
||||||
|
"privacy": "Privacidad",
|
||||||
"profile_visibility": "Visibilidad del perfil",
|
"profile_visibility": "Visibilidad del perfil",
|
||||||
"profile_visibility_description": "Elige quién puede ver tu perfil y biblioteca",
|
"profile_visibility_description": "Elige quién puede ver tu perfil y biblioteca",
|
||||||
"public": "Público",
|
|
||||||
"required_field": "Este campo es obligatorio",
|
"required_field": "Este campo es obligatorio",
|
||||||
"source_already_exists": "Esta fuente ya ha sido agregada.",
|
"source_already_exists": "Esta fuente ya ha sido agregada.",
|
||||||
"user_unblocked": "El usuario ha sido desbloqueado"
|
"must_be_valid_url": "La fuente debe ser una URL válida.",
|
||||||
|
"blocked_users": "Usuarios bloqueados",
|
||||||
|
"user_unblocked": "El usuario ha sido desbloqueado",
|
||||||
|
"enable_achievement_notifications": "Cuando un logro se desbloquea"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
@@ -227,7 +262,9 @@
|
|||||||
"repack_count_one": "{{count}} repack ha sido añadido",
|
"repack_count_one": "{{count}} repack ha sido añadido",
|
||||||
"repack_count_other": "{{count}} repacks añadidos",
|
"repack_count_other": "{{count}} repacks añadidos",
|
||||||
"new_update_available": "Version {{version}} disponible",
|
"new_update_available": "Version {{version}} disponible",
|
||||||
"restart_to_install_update": "Reinicia Hydra para instalar la actualización"
|
"restart_to_install_update": "Reinicia Hydra para instalar la actualización",
|
||||||
|
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
|
||||||
|
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Abrir Hydra",
|
"open": "Abrir Hydra",
|
||||||
@@ -238,7 +275,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 tu sistema",
|
||||||
"instructions": "Comprueba como instalar de forma correcta uno de los dos en tu distro de Linux para ejecutar el juego con normalidad"
|
"instructions": "Comprueba como instalar de forma correcta uno de los dos en tu distro de Linux para ejecutar el juego con normalidad"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
@@ -296,23 +333,44 @@
|
|||||||
"no_blocked_users": "No has bloqueado a ningún usuario",
|
"no_blocked_users": "No has bloqueado a ningún usuario",
|
||||||
"friend_code_copied": "Código de amigo copiado",
|
"friend_code_copied": "Código de amigo copiado",
|
||||||
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}",
|
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}",
|
||||||
"displayname_max_length": "El nombre para mostrar debe tener como máximo 50 caracteres",
|
|
||||||
"displayname_min_length": "El nombre para mostrar debe tener al menos 3 caracteres",
|
|
||||||
"locked_profile": "Este perfil es privado.",
|
|
||||||
"privacy_hint": "Para ajustar quién puede ver esto, ve a <0>Configuración</0>.",
|
"privacy_hint": "Para ajustar quién puede ver esto, ve a <0>Configuración</0>.",
|
||||||
"profile_locked": "",
|
"locked_profile": "Este perfil es privado",
|
||||||
"profile_reported": "Perfil reportado",
|
"image_process_failure": "Error al procesar la imagen",
|
||||||
"report": "Informe",
|
"required_field": "Este campo es obligatorio",
|
||||||
|
"displayname_min_length": "El nombre a mostrar debe tener al menos 3 caracteres",
|
||||||
|
"displayname_max_length": "El nombre a mostrar debe tener como máximo 50 caracteres",
|
||||||
|
"report_profile": "Reportar este perfil",
|
||||||
|
"report_reason": "¿Cual es el motivo del reporte?",
|
||||||
"report_description": "Información adicional",
|
"report_description": "Información adicional",
|
||||||
"report_description_placeholder": "Información adicional",
|
"report_description_placeholder": "Información adicional",
|
||||||
"report_profile": "Reportar este perfil",
|
"report": "Reportar",
|
||||||
"report_reason": "¿Por qué estás denunciando este perfil?",
|
"report_reason_hate": "Discursos de odio",
|
||||||
"report_reason_hate": "Discurso de odio",
|
|
||||||
"report_reason_other": "Otro",
|
|
||||||
"report_reason_sexual_content": "Contenido sexual",
|
"report_reason_sexual_content": "Contenido sexual",
|
||||||
"report_reason_spam": "Correo basura",
|
|
||||||
"report_reason_violence": "Violencia",
|
"report_reason_violence": "Violencia",
|
||||||
"required_field": "Este campo es obligatorio",
|
"report_reason_spam": "Spam / Contenido no deseado",
|
||||||
"image_process_failure": "Error al procesar la imagen"
|
"report_reason_other": "Otro",
|
||||||
|
"profile_reported": "Perfil reportado",
|
||||||
|
"your_friend_code": "Tu código de amigo:",
|
||||||
|
"upload_banner": "Subir un banner",
|
||||||
|
"uploading_banner": "Subiendo banner…",
|
||||||
|
"background_image_updated": "Imagen de fondo actualizada"
|
||||||
|
},
|
||||||
|
"achievement": {
|
||||||
|
"achievement_unlocked": "Logro desbloqueado",
|
||||||
|
"user_achievements": "Logros de {{displayName}}",
|
||||||
|
"your_achievements": "Tus Logros",
|
||||||
|
"unlocked_at": "Desbloqueado el:",
|
||||||
|
"subscription_needed": "Se necesita una suscripción a Hydra Cloud se necesita para ver este contenido",
|
||||||
|
"new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"subscription_tour_title": "Suscripción Hydra Cloud",
|
||||||
|
"subscribe_now": "Suscribirse ahora",
|
||||||
|
"cloud_saving": "Guardado en la nube",
|
||||||
|
"cloud_achievements": "Guarda tus logros en la nube",
|
||||||
|
"animated_profile_picture": "Fotos de perfil animadas",
|
||||||
|
"premium_support": "Soporte Premium",
|
||||||
|
"show_and_compare_achievements": "Muestra y compara tus logros con otros usuarios",
|
||||||
|
"animated_profile_banner": "Fondo de perfil animado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
376
src/locales/et/translation.json
Normal file
376
src/locales/et/translation.json
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
{
|
||||||
|
"language_name": "Eesti",
|
||||||
|
"app": {
|
||||||
|
"successfully_signed_in": "Edukalt sisse logitud"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"featured": "Esile toodud",
|
||||||
|
"surprise_me": "Üllata mind",
|
||||||
|
"no_results": "Tulemusi ei leitud",
|
||||||
|
"start_typing": "Alusta otsimiseks kirjutamist...",
|
||||||
|
"hot": "Praegu kuum",
|
||||||
|
"weekly": "📅 Nädala top mängud",
|
||||||
|
"achievements": "🏆 Mängud, mida läbida"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"catalogue": "Kataloog",
|
||||||
|
"downloads": "Allalaadimised",
|
||||||
|
"settings": "Seaded",
|
||||||
|
"my_library": "Minu kogu",
|
||||||
|
"downloading_metadata": "{{title}} (Metaandmete allalaadimine…)",
|
||||||
|
"paused": "{{title}} (Peatatud)",
|
||||||
|
"downloading": "{{title}} ({{percentage}} - Allalaadimine…)",
|
||||||
|
"filter": "Filtreeri kogu",
|
||||||
|
"home": "Avaleht",
|
||||||
|
"queued": "{{title}} (Järjekorras)",
|
||||||
|
"game_has_no_executable": "Mängul pole käivitusfaili valitud",
|
||||||
|
"sign_in": "Logi sisse",
|
||||||
|
"friends": "Sõbrad"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"search": "Otsi mänge",
|
||||||
|
"home": "Avaleht",
|
||||||
|
"catalogue": "Kataloog",
|
||||||
|
"downloads": "Allalaadimised",
|
||||||
|
"search_results": "Otsingutulemused",
|
||||||
|
"settings": "Seaded",
|
||||||
|
"version_available_install": "Versioon {{version}} on saadaval. Klõpsa siia taaskäivitamiseks ja installimiseks.",
|
||||||
|
"version_available_download": "Versioon {{version}} on saadaval. Klõpsa siia allalaadimiseks."
|
||||||
|
},
|
||||||
|
"bottom_panel": {
|
||||||
|
"no_downloads_in_progress": "Allalaadimisi pole pooleli",
|
||||||
|
"downloading_metadata": "{{title}} metaandmete allalaadimine…",
|
||||||
|
"downloading": "{{title}} allalaadimine… ({{percentage}} valmis) - Lõpp {{eta}} - {{speed}}",
|
||||||
|
"calculating_eta": "{{title}} allalaadimine… ({{percentage}} valmis) - Järelejäänud aja arvutamine…",
|
||||||
|
"checking_files": "{{title}} failide kontrollimine… ({{percentage}} valmis)"
|
||||||
|
},
|
||||||
|
"catalogue": {
|
||||||
|
"next_page": "Järgmine leht",
|
||||||
|
"previous_page": "Eelmine leht"
|
||||||
|
},
|
||||||
|
"game_details": {
|
||||||
|
"open_download_options": "Ava allalaadimise valikud",
|
||||||
|
"download_options_zero": "Allalaadimise valikuid pole",
|
||||||
|
"download_options_one": "{{count}} allalaadimise valik",
|
||||||
|
"download_options_other": "{{count}} allalaadimise valikut",
|
||||||
|
"updated_at": "Uuendatud {{updated_at}}",
|
||||||
|
"install": "Installi",
|
||||||
|
"resume": "Jätka",
|
||||||
|
"pause": "Peata",
|
||||||
|
"cancel": "Tühista",
|
||||||
|
"remove": "Eemalda",
|
||||||
|
"space_left_on_disk": "{{space}} kettaruumi järel",
|
||||||
|
"eta": "Lõpp {{eta}}",
|
||||||
|
"calculating_eta": "Järelejäänud aja arvutamine…",
|
||||||
|
"downloading_metadata": "Metaandmete allalaadimine…",
|
||||||
|
"filter": "Filtreeri repacke",
|
||||||
|
"requirements": "Süsteeminõuded",
|
||||||
|
"minimum": "Miinimum",
|
||||||
|
"recommended": "Soovitatav",
|
||||||
|
"paused": "Peatatud",
|
||||||
|
"release_date": "Välja antud {{date}}",
|
||||||
|
"publisher": "Avaldaja {{publisher}}",
|
||||||
|
"hours": "tundi",
|
||||||
|
"minutes": "minutit",
|
||||||
|
"amount_hours": "{{amount}} tundi",
|
||||||
|
"amount_minutes": "{{amount}} minutit",
|
||||||
|
"accuracy": "{{accuracy}}% täpsus",
|
||||||
|
"add_to_library": "Lisa kogusse",
|
||||||
|
"remove_from_library": "Eemalda kogust",
|
||||||
|
"no_downloads": "Allalaadimisi pole saadaval",
|
||||||
|
"play_time": "Mängitud {{amount}}",
|
||||||
|
"last_time_played": "Viimati mängitud {{period}}",
|
||||||
|
"not_played_yet": "Sa pole veel {{title}} mänginud",
|
||||||
|
"next_suggestion": "Järgmine soovitus",
|
||||||
|
"play": "Mängi",
|
||||||
|
"deleting": "Installeri kustutamine…",
|
||||||
|
"close": "Sulge",
|
||||||
|
"playing_now": "Mängib praegu",
|
||||||
|
"change": "Muuda",
|
||||||
|
"repacks_modal_description": "Vali repack, mida soovid alla laadida",
|
||||||
|
"select_folder_hint": "Vaikimisi kausta muutmiseks mine <0>Seadetesse</0>",
|
||||||
|
"download_now": "Laadi alla kohe",
|
||||||
|
"no_shop_details": "Poe andmeid ei õnnestunud laadida.",
|
||||||
|
"download_options": "Allalaadimise valikud",
|
||||||
|
"download_path": "Allalaadimise tee",
|
||||||
|
"previous_screenshot": "Eelmine kuvatõmmis",
|
||||||
|
"next_screenshot": "Järgmine kuvatõmmis",
|
||||||
|
"screenshot": "Kuvatõmmis {{number}}",
|
||||||
|
"open_screenshot": "Ava kuvatõmmis {{number}}",
|
||||||
|
"download_settings": "Allalaadimise seaded",
|
||||||
|
"downloader": "Allalaadija",
|
||||||
|
"select_executable": "Vali",
|
||||||
|
"no_executable_selected": "Käivitusfaili pole valitud",
|
||||||
|
"open_folder": "Ava kaust",
|
||||||
|
"open_download_location": "Vaata allalaaditud faile",
|
||||||
|
"create_shortcut": "Loo töölaua otsetee",
|
||||||
|
"remove_files": "Eemalda failid",
|
||||||
|
"remove_from_library_title": "Oled sa kindel?",
|
||||||
|
"remove_from_library_description": "See eemaldab {{game}} sinu kogust",
|
||||||
|
"options": "Valikud",
|
||||||
|
"executable_section_title": "Käivitusfail",
|
||||||
|
"executable_section_description": "Faili tee, mida käivitatakse \"Mängi\" nupule vajutades",
|
||||||
|
"downloads_secion_title": "Allalaadimised",
|
||||||
|
"downloads_section_description": "Vaata uuendusi või selle mängu teisi versioone",
|
||||||
|
"danger_zone_section_title": "Ohutsoon",
|
||||||
|
"danger_zone_section_description": "Eemalda see mäng oma kogust või Hydra poolt allalaaditud failid",
|
||||||
|
"download_in_progress": "Allalaadimine käimas",
|
||||||
|
"download_paused": "Allalaadimine peatatud",
|
||||||
|
"last_downloaded_option": "Viimane allalaaditud variant",
|
||||||
|
"create_shortcut_success": "Otsetee edukalt loodud",
|
||||||
|
"create_shortcut_error": "Viga otsetee loomisel",
|
||||||
|
"nsfw_content_title": "See mäng sisaldab sobimatut sisu",
|
||||||
|
"nsfw_content_description": "{{title}} sisaldab sisu, mis ei pruugi sobida kõigile vanusegruppidele. Kas soovid kindlasti jätkata?",
|
||||||
|
"allow_nsfw_content": "Jätka",
|
||||||
|
"refuse_nsfw_content": "Mine tagasi",
|
||||||
|
"stats": "Statistika",
|
||||||
|
"download_count": "Allalaadimised",
|
||||||
|
"player_count": "Aktiivsed mängijad",
|
||||||
|
"download_error": "See allalaadimise valik pole saadaval",
|
||||||
|
"download": "Laadi alla",
|
||||||
|
"executable_path_in_use": "Käivitusfail on juba kasutusel mängus \"{{game}}\"",
|
||||||
|
"warning": "Hoiatus:",
|
||||||
|
"hydra_needs_to_remain_open": "selle allalaadimise jaoks peab Hydra jääma avatuks kuni lõpuni. Kui Hydra sulgub enne lõppu, kaotad oma progressi.",
|
||||||
|
"achievements": "Saavutused",
|
||||||
|
"achievements_count": "Saavutused {{unlockedCount}}/{{achievementsCount}}",
|
||||||
|
"cloud_save": "Pilvesalvestus",
|
||||||
|
"cloud_save_description": "Salvesta oma progress pilve ja jätka mängimist mistahes seadmes",
|
||||||
|
"backups": "Varundused",
|
||||||
|
"install_backup": "Installi",
|
||||||
|
"delete_backup": "Kustuta",
|
||||||
|
"create_backup": "Uus varundus",
|
||||||
|
"last_backup_date": "Viimane varundus {{date}}",
|
||||||
|
"no_backup_preview": "Selle mängu jaoks ei leitud salvestusi",
|
||||||
|
"restoring_backup": "Varunduse taastamine ({{progress}} valmis)…",
|
||||||
|
"uploading_backup": "Varunduse üleslaadimine…",
|
||||||
|
"no_backups": "Sa pole veel selle mängu jaoks varundusi loonud",
|
||||||
|
"backup_uploaded": "Varundus üles laaditud",
|
||||||
|
"backup_deleted": "Varundus kustutatud",
|
||||||
|
"backup_restored": "Varundus taastatud",
|
||||||
|
"see_all_achievements": "Vaata kõiki saavutusi",
|
||||||
|
"sign_in_to_see_achievements": "Logi sisse, et näha saavutusi",
|
||||||
|
"mapping_method_automatic": "Automaatne",
|
||||||
|
"mapping_method_manual": "Käsitsi",
|
||||||
|
"mapping_method_label": "Kaardistamise meetod",
|
||||||
|
"files_automatically_mapped": "Failid automaatselt kaardistatud",
|
||||||
|
"no_backups_created": "Selle mängu jaoks pole varundusi loodud",
|
||||||
|
"manage_files": "Halda faile",
|
||||||
|
"loading_save_preview": "Salvestuste otsimine…",
|
||||||
|
"wine_prefix": "Wine Prefix",
|
||||||
|
"wine_prefix_description": "Wine prefix, mida kasutatakse selle mängu käivitamiseks",
|
||||||
|
"no_download_option_info": "Info pole saadaval",
|
||||||
|
"backup_deletion_failed": "Varunduse kustutamine ebaõnnestus",
|
||||||
|
"max_number_of_artifacts_reached": "Selle mängu varunduste maksimaalne arv on saavutatud",
|
||||||
|
"achievements_not_sync": "Sinu saavutused pole sünkroniseeritud",
|
||||||
|
"manage_files_description": "Hallake, millised failid varundatakse ja taastatakse",
|
||||||
|
"select_folder": "Vali kaust",
|
||||||
|
"backup_from": "Varundamine kuupäevast {{date}}",
|
||||||
|
"custom_backup_location_set": "Kohandatud varundamise asukoht määratud"
|
||||||
|
},
|
||||||
|
"activation": {
|
||||||
|
"title": "Aktiveeri Hydra",
|
||||||
|
"installation_id": "Installatsiooni ID:",
|
||||||
|
"enter_activation_code": "Sisesta oma aktiveerimiskood",
|
||||||
|
"message": "Kui sa ei tea, kust seda küsida, siis sa ei peaks seda omama.",
|
||||||
|
"activate": "Aktiveeri",
|
||||||
|
"loading": "Laadimine…"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"resume": "Jätka",
|
||||||
|
"pause": "Peata",
|
||||||
|
"eta": "Lõpp {{eta}}",
|
||||||
|
"paused": "Peatatud",
|
||||||
|
"verifying": "Kontrollimine…",
|
||||||
|
"completed": "Lõpetatud",
|
||||||
|
"removed": "Pole alla laaditud",
|
||||||
|
"cancel": "Tühista",
|
||||||
|
"filter": "Filtreeri allalaaditud mänge",
|
||||||
|
"remove": "Eemalda",
|
||||||
|
"downloading_metadata": "Metaandmete allalaadimine…",
|
||||||
|
"deleting": "Installeri kustutamine…",
|
||||||
|
"delete": "Eemalda installer",
|
||||||
|
"delete_modal_title": "Oled sa kindel?",
|
||||||
|
"delete_modal_description": "See eemaldab kõik installifailid sinu arvutist",
|
||||||
|
"install": "Installi",
|
||||||
|
"download_in_progress": "Töös",
|
||||||
|
"queued_downloads": "Järjekorras allalaadimised",
|
||||||
|
"downloads_completed": "Lõpetatud",
|
||||||
|
"queued": "Järjekorras",
|
||||||
|
"no_downloads_title": "Nii tühi",
|
||||||
|
"no_downloads_description": "Sa pole veel Hydraga midagi alla laadinud, aga pole kunagi hilja alustada.",
|
||||||
|
"checking_files": "Failide kontrollimine…"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"downloads_path": "Allalaadimiste tee",
|
||||||
|
"change": "Uuenda",
|
||||||
|
"notifications": "Teavitused",
|
||||||
|
"enable_download_notifications": "Kui allalaadimine on lõpetatud",
|
||||||
|
"enable_repack_list_notifications": "Kui uus repack on lisatud",
|
||||||
|
"real_debrid_api_token_label": "Real-Debrid API võti",
|
||||||
|
"quit_app_instead_hiding": "Ära peida Hydrat sulgemisel",
|
||||||
|
"launch_with_system": "Käivita Hydra süsteemi käivitamisel",
|
||||||
|
"general": "Üldine",
|
||||||
|
"behavior": "Käitumine",
|
||||||
|
"download_sources": "Allalaadimise allikad",
|
||||||
|
"language": "Keel",
|
||||||
|
"real_debrid_api_token": "API Võti",
|
||||||
|
"enable_real_debrid": "Luba Real-Debrid",
|
||||||
|
"real_debrid_description": "Real-Debrid on piiranguteta allalaadija, mis võimaldab sul faile alla laadida koheselt ja sinu internetiühenduse parima kiirusega.",
|
||||||
|
"real_debrid_invalid_token": "Vigane API võti",
|
||||||
|
"real_debrid_api_token_hint": "Sa saad oma API võtme <0>siit</0>",
|
||||||
|
"real_debrid_free_account_error": "Konto \"{{username}}\" on tasuta konto. Palun telli Real-Debrid",
|
||||||
|
"real_debrid_linked_message": "Konto \"{{username}}\" ühendatud",
|
||||||
|
"save_changes": "Salvesta muudatused",
|
||||||
|
"changes_saved": "Muudatused edukalt salvestatud",
|
||||||
|
"download_sources_description": "Hydra laeb allalaadimise lingid nendest allikatest. Allika URL peab olema otsene link .json failile, mis sisaldab allalaadimise linke.",
|
||||||
|
"validate_download_source": "Valideeri",
|
||||||
|
"remove_download_source": "Eemalda",
|
||||||
|
"add_download_source": "Lisa allikas",
|
||||||
|
"download_count_zero": "Allalaadimise valikuid pole",
|
||||||
|
"download_count_one": "{{countFormatted}} allalaadimise valik",
|
||||||
|
"download_count_other": "{{countFormatted}} allalaadimise valikut",
|
||||||
|
"download_source_url": "Allalaadimise allika URL",
|
||||||
|
"add_download_source_description": "Sisesta URL, mis sisaldab .json faili",
|
||||||
|
"download_source_up_to_date": "Ajakohane",
|
||||||
|
"download_source_errored": "Vigane",
|
||||||
|
"sync_download_sources": "Sünkroniseeri allikad",
|
||||||
|
"removed_download_source": "Allalaadimise allikas eemaldatud",
|
||||||
|
"added_download_source": "Allalaadimise allikas lisatud",
|
||||||
|
"download_sources_synced": "Kõik allalaadimise allikad on sünkroniseeritud",
|
||||||
|
"insert_valid_json_url": "Sisesta kehtiv JSON url",
|
||||||
|
"found_download_option_zero": "Allalaadimise valikuid ei leitud",
|
||||||
|
"found_download_option_one": "Leitud {{countFormatted}} allalaadimise valik",
|
||||||
|
"found_download_option_other": "Leitud {{countFormatted}} allalaadimise valikut",
|
||||||
|
"import": "Impordi",
|
||||||
|
"public": "Avalik",
|
||||||
|
"private": "Privaatne",
|
||||||
|
"friends_only": "Ainult sõpradele",
|
||||||
|
"privacy": "Privaatsus",
|
||||||
|
"profile_visibility": "Profiili nähtavus",
|
||||||
|
"profile_visibility_description": "Vali, kes saavad näha sinu profiili ja kogu",
|
||||||
|
"required_field": "See väli on kohustuslik",
|
||||||
|
"source_already_exists": "See allikas on juba lisatud",
|
||||||
|
"must_be_valid_url": "Allikas peab olema kehtiv URL",
|
||||||
|
"blocked_users": "Blokeeritud kasutajad",
|
||||||
|
"user_unblocked": "Kasutaja blokeering on eemaldatud",
|
||||||
|
"enable_achievement_notifications": "Kui saavutus avatakse"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"download_complete": "Allalaadimine lõpetatud",
|
||||||
|
"game_ready_to_install": "{{title}} on valmis installimiseks",
|
||||||
|
"repack_list_updated": "Repackide nimekiri uuendatud",
|
||||||
|
"repack_count_one": "{{count}} repack lisatud",
|
||||||
|
"repack_count_other": "{{count}} repacki lisatud",
|
||||||
|
"new_update_available": "Versioon {{version}} saadaval",
|
||||||
|
"restart_to_install_update": "Taaskäivita Hydra uuenduse installimiseks",
|
||||||
|
"notification_achievement_unlocked_title": "Saavutus avatud mängus {{game}}",
|
||||||
|
"notification_achievement_unlocked_body": "{{achievement}} ja veel {{count}} avati"
|
||||||
|
},
|
||||||
|
"system_tray": {
|
||||||
|
"open": "Ava Hydra",
|
||||||
|
"quit": "Välju"
|
||||||
|
},
|
||||||
|
"game_card": {
|
||||||
|
"no_downloads": "Allalaadimisi pole saadaval"
|
||||||
|
},
|
||||||
|
"binary_not_found_modal": {
|
||||||
|
"title": "Programmid pole installitud",
|
||||||
|
"description": "Wine või Lutrise käivitusfaile ei leitud sinu süsteemist",
|
||||||
|
"instructions": "Kontrolli õiget viisi nende installimiseks oma Linuxi distrol, et mäng saaks normaalselt töötada"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"close": "Sulgemise nupp"
|
||||||
|
},
|
||||||
|
"forms": {
|
||||||
|
"toggle_password_visibility": "Lülita parooli nähtavust"
|
||||||
|
},
|
||||||
|
"user_profile": {
|
||||||
|
"amount_hours": "{{amount}} tundi",
|
||||||
|
"amount_minutes": "{{amount}} minutit",
|
||||||
|
"last_time_played": "Viimati mängitud {{period}}",
|
||||||
|
"activity": "Hiljutine aktiivsus",
|
||||||
|
"library": "Kogu",
|
||||||
|
"total_play_time": "Kogu mängitud aeg: {{amount}}",
|
||||||
|
"no_recent_activity_title": "Hmmm… siin pole midagi",
|
||||||
|
"no_recent_activity_description": "Sa pole hiljuti ühtegi mängu mänginud. On aeg seda muuta!",
|
||||||
|
"display_name": "Kuvatav nimi",
|
||||||
|
"saving": "Salvestamine",
|
||||||
|
"save": "Salvesta",
|
||||||
|
"edit_profile": "Muuda profiili",
|
||||||
|
"saved_successfully": "Edukalt salvestatud",
|
||||||
|
"try_again": "Palun proovi uuesti",
|
||||||
|
"sign_out_modal_title": "Oled sa kindel?",
|
||||||
|
"cancel": "Tühista",
|
||||||
|
"successfully_signed_out": "Edukalt välja logitud",
|
||||||
|
"sign_out": "Logi välja",
|
||||||
|
"playing_for": "Mängib {{amount}}",
|
||||||
|
"sign_out_modal_text": "Sinu kogu on seotud sinu praeguse kontoga. Välja logides pole sinu kogu enam nähtav ja edasist progressi ei salvestata. Jätkata väljalogimisega?",
|
||||||
|
"add_friends": "Lisa sõpru",
|
||||||
|
"add": "Lisa",
|
||||||
|
"friend_code": "Sõbrakood",
|
||||||
|
"see_profile": "Vaata profiili",
|
||||||
|
"sending": "Saatmine",
|
||||||
|
"friend_request_sent": "Sõbrakutse saadetud",
|
||||||
|
"friends": "Sõbrad",
|
||||||
|
"friends_list": "Sõbrade nimekiri",
|
||||||
|
"user_not_found": "Kasutajat ei leitud",
|
||||||
|
"block_user": "Blokeeri kasutaja",
|
||||||
|
"add_friend": "Lisa sõbraks",
|
||||||
|
"request_sent": "Kutse saadetud",
|
||||||
|
"request_received": "Kutse saadud",
|
||||||
|
"accept_request": "Võta kutse vastu",
|
||||||
|
"ignore_request": "Ignoreeri kutset",
|
||||||
|
"cancel_request": "Tühista kutse",
|
||||||
|
"undo_friendship": "Tühista sõprus",
|
||||||
|
"request_accepted": "Kutse vastu võetud",
|
||||||
|
"user_blocked_successfully": "Kasutaja edukalt blokeeritud",
|
||||||
|
"user_block_modal_text": "See blokeerib kasutaja {{displayName}}",
|
||||||
|
"blocked_users": "Blokeeritud kasutajad",
|
||||||
|
"unblock": "Eemalda blokeering",
|
||||||
|
"no_friends_added": "Sul pole veel lisatud sõpru",
|
||||||
|
"pending": "Ootel",
|
||||||
|
"no_pending_invites": "Sul pole ootel kutseid",
|
||||||
|
"no_blocked_users": "Sul pole blokeeritud kasutajaid",
|
||||||
|
"friend_code_copied": "Sõbrakood kopeeritud",
|
||||||
|
"undo_friendship_modal_text": "See tühistab sinu sõpruse kasutajaga {{displayName}}",
|
||||||
|
"privacy_hint": "Et muuta, kes seda näevad, mine <0>Seadetesse</0>",
|
||||||
|
"locked_profile": "See profiil on privaatne",
|
||||||
|
"image_process_failure": "Viga pildi töötlemisel",
|
||||||
|
"required_field": "See väli on kohustuslik",
|
||||||
|
"displayname_min_length": "Kuvatav nimi peab olema vähemalt 3 tähemärki pikk",
|
||||||
|
"displayname_max_length": "Kuvatav nimi võib olla maksimaalselt 50 tähemärki pikk",
|
||||||
|
"report_profile": "Teata sellest profiilist",
|
||||||
|
"report_reason": "Miks sa sellest profiilist teatad?",
|
||||||
|
"report_description": "Lisainfo",
|
||||||
|
"report_description_placeholder": "Lisainfo",
|
||||||
|
"report": "Teata",
|
||||||
|
"report_reason_hate": "Vaenukõne",
|
||||||
|
"report_reason_sexual_content": "Seksuaalne sisu",
|
||||||
|
"report_reason_violence": "Vägivald",
|
||||||
|
"report_reason_spam": "Rämpspost",
|
||||||
|
"report_reason_other": "Muu",
|
||||||
|
"profile_reported": "Profiilist teatatud",
|
||||||
|
"your_friend_code": "Sinu sõbrakood:",
|
||||||
|
"upload_banner": "Lae üles bänner",
|
||||||
|
"uploading_banner": "Bänneri üleslaadimine…",
|
||||||
|
"background_image_updated": "Bänner uuendatud"
|
||||||
|
},
|
||||||
|
"achievement": {
|
||||||
|
"achievement_unlocked": "Saavutus avatud",
|
||||||
|
"user_achievements": "{{displayName}} saavutused",
|
||||||
|
"your_achievements": "Sinu saavutused",
|
||||||
|
"unlocked_at": "Avatud:",
|
||||||
|
"subscription_needed": "Selle sisu nägemiseks on vaja Hydra Cloud tellimust",
|
||||||
|
"new_achievements_unlocked": "Avatud {{achievementCount}} uut saavutust {{gameCount}} mängust"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"subscription_tour_title": "Hydra Cloud Tellimus",
|
||||||
|
"subscribe_now": "Telli kohe",
|
||||||
|
"cloud_saving": "Pilvesalvestus",
|
||||||
|
"cloud_achievements": "Salvesta oma saavutused pilve",
|
||||||
|
"animated_profile_picture": "Animeeritud profiilipildid",
|
||||||
|
"premium_support": "Premium tugi",
|
||||||
|
"show_and_compare_achievements": "Näita ja võrdle oma saavutusi teiste kasutajatega",
|
||||||
|
"animated_profile_banner": "Animeeritud profiilibänner"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "فارسی",
|
"language_name": "فارسی",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "پیشنهادی",
|
"featured": "پیشنهادی",
|
||||||
"trending": "پرطرفدار",
|
|
||||||
"surprise_me": "سوپرایزم کن",
|
"surprise_me": "سوپرایزم کن",
|
||||||
"no_results": "اتمامای پیدا نشد"
|
"no_results": "اتمامای پیدا نشد"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "Français",
|
"language_name": "Français",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "En vedette",
|
"featured": "En vedette",
|
||||||
"trending": "Tendance",
|
|
||||||
"surprise_me": "Surprenez-moi",
|
"surprise_me": "Surprenez-moi",
|
||||||
"no_results": "Aucun résultat trouvé"
|
"no_results": "Aucun résultat trouvé"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "Magyar",
|
"language_name": "Magyar",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"trending": "Népszerű",
|
|
||||||
"surprise_me": "Lepj meg",
|
"surprise_me": "Lepj meg",
|
||||||
"no_results": "Nem található"
|
"no_results": "Nem található"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Unggulan",
|
"featured": "Unggulan",
|
||||||
"trending": "Sedang Tren",
|
|
||||||
"surprise_me": "Kejutkan saya",
|
"surprise_me": "Kejutkan saya",
|
||||||
"no_results": "Tidak ada hasil ditemukan"
|
"no_results": "Tidak ada hasil ditemukan"
|
||||||
},
|
},
|
||||||
@@ -178,9 +177,6 @@
|
|||||||
"download_count_zero": "Tidak ada unduhan dalam daftar",
|
"download_count_zero": "Tidak ada unduhan dalam daftar",
|
||||||
"download_count_one": "{{countFormatted}} unduhan dalam daftar",
|
"download_count_one": "{{countFormatted}} unduhan dalam daftar",
|
||||||
"download_count_other": "{{countFormatted}} unduhan dalam daftar",
|
"download_count_other": "{{countFormatted}} unduhan dalam daftar",
|
||||||
"download_options_zero": "Tidak ada unduhan tersedia",
|
|
||||||
"download_options_one": "{{countFormatted}} unduhan tersedia",
|
|
||||||
"download_options_other": "{{countFormatted}} unduhan tersedia",
|
|
||||||
"download_source_url": "URL sumber unduhan",
|
"download_source_url": "URL sumber unduhan",
|
||||||
"add_download_source_description": "Masukkan URL yang berisi file .json",
|
"add_download_source_description": "Masukkan URL yang berisi file .json",
|
||||||
"download_source_up_to_date": "Terkini",
|
"download_source_up_to_date": "Terkini",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import ca from "./ca/translation.json";
|
|||||||
import kk from "./kk/translation.json";
|
import kk from "./kk/translation.json";
|
||||||
import cs from "./cs/translation.json";
|
import cs from "./cs/translation.json";
|
||||||
import nb from "./nb/translation.json";
|
import nb from "./nb/translation.json";
|
||||||
|
import et from "./et/translation.json";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
"pt-BR": ptBR,
|
"pt-BR": ptBR,
|
||||||
@@ -50,4 +51,5 @@ export default {
|
|||||||
kk,
|
kk,
|
||||||
cs,
|
cs,
|
||||||
nb,
|
nb,
|
||||||
|
et,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "Italiano",
|
"language_name": "Italiano",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "In primo piano",
|
"featured": "In primo piano",
|
||||||
"trending": "Di tendenza",
|
|
||||||
"surprise_me": "Sorprendimi",
|
"surprise_me": "Sorprendimi",
|
||||||
"no_results": "Nessun risultato trovato"
|
"no_results": "Nessun risultato trovato"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Ұсынылған",
|
"featured": "Ұсынылған",
|
||||||
"trending": "Трендте",
|
|
||||||
"surprise_me": "Таңқалдыр",
|
"surprise_me": "Таңқалдыр",
|
||||||
"no_results": "Ештеңе табылмады"
|
"no_results": "Ештеңе табылмады"
|
||||||
},
|
},
|
||||||
@@ -176,9 +175,6 @@
|
|||||||
"download_count_zero": "Жүктеулер тізімінде жоқ",
|
"download_count_zero": "Жүктеулер тізімінде жоқ",
|
||||||
"download_count_one": "{{countFormatted}} жүктеу тізімде",
|
"download_count_one": "{{countFormatted}} жүктеу тізімде",
|
||||||
"download_count_other": "{{countFormatted}} жүктеу тізімде",
|
"download_count_other": "{{countFormatted}} жүктеу тізімде",
|
||||||
"download_options_zero": "Қолжетімді жүктеулер жоқ",
|
|
||||||
"download_options_one": "{{countFormatted}} жүктеу нұсқасы қол жетімді",
|
|
||||||
"download_options_other": "{{countFormatted}} жүктеу нұсқалары қол жетімді",
|
|
||||||
"download_source_url": "Көздің сілтемесі",
|
"download_source_url": "Көздің сілтемесі",
|
||||||
"add_download_source_description": ".json файлға сілтемені қойыңыз",
|
"add_download_source_description": ".json файлға сілтемені қойыңыз",
|
||||||
"download_source_up_to_date": "Жаңартылған",
|
"download_source_up_to_date": "Жаңартылған",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "한국어",
|
"language_name": "한국어",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "추천",
|
"featured": "추천",
|
||||||
"trending": "인기",
|
|
||||||
"surprise_me": "무작위 추천",
|
"surprise_me": "무작위 추천",
|
||||||
"no_results": "결과 없음"
|
"no_results": "결과 없음"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Anbefalinger",
|
"featured": "Anbefalinger",
|
||||||
"trending": "Trender",
|
|
||||||
"surprise_me": "Overrask meg",
|
"surprise_me": "Overrask meg",
|
||||||
"no_results": "Ingen resultater fundet",
|
"no_results": "Ingen resultater fundet",
|
||||||
"start_typing": "Begynn å skrive for å søke...",
|
"start_typing": "Begynn å skrive for å søke...",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "Nederlands",
|
"language_name": "Nederlands",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Uitgelicht",
|
"featured": "Uitgelicht",
|
||||||
"trending": "Trending",
|
|
||||||
"surprise_me": "Verrasing",
|
"surprise_me": "Verrasing",
|
||||||
"no_results": "Geen resultaten gevonden"
|
"no_results": "Geen resultaten gevonden"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "Polski",
|
"language_name": "Polski",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Wyróżnione",
|
"featured": "Wyróżnione",
|
||||||
"trending": "Trendujące",
|
|
||||||
"surprise_me": "Zaskocz mnie",
|
"surprise_me": "Zaskocz mnie",
|
||||||
"no_results": "Nie znaleziono wyników"
|
"no_results": "Nie znaleziono wyników"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destaques",
|
"featured": "Destaques",
|
||||||
"trending": "Populares",
|
"hot": "Populares",
|
||||||
"hot": "Populares agora",
|
|
||||||
"weekly": "📅 Mais baixados da semana",
|
"weekly": "📅 Mais baixados da semana",
|
||||||
|
"achievements": "🏆 Pra platinar",
|
||||||
"surprise_me": "Surpreenda-me",
|
"surprise_me": "Surpreenda-me",
|
||||||
"no_results": "Nenhum resultado encontrado",
|
"no_results": "Nenhum resultado encontrado",
|
||||||
"start_typing": "Comece a digitar para pesquisar…"
|
"start_typing": "Comece a digitar para pesquisar…"
|
||||||
@@ -128,6 +128,7 @@
|
|||||||
"warning": "Aviso:",
|
"warning": "Aviso:",
|
||||||
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
|
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
|
||||||
"achievements": "Conquistas",
|
"achievements": "Conquistas",
|
||||||
|
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
|
||||||
"cloud_save": "Salvamento em nuvem",
|
"cloud_save": "Salvamento em nuvem",
|
||||||
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
|
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
|
||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
@@ -141,7 +142,26 @@
|
|||||||
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
||||||
"backup_uploaded": "Backup criado",
|
"backup_uploaded": "Backup criado",
|
||||||
"backup_deleted": "Backup apagado",
|
"backup_deleted": "Backup apagado",
|
||||||
"backup_restored": "Backup restaurado"
|
"backup_restored": "Backup restaurado",
|
||||||
|
"see_all_achievements": "Ver todas as conquistas",
|
||||||
|
"sign_in_to_see_achievements": "Faça login para ver as conquistas",
|
||||||
|
"mapping_method_automatic": "Automático",
|
||||||
|
"mapping_method_manual": "Manual",
|
||||||
|
"mapping_method_label": "Método de mapeamento",
|
||||||
|
"files_automatically_mapped": "Arquivos automaticamente mapeados",
|
||||||
|
"no_backups_created": "Nenhum backup criado para este jogo",
|
||||||
|
"manage_files": "Gerenciar arquivos",
|
||||||
|
"loading_save_preview": "Buscando por arquivos de salvamento…",
|
||||||
|
"wine_prefix": "Prefixo Wine",
|
||||||
|
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
|
||||||
|
"no_download_option_info": "Sem informações disponíveis",
|
||||||
|
"backup_deletion_failed": "Falha ao apagar backup",
|
||||||
|
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||||
|
"achievements_not_sync": "Suas conquistas não estão sincronizadas",
|
||||||
|
"backup_from": "Backup de {{date}}",
|
||||||
|
"custom_backup_location_set": "Localização customizada selecionada",
|
||||||
|
"select_folder": "Selecione a pasta",
|
||||||
|
"manage_files_description": "Gerencie quais arquivos serão feitos backup"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@@ -205,9 +225,6 @@
|
|||||||
"download_count_zero": "Sem downloads na lista",
|
"download_count_zero": "Sem downloads na lista",
|
||||||
"download_count_one": "{{countFormatted}} download na lista",
|
"download_count_one": "{{countFormatted}} download na lista",
|
||||||
"download_count_other": "{{countFormatted}} downloads na lista",
|
"download_count_other": "{{countFormatted}} downloads na lista",
|
||||||
"download_options_zero": "Sem downloads disponíveis",
|
|
||||||
"download_options_one": "{{countFormatted}} download disponível",
|
|
||||||
"download_options_other": "{{countFormatted}} downloads disponíveis",
|
|
||||||
"download_source_url": "URL da fonte",
|
"download_source_url": "URL da fonte",
|
||||||
"add_download_source_description": "Insira a URL contendo o arquivo .json",
|
"add_download_source_description": "Insira a URL contendo o arquivo .json",
|
||||||
"download_source_up_to_date": "Sincronizada",
|
"download_source_up_to_date": "Sincronizada",
|
||||||
@@ -231,7 +248,8 @@
|
|||||||
"source_already_exists": "Essa fonte já foi adicionada",
|
"source_already_exists": "Essa fonte já foi adicionada",
|
||||||
"must_be_valid_url": "A fonte deve ser uma URL válida",
|
"must_be_valid_url": "A fonte deve ser uma URL válida",
|
||||||
"blocked_users": "Usuários bloqueados",
|
"blocked_users": "Usuários bloqueados",
|
||||||
"user_unblocked": "Usuário desbloqueado"
|
"user_unblocked": "Usuário desbloqueado",
|
||||||
|
"enable_achievement_notifications": "Quando uma conquista é desbloqueada"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
@@ -314,7 +332,6 @@
|
|||||||
"friend_code_copied": "Código de amigo copiado",
|
"friend_code_copied": "Código de amigo copiado",
|
||||||
"undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}",
|
"undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}",
|
||||||
"privacy_hint": "Pra controlar quem pode ver seu perfil, acesse a <0>Tela de Configurações</0>",
|
"privacy_hint": "Pra controlar quem pode ver seu perfil, acesse a <0>Tela de Configurações</0>",
|
||||||
"profile_locked": "Este perfil é privado",
|
|
||||||
"image_process_failure": "Falha ao processar a imagem",
|
"image_process_failure": "Falha ao processar a imagem",
|
||||||
"required_field": "Este campo é obrigatório",
|
"required_field": "Este campo é obrigatório",
|
||||||
"displayname_min_length": "Nome de exibição deve ter pelo menos 3 caracteres",
|
"displayname_min_length": "Nome de exibição deve ter pelo menos 3 caracteres",
|
||||||
@@ -331,9 +348,27 @@
|
|||||||
"report_reason_spam": "Spam",
|
"report_reason_spam": "Spam",
|
||||||
"report_reason_other": "Outro",
|
"report_reason_other": "Outro",
|
||||||
"profile_reported": "Perfil reportado",
|
"profile_reported": "Perfil reportado",
|
||||||
"your_friend_code": "Seu código de amigo:"
|
"your_friend_code": "Seu código de amigo:",
|
||||||
|
"upload_banner": "Carregar banner",
|
||||||
|
"uploading_banner": "Carregando banner…",
|
||||||
|
"background_image_updated": "Imagem de fundo salva"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada"
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
"your_achievements": "Suas Conquistas",
|
||||||
|
"user_achievements": "Conquistas de {{displayName}}",
|
||||||
|
"unlocked_at": "Desbloqueado em:",
|
||||||
|
"subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo",
|
||||||
|
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"subscription_tour_title": "Assinatura Hydra Cloud",
|
||||||
|
"subscribe_now": "Inscreva-se agora",
|
||||||
|
"cloud_achievements": "Salvamento de conquistas em nuvem",
|
||||||
|
"animated_profile_picture": "Fotos de perfil animadas",
|
||||||
|
"premium_support": "Suporte Premium",
|
||||||
|
"show_and_compare_achievements": "Exiba e compare suas conquistas com outros usuários",
|
||||||
|
"animated_profile_banner": "Banner animado no perfil",
|
||||||
|
"cloud_saving": "Saves de jogos em nuvem"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destaques",
|
"featured": "Destaques",
|
||||||
"trending": "Populares",
|
"hot": "Populares",
|
||||||
|
"weekly": "📅 Mais descarregados esta semana",
|
||||||
|
"achievements": "🏆 Para completar",
|
||||||
"surprise_me": "Surpreende-me",
|
"surprise_me": "Surpreende-me",
|
||||||
"no_results": "Nenhum resultado encontrado",
|
"no_results": "Nenhum resultado encontrado",
|
||||||
"start_typing": "Comece a digitar para pesquisar…"
|
"start_typing": "Começa a escrever para pesquisar…"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
@@ -16,13 +18,14 @@
|
|||||||
"settings": "Definições",
|
"settings": "Definições",
|
||||||
"my_library": "Biblioteca",
|
"my_library": "Biblioteca",
|
||||||
"downloading_metadata": "{{title}} (A transferir metadados…)",
|
"downloading_metadata": "{{title}} (A transferir metadados…)",
|
||||||
"paused": "{{title}} (Pausado)",
|
"paused": "{{title}} (Em pausa)",
|
||||||
"downloading": "{{title}} ({{percentage}} - A transferir…)",
|
"downloading": "{{title}} ({{percentage}} - A transferir…)",
|
||||||
"filter": "Procurar",
|
"filter": "Procurar",
|
||||||
"home": "Início",
|
"home": "Início",
|
||||||
"queued": "{{title}} (Na fila)",
|
"queued": "{{title}} (Na fila)",
|
||||||
"game_has_no_executable": "Jogo não tem executável selecionado",
|
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
||||||
"sign_in": "Iniciar sessão"
|
"sign_in": "Iniciar sessão",
|
||||||
|
"friends": "Amigos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Procurar jogos",
|
"search": "Procurar jogos",
|
||||||
@@ -31,8 +34,8 @@
|
|||||||
"search_results": "Resultados da pesquisa",
|
"search_results": "Resultados da pesquisa",
|
||||||
"settings": "Definições",
|
"settings": "Definições",
|
||||||
"home": "Início",
|
"home": "Início",
|
||||||
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
|
"version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.",
|
||||||
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
|
"version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download."
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Sem transferências em andamento",
|
"no_downloads_in_progress": "Sem transferências em andamento",
|
||||||
@@ -47,19 +50,19 @@
|
|||||||
"download_options_one": "{{count}} opção de transferência",
|
"download_options_one": "{{count}} opção de transferência",
|
||||||
"download_options_other": "{{count}} opções de transferência",
|
"download_options_other": "{{count}} opções de transferência",
|
||||||
"updated_at": "Atualizado a {{updated_at}}",
|
"updated_at": "Atualizado a {{updated_at}}",
|
||||||
"resume": "Retomar",
|
"resume": "Continuar",
|
||||||
"pause": "Pausar",
|
"pause": "Colocar em pausa",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"remove": "Remover",
|
"remove": "Remover",
|
||||||
"space_left_on_disk": "{{space}} livres no disco",
|
"space_left_on_disk": "{{space}} livres no disco",
|
||||||
"eta": "Conclusão {{eta}}",
|
"eta": "Conclusão {{eta}}",
|
||||||
"calculating_eta": "A calcular tempo restante…",
|
"calculating_eta": "A calcular tempo restante…",
|
||||||
"downloading_metadata": "A transferir metadados…",
|
"downloading_metadata": "A transferir metadados…",
|
||||||
"filter": "Filtrar repacks",
|
"filter": "Filtrar opções de transferência",
|
||||||
"requirements": "Requisitos do sistema",
|
"requirements": "Requisitos do sistema",
|
||||||
"minimum": "Mínimos",
|
"minimum": "Mínimos",
|
||||||
"recommended": "Recomendados",
|
"recommended": "Recomendados",
|
||||||
"paused": "Pausado",
|
"paused": "Em pausa",
|
||||||
"release_date": "Lançado em {{date}}",
|
"release_date": "Lançado em {{date}}",
|
||||||
"publisher": "Publicado por {{publisher}}",
|
"publisher": "Publicado por {{publisher}}",
|
||||||
"hours": "horas",
|
"hours": "horas",
|
||||||
@@ -70,24 +73,24 @@
|
|||||||
"add_to_library": "Adicionar à biblioteca",
|
"add_to_library": "Adicionar à biblioteca",
|
||||||
"remove_from_library": "Remover da biblioteca",
|
"remove_from_library": "Remover da biblioteca",
|
||||||
"no_downloads": "Nenhuma transferência disponível",
|
"no_downloads": "Nenhuma transferência disponível",
|
||||||
"play_time": "Jogou por {{amount}}",
|
"play_time": "Jogaste por {{amount}}",
|
||||||
"next_suggestion": "Próxima sugestão",
|
"next_suggestion": "Próxima sugestão",
|
||||||
"install": "Instalar",
|
"install": "Instalar",
|
||||||
"last_time_played": "Última sessão {{period}}",
|
"last_time_played": "Última sessão {{period}}",
|
||||||
"play": "Jogar",
|
"play": "Jogar",
|
||||||
"not_played_yet": "Ainda não jogou {{title}}",
|
"not_played_yet": "Ainda não jogaste {{title}}",
|
||||||
"close": "Fechar",
|
"close": "Fechar",
|
||||||
"deleting": "A eliminar instalador…",
|
"deleting": "A eliminar instalador…",
|
||||||
"playing_now": "A jogar agora",
|
"playing_now": "A jogar agora",
|
||||||
"change": "Explorar",
|
"change": "Explorar",
|
||||||
"repacks_modal_description": "Escolha o repack do jogo que deseja transferir",
|
"repacks_modal_description": "Escolhe a versão do jogo que desejas transferir",
|
||||||
"select_folder_hint": "Para trocar o diretório padrão, aceda à <0>Tela de Definições</0>",
|
"select_folder_hint": "Para alterar o local predefinido, acede às <0>Definições</0>",
|
||||||
"download_now": "Iniciar transferência",
|
"download_now": "Iniciar transferência",
|
||||||
"no_shop_details": "Não foi possível obter os detalhes da loja.",
|
"no_shop_details": "Não foi possível obter os detalhes da loja.",
|
||||||
"download_options": "Opções de transferência",
|
"download_options": "Opções de transferência",
|
||||||
"download_path": "Diretório de transferência",
|
"download_path": "Local de transferência",
|
||||||
"previous_screenshot": "Captura de ecrã anterior",
|
"previous_screenshot": "Captura de ecrã anterior",
|
||||||
"next_screenshot": "Próxima captura de ecrã",
|
"next_screenshot": "Captura de ecrã seguinte",
|
||||||
"screenshot": "Captura de ecrã {{number}}",
|
"screenshot": "Captura de ecrã {{number}}",
|
||||||
"open_screenshot": "Ver captura de ecrã {{number}}",
|
"open_screenshot": "Ver captura de ecrã {{number}}",
|
||||||
"download_settings": "Definições de transferência",
|
"download_settings": "Definições de transferência",
|
||||||
@@ -99,61 +102,99 @@
|
|||||||
"create_shortcut": "Criar atalho no ambiente de trabalho",
|
"create_shortcut": "Criar atalho no ambiente de trabalho",
|
||||||
"remove_files": "Remover ficheiros",
|
"remove_files": "Remover ficheiros",
|
||||||
"options": "Gerir",
|
"options": "Gerir",
|
||||||
"remove_from_library_description": "Isto irá remover {{game}} da sua biblioteca",
|
"remove_from_library_description": "Isto vai remover {{game}} da tua biblioteca",
|
||||||
"remove_from_library_title": "Tem a certeza?",
|
"remove_from_library_title": "Tens a certeza?",
|
||||||
"executable_section_title": "Executável",
|
"executable_section_title": "Executável",
|
||||||
"executable_section_description": "O caminho do ficheiro que será executado ao clicar em \"Jogar\"",
|
"executable_section_description": "O caminho do ficheiro que vai ser executado ao clicar em \"Jogar\"",
|
||||||
"downloads_secion_title": "Transferências",
|
"downloads_secion_title": "Transferências",
|
||||||
"downloads_section_description": "Confira atualizações ou versões diferentes para este mesmo título",
|
"downloads_section_description": "Encontra atualizações ou versões diferentes para este mesmo título",
|
||||||
"danger_zone_section_title": "Zona de perigo",
|
"danger_zone_section_title": "Zona de perigo",
|
||||||
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os ficheiros que foram transferidos pelo Hydra",
|
"danger_zone_section_description": "Remove o jogo da tua biblioteca ou os ficheiros que foram transferidos pelo Hydra",
|
||||||
"download_in_progress": "Transferência em andamento",
|
"download_in_progress": "Transferência em andamento",
|
||||||
"download_paused": "Transferência pausada",
|
"download_paused": "Transferência em pausa",
|
||||||
"last_downloaded_option": "Última opção transferida",
|
"last_downloaded_option": "Última opção transferida",
|
||||||
"create_shortcut_success": "Atalho criado com sucesso",
|
"create_shortcut_success": "Atalho criado com sucesso",
|
||||||
"create_shortcut_error": "Erro ao criar atalho",
|
"create_shortcut_error": "Erro ao criar atalho",
|
||||||
|
"nsfw_content_title": "Este jogo contém conteúdo inapropriado",
|
||||||
|
"nsfw_content_description": "{{title}} contém conteúdo que pode não ser apropriado para todas as idades. Desejas continuar?",
|
||||||
|
"allow_nsfw_content": "Continuar",
|
||||||
|
"refuse_nsfw_content": "Voltar",
|
||||||
|
"stats": "Estatísticas",
|
||||||
|
"download_count": "Transferências",
|
||||||
|
"player_count": "Jogadores ativos",
|
||||||
|
"download_error": "Esta opção de transferência falhou",
|
||||||
"download": "Transferir",
|
"download": "Transferir",
|
||||||
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
||||||
"warning": "Aviso:",
|
"warning": "Aviso:",
|
||||||
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
|
"hydra_needs_to_remain_open": "para esta transferência, o Hydra precisa de ficar aberto até a conclusão. Caso o Hydra encerre antes da transferência terminar, vais perder o teu progresso.",
|
||||||
"achievements": "Conquistas"
|
"achievements": "Conquistas",
|
||||||
|
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
|
||||||
|
"cloud_save": "Gravação na nuvem",
|
||||||
|
"cloud_save_description": "Mantém o teu progresso guardado na nuvem e continua de onde paraste em qualquer dispositivo",
|
||||||
|
"backups": "Backups",
|
||||||
|
"install_backup": "Restaurar",
|
||||||
|
"delete_backup": "Apagar",
|
||||||
|
"create_backup": "Novo backup",
|
||||||
|
"last_backup_date": "Último backup realizado em {{date}}",
|
||||||
|
"no_backup_preview": "Não foi possível encontrar nenhum ficheiro de gravação para este jogo",
|
||||||
|
"restoring_backup": "A restaurar backup ({{progress}} concluído)…",
|
||||||
|
"uploading_backup": "A criar backup…",
|
||||||
|
"no_backups": "Ainda não fizeste nenhum backup deste jogo",
|
||||||
|
"backup_uploaded": "Backup criado",
|
||||||
|
"backup_deleted": "Backup apagado",
|
||||||
|
"backup_restored": "Backup restaurado",
|
||||||
|
"see_all_achievements": "Ver todas as conquistas",
|
||||||
|
"sign_in_to_see_achievements": "Faz login para vez as conquistas",
|
||||||
|
"mapping_method_automatic": "Automático",
|
||||||
|
"mapping_method_manual": "Manual",
|
||||||
|
"mapping_method_label": "Modo de mapeamento",
|
||||||
|
"files_automatically_mapped": "Ficheiros automaticamente mapeados",
|
||||||
|
"no_backups_created": "Nenhum backup criado para este jogo",
|
||||||
|
"manage_files": "Gerir ficheiros",
|
||||||
|
"loading_save_preview": "A procurar ficheiros de gravação…",
|
||||||
|
"wine_prefix": "Prefixo Wine",
|
||||||
|
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
|
||||||
|
"no_download_option_info": "Sem informações disponíveis",
|
||||||
|
"backup_deletion_failed": "Falha ao apagar o backup",
|
||||||
|
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||||
|
"achievements_not_sync": "As tuas conquistas não estão sincronizadas"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
"installation_id": "ID da instalação:",
|
"installation_id": "ID da instalação:",
|
||||||
"enter_activation_code": "Insira o seu código de ativação",
|
"enter_activation_code": "Insere o teu código de ativação",
|
||||||
"message": "Se não sabe onde conseguir o código, talvez não devesse estar aqui.",
|
"message": "Se não souberes onde conseguir o código, talvez não devias estar aqui.",
|
||||||
"activate": "Ativar",
|
"activate": "Ativar",
|
||||||
"loading": "A carregar…"
|
"loading": "A carregar…"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "Retomar",
|
"resume": "Continuar",
|
||||||
"pause": "Pausar",
|
"pause": "Colocar em pausa",
|
||||||
"eta": "Conclusão {{eta}}",
|
"eta": "Conclusão {{eta}}",
|
||||||
"paused": "Pausado",
|
"paused": "Em pausa",
|
||||||
"verifying": "A verificar…",
|
"verifying": "A verificar…",
|
||||||
"completed": "Concluído",
|
"completed": "Concluído",
|
||||||
"removed": "Cancelado",
|
"removed": "Cancelado",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"filter": "Filtrar jogos transferidos",
|
"filter": "Filtrar jogos descarregados",
|
||||||
"remove": "Remover",
|
"remove": "Remover",
|
||||||
"downloading_metadata": "A transferir metadados…",
|
"downloading_metadata": "A transferir metadados…",
|
||||||
"delete": "Remover instalador",
|
"delete": "Remover instalador",
|
||||||
"delete_modal_description": "Isto removerá todos os ficheiros de instalação do seu computador",
|
"delete_modal_description": "Isto vai remover todos os ficheiros de instalação do teu computador",
|
||||||
"delete_modal_title": "Tem a certeza?",
|
"delete_modal_title": "Tens a certeza?",
|
||||||
"deleting": "A eliminar instalador…",
|
"deleting": "A remover o instalador…",
|
||||||
"install": "Instalar",
|
"install": "Instalar",
|
||||||
"download_in_progress": "A transferir agora",
|
"download_in_progress": "A descarregar agora",
|
||||||
"queued_downloads": "Na fila",
|
"queued_downloads": "Na fila",
|
||||||
"downloads_completed": "Concluído",
|
"downloads_completed": "Concluído",
|
||||||
"queued": "Na fila",
|
"queued": "Na fila",
|
||||||
"no_downloads_title": "Nada por aqui…",
|
"no_downloads_title": "Nada por aqui…",
|
||||||
"no_downloads_description": "Ainda não transferiu nada pelo Hydra, mas nunca é tarde para começar.",
|
"no_downloads_description": "Ainda não descarregaste nada pelo Hydra, mas nunca é tarde para começar.",
|
||||||
"checking_files": "A verificar ficheiros…"
|
"checking_files": "A verificar ficheiros…"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Diretório das transferências",
|
"downloads_path": "Local das transferências",
|
||||||
"change": "Explorar...",
|
"change": "Procurar...",
|
||||||
"notifications": "Notificações",
|
"notifications": "Notificações",
|
||||||
"enable_download_notifications": "Quando uma transferência for concluída",
|
"enable_download_notifications": "Quando uma transferência for concluída",
|
||||||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||||
@@ -166,60 +207,72 @@
|
|||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token de API",
|
"real_debrid_api_token": "Token de API",
|
||||||
"enable_real_debrid": "Ativar Real-Debrid",
|
"enable_real_debrid": "Ativar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Pode obter o seu token de API <0>aqui</0>",
|
"real_debrid_api_token_hint": "Podes obter o teu token de API <0>aqui</0>",
|
||||||
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite transferir ficheiros instantaneamente e com a melhor velocidade da sua Internet.",
|
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite descarregar ficheiros instantaneamente e com a melhor velocidade da tua Internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"real_debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreva o Real-Debrid",
|
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreve o Real-Debrid",
|
||||||
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
"real_debrid_linked_message": "Conta \"{{username}}\" associada",
|
||||||
"save_changes": "Guardar alterações",
|
"save_changes": "Guardar alterações",
|
||||||
"changes_saved": "Definições guardadas com sucesso",
|
"changes_saved": "Alterações guardadas com sucesso",
|
||||||
"download_sources_description": "O Hydra vai procurar links de transferência em todas as fontes ativadas. A URL da página de detalhes da loja não é guardada no seu dispositivo. Utilizamos um sistema de metadados criado pela comunidade para fornecer suporte a mais fontes de transferência de jogos.",
|
"download_sources_description": "O Hydra vai procurar links de download em todas as fontes ativadas. O URL da fonte deve ser um link direto para um ficheiro .json que contenha uma lista de links.",
|
||||||
"validate_download_source": "Validar",
|
"validate_download_source": "Validar",
|
||||||
"remove_download_source": "Remover",
|
"remove_download_source": "Remover",
|
||||||
"add_download_source": "Adicionar fonte",
|
"add_download_source": "Adicionar fonte",
|
||||||
"download_count_zero": "Sem transferências na lista",
|
"download_count_zero": "Sem downloads na lista",
|
||||||
"download_count_one": "{{countFormatted}} transferência na lista",
|
"download_count_one": "{{countFormatted}} download na lista",
|
||||||
"download_count_other": "{{countFormatted}} transferências na lista",
|
"download_count_other": "{{countFormatted}} downloads na lista",
|
||||||
"download_options_zero": "Sem transferências disponíveis",
|
"download_options_zero": "Sem downloads disponíveis",
|
||||||
"download_options_one": "{{countFormatted}} transferência disponível",
|
"download_options_one": "{{countFormatted}} download disponível",
|
||||||
"download_options_other": "{{countFormatted}} transferências disponíveis",
|
"download_options_other": "{{countFormatted}} downloads disponíveis",
|
||||||
"download_source_url": "URL da fonte",
|
"download_source_url": "URL da fonte",
|
||||||
"add_download_source_description": "Insira o URL contendo o arquivo .json",
|
"add_download_source_description": "Insere o URL que contém o ficheiro .json",
|
||||||
"download_source_up_to_date": "Sincronizada",
|
"download_source_up_to_date": "Sincronizada",
|
||||||
"download_source_errored": "Falhou",
|
"download_source_errored": "Falhou",
|
||||||
"sync_download_sources": "Sincronizar",
|
"sync_download_sources": "Sincronizar",
|
||||||
"removed_download_source": "Fonte removida",
|
"removed_download_source": "Fonte removida",
|
||||||
"added_download_source": "Fonte adicionada",
|
"added_download_source": "Fonte adicionada",
|
||||||
"download_sources_synced": "As fontes foram sincronizadas",
|
"download_sources_synced": "As fontes foram sincronizadas",
|
||||||
"insert_valid_json_url": "Insira o URL de um JSON válido",
|
"insert_valid_json_url": "Insere o URL de um JSON válido",
|
||||||
"found_download_option_zero": "Nenhuma opção de transferência encontrada",
|
"found_download_option_zero": "Nenhuma opção de transferência encontrada",
|
||||||
"found_download_option_one": "{{countFormatted}} opção de transferência encontrada",
|
"found_download_option_one": "{{countFormatted}} opção de transferência encontrada",
|
||||||
"found_download_option_other": "{{countFormatted}} opções de transferências encontradas",
|
"found_download_option_other": "{{countFormatted}} opções de transferência encontradas",
|
||||||
"import": "Importar"
|
"import": "Importar",
|
||||||
|
"privacy": "Privacidade",
|
||||||
|
"private": "Privado",
|
||||||
|
"friends_only": "Apenas amigos",
|
||||||
|
"public": "Público",
|
||||||
|
"profile_visibility": "Visibilidade do perfil",
|
||||||
|
"profile_visibility_description": "Escolhe quem pode ver o teu perfil e biblioteca",
|
||||||
|
"required_field": "Este campo é obrigatório",
|
||||||
|
"source_already_exists": "Essa fonte já foi adicionada",
|
||||||
|
"must_be_valid_url": "A fonte deve ser um URL válido",
|
||||||
|
"blocked_users": "Utilizadores bloqueados",
|
||||||
|
"user_unblocked": "Utilizador desbloqueado",
|
||||||
|
"enable_achievement_notifications": "Quando uma conquista é desbloqueada"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Transferência concluída",
|
"download_complete": "Transferência concluída",
|
||||||
"game_ready_to_install": "{{title}} está pronto para ser descarregado",
|
"game_ready_to_install": "{{title}} está pronto para ser instalado",
|
||||||
"repack_list_updated": "Lista de repacks atualizada",
|
"repack_list_updated": "Lista de repacks atualizada",
|
||||||
"repack_count_one": "{{count}} novo repack",
|
"repack_count_one": "{{count}} novo repack",
|
||||||
"repack_count_other": "{{count}} novos repacks",
|
"repack_count_other": "{{count}} novos repacks",
|
||||||
"new_update_available": "Versão {{version}} disponível",
|
"new_update_available": "Versão {{version}} disponível",
|
||||||
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão"
|
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Abrir Hydra",
|
"open": "Abrir o Hydra",
|
||||||
"quit": "Fechar"
|
"quit": "Sair"
|
||||||
},
|
},
|
||||||
"game_card": {
|
"game_card": {
|
||||||
"no_downloads": "Sem transferências disponíveis"
|
"no_downloads": "Sem downloads disponíveis"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Programas não instalados",
|
"title": "Programas não instalados",
|
||||||
"description": "Os executáveis do Wine ou Lutris não foram encontrados em seu sistema.",
|
"description": "Os executáveis do Wine ou Lutris não foram encontrados no teu sistema.",
|
||||||
"instructions": "Verifique a forma correta de instalar algum deles na sua distro Linux, garantindo assim a execução normal do jogo."
|
"instructions": "Verifica a forma correta de instalar algum deles na tua distribuição Linux, para garantir a execução normal do jogo"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "Próxima página",
|
"next_page": "Página seguinte",
|
||||||
"previous_page": "Página anterior"
|
"previous_page": "Página anterior"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
@@ -232,24 +285,24 @@
|
|||||||
"amount_hours": "{{amount}} horas",
|
"amount_hours": "{{amount}} horas",
|
||||||
"amount_minutes": "{{amount}} minutos",
|
"amount_minutes": "{{amount}} minutos",
|
||||||
"last_time_played": "Última sessão {{period}}",
|
"last_time_played": "Última sessão {{period}}",
|
||||||
"activity": "Atividades recentes",
|
"activity": "Atividade recente",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"total_play_time": "Tempo total de jogo: {{amount}}",
|
"total_play_time": "Tempo total de jogo: {{amount}}",
|
||||||
"no_recent_activity_title": "Hmmm… nada por aqui",
|
"no_recent_activity_title": "Hmmm… não há nada por aqui",
|
||||||
"no_recent_activity_description": "Parece que não jogaste nada recentemente. Que tal começar agora?",
|
"no_recent_activity_description": "Parece que não jogaste nada recentemente. Que tal começar agora?",
|
||||||
"display_name": "Nome de exibição",
|
"display_name": "Nome de apresentação",
|
||||||
"saving": "a guardar…",
|
"saving": "A guardar…",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"edit_profile": "Editar perfil",
|
"edit_profile": "Editar perfil",
|
||||||
"saved_successfully": "Guardado com sucesso",
|
"saved_successfully": "Guardado com sucesso",
|
||||||
"try_again": "Por favor, tenta novamente",
|
"try_again": "Por favor, tenta novamente",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"successfully_signed_out": "Terminado com sucesso",
|
"successfully_signed_out": "Sessão terminada com sucesso",
|
||||||
"sign_out": "Terminar sessão",
|
"sign_out": "Terminar sessão",
|
||||||
"sign_out_modal_title": "Tens a certeza?",
|
"sign_out_modal_title": "Desejas mesmo terminar sessão?",
|
||||||
"playing_for": "A jogar há {{amount}}",
|
"playing_for": "A jogar por {{amount}}",
|
||||||
"sign_out_modal_text": "A tua biblioteca de jogos está associada com a tua conta atual. Ao sair, a tua biblioteca não aparecerá mais no Hydra e qualquer progresso não será guardado. Desejas continuar?",
|
"sign_out_modal_text": "A tua biblioteca de jogos está associada à conta atual. Ao terminar sessão, a tua biblioteca não irá aparecer mais no Hydra e qualquer progresso não será guardado. Desejas continuar?",
|
||||||
"add_friends": "Adicionar Amigos",
|
"add_friends": "Adicionar amigos",
|
||||||
"friend_code": "Código de amigo",
|
"friend_code": "Código de amigo",
|
||||||
"see_profile": "Ver perfil",
|
"see_profile": "Ver perfil",
|
||||||
"friend_request_sent": "Pedido de amizade enviado",
|
"friend_request_sent": "Pedido de amizade enviado",
|
||||||
@@ -271,15 +324,49 @@
|
|||||||
"user_block_modal_text": "Bloquear {{displayName}}",
|
"user_block_modal_text": "Bloquear {{displayName}}",
|
||||||
"blocked_users": "Utilizadores bloqueados",
|
"blocked_users": "Utilizadores bloqueados",
|
||||||
"unblock": "Desbloquear",
|
"unblock": "Desbloquear",
|
||||||
"no_friends_added": "Ainda não adicionaste amigos",
|
"no_friends_added": "Ainda não adicionaste nenhum amigo",
|
||||||
"pending": "Pendentes",
|
"pending": "Pendentes",
|
||||||
"no_pending_invites": "Não tens convites de amizade pendentes",
|
"no_pending_invites": "Não tens pedidos de amizade pendentes",
|
||||||
"no_blocked_users": "Não tens nenhum utilizador bloqueado",
|
"no_blocked_users": "Não tens nenhum utilizador bloqueado",
|
||||||
"friend_code_copied": "Código de amigo copiado",
|
"friend_code_copied": "Código de amigo copiado",
|
||||||
|
"undo_friendship_modal_text": "Isto vai remover a tua amizade com {{displayName}}",
|
||||||
|
"privacy_hint": "Para controlar quem pode ver o teu perfil, acede às <0>Definições</0>",
|
||||||
|
"profile_locked": "Este perfil é privado",
|
||||||
"image_process_failure": "Falha ao processar a imagem",
|
"image_process_failure": "Falha ao processar a imagem",
|
||||||
"your_friend_code": "Seu código de amigo:"
|
"required_field": "Este campo é obrigatório",
|
||||||
|
"displayname_min_length": "O nome de apresentação deve ter pelo menos 3 caracteres",
|
||||||
|
"displayname_max_length": "O nome de apresentação deve ter no máximo 50 caracteres",
|
||||||
|
"locked_profile": "Este perfil é privado",
|
||||||
|
"report_profile": "Denunciar este perfil",
|
||||||
|
"report_reason": "Por que é que desejas denunciar este perfil?",
|
||||||
|
"report_description": "Informações adicionais",
|
||||||
|
"report_description_placeholder": "Insere aqui",
|
||||||
|
"report": "Denunciar",
|
||||||
|
"report_reason_hate": "Discurso de ódio",
|
||||||
|
"report_reason_sexual_content": "Conteúdo sexual",
|
||||||
|
"report_reason_violence": "Violência",
|
||||||
|
"report_reason_spam": "Spam",
|
||||||
|
"report_reason_other": "Outro",
|
||||||
|
"profile_reported": "Perfil denunciado",
|
||||||
|
"your_friend_code": "O teu código de amigo:",
|
||||||
|
"upload_banner": "Fazer upload do banner",
|
||||||
|
"uploading_banner": "A fazer upload do banner…"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada"
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
"your_achievements": "As tuas Conquistas",
|
||||||
|
"user_achievements": "Conquistas de {{displayName}}",
|
||||||
|
"unlocked_at": "Desbloqueada em:",
|
||||||
|
"subscription_needed": "Precisas de uma subscrição Hydra Cloud para visualizar este conteúdo",
|
||||||
|
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"subscription_tour_title": "Subscrição Hydra Cloud",
|
||||||
|
"subscribe_now": "Subscreve agora",
|
||||||
|
"cloud_achievements": "Gravação de conquistas na nuvem",
|
||||||
|
"animated_profile_picture": "Fotos de perfil animadas",
|
||||||
|
"premium_support": "Apoio Premium",
|
||||||
|
"show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores",
|
||||||
|
"animated_profile_banner": "Banner animado no perfil"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "Română",
|
"language_name": "Română",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Recomandate",
|
"featured": "Recomandate",
|
||||||
"trending": "Populare",
|
|
||||||
"surprise_me": "Surprinde-mă",
|
"surprise_me": "Surprinde-mă",
|
||||||
"no_results": "Niciun rezultat găsit"
|
"no_results": "Niciun rezultat găsit"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Рекомендованное",
|
"featured": "Рекомендованное",
|
||||||
"trending": "В тренде",
|
|
||||||
"surprise_me": "Удиви меня",
|
"surprise_me": "Удиви меня",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
"hot": "Сейчас жарко",
|
"hot": "Сейчас жарко",
|
||||||
@@ -206,9 +205,6 @@
|
|||||||
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
|
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
|
||||||
"import": "Импортировать",
|
"import": "Импортировать",
|
||||||
"blocked_users": "Заблокированные пользователи",
|
"blocked_users": "Заблокированные пользователи",
|
||||||
"download_options_one": "",
|
|
||||||
"download_options_other": "",
|
|
||||||
"download_options_zero": "",
|
|
||||||
"friends_only": "Только друзья",
|
"friends_only": "Только друзья",
|
||||||
"must_be_valid_url": "Источник должен быть действительным URL-адресом.",
|
"must_be_valid_url": "Источник должен быть действительным URL-адресом.",
|
||||||
"privacy": "Конфиденциальность",
|
"privacy": "Конфиденциальность",
|
||||||
@@ -300,7 +296,6 @@
|
|||||||
"image_process_failure": "Сбой при обработке изображения",
|
"image_process_failure": "Сбой при обработке изображения",
|
||||||
"locked_profile": "Этот профиль является частным",
|
"locked_profile": "Этот профиль является частным",
|
||||||
"privacy_hint": "Чтобы указать, кто может это видеть, перейдите в <0>Настройки</0>.",
|
"privacy_hint": "Чтобы указать, кто может это видеть, перейдите в <0>Настройки</0>.",
|
||||||
"profile_locked": "",
|
|
||||||
"profile_reported": "Профиль сообщил",
|
"profile_reported": "Профиль сообщил",
|
||||||
"report": "Отчет",
|
"report": "Отчет",
|
||||||
"report_description": "Дополнительная информация",
|
"report_description": "Дополнительная информация",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"language_name": "Türkçe",
|
"language_name": "Türkçe",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Öne çıkan",
|
"featured": "Öne çıkan",
|
||||||
"trending": "Popüler",
|
|
||||||
"surprise_me": "Şaşırt beni",
|
"surprise_me": "Şaşırt beni",
|
||||||
"no_results": "Sonuç bulunamadı"
|
"no_results": "Sonuç bulunamadı"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Рекомендоване",
|
"featured": "Рекомендоване",
|
||||||
"trending": "У тренді",
|
|
||||||
"surprise_me": "Здивуй мене",
|
"surprise_me": "Здивуй мене",
|
||||||
"no_results": "Результатів не знайдено"
|
"no_results": "Результатів не знайдено"
|
||||||
},
|
},
|
||||||
@@ -161,9 +160,6 @@
|
|||||||
"download_count_one": "{{countFormatted}} завантаження в списку",
|
"download_count_one": "{{countFormatted}} завантаження в списку",
|
||||||
"download_count_other": "{{countFormatted}} завантажень в списку",
|
"download_count_other": "{{countFormatted}} завантажень в списку",
|
||||||
"download_count_zero": "В списку немає завантажень",
|
"download_count_zero": "В списку немає завантажень",
|
||||||
"download_options_one": "{{countFormatted}} доступний варіант завантаження",
|
|
||||||
"download_options_other": "{{countFormatted}} доступних варіантів завантаження",
|
|
||||||
"download_options_zero": "Немає доступних завантажень",
|
|
||||||
"download_source_errored": "Помилка",
|
"download_source_errored": "Помилка",
|
||||||
"download_source_up_to_date": "Оновлено",
|
"download_source_up_to_date": "Оновлено",
|
||||||
"download_source_url": "Посилання на джерело",
|
"download_source_url": "Посилання на джерело",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "特色推荐",
|
"featured": "特色推荐",
|
||||||
"trending": "最近热门",
|
|
||||||
"surprise_me": "向我推荐",
|
"surprise_me": "向我推荐",
|
||||||
"no_results": "没有找到结果"
|
"no_results": "没有找到结果"
|
||||||
},
|
},
|
||||||
@@ -170,9 +169,6 @@
|
|||||||
"download_count_zero": "列表中无下载",
|
"download_count_zero": "列表中无下载",
|
||||||
"download_count_one": "列表中有 {{countFormatted}} 个下载",
|
"download_count_one": "列表中有 {{countFormatted}} 个下载",
|
||||||
"download_count_other": "列表中有 {{countFormatted}} 个下载",
|
"download_count_other": "列表中有 {{countFormatted}} 个下载",
|
||||||
"download_options_zero": "无可用下载",
|
|
||||||
"download_options_one": "有 {{countFormatted}} 个下载可用",
|
|
||||||
"download_options_other": "有 {{countFormatted}} 个下载可用",
|
|
||||||
"download_source_url": "下载源 URL",
|
"download_source_url": "下载源 URL",
|
||||||
"add_download_source_description": "插入包含 .json 文件的 URL",
|
"add_download_source_description": "插入包含 .json 文件的 URL",
|
||||||
"download_source_up_to_date": "已更新",
|
"download_source_up_to_date": "已更新",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
|
export const LUDUSAVI_MANIFEST_URL = "https://cdn.losbroxas.org/manifest.yaml";
|
||||||
|
|
||||||
export const defaultDownloadsPath = app.getPath("downloads");
|
export const defaultDownloadsPath = app.getPath("downloads");
|
||||||
|
|
||||||
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
UserPreferences,
|
UserPreferences,
|
||||||
UserAuth,
|
UserAuth,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
|
UserSubscription,
|
||||||
} from "@main/entity";
|
} from "@main/entity";
|
||||||
|
|
||||||
import { databasePath } from "./constants";
|
import { databasePath } from "./constants";
|
||||||
@@ -17,11 +18,12 @@ export const dataSource = new DataSource({
|
|||||||
entities: [
|
entities: [
|
||||||
Game,
|
Game,
|
||||||
Repack,
|
Repack,
|
||||||
|
UserAuth,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
|
UserSubscription,
|
||||||
GameShopCache,
|
GameShopCache,
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
DownloadQueue,
|
DownloadQueue,
|
||||||
UserAuth,
|
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
],
|
],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export class GameAchievement {
|
|||||||
shop: string;
|
shop: string;
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
unlockedAchievements: string;
|
unlockedAchievements: string | null;
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
achievements: string;
|
achievements: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export class GameShopCache {
|
|||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
serializedData: string;
|
serializedData: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use IndexedDB's `howLongToBeatEntries` instead
|
||||||
|
*/
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
howLongToBeatSerializedData: string;
|
howLongToBeatSerializedData: string;
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export class Game {
|
|||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
executablePath: string | null;
|
executablePath: string | null;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true })
|
||||||
|
winePrefixPath: string | null;
|
||||||
|
|
||||||
@Column("int", { default: 0 })
|
@Column("int", { default: 0 })
|
||||||
playTimeInMilliseconds: number;
|
playTimeInMilliseconds: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
export * from "./game.entity";
|
export * from "./game.entity";
|
||||||
export * from "./repack.entity";
|
export * from "./repack.entity";
|
||||||
|
export * from "./user-auth.entity";
|
||||||
export * from "./user-preferences.entity";
|
export * from "./user-preferences.entity";
|
||||||
|
export * from "./user-subscription.entity";
|
||||||
export * from "./game-shop-cache.entity";
|
export * from "./game-shop-cache.entity";
|
||||||
export * from "./game.entity";
|
export * from "./game.entity";
|
||||||
export * from "./game-achievements.entity";
|
export * from "./game-achievements.entity";
|
||||||
export * from "./download-source.entity";
|
export * from "./download-source.entity";
|
||||||
export * from "./download-queue.entity";
|
export * from "./download-queue.entity";
|
||||||
export * from "./user-auth";
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
|
OneToOne,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
|
import { UserSubscription } from "./user-subscription.entity";
|
||||||
|
|
||||||
@Entity("user_auth")
|
@Entity("user_auth")
|
||||||
export class UserAuth {
|
export class UserAuth {
|
||||||
@@ -20,6 +22,9 @@ export class UserAuth {
|
|||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true })
|
||||||
|
backgroundImageUrl: string | null;
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
@Column("text", { default: "" })
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
|
||||||
@@ -29,6 +34,9 @@ export class UserAuth {
|
|||||||
@Column("int", { default: 0 })
|
@Column("int", { default: 0 })
|
||||||
tokenExpirationTimestamp: number;
|
tokenExpirationTimestamp: number;
|
||||||
|
|
||||||
|
@OneToOne("UserSubscription", "user")
|
||||||
|
subscription: UserSubscription | null;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@@ -26,6 +26,9 @@ export class UserPreferences {
|
|||||||
@Column("boolean", { default: false })
|
@Column("boolean", { default: false })
|
||||||
repackUpdatesNotificationsEnabled: boolean;
|
repackUpdatesNotificationsEnabled: boolean;
|
||||||
|
|
||||||
|
@Column("boolean", { default: true })
|
||||||
|
achievementNotificationsEnabled: boolean;
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
@Column("boolean", { default: false })
|
||||||
preferQuitInsteadOfHiding: boolean;
|
preferQuitInsteadOfHiding: boolean;
|
||||||
|
|
||||||
|
|||||||
42
src/main/entity/user-subscription.entity.ts
Normal file
42
src/main/entity/user-subscription.entity.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { SubscriptionStatus } from "@types";
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
OneToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from "typeorm";
|
||||||
|
import { UserAuth } from "./user-auth.entity";
|
||||||
|
|
||||||
|
@Entity("user_subscription")
|
||||||
|
export class UserSubscription {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column("text", { default: "" })
|
||||||
|
subscriptionId: string;
|
||||||
|
|
||||||
|
@OneToOne("UserAuth", "subscription")
|
||||||
|
@JoinColumn()
|
||||||
|
user: UserAuth;
|
||||||
|
|
||||||
|
@Column("text", { default: "" })
|
||||||
|
status: SubscriptionStatus;
|
||||||
|
|
||||||
|
@Column("text", { default: "" })
|
||||||
|
planId: string;
|
||||||
|
|
||||||
|
@Column("text", { default: "" })
|
||||||
|
planName: string;
|
||||||
|
|
||||||
|
@Column("datetime", { nullable: true })
|
||||||
|
expiresAt: Date | null;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
gamesPlaytime,
|
gamesPlaytime,
|
||||||
} from "@main/services";
|
} from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import { DownloadQueue, Game, UserAuth } from "@main/entity";
|
import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity";
|
||||||
|
|
||||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const databaseOperations = dataSource
|
const databaseOperations = dataSource
|
||||||
@@ -19,6 +19,10 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
await transactionalEntityManager
|
await transactionalEntityManager
|
||||||
.getRepository(UserAuth)
|
.getRepository(UserAuth)
|
||||||
.delete({ id: 1 });
|
.delete({ id: 1 });
|
||||||
|
|
||||||
|
await transactionalEntityManager
|
||||||
|
.getRepository(UserSubscription)
|
||||||
|
.delete({ id: 1 });
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
/* Removes all games being played */
|
/* Removes all games being played */
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppUpdaterEvent } from "@types";
|
import type { AppUpdaterEvent } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import updater, { UpdateInfo } from "electron-updater";
|
import updater, { UpdateInfo } from "electron-updater";
|
||||||
import { WindowManager } from "@main/services";
|
import { WindowManager } from "@main/services";
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const getCatalogue = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
|
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
|
||||||
`/games/${category}?${params.toString()}`,
|
`/catalogue/${category}?${params.toString()}`,
|
||||||
{},
|
{},
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import type { GameAchievement, GameShop } from "@types";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { gameAchievementRepository } from "@main/repository";
|
|
||||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
|
||||||
|
|
||||||
const getGameAchievements = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
objectId: string,
|
|
||||||
shop: GameShop
|
|
||||||
): Promise<GameAchievement[]> => {
|
|
||||||
const cachedAchievements = await gameAchievementRepository.findOne({
|
|
||||||
where: { objectId, shop },
|
|
||||||
});
|
|
||||||
|
|
||||||
const achievementsData = cachedAchievements?.achievements
|
|
||||||
? JSON.parse(cachedAchievements.achievements)
|
|
||||||
: await getGameAchievementData(objectId, shop);
|
|
||||||
|
|
||||||
const unlockedAchievements = JSON.parse(
|
|
||||||
cachedAchievements?.unlockedAchievements || "[]"
|
|
||||||
) as { name: string; unlockTime: number }[];
|
|
||||||
|
|
||||||
return achievementsData
|
|
||||||
.map((achievementData) => {
|
|
||||||
const unlockedAchiement = unlockedAchievements.find(
|
|
||||||
(localAchievement) => {
|
|
||||||
return (
|
|
||||||
localAchievement.name.toUpperCase() ==
|
|
||||||
achievementData.name.toUpperCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (unlockedAchiement) {
|
|
||||||
return {
|
|
||||||
...achievementData,
|
|
||||||
unlocked: true,
|
|
||||||
unlockTime: unlockedAchiement.unlockTime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...achievementData, unlocked: false, unlockTime: null };
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.unlocked && !b.unlocked) return -1;
|
|
||||||
if (!a.unlocked && b.unlocked) return 1;
|
|
||||||
return b.unlockTime - a.unlockTime;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getGameAchievements", getGameAchievements);
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { GameShop } from "@types";
|
import type { GameShop, GameStats } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import type { GameStats } from "@types";
|
|
||||||
|
|
||||||
const getGameStats = async (
|
const getGameStats = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
|||||||
@@ -1,45 +1,23 @@
|
|||||||
import type { GameShop, HowLongToBeatCategory } from "@types";
|
import type { HowLongToBeatCategory } from "@types";
|
||||||
import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services";
|
import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameShopCacheRepository } from "@main/repository";
|
import { formatName } from "@shared";
|
||||||
|
|
||||||
const getHowLongToBeat = async (
|
const getHowLongToBeat = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
|
||||||
shop: GameShop,
|
|
||||||
title: string
|
title: string
|
||||||
): Promise<HowLongToBeatCategory[] | null> => {
|
): Promise<HowLongToBeatCategory[] | null> => {
|
||||||
const searchHowLongToBeatPromise = searchHowLongToBeat(title);
|
const response = await searchHowLongToBeat(title);
|
||||||
|
|
||||||
const gameShopCache = await gameShopCacheRepository.findOne({
|
const game = response.data.find((game) => {
|
||||||
where: { objectID: objectId, shop },
|
return formatName(game.game_name) === formatName(title);
|
||||||
});
|
});
|
||||||
|
|
||||||
const howLongToBeatCachedData = gameShopCache?.howLongToBeatSerializedData
|
if (!game) return null;
|
||||||
? JSON.parse(gameShopCache?.howLongToBeatSerializedData)
|
const howLongToBeat = await getHowLongToBeatGame(String(game.game_id));
|
||||||
: null;
|
|
||||||
if (howLongToBeatCachedData) return howLongToBeatCachedData;
|
|
||||||
|
|
||||||
return searchHowLongToBeatPromise.then(async (response) => {
|
return howLongToBeat;
|
||||||
const game = response.data.find(
|
|
||||||
(game) => game.profile_steam === Number(objectId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!game) return null;
|
|
||||||
const howLongToBeat = await getHowLongToBeatGame(String(game.game_id));
|
|
||||||
|
|
||||||
gameShopCacheRepository.upsert(
|
|
||||||
{
|
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
howLongToBeatSerializedData: JSON.stringify(howLongToBeat),
|
|
||||||
},
|
|
||||||
["objectID"]
|
|
||||||
);
|
|
||||||
|
|
||||||
return howLongToBeat;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getHowLongToBeat", getHowLongToBeat);
|
registerEvent("getHowLongToBeat", getHowLongToBeat);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
import { TrendingGame } from "@types";
|
import type { TrendingGame } from "@types";
|
||||||
|
|
||||||
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await userPreferencesRepository.findOne({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||||
import { CatalogueEntry } from "@types";
|
import type { CatalogueEntry } from "@types";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
const searchGamesEvent = async (
|
const searchGamesEvent = async (
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { GameShop } from "@types";
|
|
||||||
import { Ludusavi } from "@main/services";
|
|
||||||
|
|
||||||
const checkGameCloudSyncSupport = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
objectId: string,
|
|
||||||
shop: GameShop
|
|
||||||
) => {
|
|
||||||
const games = await Ludusavi.findGames(shop, objectId);
|
|
||||||
return games.length === 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("checkGameCloudSyncSupport", checkGameCloudSyncSupport);
|
|
||||||
@@ -3,12 +3,14 @@ import fs from "node:fs";
|
|||||||
import * as tar from "tar";
|
import * as tar from "tar";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import os from "node:os";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { backupsPath } from "@main/constants";
|
import { backupsPath } from "@main/constants";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
import YAML from "yaml";
|
import YAML from "yaml";
|
||||||
|
import { normalizePath } from "@main/helpers";
|
||||||
|
|
||||||
export interface LudusaviBackup {
|
export interface LudusaviBackup {
|
||||||
files: {
|
files: {
|
||||||
@@ -20,9 +22,11 @@ export interface LudusaviBackup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const replaceLudusaviBackupWithCurrentUser = (
|
const replaceLudusaviBackupWithCurrentUser = (
|
||||||
gameBackupPath: string,
|
backupPath: string,
|
||||||
backupHomeDir: string
|
title: string,
|
||||||
|
homeDir: string
|
||||||
) => {
|
) => {
|
||||||
|
const gameBackupPath = path.join(backupPath, title);
|
||||||
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
|
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
|
||||||
|
|
||||||
const data = fs.readFileSync(mappingYamlPath, "utf8");
|
const data = fs.readFileSync(mappingYamlPath, "utf8");
|
||||||
@@ -31,31 +35,30 @@ const replaceLudusaviBackupWithCurrentUser = (
|
|||||||
drives: Record<string, string>;
|
drives: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentHomeDir = app.getPath("home");
|
const currentHomeDir = normalizePath(app.getPath("home"));
|
||||||
|
|
||||||
// TODO: Only works on Windows
|
/* Renaming logic */
|
||||||
const usersDirPath = path.join(gameBackupPath, "drive-C", "Users");
|
if (os.platform() === "win32") {
|
||||||
|
const mappedHomeDir = path.join(
|
||||||
|
gameBackupPath,
|
||||||
|
path.join("drive-C", homeDir.replace("C:", ""))
|
||||||
|
);
|
||||||
|
|
||||||
const oldPath = path.join(usersDirPath, path.basename(backupHomeDir));
|
if (fs.existsSync(mappedHomeDir)) {
|
||||||
const newPath = path.join(usersDirPath, path.basename(currentHomeDir));
|
fs.renameSync(
|
||||||
|
mappedHomeDir,
|
||||||
// Directories are different, rename
|
path.join(gameBackupPath, "drive-C", currentHomeDir.replace("C:", ""))
|
||||||
if (backupHomeDir !== currentHomeDir) {
|
);
|
||||||
if (fs.existsSync(newPath)) {
|
|
||||||
fs.rmSync(newPath, {
|
|
||||||
recursive: true,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.renameSync(oldPath, newPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const backups = manifest.backups.map((backup: LudusaviBackup) => {
|
const backups = manifest.backups.map((backup: LudusaviBackup) => {
|
||||||
const files = Object.entries(backup.files).reduce((prev, [key, value]) => {
|
const files = Object.entries(backup.files).reduce((prev, [key, value]) => {
|
||||||
|
const updatedKey = key.replace(homeDir, currentHomeDir);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[key.replace(backupHomeDir, currentHomeDir)]: value,
|
[updatedKey]: value,
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
@@ -74,66 +77,71 @@ const downloadGameArtifact = async (
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
gameArtifactId: string
|
gameArtifactId: string
|
||||||
) => {
|
) => {
|
||||||
const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
|
try {
|
||||||
downloadUrl: string;
|
const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
|
||||||
objectKey: string;
|
downloadUrl: string;
|
||||||
homeDir: string;
|
objectKey: string;
|
||||||
}>(`/profile/games/artifacts/${gameArtifactId}/download`);
|
homeDir: string;
|
||||||
|
}>(`/profile/games/artifacts/${gameArtifactId}/download`);
|
||||||
|
|
||||||
const zipLocation = path.join(app.getPath("userData"), objectKey);
|
const zipLocation = path.join(app.getPath("userData"), objectKey);
|
||||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||||
|
|
||||||
if (fs.existsSync(backupPath)) {
|
if (fs.existsSync(backupPath)) {
|
||||||
fs.rmSync(backupPath, {
|
fs.rmSync(backupPath, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true,
|
force: true,
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.get(downloadUrl, {
|
|
||||||
responseType: "stream",
|
|
||||||
onDownloadProgress: (progressEvent) => {
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
|
||||||
`on-backup-download-progress-${objectId}-${shop}`,
|
|
||||||
progressEvent
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const writer = fs.createWriteStream(zipLocation);
|
|
||||||
|
|
||||||
response.data.pipe(writer);
|
|
||||||
|
|
||||||
writer.on("error", (err) => {
|
|
||||||
logger.error("Failed to write zip", err);
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.mkdirSync(backupPath, { recursive: true });
|
|
||||||
|
|
||||||
writer.on("close", () => {
|
|
||||||
tar
|
|
||||||
.x({
|
|
||||||
file: zipLocation,
|
|
||||||
cwd: backupPath,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
const [game] = await Ludusavi.findGames(shop, objectId);
|
|
||||||
if (!game) throw new Error("Game not found in Ludusavi manifest");
|
|
||||||
|
|
||||||
replaceLudusaviBackupWithCurrentUser(
|
|
||||||
path.join(backupPath, game),
|
|
||||||
path.normalize(homeDir).replace(/\\/g, "/")
|
|
||||||
);
|
|
||||||
|
|
||||||
Ludusavi.restoreBackup(backupPath).then(() => {
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
|
||||||
`on-backup-download-complete-${objectId}-${shop}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const response = await axios.get(downloadUrl, {
|
||||||
|
responseType: "stream",
|
||||||
|
onDownloadProgress: (progressEvent) => {
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-backup-download-progress-${objectId}-${shop}`,
|
||||||
|
progressEvent
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const writer = fs.createWriteStream(zipLocation);
|
||||||
|
|
||||||
|
response.data.pipe(writer);
|
||||||
|
|
||||||
|
writer.on("error", (err) => {
|
||||||
|
logger.error("Failed to write zip", err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.mkdirSync(backupPath, { recursive: true });
|
||||||
|
|
||||||
|
writer.on("close", () => {
|
||||||
|
tar
|
||||||
|
.x({
|
||||||
|
file: zipLocation,
|
||||||
|
cwd: backupPath,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
replaceLudusaviBackupWithCurrentUser(
|
||||||
|
backupPath,
|
||||||
|
objectId,
|
||||||
|
normalizePath(homeDir)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ludusavi.restoreBackup(backupPath).then(() => {
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-backup-download-complete-${objectId}-${shop}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-backup-download-complete-${objectId}-${shop}`,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("downloadGameArtifact", downloadGameArtifact);
|
registerEvent("downloadGameArtifact", downloadGameArtifact);
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { Ludusavi } from "@main/services";
|
import { Ludusavi } from "@main/services";
|
||||||
import path from "node:path";
|
import { gameRepository } from "@main/repository";
|
||||||
import { backupsPath } from "@main/constants";
|
|
||||||
|
|
||||||
const getGameBackupPreview = async (
|
const getGameBackupPreview = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => {
|
) => {
|
||||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
const game = await gameRepository.findOne({
|
||||||
|
where: {
|
||||||
|
objectID: objectId,
|
||||||
|
shop,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return Ludusavi.getBackupPreview(shop, objectId, backupPath);
|
return Ludusavi.getBackupPreview(shop, objectId, game?.winePrefixPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getGameBackupPreview", getGameBackupPreview);
|
registerEvent("getGameBackupPreview", getGameBackupPreview);
|
||||||
|
|||||||
14
src/main/events/cloud-save/select-game-backup-path.ts
Normal file
14
src/main/events/cloud-save/select-game-backup-path.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
import { Ludusavi } from "@main/services";
|
||||||
|
|
||||||
|
const selectGameBackupPath = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
_shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
backupPath: string | null
|
||||||
|
) => {
|
||||||
|
return Ludusavi.addCustomGame(objectId, backupPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("selectGameBackupPath", selectGameBackupPath);
|
||||||
@@ -4,18 +4,29 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import * as tar from "tar";
|
import * as tar from "tar";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { backupsPath } from "@main/constants";
|
import { backupsPath } from "@main/constants";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
import { normalizePath } from "@main/helpers";
|
||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
const bundleBackup = async (shop: GameShop, objectId: string) => {
|
const bundleBackup = async (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
winePrefix: string | null
|
||||||
|
) => {
|
||||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||||
|
|
||||||
await Ludusavi.backupGame(shop, objectId, backupPath);
|
// Remove existing backup
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
fs.rmSync(backupPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.zip`);
|
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||||
|
|
||||||
|
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
||||||
|
|
||||||
await tar.create(
|
await tar.create(
|
||||||
{
|
{
|
||||||
@@ -32,9 +43,21 @@ const bundleBackup = async (shop: GameShop, objectId: string) => {
|
|||||||
const uploadSaveGame = async (
|
const uploadSaveGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop,
|
||||||
|
downloadOptionTitle: string | null
|
||||||
) => {
|
) => {
|
||||||
const bundleLocation = await bundleBackup(shop, objectId);
|
const game = await gameRepository.findOne({
|
||||||
|
where: {
|
||||||
|
objectID: objectId,
|
||||||
|
shop,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bundleLocation = await bundleBackup(
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
game?.winePrefixPath ?? null
|
||||||
|
);
|
||||||
|
|
||||||
fs.stat(bundleLocation, async (err, stat) => {
|
fs.stat(bundleLocation, async (err, stat) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -50,7 +73,8 @@ const uploadSaveGame = async (
|
|||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
homeDir: path.normalize(app.getPath("home")).replace(/\\/g, "/"),
|
homeDir: normalizePath(app.getPath("home")),
|
||||||
|
downloadOptionTitle,
|
||||||
platform: os.platform(),
|
platform: os.platform(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import "./catalogue/get-random-game";
|
|||||||
import "./catalogue/search-games";
|
import "./catalogue/search-games";
|
||||||
import "./catalogue/get-game-stats";
|
import "./catalogue/get-game-stats";
|
||||||
import "./catalogue/get-trending-games";
|
import "./catalogue/get-trending-games";
|
||||||
import "./catalogue/get-game-achievements";
|
|
||||||
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/create-game-shortcut";
|
import "./library/create-game-shortcut";
|
||||||
@@ -25,6 +24,8 @@ import "./library/update-executable-path";
|
|||||||
import "./library/verify-executable-path";
|
import "./library/verify-executable-path";
|
||||||
import "./library/remove-game";
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
|
import "./library/select-game-wine-prefix";
|
||||||
|
import "./misc/open-checkout";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
@@ -49,6 +50,8 @@ import "./user/unblock-user";
|
|||||||
import "./user/get-user-friends";
|
import "./user/get-user-friends";
|
||||||
import "./user/get-user-stats";
|
import "./user/get-user-stats";
|
||||||
import "./user/report-user";
|
import "./user/report-user";
|
||||||
|
import "./user/get-unlocked-achievements";
|
||||||
|
import "./user/get-compared-unlocked-achievements";
|
||||||
import "./profile/get-friend-requests";
|
import "./profile/get-friend-requests";
|
||||||
import "./profile/get-me";
|
import "./profile/get-me";
|
||||||
import "./profile/undo-friendship";
|
import "./profile/undo-friendship";
|
||||||
@@ -61,10 +64,11 @@ import "./cloud-save/download-game-artifact";
|
|||||||
import "./cloud-save/get-game-artifacts";
|
import "./cloud-save/get-game-artifacts";
|
||||||
import "./cloud-save/get-game-backup-preview";
|
import "./cloud-save/get-game-backup-preview";
|
||||||
import "./cloud-save/upload-save-game";
|
import "./cloud-save/upload-save-game";
|
||||||
import "./cloud-save/check-game-cloud-sync-support";
|
|
||||||
import "./cloud-save/delete-game-artifact";
|
import "./cloud-save/delete-game-artifact";
|
||||||
|
import "./cloud-save/select-game-backup-path";
|
||||||
import "./notifications/publish-new-repacks-notification";
|
import "./notifications/publish-new-repacks-notification";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
import "./misc/show-item-in-folder";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => appVersion);
|
ipcMain.handle("getVersion", () => appVersion);
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ const openGameInstaller = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fs.lstatSync(gamePath).isFile()) {
|
if (fs.lstatSync(gamePath).isFile()) {
|
||||||
return executeGameInstaller(gamePath);
|
shell.showItemInFolder(gamePath);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupPath = path.join(gamePath, "setup.exe");
|
const setupPath = path.join(gamePath, "setup.exe");
|
||||||
|
|||||||
13
src/main/events/library/select-game-wine-prefix.ts
Normal file
13
src/main/events/library/select-game-wine-prefix.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const selectGameWinePrefix = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
id: number,
|
||||||
|
winePrefixPath: string
|
||||||
|
) => {
|
||||||
|
return gameRepository.update({ id }, { winePrefixPath });
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("selectGameWinePrefix", selectGameWinePrefix);
|
||||||
26
src/main/events/misc/open-checkout.ts
Normal file
26
src/main/events/misc/open-checkout.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { shell } from "electron";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { userAuthRepository } from "@main/repository";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
|
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||||
|
|
||||||
|
if (!userAuth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentToken = await HydraApi.post("/auth/payment", {
|
||||||
|
refreshToken: userAuth.refreshToken,
|
||||||
|
}).then((response) => response.accessToken);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
token: paymentToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
shell.openExternal(
|
||||||
|
`${import.meta.env.MAIN_VITE_CHECKOUT_URL}?${params.toString()}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("openCheckout", openCheckout);
|
||||||
11
src/main/events/misc/show-item-in-folder.ts
Normal file
11
src/main/events/misc/show-item-in-folder.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { shell } from "electron";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const showItemInFolder = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
filePath: string
|
||||||
|
) => {
|
||||||
|
return shell.showItemInFolder(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("showItemInFolder", showItemInFolder);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { FriendRequest } from "@types";
|
import type { FriendRequest } from "@types";
|
||||||
|
|
||||||
const getFriendRequests = async (
|
const getFriendRequests = async (
|
||||||
_event: Electron.IpcMainInvokeEvent
|
_event: Electron.IpcMainInvokeEvent
|
||||||
|
|||||||
@@ -1,48 +1,11 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import type { UserDetails } from "@types";
|
||||||
import { HydraApi } from "@main/services";
|
import { getUserData } from "@main/services/user/get-user-data";
|
||||||
import { ProfileVisibility, UserDetails } from "@types";
|
|
||||||
import { userAuthRepository } from "@main/repository";
|
|
||||||
import { UserNotLoggedInError } from "@shared";
|
|
||||||
|
|
||||||
const getMe = async (
|
const getMe = async (
|
||||||
_event: Electron.IpcMainInvokeEvent
|
_event: Electron.IpcMainInvokeEvent
|
||||||
): Promise<UserDetails | null> => {
|
): Promise<UserDetails | null> => {
|
||||||
return HydraApi.get<UserDetails>(`/profile/me`)
|
return getUserData();
|
||||||
.then(async (me) => {
|
|
||||||
userAuthRepository.upsert(
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
displayName: me.displayName,
|
|
||||||
profileImageUrl: me.profileImageUrl,
|
|
||||||
userId: me.id,
|
|
||||||
},
|
|
||||||
["id"]
|
|
||||||
);
|
|
||||||
|
|
||||||
Sentry.setUser({ id: me.id, username: me.username });
|
|
||||||
|
|
||||||
return me;
|
|
||||||
})
|
|
||||||
.catch(async (err) => {
|
|
||||||
if (err instanceof UserNotLoggedInError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
|
||||||
|
|
||||||
if (loggedUser) {
|
|
||||||
return {
|
|
||||||
...loggedUser,
|
|
||||||
id: loggedUser.userId,
|
|
||||||
username: "",
|
|
||||||
bio: "",
|
|
||||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getMe", getMe);
|
registerEvent("getMe", getMe);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
import { FriendRequestSync } from "@types";
|
import type { FriendRequestSync } from "@types";
|
||||||
|
|
||||||
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
|
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`).catch(
|
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`).catch(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { FriendRequestAction } from "@types";
|
import type { FriendRequestAction } from "@types";
|
||||||
|
|
||||||
const updateFriendRequest = async (
|
const updateFriendRequest = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
|||||||
@@ -1,56 +1,75 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi, PythonInstance } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { UpdateProfileRequest, UserProfile } from "@types";
|
import type { UpdateProfileRequest, UserProfile } from "@types";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { fileTypeFromFile } from "file-type";
|
||||||
interface PresignedResponse {
|
|
||||||
presignedUrl: string;
|
|
||||||
profileImageUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
||||||
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNewProfileImageUrl = async (localImageUrl: string) => {
|
const uploadImage = async (
|
||||||
const { imagePath, mimeType } =
|
type: "profile-image" | "background-image",
|
||||||
await PythonInstance.processProfileImage(localImageUrl);
|
imagePath: string
|
||||||
|
) => {
|
||||||
const stats = fs.statSync(imagePath);
|
const stat = fs.statSync(imagePath);
|
||||||
const fileBuffer = fs.readFileSync(imagePath);
|
const fileBuffer = fs.readFileSync(imagePath);
|
||||||
const fileSizeInBytes = stats.size;
|
const fileSizeInBytes = stat.size;
|
||||||
|
|
||||||
const { presignedUrl, profileImageUrl } =
|
const response = await HydraApi.post<{ presignedUrl: string }>(
|
||||||
await HydraApi.post<PresignedResponse>(`/presigned-urls/profile-image`, {
|
`/presigned-urls/${type}`,
|
||||||
|
{
|
||||||
imageExt: path.extname(imagePath).slice(1),
|
imageExt: path.extname(imagePath).slice(1),
|
||||||
imageLength: fileSizeInBytes,
|
imageLength: fileSizeInBytes,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await axios.put(presignedUrl, fileBuffer, {
|
const mimeType = await fileTypeFromFile(imagePath);
|
||||||
|
|
||||||
|
await axios.put(response.presignedUrl, fileBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": mimeType,
|
"Content-Type": mimeType?.mime,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return profileImageUrl;
|
if (type === "background-image") {
|
||||||
|
return response["backgroundImageUrl"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response["profileImageUrl"];
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProfile = async (
|
const updateProfile = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
updateProfile: UpdateProfileRequest
|
updateProfile: UpdateProfileRequest
|
||||||
) => {
|
) => {
|
||||||
if (!updateProfile.profileImageUrl) {
|
const payload = omit(updateProfile, [
|
||||||
return patchUserProfile(omit(updateProfile, "profileImageUrl"));
|
"profileImageUrl",
|
||||||
|
"backgroundImageUrl",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (updateProfile.profileImageUrl) {
|
||||||
|
const profileImageUrl = await uploadImage(
|
||||||
|
"profile-image",
|
||||||
|
updateProfile.profileImageUrl
|
||||||
|
).catch(() => undefined);
|
||||||
|
|
||||||
|
payload["profileImageUrl"] = profileImageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileImageUrl = await getNewProfileImageUrl(
|
if (updateProfile.backgroundImageUrl) {
|
||||||
updateProfile.profileImageUrl
|
const backgroundImageUrl = await uploadImage(
|
||||||
).catch(() => undefined);
|
"background-image",
|
||||||
|
updateProfile.backgroundImageUrl
|
||||||
|
).catch(() => undefined);
|
||||||
|
|
||||||
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
payload["backgroundImageUrl"] = backgroundImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return patchUserProfile(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateProfile", updateProfile);
|
registerEvent("updateProfile", updateProfile);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
import { UserBlocks } from "@types";
|
import type { UserBlocks } from "@types";
|
||||||
|
|
||||||
export const getBlockedUsers = async (
|
export const getBlockedUsers = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
|||||||
44
src/main/events/user/get-compared-unlocked-achievements.ts
Normal file
44
src/main/events/user/get-compared-unlocked-achievements.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { ComparedAchievements, GameShop } from "@types";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
|
const getComparedUnlockedAchievements = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
userId: string
|
||||||
|
) => {
|
||||||
|
const userPreferences = await userPreferencesRepository.findOne({
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
return HydraApi.get<ComparedAchievements>(
|
||||||
|
`/users/${userId}/games/achievements/compare`,
|
||||||
|
{
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
language: userPreferences?.language || "en",
|
||||||
|
}
|
||||||
|
).then((achievements) => {
|
||||||
|
const sortedAchievements = achievements.achievements.sort((a, b) => {
|
||||||
|
if (a.targetStat.unlocked && !b.targetStat.unlocked) return -1;
|
||||||
|
if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1;
|
||||||
|
if (a.targetStat.unlocked && b.targetStat.unlocked) {
|
||||||
|
return b.targetStat.unlockTime! - a.targetStat.unlockTime!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(a.hidden) - Number(b.hidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...achievements,
|
||||||
|
achievements: sortedAchievements,
|
||||||
|
} as ComparedAchievements;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent(
|
||||||
|
"getComparedUnlockedAchievements",
|
||||||
|
getComparedUnlockedAchievements
|
||||||
|
);
|
||||||
73
src/main/events/user/get-unlocked-achievements.ts
Normal file
73
src/main/events/user/get-unlocked-achievements.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { GameShop, UnlockedAchievement, UserAchievement } from "@types";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gameAchievementRepository } from "@main/repository";
|
||||||
|
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||||
|
|
||||||
|
export const getUnlockedAchievements = async (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
useCachedData: boolean
|
||||||
|
): Promise<UserAchievement[]> => {
|
||||||
|
const cachedAchievements = await gameAchievementRepository.findOne({
|
||||||
|
where: { objectId, shop },
|
||||||
|
});
|
||||||
|
|
||||||
|
const achievementsData = await getGameAchievementData(
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
useCachedData
|
||||||
|
);
|
||||||
|
|
||||||
|
const unlockedAchievements = JSON.parse(
|
||||||
|
cachedAchievements?.unlockedAchievements || "[]"
|
||||||
|
) as UnlockedAchievement[];
|
||||||
|
|
||||||
|
return achievementsData
|
||||||
|
.map((achievementData) => {
|
||||||
|
const unlockedAchiementData = unlockedAchievements.find(
|
||||||
|
(localAchievement) => {
|
||||||
|
return (
|
||||||
|
localAchievement.name.toUpperCase() ==
|
||||||
|
achievementData.name.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const icongray = achievementData.icongray.endsWith("/")
|
||||||
|
? achievementData.icon
|
||||||
|
: achievementData.icongray;
|
||||||
|
|
||||||
|
if (unlockedAchiementData) {
|
||||||
|
return {
|
||||||
|
...achievementData,
|
||||||
|
unlocked: true,
|
||||||
|
unlockTime: unlockedAchiementData.unlockTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...achievementData,
|
||||||
|
unlocked: false,
|
||||||
|
unlockTime: null,
|
||||||
|
icongray: icongray,
|
||||||
|
} as UserAchievement;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.unlocked && !b.unlocked) return -1;
|
||||||
|
if (!a.unlocked && b.unlocked) return 1;
|
||||||
|
if (a.unlocked && b.unlocked) {
|
||||||
|
return b.unlockTime! - a.unlockTime!;
|
||||||
|
}
|
||||||
|
return Number(a.hidden) - Number(b.hidden);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUnlockedAchievementsEvent = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop
|
||||||
|
): Promise<UserAchievement[]> => {
|
||||||
|
return getUnlockedAchievements(objectId, shop, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getUnlockedAchievements", getUnlockedAchievementsEvent);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { userAuthRepository } from "@main/repository";
|
import { userAuthRepository } from "@main/repository";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { UserFriends } from "@types";
|
import type { UserFriends } from "@types";
|
||||||
|
|
||||||
export const getUserFriends = async (
|
export const getUserFriends = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import UserAgent from "user-agents";
|
import UserAgent from "user-agents";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
export const getFileBuffer = async (url: string) =>
|
export const getFileBuffer = async (url: string) =>
|
||||||
fetch(url, { method: "GET" }).then((response) =>
|
fetch(url, { method: "GET" }).then((response) =>
|
||||||
@@ -25,5 +26,9 @@ export const requestWebPage = async (url: string) => {
|
|||||||
return window.document;
|
return window.document;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPortableVersion = () =>
|
export const isPortableVersion = () => {
|
||||||
process.env.PORTABLE_EXECUTABLE_FILE !== null;
|
return !!process.env.PORTABLE_EXECUTABLE_FILE;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizePath = (str: string) =>
|
||||||
|
path.posix.normalize(str).replace(/\\/g, "/");
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
||||||
import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement";
|
import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement";
|
||||||
|
import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference";
|
||||||
|
import { CreateUserSubscription } from "./migrations/20241015235142_create_user_subscription";
|
||||||
|
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
||||||
|
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
|
||||||
|
|
||||||
export type HydraMigration = Knex.Migration & { name: string };
|
export type HydraMigration = Knex.Migration & { name: string };
|
||||||
|
|
||||||
@@ -19,6 +23,10 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
EnsureRepackUris,
|
EnsureRepackUris,
|
||||||
FixMissingColumns,
|
FixMissingColumns,
|
||||||
CreateGameAchievement,
|
CreateGameAchievement,
|
||||||
|
AddAchievementNotificationPreference,
|
||||||
|
CreateUserSubscription,
|
||||||
|
AddBackgroundImageUrl,
|
||||||
|
AddWinePrefixToGame,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
getMigrationName(migration: HydraMigration): string {
|
getMigrationName(migration: HydraMigration): string {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { DownloadManager, PythonInstance, startMainLoop } from "./services";
|
import {
|
||||||
|
DownloadManager,
|
||||||
|
Ludusavi,
|
||||||
|
PythonInstance,
|
||||||
|
startMainLoop,
|
||||||
|
} from "./services";
|
||||||
import {
|
import {
|
||||||
downloadQueueRepository,
|
downloadQueueRepository,
|
||||||
userPreferencesRepository,
|
userPreferencesRepository,
|
||||||
@@ -15,6 +20,8 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
|
||||||
HydraApi.setupApi().then(() => {
|
HydraApi.setupApi().then(() => {
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const AddAchievementNotificationPreference: HydraMigration = {
|
||||||
|
name: "AddAchievementNotificationPreference",
|
||||||
|
up: (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("user_preferences", (table) => {
|
||||||
|
return table.boolean("achievementNotificationsEnabled").defaultTo(true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("user_preferences", (table) => {
|
||||||
|
return table.dropColumn("achievementNotificationsEnabled");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const CreateUserSubscription: HydraMigration = {
|
||||||
|
name: "CreateUserSubscription",
|
||||||
|
up: async (knex: Knex) => {
|
||||||
|
return knex.schema.createTable("user_subscription", (table) => {
|
||||||
|
table.increments("id").primary();
|
||||||
|
table.string("subscriptionId").defaultTo("");
|
||||||
|
table
|
||||||
|
.text("userId")
|
||||||
|
.notNullable()
|
||||||
|
.references("user_auth.id")
|
||||||
|
.onDelete("CASCADE");
|
||||||
|
table.string("status").defaultTo("");
|
||||||
|
table.string("planId").defaultTo("");
|
||||||
|
table.string("planName").defaultTo("");
|
||||||
|
table.dateTime("expiresAt").nullable();
|
||||||
|
table.dateTime("createdAt").defaultTo(knex.fn.now());
|
||||||
|
table.dateTime("updatedAt").defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.dropTable("user_subscription");
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const AddBackgroundImageUrl: HydraMigration = {
|
||||||
|
name: "AddBackgroundImageUrl",
|
||||||
|
up: (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("user_auth", (table) => {
|
||||||
|
return table.text("backgroundImageUrl").nullable();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("user_auth", (table) => {
|
||||||
|
return table.dropColumn("backgroundImageUrl");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const AddWinePrefixToGame: HydraMigration = {
|
||||||
|
name: "AddWinePrefixToGame",
|
||||||
|
up: (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.text("winePrefixPath").nullable();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.dropColumn("winePrefixPath");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,8 +3,8 @@ import type { Knex } from "knex";
|
|||||||
|
|
||||||
export const MigrationName: HydraMigration = {
|
export const MigrationName: HydraMigration = {
|
||||||
name: "MigrationName",
|
name: "MigrationName",
|
||||||
up: async (knex: Knex) => {
|
up: (knex: Knex) => {
|
||||||
await knex.schema.createTable("table_name", (table) => {});
|
return knex.schema.createTable("table_name", async (table) => {});
|
||||||
},
|
},
|
||||||
|
|
||||||
down: async (knex: Knex) => {},
|
down: async (knex: Knex) => {},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
UserPreferences,
|
UserPreferences,
|
||||||
UserAuth,
|
UserAuth,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
|
UserSubscription,
|
||||||
} from "@main/entity";
|
} from "@main/entity";
|
||||||
|
|
||||||
export const gameRepository = dataSource.getRepository(Game);
|
export const gameRepository = dataSource.getRepository(Game);
|
||||||
@@ -26,5 +27,8 @@ export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
|||||||
|
|
||||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||||
|
|
||||||
|
export const userSubscriptionRepository =
|
||||||
|
dataSource.getRepository(UserSubscription);
|
||||||
|
|
||||||
export const gameAchievementRepository =
|
export const gameAchievementRepository =
|
||||||
dataSource.getRepository(GameAchievement);
|
dataSource.getRepository(GameAchievement);
|
||||||
|
|||||||
261
src/main/services/achievements/achievement-watcher-manager.ts
Normal file
261
src/main/services/achievements/achievement-watcher-manager.ts
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { parseAchievementFile } from "./parse-achievement-file";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
import { mergeAchievements } from "./merge-achievements";
|
||||||
|
import fs, { readdirSync } from "node:fs";
|
||||||
|
import {
|
||||||
|
findAchievementFileInExecutableDirectory,
|
||||||
|
findAchievementFiles,
|
||||||
|
findAllAchievementFiles,
|
||||||
|
getAlternativeObjectIds,
|
||||||
|
} from "./find-achivement-files";
|
||||||
|
import type { AchievementFile, UnlockedAchievement } from "@types";
|
||||||
|
import { achievementsLogger } from "../logger";
|
||||||
|
import { Cracker } from "@shared";
|
||||||
|
import { IsNull, Not } from "typeorm";
|
||||||
|
import { WindowManager } from "../window-manager";
|
||||||
|
|
||||||
|
const fileStats: Map<string, number> = new Map();
|
||||||
|
const fltFiles: Map<string, Set<string>> = new Map();
|
||||||
|
|
||||||
|
const watchAchievementsWindows = async () => {
|
||||||
|
const games = await gameRepository.find({
|
||||||
|
where: {
|
||||||
|
isDeleted: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (games.length === 0) return;
|
||||||
|
|
||||||
|
const achievementFiles = findAllAchievementFiles();
|
||||||
|
|
||||||
|
for (const game of games) {
|
||||||
|
const gameAchievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
|
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||||
|
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
|
||||||
|
|
||||||
|
gameAchievementFiles.push(
|
||||||
|
...findAchievementFileInExecutableDirectory(game)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of gameAchievementFiles) {
|
||||||
|
compareFile(game, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const watchAchievementsWithWine = async () => {
|
||||||
|
const games = await gameRepository.find({
|
||||||
|
where: {
|
||||||
|
isDeleted: false,
|
||||||
|
winePrefixPath: Not(IsNull()),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const game of games) {
|
||||||
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
const achievementFileInsideDirectory =
|
||||||
|
findAchievementFileInExecutableDirectory(game);
|
||||||
|
|
||||||
|
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||||
|
|
||||||
|
for (const file of gameAchievementFiles) {
|
||||||
|
compareFile(game, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareFltFolder = async (game: Game, file: AchievementFile) => {
|
||||||
|
try {
|
||||||
|
const currentAchievements = new Set(readdirSync(file.filePath));
|
||||||
|
const previousAchievements = fltFiles.get(file.filePath);
|
||||||
|
|
||||||
|
fltFiles.set(file.filePath, currentAchievements);
|
||||||
|
if (
|
||||||
|
!previousAchievements ||
|
||||||
|
currentAchievements.difference(previousAchievements).size === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
achievementsLogger.log("Detected change in FLT folder", file.filePath);
|
||||||
|
await processAchievementFileDiff(game, file);
|
||||||
|
} catch (err) {
|
||||||
|
achievementsLogger.error(err);
|
||||||
|
fltFiles.set(file.filePath, new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareFile = (game: Game, file: AchievementFile) => {
|
||||||
|
if (file.type === Cracker.flt) {
|
||||||
|
return compareFltFolder(game, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentStat = fs.statSync(file.filePath);
|
||||||
|
const previousStat = fileStats.get(file.filePath);
|
||||||
|
fileStats.set(file.filePath, currentStat.mtimeMs);
|
||||||
|
|
||||||
|
if (!previousStat || previousStat === -1) {
|
||||||
|
if (currentStat.mtimeMs) {
|
||||||
|
achievementsLogger.log(
|
||||||
|
"First change in file",
|
||||||
|
file.filePath,
|
||||||
|
previousStat,
|
||||||
|
currentStat.mtimeMs
|
||||||
|
);
|
||||||
|
|
||||||
|
return processAchievementFileDiff(game, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousStat === currentStat.mtimeMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
achievementsLogger.log(
|
||||||
|
"Detected change in file",
|
||||||
|
file.filePath,
|
||||||
|
previousStat,
|
||||||
|
currentStat.mtimeMs
|
||||||
|
);
|
||||||
|
return processAchievementFileDiff(game, file);
|
||||||
|
} catch (err) {
|
||||||
|
fileStats.set(file.filePath, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processAchievementFileDiff = async (
|
||||||
|
game: Game,
|
||||||
|
file: AchievementFile
|
||||||
|
) => {
|
||||||
|
const unlockedAchievements = parseAchievementFile(file.filePath, file.type);
|
||||||
|
|
||||||
|
if (unlockedAchievements.length) {
|
||||||
|
return mergeAchievements(game, unlockedAchievements, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AchievementWatcherManager {
|
||||||
|
private static hasFinishedMergingWithRemote = false;
|
||||||
|
|
||||||
|
public static watchAchievements = () => {
|
||||||
|
if (!this.hasFinishedMergingWithRemote) return;
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return watchAchievementsWindows();
|
||||||
|
}
|
||||||
|
|
||||||
|
return watchAchievementsWithWine();
|
||||||
|
};
|
||||||
|
|
||||||
|
private static preProcessGameAchievementFiles = (
|
||||||
|
game: Game,
|
||||||
|
gameAchievementFiles: AchievementFile[]
|
||||||
|
) => {
|
||||||
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
|
const parsedAchievements = parseAchievementFile(
|
||||||
|
achievementFile.filePath,
|
||||||
|
achievementFile.type
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentStat = fs.statSync(achievementFile.filePath);
|
||||||
|
fileStats.set(achievementFile.filePath, currentStat.mtimeMs);
|
||||||
|
} catch {
|
||||||
|
fileStats.set(achievementFile.filePath, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedAchievements.length) {
|
||||||
|
unlockedAchievements.push(...parsedAchievements);
|
||||||
|
|
||||||
|
achievementsLogger.log(
|
||||||
|
"Achievement file for",
|
||||||
|
game.title,
|
||||||
|
achievementFile.filePath,
|
||||||
|
parsedAchievements
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeAchievements(game, unlockedAchievements, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
private static preSearchAchievementsWindows = async () => {
|
||||||
|
const games = await gameRepository.find({
|
||||||
|
where: {
|
||||||
|
isDeleted: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
games.map((game) => {
|
||||||
|
const gameAchievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
|
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||||
|
gameAchievementFiles.push(
|
||||||
|
...(gameAchievementFilesMap.get(objectId) || [])
|
||||||
|
);
|
||||||
|
|
||||||
|
gameAchievementFiles.push(
|
||||||
|
...findAchievementFileInExecutableDirectory(game)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private static preSearchAchievementsWithWine = async () => {
|
||||||
|
const games = await gameRepository.find({
|
||||||
|
where: {
|
||||||
|
isDeleted: false,
|
||||||
|
winePrefixPath: Not(IsNull()),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
games.map((game) => {
|
||||||
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
const achievementFileInsideDirectory =
|
||||||
|
findAchievementFileInExecutableDirectory(game);
|
||||||
|
|
||||||
|
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||||
|
|
||||||
|
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public static preSearchAchievements = async () => {
|
||||||
|
const newAchievementsCount =
|
||||||
|
process.platform === "win32"
|
||||||
|
? await this.preSearchAchievementsWindows()
|
||||||
|
: await this.preSearchAchievementsWithWine();
|
||||||
|
|
||||||
|
const totalNewGamesWithAchievements = newAchievementsCount.filter(
|
||||||
|
(achievements) => achievements
|
||||||
|
).length;
|
||||||
|
const totalNewAchievements = newAchievementsCount.reduce(
|
||||||
|
(acc, val) => acc + val,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
WindowManager.notificationWindow?.webContents.send(
|
||||||
|
"on-combined-achievements-unlocked",
|
||||||
|
totalNewGamesWithAchievements,
|
||||||
|
totalNewAchievements
|
||||||
|
);
|
||||||
|
|
||||||
|
this.hasFinishedMergingWithRemote = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { parseAchievementFile } from "./parse-achievement-file";
|
|
||||||
import { Game } from "@main/entity";
|
|
||||||
import { mergeAchievements } from "./merge-achievements";
|
|
||||||
import fs, { readdirSync } from "node:fs";
|
|
||||||
import {
|
|
||||||
findAchievementFileInExecutableDirectory,
|
|
||||||
findAllAchievementFiles,
|
|
||||||
getAlternativeObjectIds,
|
|
||||||
} from "./find-achivement-files";
|
|
||||||
import type { AchievementFile } from "@types";
|
|
||||||
import { achievementsLogger, logger } from "../logger";
|
|
||||||
import { Cracker } from "@shared";
|
|
||||||
|
|
||||||
const fileStats: Map<string, number> = new Map();
|
|
||||||
const fltFiles: Map<string, Set<string>> = new Map();
|
|
||||||
|
|
||||||
export const watchAchievements = async () => {
|
|
||||||
const games = await gameRepository.find({
|
|
||||||
where: {
|
|
||||||
isDeleted: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (games.length === 0) return;
|
|
||||||
|
|
||||||
const achievementFiles = findAllAchievementFiles();
|
|
||||||
|
|
||||||
for (const game of games) {
|
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
|
||||||
const gameAchievementFiles = achievementFiles.get(objectId) || [];
|
|
||||||
const achievementFileInsideDirectory =
|
|
||||||
findAchievementFileInExecutableDirectory(game);
|
|
||||||
|
|
||||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
|
||||||
|
|
||||||
if (!gameAchievementFiles.length) continue;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Achievements files to observe for:",
|
|
||||||
game.title,
|
|
||||||
gameAchievementFiles
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const file of gameAchievementFiles) {
|
|
||||||
compareFile(game, file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const processAchievementFileDiff = async (
|
|
||||||
game: Game,
|
|
||||||
file: AchievementFile
|
|
||||||
) => {
|
|
||||||
const unlockedAchievements = parseAchievementFile(file.filePath, file.type);
|
|
||||||
|
|
||||||
logger.log("Achievements from file", file.filePath, unlockedAchievements);
|
|
||||||
|
|
||||||
if (unlockedAchievements.length) {
|
|
||||||
return mergeAchievements(
|
|
||||||
game.objectID,
|
|
||||||
game.shop,
|
|
||||||
unlockedAchievements,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const compareFltFolder = async (game: Game, file: AchievementFile) => {
|
|
||||||
try {
|
|
||||||
const currentAchievements = new Set(readdirSync(file.filePath));
|
|
||||||
const previousAchievements = fltFiles.get(file.filePath);
|
|
||||||
|
|
||||||
fltFiles.set(file.filePath, currentAchievements);
|
|
||||||
if (
|
|
||||||
!previousAchievements ||
|
|
||||||
currentAchievements.difference(previousAchievements).size === 0
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log("Detected change in FLT folder", file.filePath);
|
|
||||||
await processAchievementFileDiff(game, file);
|
|
||||||
} catch (err) {
|
|
||||||
achievementsLogger.error(err);
|
|
||||||
fltFiles.set(file.filePath, new Set());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const compareFile = async (game: Game, file: AchievementFile) => {
|
|
||||||
if (file.type === Cracker.flt) {
|
|
||||||
await compareFltFolder(game, file);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const currentStat = fs.statSync(file.filePath);
|
|
||||||
const previousStat = fileStats.get(file.filePath);
|
|
||||||
fileStats.set(file.filePath, currentStat.mtimeMs);
|
|
||||||
|
|
||||||
if (!previousStat) {
|
|
||||||
if (currentStat.mtimeMs) {
|
|
||||||
await processAchievementFileDiff(game, file);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousStat === currentStat.mtimeMs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
"Detected change in file",
|
|
||||||
file.filePath,
|
|
||||||
currentStat.mtimeMs,
|
|
||||||
fileStats.get(file.filePath)
|
|
||||||
);
|
|
||||||
await processAchievementFileDiff(game, file);
|
|
||||||
} catch (err) {
|
|
||||||
fileStats.set(file.filePath, -1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -6,12 +6,58 @@ import { Cracker } from "@shared";
|
|||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
|
|
||||||
|
const getAppDataPath = () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return app.getPath("appData");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = app.getPath("home").split("/").pop();
|
||||||
|
|
||||||
|
return path.join("drive_c", "users", user || "", "AppData", "Roaming");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDocumentsPath = () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return app.getPath("documents");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = app.getPath("home").split("/").pop();
|
||||||
|
|
||||||
|
return path.join("drive_c", "users", user || "", "Documents");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicDocumentsPath = () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return path.join("C:", "Users", "Public", "Documents");
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join("drive_c", "users", "Public", "Documents");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalAppDataPath = () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return path.join(appData, "..", "Local");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = app.getPath("home").split("/").pop();
|
||||||
|
|
||||||
|
return path.join("drive_c", "users", user || "", "AppData", "Local");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgramDataPath = () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return path.join("C:", "ProgramData");
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join("drive_c", "ProgramData");
|
||||||
|
};
|
||||||
|
|
||||||
//TODO: change to a automatized method
|
//TODO: change to a automatized method
|
||||||
const publicDocuments = path.join("C:", "Users", "Public", "Documents");
|
const publicDocuments = getPublicDocumentsPath();
|
||||||
const programData = path.join("C:", "ProgramData");
|
const programData = getProgramDataPath();
|
||||||
const appData = app.getPath("appData");
|
const appData = getAppDataPath();
|
||||||
const documents = app.getPath("documents");
|
const documents = getDocumentsPath();
|
||||||
const localAppData = path.join(appData, "..", "Local");
|
const localAppData = getLocalAppDataPath();
|
||||||
|
|
||||||
const crackers = [
|
const crackers = [
|
||||||
Cracker.codex,
|
Cracker.codex,
|
||||||
@@ -25,6 +71,7 @@ const crackers = [
|
|||||||
Cracker.smartSteamEmu,
|
Cracker.smartSteamEmu,
|
||||||
Cracker.empress,
|
Cracker.empress,
|
||||||
Cracker.flt,
|
Cracker.flt,
|
||||||
|
Cracker.razor1911,
|
||||||
];
|
];
|
||||||
|
|
||||||
const getPathFromCracker = (cracker: Cracker) => {
|
const getPathFromCracker = (cracker: Cracker) => {
|
||||||
@@ -32,11 +79,11 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(publicDocuments, "Steam", "CODEX"),
|
folderPath: path.join(publicDocuments, "Steam", "CODEX"),
|
||||||
fileLocation: ["achievements.ini"],
|
fileLocation: ["<objectId>", "achievements.ini"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "Steam", "CODEX"),
|
folderPath: path.join(appData, "Steam", "CODEX"),
|
||||||
fileLocation: ["achievements.ini"],
|
fileLocation: ["<objectId>", "achievements.ini"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -45,7 +92,7 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(publicDocuments, "Steam", "RUNE"),
|
folderPath: path.join(publicDocuments, "Steam", "RUNE"),
|
||||||
fileLocation: ["achievements.ini"],
|
fileLocation: ["<objectId>", "achievements.ini"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -53,8 +100,12 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
if (cracker === Cracker.onlineFix) {
|
if (cracker === Cracker.onlineFix) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(publicDocuments, Cracker.onlineFix),
|
folderPath: path.join(publicDocuments, "OnlineFix"),
|
||||||
fileLocation: ["Stats", "Achievements.ini"],
|
fileLocation: ["<objectId>", "Stats", "Achievements.ini"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
folderPath: path.join(publicDocuments, "OnlineFix"),
|
||||||
|
fileLocation: ["<objectId>", "Achievements.ini"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -63,11 +114,11 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "Goldberg SteamEmu Saves"),
|
folderPath: path.join(appData, "Goldberg SteamEmu Saves"),
|
||||||
fileLocation: ["achievements.json"],
|
fileLocation: ["<objectId>", "achievements.json"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "GSE Saves"),
|
folderPath: path.join(appData, "GSE Saves"),
|
||||||
fileLocation: ["achievements.json"],
|
fileLocation: ["<objectId>", "achievements.json"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -80,15 +131,19 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(programData, "RLD!"),
|
folderPath: path.join(programData, "RLD!"),
|
||||||
fileLocation: ["achievements.ini"],
|
fileLocation: ["<objectId>", "achievements.ini"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folderPath: path.join(programData, "Steam", "Player"),
|
folderPath: path.join(programData, "Steam", "Player"),
|
||||||
fileLocation: ["stats", "achievements.ini"],
|
fileLocation: ["<objectId>", "stats", "achievements.ini"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
folderPath: path.join(programData, "Steam", "RLD!"),
|
||||||
|
fileLocation: ["<objectId>", "stats", "achievements.ini"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folderPath: path.join(programData, "Steam", "dodi"),
|
folderPath: path.join(programData, "Steam", "dodi"),
|
||||||
fileLocation: ["stats", "achievements.ini"],
|
fileLocation: ["<objectId>", "stats", "achievements.ini"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -97,11 +152,16 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "EMPRESS", "remote"),
|
folderPath: path.join(appData, "EMPRESS", "remote"),
|
||||||
fileLocation: ["achievements.json"],
|
fileLocation: ["<objectId>", "achievements.json"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folderPath: path.join(publicDocuments, "EMPRESS", "remote"),
|
folderPath: path.join(publicDocuments, "EMPRESS"),
|
||||||
fileLocation: ["achievements.json"],
|
fileLocation: [
|
||||||
|
"<objectId>",
|
||||||
|
"remote",
|
||||||
|
"<objectId>",
|
||||||
|
"achievements.json",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -110,15 +170,15 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(documents, "SKIDROW"),
|
folderPath: path.join(documents, "SKIDROW"),
|
||||||
fileLocation: ["SteamEmu", "UserStats", "achiev.ini"],
|
fileLocation: ["<objectId>", "SteamEmu", "UserStats", "achiev.ini"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folderPath: path.join(documents, "Player"),
|
folderPath: path.join(documents, "Player"),
|
||||||
fileLocation: ["SteamEmu", "UserStats", "achiev.ini"],
|
fileLocation: ["<objectId>", "SteamEmu", "UserStats", "achiev.ini"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folderPath: path.join(localAppData, "SKIDROW"),
|
folderPath: path.join(localAppData, "SKIDROW"),
|
||||||
fileLocation: ["SteamEmu", "UserStats", "achiev.ini"],
|
fileLocation: ["<objectId>", "SteamEmu", "UserStats", "achiev.ini"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -127,7 +187,7 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "CreamAPI"),
|
folderPath: path.join(appData, "CreamAPI"),
|
||||||
fileLocation: ["stats", "CreamAPI.Achievements.cfg"],
|
fileLocation: ["<objectId>", "stats", "CreamAPI.Achievements.cfg"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -136,7 +196,7 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "SmartSteamEmu"),
|
folderPath: path.join(appData, "SmartSteamEmu"),
|
||||||
fileLocation: ["User", "Achievements.ini"],
|
fileLocation: ["<objectId>", "User", "Achievements.ini"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -147,10 +207,10 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
|
|
||||||
if (cracker === Cracker.flt) {
|
if (cracker === Cracker.flt) {
|
||||||
return [
|
return [
|
||||||
{
|
// {
|
||||||
folderPath: path.join(appData, "FLT"),
|
// folderPath: path.join(appData, "FLT"),
|
||||||
fileLocation: ["stats"],
|
// fileLocation: ["stats"],
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +218,20 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "RLE"),
|
folderPath: path.join(appData, "RLE"),
|
||||||
fileLocation: ["achievements.ini"],
|
fileLocation: ["<objectId>", "achievements.ini"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "RLE"),
|
folderPath: path.join(appData, "RLE"),
|
||||||
fileLocation: ["Achievements.ini"],
|
fileLocation: ["<objectId>", "Achievements.ini"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cracker == Cracker.razor1911) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(appData, ".1911"),
|
||||||
|
fileLocation: ["<objectId>", "achievement"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -186,7 +255,11 @@ export const findAchievementFiles = (game: Game) => {
|
|||||||
for (const cracker of crackers) {
|
for (const cracker of crackers) {
|
||||||
for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) {
|
for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) {
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||||
const filePath = path.join(folderPath, objectId, ...fileLocation);
|
const filePath = path.join(
|
||||||
|
game.winePrefixPath ?? "",
|
||||||
|
folderPath,
|
||||||
|
...mapFileLocationWithObjectId(fileLocation, objectId)
|
||||||
|
);
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
achievementFiles.push({
|
achievementFiles.push({
|
||||||
@@ -212,6 +285,7 @@ export const findAchievementFileInExecutableDirectory = (
|
|||||||
{
|
{
|
||||||
type: Cracker.userstats,
|
type: Cracker.userstats,
|
||||||
filePath: path.join(
|
filePath: path.join(
|
||||||
|
game.winePrefixPath ?? "",
|
||||||
game.executablePath,
|
game.executablePath,
|
||||||
"..",
|
"..",
|
||||||
"SteamData",
|
"SteamData",
|
||||||
@@ -221,6 +295,7 @@ export const findAchievementFileInExecutableDirectory = (
|
|||||||
{
|
{
|
||||||
type: Cracker._3dm,
|
type: Cracker._3dm,
|
||||||
filePath: path.join(
|
filePath: path.join(
|
||||||
|
game.winePrefixPath ?? "",
|
||||||
game.executablePath,
|
game.executablePath,
|
||||||
"..",
|
"..",
|
||||||
"3DMGAME",
|
"3DMGAME",
|
||||||
@@ -232,6 +307,15 @@ export const findAchievementFileInExecutableDirectory = (
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapFileLocationWithObjectId = (
|
||||||
|
fileLocation: string[],
|
||||||
|
objectId: string
|
||||||
|
) => {
|
||||||
|
return fileLocation.map((location) =>
|
||||||
|
location.replace("<objectId>", objectId)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const findAllAchievementFiles = () => {
|
export const findAllAchievementFiles = () => {
|
||||||
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
||||||
|
|
||||||
@@ -244,7 +328,10 @@ export const findAllAchievementFiles = () => {
|
|||||||
const objectIds = fs.readdirSync(folderPath);
|
const objectIds = fs.readdirSync(folderPath);
|
||||||
|
|
||||||
for (const objectId of objectIds) {
|
for (const objectId of objectIds) {
|
||||||
const filePath = path.join(folderPath, objectId, ...fileLocation);
|
const filePath = path.join(
|
||||||
|
folderPath,
|
||||||
|
...mapFileLocationWithObjectId(fileLocation, objectId)
|
||||||
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) continue;
|
if (!fs.existsSync(filePath)) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,36 @@ import {
|
|||||||
userPreferencesRepository,
|
userPreferencesRepository,
|
||||||
} from "@main/repository";
|
} from "@main/repository";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
|
import type { AchievementData, GameShop } from "@types";
|
||||||
|
import { UserNotLoggedInError } from "@shared";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
export const getGameAchievementData = async (
|
export const getGameAchievementData = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: string
|
shop: GameShop,
|
||||||
|
useCachedData: boolean
|
||||||
) => {
|
) => {
|
||||||
|
if (useCachedData) {
|
||||||
|
const cachedAchievements = await gameAchievementRepository.findOne({
|
||||||
|
where: { objectId, shop },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cachedAchievements && cachedAchievements.achievements) {
|
||||||
|
return JSON.parse(cachedAchievements.achievements) as AchievementData[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await userPreferencesRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
return HydraApi.get("/games/achievements", {
|
return HydraApi.get<AchievementData[]>("/games/achievements", {
|
||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
language: userPreferences?.language || "en",
|
language: userPreferences?.language || "en",
|
||||||
})
|
})
|
||||||
.then(async (achievements) => {
|
.then((achievements) => {
|
||||||
await gameAchievementRepository.upsert(
|
gameAchievementRepository.upsert(
|
||||||
{
|
{
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
@@ -29,5 +43,19 @@ export const getGameAchievementData = async (
|
|||||||
|
|
||||||
return achievements;
|
return achievements;
|
||||||
})
|
})
|
||||||
.catch(() => []);
|
.catch((err) => {
|
||||||
|
if (err instanceof UserNotLoggedInError) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
logger.error("Failed to get game achievements", err);
|
||||||
|
return gameAchievementRepository
|
||||||
|
.findOne({
|
||||||
|
where: { objectId, shop },
|
||||||
|
})
|
||||||
|
.then((gameAchievements) => {
|
||||||
|
return JSON.parse(
|
||||||
|
gameAchievements?.achievements || "[]"
|
||||||
|
) as AchievementData[];
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
import {
|
||||||
import type { GameShop, UnlockedAchievement } from "@types";
|
gameAchievementRepository,
|
||||||
|
userPreferencesRepository,
|
||||||
|
} from "@main/repository";
|
||||||
|
import type { AchievementData, GameShop, UnlockedAchievement } from "@types";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
|
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
import { achievementsLogger } from "../logger";
|
||||||
|
import { SubscriptionRequiredError } from "@shared";
|
||||||
|
|
||||||
const saveAchievementsOnLocal = async (
|
const saveAchievementsOnLocal = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: string,
|
shop: GameShop,
|
||||||
achievements: any[]
|
achievements: any[],
|
||||||
|
sendUpdateEvent: boolean
|
||||||
) => {
|
) => {
|
||||||
return gameAchievementRepository
|
return gameAchievementRepository
|
||||||
.upsert(
|
.upsert(
|
||||||
@@ -18,38 +26,49 @@ const saveAchievementsOnLocal = async (
|
|||||||
["objectId", "shop"]
|
["objectId", "shop"]
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
WindowManager.mainWindow?.webContents.send(
|
if (!sendUpdateEvent) return;
|
||||||
"on-achievement-unlocked",
|
|
||||||
objectId,
|
return getUnlockedAchievements(objectId, shop, true)
|
||||||
shop
|
.then((achievements) => {
|
||||||
);
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-update-achievements-${objectId}-${shop}`,
|
||||||
|
achievements
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mergeAchievements = async (
|
export const mergeAchievements = async (
|
||||||
objectId: string,
|
game: Game,
|
||||||
shop: string,
|
|
||||||
achievements: UnlockedAchievement[],
|
achievements: UnlockedAchievement[],
|
||||||
publishNotification: boolean
|
publishNotification: boolean
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const [localGameAchievement, userPreferences] = await Promise.all([
|
||||||
where: { objectID: objectId, shop: shop as GameShop },
|
gameAchievementRepository.findOne({
|
||||||
});
|
where: {
|
||||||
|
objectId: game.objectID,
|
||||||
|
shop: game.shop,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!game) return;
|
const achievementsData = JSON.parse(
|
||||||
|
localGameAchievement?.achievements || "[]"
|
||||||
const localGameAchievement = await gameAchievementRepository.findOne({
|
) as AchievementData[];
|
||||||
where: {
|
|
||||||
objectId,
|
|
||||||
shop,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const unlockedAchievements = JSON.parse(
|
const unlockedAchievements = JSON.parse(
|
||||||
localGameAchievement?.unlockedAchievements || "[]"
|
localGameAchievement?.unlockedAchievements || "[]"
|
||||||
).filter((achievement) => achievement.name);
|
).filter((achievement) => achievement.name) as UnlockedAchievement[];
|
||||||
|
|
||||||
const newAchievements = achievements
|
const newAchievementsMap = new Map(
|
||||||
|
achievements.reverse().map((achievement) => {
|
||||||
|
return [achievement.name.toUpperCase(), achievement];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const newAchievements = [...newAchievementsMap.values()]
|
||||||
.filter((achievement) => {
|
.filter((achievement) => {
|
||||||
return !unlockedAchievements.some((localAchievement) => {
|
return !unlockedAchievements.some((localAchievement) => {
|
||||||
return (
|
return (
|
||||||
@@ -64,55 +83,78 @@ export const mergeAchievements = async (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newAchievements.length && publishNotification) {
|
if (
|
||||||
|
newAchievements.length &&
|
||||||
|
publishNotification &&
|
||||||
|
userPreferences?.achievementNotificationsEnabled
|
||||||
|
) {
|
||||||
const achievementsInfo = newAchievements
|
const achievementsInfo = newAchievements
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.unlockTime - b.unlockTime;
|
return a.unlockTime - b.unlockTime;
|
||||||
})
|
})
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
return JSON.parse(localGameAchievement?.achievements || "[]").find(
|
return achievementsData.find((steamAchievement) => {
|
||||||
(steamAchievement) => {
|
return (
|
||||||
return (
|
achievement.name.toUpperCase() ===
|
||||||
achievement.name.toUpperCase() ===
|
steamAchievement.name.toUpperCase()
|
||||||
steamAchievement.name.toUpperCase()
|
);
|
||||||
);
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.filter((achievement) => achievement)
|
.filter((achievement) => achievement)
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
return {
|
return {
|
||||||
displayName: achievement.displayName,
|
displayName: achievement!.displayName,
|
||||||
iconUrl: achievement.icon,
|
iconUrl: achievement!.icon,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowManager.notificationWindow?.webContents.send(
|
WindowManager.notificationWindow?.webContents.send(
|
||||||
"on-achievement-unlocked",
|
"on-achievement-unlocked",
|
||||||
objectId,
|
game.objectID,
|
||||||
shop,
|
game.shop,
|
||||||
achievementsInfo
|
achievementsInfo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||||
|
|
||||||
if (game?.remoteId) {
|
if (game.remoteId) {
|
||||||
return HydraApi.put("/profile/games/achievements", {
|
await HydraApi.put(
|
||||||
id: game.remoteId,
|
"/profile/games/achievements",
|
||||||
achievements: mergedLocalAchievements,
|
{
|
||||||
})
|
id: game.remoteId,
|
||||||
|
achievements: mergedLocalAchievements,
|
||||||
|
},
|
||||||
|
{ needsSubscription: true }
|
||||||
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return saveAchievementsOnLocal(
|
return saveAchievementsOnLocal(
|
||||||
response.objectId,
|
response.objectId,
|
||||||
response.shop,
|
response.shop,
|
||||||
response.achievements
|
response.achievements,
|
||||||
|
publishNotification
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
return saveAchievementsOnLocal(objectId, shop, mergedLocalAchievements);
|
if (!(err instanceof SubscriptionRequiredError)) {
|
||||||
|
achievementsLogger.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveAchievementsOnLocal(
|
||||||
|
game.objectID,
|
||||||
|
game.shop,
|
||||||
|
mergedLocalAchievements,
|
||||||
|
publishNotification
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await saveAchievementsOnLocal(
|
||||||
|
game.objectID,
|
||||||
|
game.shop,
|
||||||
|
mergedLocalAchievements,
|
||||||
|
publishNotification
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveAchievementsOnLocal(objectId, shop, mergedLocalAchievements);
|
return newAchievements.length;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ export const parseAchievementFile = (
|
|||||||
return processCreamAPI(parsed);
|
return processCreamAPI(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === Cracker.empress) {
|
||||||
|
const parsed = jsonParse(filePath);
|
||||||
|
return processGoldberg(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === Cracker.razor1911) {
|
||||||
|
return processRazor1911(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
achievementsLogger.log(
|
achievementsLogger.log(
|
||||||
`Unprocessed ${type} achievements found on ${filePath}`
|
`Unprocessed ${type} achievements found on ${filePath}`
|
||||||
);
|
);
|
||||||
@@ -73,7 +82,12 @@ export const parseAchievementFile = (
|
|||||||
|
|
||||||
const iniParse = (filePath: string) => {
|
const iniParse = (filePath: string) => {
|
||||||
try {
|
try {
|
||||||
const lines = readFileSync(filePath, "utf-8").split(/[\r\n]+/);
|
const fileContent = readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
|
const lines =
|
||||||
|
fileContent.charCodeAt(0) === 0xfeff
|
||||||
|
? fileContent.slice(1).split(/[\r\n]+/)
|
||||||
|
: fileContent.split(/[\r\n]+/);
|
||||||
|
|
||||||
let objectName = "";
|
let objectName = "";
|
||||||
const object: Record<string, Record<string, string | number>> = {};
|
const object: Record<string, Record<string, string | number>> = {};
|
||||||
@@ -93,7 +107,7 @@ const iniParse = (filePath: string) => {
|
|||||||
return object;
|
return object;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
achievementsLogger.error(`Error parsing ${filePath}`, err);
|
achievementsLogger.error(`Error parsing ${filePath}`, err);
|
||||||
return null;
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,7 +116,36 @@ const jsonParse = (filePath: string) => {
|
|||||||
return JSON.parse(readFileSync(filePath, "utf-8"));
|
return JSON.parse(readFileSync(filePath, "utf-8"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
achievementsLogger.error(`Error parsing ${filePath}`, err);
|
achievementsLogger.error(`Error parsing ${filePath}`, err);
|
||||||
return null;
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processRazor1911 = (filePath: string): UnlockedAchievement[] => {
|
||||||
|
try {
|
||||||
|
const fileContent = readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
|
const lines =
|
||||||
|
fileContent.charCodeAt(0) === 0xfeff
|
||||||
|
? fileContent.slice(1).split(/[\r\n]+/)
|
||||||
|
: fileContent.split(/[\r\n]+/);
|
||||||
|
|
||||||
|
const achievements: UnlockedAchievement[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.length) continue;
|
||||||
|
|
||||||
|
const [name, unlocked, unlockTime] = line.split(" ");
|
||||||
|
if (unlocked === "1") {
|
||||||
|
achievements.push({
|
||||||
|
name,
|
||||||
|
unlockTime: Number(unlockTime) * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return achievements;
|
||||||
|
} catch (err) {
|
||||||
|
achievementsLogger.error(`Error processing ${filePath}`, err);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,11 +155,21 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
if (unlockedAchievement?.achieved) {
|
if (unlockedAchievement?.achieved == "true") {
|
||||||
parsedUnlockedAchievements.push({
|
parsedUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
unlockTime: unlockedAchievement.timestamp * 1000,
|
unlockTime: unlockedAchievement.timestamp * 1000,
|
||||||
});
|
});
|
||||||
|
} else if (unlockedAchievement?.Achieved == "true") {
|
||||||
|
const unlockTime = unlockedAchievement.TimeUnlocked;
|
||||||
|
|
||||||
|
parsedUnlockedAchievements.push({
|
||||||
|
name: achievement,
|
||||||
|
unlockTime:
|
||||||
|
unlockTime.length === 7
|
||||||
|
? unlockTime * 1000 * 1000
|
||||||
|
: unlockTime * 1000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +182,7 @@ const processCreamAPI = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
if (unlockedAchievement?.achieved) {
|
if (unlockedAchievement?.achieved == "true") {
|
||||||
const unlockTime = unlockedAchievement.unlocktime;
|
const unlockTime = unlockedAchievement.unlocktime;
|
||||||
parsedUnlockedAchievements.push({
|
parsedUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
@@ -207,7 +260,7 @@ const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
if (unlockedAchievement?.Achieved) {
|
if (unlockedAchievement?.Achieved == "1") {
|
||||||
newUnlockedAchievements.push({
|
newUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
unlockTime: unlockedAchievement.UnlockTime * 1000,
|
unlockTime: unlockedAchievement.UnlockTime * 1000,
|
||||||
@@ -227,15 +280,23 @@ const processRld = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
if (unlockedAchievement?.State) {
|
if (unlockedAchievement?.State) {
|
||||||
newUnlockedAchievements.push({
|
const unlocked = new DataView(
|
||||||
name: achievement,
|
new Uint8Array(
|
||||||
unlockTime:
|
Buffer.from(unlockedAchievement.State.toString(), "hex")
|
||||||
new DataView(
|
).buffer
|
||||||
new Uint8Array(
|
).getUint32(0, true);
|
||||||
Buffer.from(unlockedAchievement.Time.toString(), "hex")
|
|
||||||
).buffer
|
if (unlocked === 1) {
|
||||||
).getUint32(0, true) * 1000,
|
newUnlockedAchievements.push({
|
||||||
});
|
name: achievement,
|
||||||
|
unlockTime:
|
||||||
|
new DataView(
|
||||||
|
new Uint8Array(
|
||||||
|
Buffer.from(unlockedAchievement.Time.toString(), "hex")
|
||||||
|
).buffer
|
||||||
|
).getUint32(0, true) * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,12 @@
|
|||||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
|
||||||
import {
|
import {
|
||||||
findAllAchievementFiles,
|
|
||||||
findAchievementFiles,
|
findAchievementFiles,
|
||||||
findAchievementFileInExecutableDirectory,
|
findAchievementFileInExecutableDirectory,
|
||||||
getAlternativeObjectIds,
|
|
||||||
} from "./find-achivement-files";
|
} from "./find-achivement-files";
|
||||||
import { parseAchievementFile } from "./parse-achievement-file";
|
import { parseAchievementFile } from "./parse-achievement-file";
|
||||||
import { mergeAchievements } from "./merge-achievements";
|
import { mergeAchievements } from "./merge-achievements";
|
||||||
import type { UnlockedAchievement } from "@types";
|
import type { UnlockedAchievement } from "@types";
|
||||||
import { getGameAchievementData } from "./get-game-achievement-data";
|
|
||||||
import { achievementsLogger } from "../logger";
|
|
||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
|
|
||||||
export const updateAllLocalUnlockedAchievements = async () => {
|
|
||||||
const gameAchievementFilesMap = findAllAchievementFiles();
|
|
||||||
|
|
||||||
const games = await gameRepository.find({
|
|
||||||
where: {
|
|
||||||
isDeleted: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const game of games) {
|
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
|
||||||
const gameAchievementFiles = gameAchievementFilesMap.get(objectId) || [];
|
|
||||||
const achievementFileInsideDirectory =
|
|
||||||
findAchievementFileInExecutableDirectory(game);
|
|
||||||
|
|
||||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
|
||||||
|
|
||||||
gameAchievementRepository
|
|
||||||
.findOne({
|
|
||||||
where: { objectId: game.objectID, shop: "steam" },
|
|
||||||
})
|
|
||||||
.then((localAchievements) => {
|
|
||||||
if (!localAchievements || !localAchievements.achievements) {
|
|
||||||
getGameAchievementData(game.objectID, "steam");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
|
||||||
|
|
||||||
for (const achievementFile of gameAchievementFiles) {
|
|
||||||
const parsedAchievements = parseAchievementFile(
|
|
||||||
achievementFile.filePath,
|
|
||||||
achievementFile.type
|
|
||||||
);
|
|
||||||
|
|
||||||
if (parsedAchievements.length) {
|
|
||||||
unlockedAchievements.push(...parsedAchievements);
|
|
||||||
}
|
|
||||||
|
|
||||||
achievementsLogger.log(
|
|
||||||
"Achievement file for",
|
|
||||||
game.title,
|
|
||||||
achievementFile.filePath,
|
|
||||||
parsedAchievements
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeAchievements(game.objectID, "steam", unlockedAchievements, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
|
||||||
@@ -72,8 +15,6 @@ export const updateLocalUnlockedAchivements = async (game: Game) => {
|
|||||||
|
|
||||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||||
|
|
||||||
console.log("Achievements files for", game.title, gameAchievementFiles);
|
|
||||||
|
|
||||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
for (const achievementFile of gameAchievementFiles) {
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
@@ -87,5 +28,5 @@ export const updateLocalUnlockedAchivements = async (game: Game) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeAchievements(game.objectID, "steam", unlockedAchievements, false);
|
mergeAchievements(game, unlockedAchievements, false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
startTorrentClient as startRPCClient,
|
startTorrentClient as startRPCClient,
|
||||||
} from "./torrent-client";
|
} from "./torrent-client";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { DownloadProgress } from "@types";
|
import type { DownloadProgress } from "@types";
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||||
import { calculateETA } from "./helpers";
|
import { calculateETA } from "./helpers";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|||||||
@@ -1,32 +1,65 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { requestWebPage } from "@main/helpers";
|
import { requestWebPage } from "@main/helpers";
|
||||||
import { HowLongToBeatCategory } from "@types";
|
import type {
|
||||||
|
HowLongToBeatCategory,
|
||||||
|
HowLongToBeatSearchResponse,
|
||||||
|
} from "@types";
|
||||||
import { formatName } from "@shared";
|
import { formatName } from "@shared";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
|
import UserAgent from "user-agents";
|
||||||
|
|
||||||
export interface HowLongToBeatResult {
|
const state = {
|
||||||
game_id: number;
|
apiKey: null as string | null,
|
||||||
profile_steam: number;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export interface HowLongToBeatSearchResponse {
|
const getHowLongToBeatSearchApiKey = async () => {
|
||||||
data: HowLongToBeatResult[];
|
const userAgent = new UserAgent();
|
||||||
}
|
|
||||||
|
const document = await requestWebPage("https://howlongtobeat.com/");
|
||||||
|
const scripts = Array.from(document.querySelectorAll("script"));
|
||||||
|
|
||||||
|
const appScript = scripts.find((script) =>
|
||||||
|
script.src.startsWith("/_next/static/chunks/pages/_app")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!appScript) return null;
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`https://howlongtobeat.com${appScript.src}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"User-Agent": userAgent.toString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = /fetch\("\/api\/search\/"\.concat\("(.*?)"\)/gm.exec(
|
||||||
|
response.data
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!results) return null;
|
||||||
|
|
||||||
|
return results[1];
|
||||||
|
};
|
||||||
|
|
||||||
export const searchHowLongToBeat = async (gameName: string) => {
|
export const searchHowLongToBeat = async (gameName: string) => {
|
||||||
|
state.apiKey = state.apiKey ?? (await getHowLongToBeatSearchApiKey());
|
||||||
|
if (!state.apiKey) return { data: [] };
|
||||||
|
|
||||||
|
const userAgent = new UserAgent();
|
||||||
|
|
||||||
const response = await axios
|
const response = await axios
|
||||||
.post(
|
.post(
|
||||||
"https://howlongtobeat.com/api/search",
|
`https://howlongtobeat.com/api/search/${state.apiKey}`,
|
||||||
{
|
{
|
||||||
searchType: "games",
|
searchType: "games",
|
||||||
searchTerms: formatName(gameName).split(" "),
|
searchTerms: formatName(gameName).split(" "),
|
||||||
searchPage: 1,
|
searchPage: 1,
|
||||||
size: 100,
|
size: 20,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent":
|
"User-Agent": userAgent.toString(),
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
|
||||||
Referer: "https://howlongtobeat.com/",
|
Referer: "https://howlongtobeat.com/",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user