mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
127 Commits
feature/sh
...
feat/polyc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde35db7d7 | ||
|
|
f6eedde976 | ||
|
|
18c6994df6 | ||
|
|
d4c414b96b | ||
|
|
239b29687c | ||
|
|
0f0a67b55e | ||
|
|
eb11eb2bd0 | ||
|
|
facca3e26c | ||
|
|
f1e0ba4dd6 | ||
|
|
dfc2dd1c8b | ||
|
|
d760d0139d | ||
|
|
1f0e195854 | ||
|
|
8a30e946e3 | ||
|
|
cd871ec359 | ||
|
|
548b7c3f41 | ||
|
|
bbcdb42708 | ||
|
|
e9d541498e | ||
|
|
4e34f41ee0 | ||
|
|
049c27cdb7 | ||
|
|
2ba653429f | ||
|
|
9574e39d75 | ||
|
|
8ebb5edfbc | ||
|
|
69787ee068 | ||
|
|
d1fa4895e4 | ||
|
|
5d0e825880 | ||
|
|
b06339d362 | ||
|
|
3d0bf11359 | ||
|
|
2346a5bf86 | ||
|
|
81cb73c243 | ||
|
|
923f7d7e80 | ||
|
|
153ab05174 | ||
|
|
ff0ef74066 | ||
|
|
bc2ee2dc4c | ||
|
|
d866face54 | ||
|
|
44fd971c95 | ||
|
|
c115040e90 | ||
|
|
a23106b0b1 | ||
|
|
9941460c60 | ||
|
|
15f721ac39 | ||
|
|
56fabb2881 | ||
|
|
ffd3e37b48 | ||
|
|
c4378c0ffc | ||
|
|
af4fcb8f06 | ||
|
|
2c881a6100 | ||
|
|
8b47082047 | ||
|
|
c59b039eb4 | ||
|
|
08bcf09641 | ||
|
|
2c5fb8a037 | ||
|
|
09c1170407 | ||
|
|
89f1ce5ead | ||
|
|
392279c4e1 | ||
|
|
8fbe23e61c | ||
|
|
d4be5b8c66 | ||
|
|
21a88b889f | ||
|
|
a0a3697516 | ||
|
|
317434f663 | ||
|
|
cac2a7a70e | ||
|
|
11700b7c16 | ||
|
|
2407be0fb2 | ||
|
|
2a31c32cda | ||
|
|
7a7f270482 | ||
|
|
ab2d8c351b | ||
|
|
87acdea5ab | ||
|
|
385db5c936 | ||
|
|
cade56bb12 | ||
|
|
3efb1425b9 | ||
|
|
2df57b071d | ||
|
|
5061695500 | ||
|
|
190ddeb46e | ||
|
|
e6d76a5dbe | ||
|
|
2ddda4e4d2 | ||
|
|
ef3bf98903 | ||
|
|
b68fe300ba | ||
|
|
29ba0cca85 | ||
|
|
82b1d710c2 | ||
|
|
93b86f8c6c | ||
|
|
8cf549ff05 | ||
|
|
257a71d626 | ||
|
|
f3d617a13a | ||
|
|
9672e649e4 | ||
|
|
e2f798c627 | ||
|
|
52c159fe51 | ||
|
|
9849fbb31c | ||
|
|
addc2a74d3 | ||
|
|
10766526c5 | ||
|
|
bfdc2787d4 | ||
|
|
c60cd4bee4 | ||
|
|
59bc23bbd8 | ||
|
|
16b50fc22b | ||
|
|
720a7aa2a0 | ||
|
|
5c2bafcfe8 | ||
|
|
c30c685ee4 | ||
|
|
fba86002d1 | ||
|
|
a121ef77c0 | ||
|
|
bd653be071 | ||
|
|
297ca5a190 | ||
|
|
6278600b98 | ||
|
|
1226483deb | ||
|
|
0661cbd661 | ||
|
|
ad204e3879 | ||
|
|
cbbe6993bd | ||
|
|
3edadea0f3 | ||
|
|
16259b5399 | ||
|
|
726c753568 | ||
|
|
ba2ac1eb93 | ||
|
|
726d99a5c7 | ||
|
|
402e5df9ac | ||
|
|
0ee55b7fd5 | ||
|
|
8286390d9f | ||
|
|
6e00fb8e13 | ||
|
|
9f5d8cadda | ||
|
|
9060d435cf | ||
|
|
d8e7fca224 | ||
|
|
3bef2633fd | ||
|
|
db2688f3a7 | ||
|
|
4e282921ef | ||
|
|
b22b998c29 | ||
|
|
423693040b | ||
|
|
c098d8ffcf | ||
|
|
e1904b853e | ||
|
|
520eb91a55 | ||
|
|
16eaf4261a | ||
|
|
ec289fe4c7 | ||
|
|
f5d5aa39dc | ||
|
|
afcfcbf482 | ||
|
|
ac6eb247df | ||
|
|
47a5f4d327 |
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -43,11 +43,12 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.MAIN_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -56,11 +57,12 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.MAIN_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
|
||||
- name: Test Upload build
|
||||
env:
|
||||
@@ -72,6 +74,7 @@ jobs:
|
||||
BUILDS_URL: ${{ secrets.BUILDS_URL }}
|
||||
BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
|
||||
run: node scripts/upload-build.cjs
|
||||
|
||||
- name: Create artifact
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -47,9 +47,12 @@ jobs:
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.MAIN_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: yarn build:win
|
||||
@@ -59,9 +62,12 @@ jobs:
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.MAIN_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
|
||||
- name: Create artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ ludusavi/
|
||||
hydra-python-rpc/
|
||||
aria2/
|
||||
.python-version
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
!macro customUnInstall
|
||||
${ifNot} ${isUpdated}
|
||||
RMDir /r "$APPDATA\${APP_PACKAGE_NAME}"
|
||||
RMDir /r "$APPDATA\hydra"
|
||||
RMDir /r "$LOCALAPPDATA\hydralauncher-updater"
|
||||
${endIf}
|
||||
!macroend
|
||||
|
||||
@@ -125,6 +125,10 @@ cd hydra
|
||||
yarn
|
||||
```
|
||||
|
||||
### <a name="install-openssl-11"></a> Instale OpenSSL 1.1
|
||||
|
||||
[OpenSSL 1.1](https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe) é exigido pelo libtorrent em ambientes Windows.
|
||||
|
||||
### <a name="install-python-39"></a> Instale Python 3.9
|
||||
|
||||
Certifique-se de ter o Python 3.9 instalado em sua máquina. Você pode baixá-lo e instalá-lo em [python.org](https://www.python.org/downloads/release/python-3913/).
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
loadEnv(mode);
|
||||
@@ -37,6 +38,13 @@ export default defineConfig(({ mode }) => {
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern",
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@renderer": resolve("src/renderer/src"),
|
||||
@@ -44,7 +52,16 @@ export default defineConfig(({ mode }) => {
|
||||
"@shared": resolve("src/shared"),
|
||||
},
|
||||
},
|
||||
plugins: [svgr(), react(), vanillaExtractPlugin()],
|
||||
plugins: [
|
||||
svgr(),
|
||||
react(),
|
||||
vanillaExtractPlugin(),
|
||||
sentryVitePlugin({
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
org: "hydra-launcher",
|
||||
project: "hydra-renderer",
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.1.2",
|
||||
"version": "3.1.5",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
@@ -39,19 +39,21 @@
|
||||
"@primer/octicons-react": "^19.9.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
"@sentry/react": "^8.47.0",
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"@vanilla-extract/css": "^1.14.2",
|
||||
"@vanilla-extract/dynamic": "^2.1.2",
|
||||
"@vanilla-extract/recipes": "^0.5.2",
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"check-disk-space": "^3.4.0",
|
||||
"classnames": "^2.5.1",
|
||||
"color": "^4.2.3",
|
||||
"color.js": "^1.2.0",
|
||||
"create-desktop-shortcuts": "^1.11.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dexie": "^4.0.10",
|
||||
"diskusage": "^1.2.0",
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-updater": "^6.3.9",
|
||||
"file-type": "^19.6.0",
|
||||
@@ -60,6 +62,7 @@
|
||||
"jsdom": "^24.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"level": "^9.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parse-torrent": "^11.0.17",
|
||||
"piscina": "^4.7.0",
|
||||
@@ -72,7 +75,6 @@
|
||||
"sound-play": "^1.1.0",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tar": "^7.4.3",
|
||||
"typeorm": "^0.3.20",
|
||||
"user-agents": "^1.1.387",
|
||||
"yaml": "^2.6.1",
|
||||
"yup": "^1.5.0",
|
||||
|
||||
@@ -48,11 +48,6 @@ const downloadLudusavi = async () => {
|
||||
};
|
||||
|
||||
const downloadAria2WindowsAndLinux = async () => {
|
||||
if (fs.existsSync("aria2")) {
|
||||
console.log("Aria2 already exists, skipping download...");
|
||||
return;
|
||||
}
|
||||
|
||||
const file =
|
||||
process.platform === "win32"
|
||||
? "aria2-1.37.0-win-64bit-build1.zip"
|
||||
@@ -111,10 +106,17 @@ const copyAria2Macos = async () => {
|
||||
await exec(`cp $(which aria2c) aria2/aria2c`);
|
||||
};
|
||||
|
||||
if (process.platform == "darwin") {
|
||||
copyAria2Macos();
|
||||
} else {
|
||||
downloadAria2WindowsAndLinux();
|
||||
}
|
||||
const copyAria2 = () => {
|
||||
if (fs.existsSync("aria2")) {
|
||||
console.log("Aria2 already exists, skipping download...");
|
||||
return;
|
||||
}
|
||||
if (process.platform == "darwin") {
|
||||
copyAria2Macos();
|
||||
} else {
|
||||
downloadAria2WindowsAndLinux();
|
||||
}
|
||||
};
|
||||
|
||||
copyAria2();
|
||||
downloadLudusavi();
|
||||
|
||||
@@ -49,14 +49,14 @@ fs.readdir(dist, async (err, files) => {
|
||||
})
|
||||
);
|
||||
|
||||
if (uploads.length > 0) {
|
||||
for (const upload of uploads) {
|
||||
await fetch(process.env.BUILD_WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
uploads,
|
||||
upload,
|
||||
branchName: process.env.BRANCH_NAME,
|
||||
version: packageJson.version,
|
||||
githubActor: process.env.GITHUB_ACTOR,
|
||||
|
||||
@@ -4,399 +4,414 @@
|
||||
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
|
||||
},
|
||||
"home": {
|
||||
"featured": "مميّز",
|
||||
"surprise_me": "فاجئني",
|
||||
"no_results": "لم يتم العثور على نتائج",
|
||||
"start_typing": "بدء الكتابة للبحث...",
|
||||
"hot": "الأكثر رواجا الآن",
|
||||
"weekly": "📅 أفضل ألعاب الأسبوع",
|
||||
"achievements": "🏆 ألعاب للتغلب عليها"
|
||||
"featured": "مُتَمَيِّز",
|
||||
"surprise_me": "فَاجِئْنِي",
|
||||
"no_results": "لَمْ يُعْثَرْ عَلَى نَتائِج",
|
||||
"start_typing": "اِبْدَأْ بِالْكِتَابَةِ لِلْبَحْثِ...",
|
||||
"hot": "اَلْأَكْثَرُ شُيُوعًا الْآن",
|
||||
"weekly": "📅 أَفْضَلُ أَلْعَابِ الْأُسْبُوعِ",
|
||||
"achievements": "🏆 أَلْعَابٌ لِلتَّغَلُّبِ عَلَيْهَا"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "قائمة الألعاب",
|
||||
"downloads": "التنزيلات",
|
||||
"settings": "إعدادات",
|
||||
"my_library": "مكتبتي",
|
||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
||||
"paused": "{{title}} (متوقف مؤقتًا)",
|
||||
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
||||
"filter": "بحث في المكتبة",
|
||||
"home": "الرئيسية",
|
||||
"queued": "{{title}} (في قائمة الانتظار)",
|
||||
"game_has_no_executable": "لم يتم تحديد اللعبة القابلة للتنفيذ",
|
||||
"sign_in": "تسجيل الدخول",
|
||||
"friends": "أصدقاء",
|
||||
"need_help": "بحاجة الى مساعدة؟"
|
||||
"catalogue": "الْفِهْرِسُ",
|
||||
"downloads": "التَّنْزِيلَاتُ",
|
||||
"settings": "الإعْدَادَاتُ",
|
||||
"my_library": "مَكْتَبَتِي",
|
||||
"downloading_metadata": "{{title}} (جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...)",
|
||||
"paused": "{{title}} (مُوْقَفٌ)",
|
||||
"downloading": "{{title}} ({{percentage}} - جَارٍ التَّنْزِيلُ...)",
|
||||
"filter": "تَصْفِيَةُ الْمَكْتَبَةِ",
|
||||
"home": "الرَّئِيسِيَّةُ",
|
||||
"queued": "{{title}} (فِي الْانْتِظَارِ)",
|
||||
"game_has_no_executable": "اللُّعْبَةُ لَيْسَ لَدَيْهَا مِلَفٌّ تَنْفِيذِيٌّ مُحَدَّدٌ",
|
||||
"sign_in": "تَسْجِيلُ الدُّخُولِ",
|
||||
"friends": "الْأَصْدِقَاءُ",
|
||||
"need_help": "هَلْ تَحْتَاجُ إِلَى مُسَاعَدَةٍ؟"
|
||||
},
|
||||
"header": {
|
||||
"search": "ابحث عن الألعاب",
|
||||
"home": "الرئيسية",
|
||||
"catalogue": "قائمة الألعاب",
|
||||
"downloads": "التنزيلات",
|
||||
"search_results": "نتائج البحث",
|
||||
"settings": "إعدادات",
|
||||
"version_available_install": "إصدار {{version}} متاح. ",
|
||||
"version_available_download": "إصدار {{version}} متاح. "
|
||||
"search": "بَحْثُ الْأَلْعَابِ",
|
||||
"home": "الرَّئِيسِيَّةُ",
|
||||
"catalogue": "الْفِهْرِسُ",
|
||||
"downloads": "التَّنْزِيلَاتُ",
|
||||
"search_results": "نَتائِجُ الْبَحْثِ",
|
||||
"settings": "الإعْدَادَاتُ",
|
||||
"version_available_install": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِإِعَادَةِ التَّشْغِيلِ وَالتَّثْبِيتِ.",
|
||||
"version_available_download": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِلتَّنْزِيلِ."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
||||
"downloading_metadata": "جارٍ التنزيل {{title}} البيانات الوصفية...",
|
||||
"downloading": "جارٍ التنزيل {{title}}… ({{percentage}} مكتملة) - الانتهاء {{eta}} - {{speed}}",
|
||||
"calculating_eta": "جارٍ التنزيل {{title}}… ({{percentage}} مكتمل) - حساب الوقت المتبقي...",
|
||||
"checking_files": "التحقق {{title}} ملفات…({{percentage}} مكتمل)"
|
||||
"no_downloads_in_progress": "لَا تَوْجَدُ تَنْزِيلَاتٌ جَارِيَةٌ",
|
||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ لِـ {{title}}...",
|
||||
"downloading": "جَارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - الِاكْتِمَالُ {{eta}} - {{speed}}",
|
||||
"calculating_eta": "جَارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
|
||||
"checking_files": "جَارٍ التَّحَقُّقُ مِنْ مَلَفَّاتِ {{title}}... ({{percentage}} مَكْتُومٌ)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "الصفحة التالية",
|
||||
"previous_page": "الصفحة السابقة"
|
||||
"search": "تَصْفِيَةٌ...",
|
||||
"developers": "الْمُطَوِّرُونَ",
|
||||
"genres": "الْأَنْوَاعُ",
|
||||
"tags": "الْعَلَامَاتُ",
|
||||
"publishers": "النَّاشِرُونَ",
|
||||
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
|
||||
"result_count": "{{resultCount}} نَتائِجُ",
|
||||
"filter_count": "{{filterCount}} مَتَوَفِّرٌ",
|
||||
"clear_filters": "مَسْحُ {{filterCount}} الْمُخْتَارَةِ"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "افتح خيارات التنزيل",
|
||||
"download_options_zero": "{{count}} خيارات التنزيل",
|
||||
"updated_at": "تم التحديث {{updated_at}}",
|
||||
"install": "ثَبَّتَ",
|
||||
"resume": "استئناف",
|
||||
"pause": "إيقاف",
|
||||
"cancel": "إلغاء",
|
||||
"remove": "إزالة",
|
||||
"space_left_on_disk": "{{space}} متبقية على القرص",
|
||||
"eta": "الوقت المتبقي {{eta}}",
|
||||
"calculating_eta": "جارٍ حساب الوقت المتبقي…",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية…",
|
||||
"filter": "إعادة حزم التصفية",
|
||||
"requirements": "متطلبات النظام",
|
||||
"minimum": "الحد الأدنى",
|
||||
"recommended": "مُستَحسَن",
|
||||
"paused": "متوقف مؤقتًا",
|
||||
"release_date": "صدر بتاريخ {{date}}",
|
||||
"publisher": "نشرت من قبل {{publisher}}",
|
||||
"hours": "ساعات",
|
||||
"minutes": "دقائق",
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"accuracy": "{{accuracy}}٪ دقة",
|
||||
"add_to_library": "أضف إلى المكتبة",
|
||||
"remove_from_library": "إزالة من المكتبة",
|
||||
"no_downloads": "لا التنزيلات المتاحة",
|
||||
"play_time": "تم اللعب لمدة {{amount}}",
|
||||
"last_time_played": "لعبت آخر مرة {{period}}",
|
||||
"not_played_yet": "أنت لم تلعب {{title}} حتى الآن",
|
||||
"next_suggestion": "الاقتراح التالي",
|
||||
"play": "لعب",
|
||||
"deleting": "جارٍ حذف المثبت…",
|
||||
"close": "إغلاق",
|
||||
"playing_now": "قيداللعب الآن",
|
||||
"change": "تغيير",
|
||||
"repacks_modal_description": "اختر الحزمة التي تريد تنزيلها",
|
||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>إعدادات</0>",
|
||||
"download_now": "قم بالتنزيل الآن",
|
||||
"no_shop_details": "لا يمكن استرداد تفاصيل المتجر.",
|
||||
"download_options": "خيارات التنزيل",
|
||||
"download_path": "مسار التحميل",
|
||||
"previous_screenshot": "لقطة الشاشة السابقة",
|
||||
"next_screenshot": "لقطة الشاشة التالية",
|
||||
"screenshot": "لقطة الشاشة {{number}}",
|
||||
"open_screenshot": "فتح لقطة الشاشة {{number}}",
|
||||
"download_settings": "تحميل الإعدادات",
|
||||
"downloader": "أداة التنزيل",
|
||||
"select_executable": "يختار",
|
||||
"no_executable_selected": "لم يتم تحديد أي ملف قابل للتنفيذ",
|
||||
"open_folder": "افتح المجلد",
|
||||
"open_download_location": "انظر الملفات التي تم تنزيلها",
|
||||
"create_shortcut": "إنشاء اختصار سطح المكتب",
|
||||
"clear": "واضح",
|
||||
"remove_files": "إزالة الملفات",
|
||||
"remove_from_library_title": "هل أنت متأكد؟",
|
||||
"remove_from_library_description": "سيتم إزالة هذا {{game}} من مكتبتك",
|
||||
"options": "خيارات",
|
||||
"executable_section_title": "قابل للتنفيذ",
|
||||
"executable_section_description": "مسار الملف الذي سيتم تنفيذه عند النقر فوق \"تشغيل\".",
|
||||
"downloads_secion_title": "التنزيلات",
|
||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى من هذه اللعبة",
|
||||
"danger_zone_section_title": "منطقة الخطر",
|
||||
"danger_zone_section_description": "قم بإزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||
"download_in_progress": "التنزيل قيد التقدم",
|
||||
"download_paused": "تم إيقاف التنزيل مؤقتًا",
|
||||
"last_downloaded_option": "آخر خيار تم تنزيله",
|
||||
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
||||
"create_shortcut_error": "حدث خطأ أثناء إنشاء الاختصار",
|
||||
"nsfw_content_title": "تحتوي هذه اللعبة على محتوى غير مناسب",
|
||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يكون مناسبًا لجميع الأعمار. ",
|
||||
"allow_nsfw_content": "اسمح",
|
||||
"refuse_nsfw_content": "عُد",
|
||||
"stats": "احصائيات",
|
||||
"download_count": "التنزيلات",
|
||||
"player_count": "اللاعبين النشطين",
|
||||
"download_error": "خيار التنزيل هذا غير متوفر",
|
||||
"download": "تحميل",
|
||||
"executable_path_in_use": "قابل للتنفيذ قيد الاستخدام بالفعل بواسطة \"{{game}}\"",
|
||||
"warning": "تحذير:",
|
||||
"hydra_needs_to_remain_open": "لإجراء هذا التنزيل، يجب أن يظل Hydra مفتوحًا حتى اكتماله. ",
|
||||
"achievements": "الإنجازات",
|
||||
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "حفظ السحابة",
|
||||
"cloud_save_description": "احفظ تقدمك في السحابة واستمر في اللعب على أي جهاز",
|
||||
"backups": "النسخ الاحتياطية",
|
||||
"install_backup": "ثَبَّتَ",
|
||||
"delete_backup": "يمسح",
|
||||
"create_backup": "نسخة احتياطية جديدة",
|
||||
"last_backup_date": "آخر نسخة احتياطية قيد التشغيل {{date}}",
|
||||
"no_backup_preview": "لم يتم العثور على ألعاب محفوظة لهذا العنوان",
|
||||
"restoring_backup": "استعادة النسخة الاحتياطية ({{progress}} مكتمل)…",
|
||||
"uploading_backup": "جارٍ تحميل النسخة الاحتياطية…",
|
||||
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة حتى الآن",
|
||||
"backup_uploaded": "تم تحميل النسخة الاحتياطية",
|
||||
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
||||
"backup_restored": "تمت استعادة النسخة الاحتياطية",
|
||||
"see_all_achievements": "شاهد جميع الإنجازات",
|
||||
"sign_in_to_see_achievements": "قم بتسجيل الدخول لرؤية الإنجازات",
|
||||
"mapping_method_automatic": "تلقائي",
|
||||
"mapping_method_manual": "يدوي",
|
||||
"mapping_method_label": "طريقة رسم الخرائط",
|
||||
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
||||
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
||||
"manage_files": "إدارة الملفات",
|
||||
"loading_save_preview": "جارٍ البحث عن حفظ الألعاب...",
|
||||
"wine_prefix": "بادئة النبيذ",
|
||||
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
||||
"no_download_option_info": "لا توجد معلومات متاحة",
|
||||
"backup_deletion_failed": "فشل في حذف النسخة الاحتياطية",
|
||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة",
|
||||
"achievements_not_sync": "لا تتم مزامنة إنجازاتك",
|
||||
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
||||
"select_folder": "حدد المجلد",
|
||||
"backup_from": "نسخة احتياطية من {{date}}",
|
||||
"custom_backup_location_set": "تعيين موقع النسخ الاحتياطي المخصص",
|
||||
"no_directory_selected": "لم يتم تحديد أي دليل",
|
||||
"download_options_one": "{{count}} خيار التنزيل",
|
||||
"download_options_two": "{{count}} خيارات التنزيل",
|
||||
"download_options_few": "{{count}} خيارات التنزيل",
|
||||
"download_options_many": "{{count}} خيارات التنزيل",
|
||||
"download_options_other": "{{count}} خيارات التنزيل"
|
||||
"open_download_options": "فَتْحُ خِيَارَاتِ التَّنْزِيلِ",
|
||||
"download_options_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
|
||||
"download_options_one": "{{count}} خِيَارُ تَنْزِيلٍ",
|
||||
"download_options_other": "{{count}} خِيَارَاتُ تَنْزِيلٍ",
|
||||
"updated_at": "تَمَّ التَّحْدِيثُ فِي {{updated_at}}",
|
||||
"install": "تَثْبِيتٌ",
|
||||
"resume": "اسْتِئْنَافٌ",
|
||||
"pause": "إِيقَافٌ",
|
||||
"cancel": "إِلْغَاءٌ",
|
||||
"remove": "إِزَالَةٌ",
|
||||
"space_left_on_disk": "{{space}} مُتَبَقٍّ عَلَى الْقُرْصِ",
|
||||
"eta": "الِاكْتِمَالُ {{eta}}",
|
||||
"calculating_eta": "جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
|
||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
|
||||
"filter": "تَصْفِيَةُ الْإِصْدَارَاتِ الْمُعَادِ تَغْلِيفُهَا",
|
||||
"requirements": "مُتَطَلَّبَاتُ النِّظَامِ",
|
||||
"minimum": "الْأَدْنَى",
|
||||
"recommended": "الْمُوَصَّى بِهِ",
|
||||
"paused": "مُوْقَفٌ",
|
||||
"release_date": "تَمَّ الْإِصْدَارُ فِي {{date}}",
|
||||
"publisher": "نُشِرَ بِوَاسِطَةِ {{publisher}}",
|
||||
"hours": "سَاعَاتٌ",
|
||||
"minutes": "دَقَائِقُ",
|
||||
"amount_hours": "{{amount}} سَاعَاتٌ",
|
||||
"amount_minutes": "{{amount}} دَقَائِقُ",
|
||||
"accuracy": "دِقَّةٌ {{accuracy}}%",
|
||||
"add_to_library": "إِضَافَةٌ إِلَى الْمَكْتَبَةِ",
|
||||
"remove_from_library": "إِزَالَةٌ مِنَ الْمَكْتَبَةِ",
|
||||
"no_downloads": "لَا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ",
|
||||
"play_time": "لُعِبَ لِمُدَّةِ {{amount}}",
|
||||
"last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
|
||||
"not_played_yet": "لَمْ تَلْعَبْ {{title}} بَعْدُ",
|
||||
"next_suggestion": "الِاقْتِرَاحُ التَّالِي",
|
||||
"play": "لَعِبٌ",
|
||||
"deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
|
||||
"close": "إِغْلَاقٌ",
|
||||
"playing_now": "جَارِي اللَّعِبُ الْآن",
|
||||
"change": "تَغْيِيرٌ",
|
||||
"repacks_modal_description": "اخْتَرِ الْإِصْدَارَ الْمُعَادَ تَغْلِيفُهُ الَّذِي تُرِيدُ تَنْزِيلَهُ",
|
||||
"select_folder_hint": "لِتَغْيِيرِ الْمَجَلَّدِ الافْتِرَاضِيِّ، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
|
||||
"download_now": "تَنْزِيلٌ الْآن",
|
||||
"no_shop_details": "لَمْ يَتَمَكَّنْ مِنْ اسْتِرْدَادِ تَفَاصِيلِ الْمَتْجَرِ.",
|
||||
"download_options": "خِيَارَاتُ التَّنْزِيلِ",
|
||||
"download_path": "مَسَارُ التَّنْزِيلِ",
|
||||
"previous_screenshot": "لَقْطَةُ الشَّاشَةِ السَّابِقَةُ",
|
||||
"next_screenshot": "لَقْطَةُ الشَّاشَةِ التَّالِيَةُ",
|
||||
"screenshot": "لَقْطَةُ الشَّاشَةِ {{number}}",
|
||||
"open_screenshot": "فَتْحُ لَقْطَةِ الشَّاشَةِ {{number}}",
|
||||
"download_settings": "إعْدَادَاتُ التَّنْزِيلِ",
|
||||
"downloader": "الْمُنَزِّلُ",
|
||||
"select_executable": "تَحْدِيدٌ",
|
||||
"no_executable_selected": "لَمْ يُحَدَّدْ مِلَفٌّ تَنْفِيذِيٌّ",
|
||||
"open_folder": "فَتْحُ الْمَجَلَّدِ",
|
||||
"open_download_location": "مُشَاهَدَةُ الْمَلَفَّاتِ الْمُنَزَّلَةِ",
|
||||
"create_shortcut": "إِنْشَاءُ طَرِيقٍ مُخْتَصَرٍ عَلَى سَطْحِ الْمَكْتَبِ",
|
||||
"clear": "مَسْحٌ",
|
||||
"remove_files": "إِزَالَةُ الْمَلَفَّاتِ",
|
||||
"remove_from_library_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
||||
"remove_from_library_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ {{game}} مِنْ مَكْتَبَتِكَ",
|
||||
"options": "خِيَارَاتٌ",
|
||||
"executable_section_title": "الْمِلَفُّ التَّنْفِيذِيُّ",
|
||||
"executable_section_description": "مَسَارُ الْمِلَفِّ الَّذِي سَيَتِمُّ تَنْفِيذُهُ عِنْدَ النَّقْرِ عَلَى \"لَعِبٌ\"",
|
||||
"downloads_secion_title": "التَّنْزِيلَاتُ",
|
||||
"downloads_section_description": "تَحَقَّقْ مِنَ التَّحْدِيثَاتِ أَوِ الْإِصْدَارَاتِ الْأُخْرَى لِهَذِهِ اللُّعْبَةِ",
|
||||
"danger_zone_section_title": "مِنْطَقَةُ الْخَطَرِ",
|
||||
"danger_zone_section_description": "إِزَالَةُ هَذِهِ اللُّعْبَةِ مِنْ مَكْتَبَتِكَ أَوِ الْمَلَفَّاتِ الْمُنَزَّلَةِ بِوَاسِطَةِ Hydra",
|
||||
"download_in_progress": "جَارٍ التَّنْزِيلُ",
|
||||
"download_paused": "التَّنْزِيلُ مُوْقَفٌ",
|
||||
"last_downloaded_option": "خِيَارُ التَّنْزِيلِ الْأَخِيرُ",
|
||||
"create_shortcut_success": "تَمَّ إِنْشَاءُ الطَّرِيقِ الْمُخْتَصَرِ بِنَجَاحٍ",
|
||||
"create_shortcut_error": "خَطَأٌ فِي إِنْشَاءِ الطَّرِيقِ الْمُخْتَصَرِ",
|
||||
"nsfw_content_title": "هَذِهِ اللُّعْبَةُ تَحْتَوِي عَلَى مُحْتَوًى غَيْرِ لَائِقٍ",
|
||||
"nsfw_content_description": "{{title}} تَحْتَوِي عَلَى مُحْتَوًى قَدْ لَا يَكُونُ مُنَاسِبًا لِجَمِيعِ الْأَعْمَارِ. هَلْ أَنْتَ مُتَأَكِّدٌ مِنْ أَنَّكَ تُرِيدُ الْمُتَابَعَةَ؟",
|
||||
"allow_nsfw_content": "الْمُتَابَعَةُ",
|
||||
"refuse_nsfw_content": "الرُّجُوعُ",
|
||||
"stats": "الإحْصَائِيَّاتُ",
|
||||
"download_count": "التَّنْزِيلَاتُ",
|
||||
"player_count": "اللَّاعِبُونَ النَّشِطُونَ",
|
||||
"download_error": "هَذَا خِيَارُ التَّنْزِيلِ غَيْرُ مَتَوَفِّرٍ",
|
||||
"download": "تَنْزِيلٌ",
|
||||
"executable_path_in_use": "الْمِلَفُّ التَّنْفِيذِيُّ مُسْتَخْدَمٌ بِوَاسِطَةِ \"{{game}}\"",
|
||||
"warning": "تَنْبِيهٌ:",
|
||||
"hydra_needs_to_remain_open": "لِهَذَا التَّنْزِيلِ، يَجِبُ أَنْ يَبْقَى Hydra مَفْتُوحًا حَتَّى يَتِمَّ الِاكْتِمَالُ. إِذَا أُغْلِقَ Hydra قَبْلَ الِاكْتِمَالِ، سَتَفْقِدُ تَقَدُّمَكَ.",
|
||||
"achievements": "الإِنْجَازَاتُ",
|
||||
"achievements_count": "الإِنْجَازَاتُ {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "حِفْظٌ سَحَابِيٌّ",
|
||||
"cloud_save_description": "احْفَظْ تَقَدُّمَكَ فِي السَّحَابَةِ وَاسْتَمِرَّ فِي اللَّعِبِ عَلَى أَيِّ جِهَازٍ",
|
||||
"backups": "الْنُسَخُ الِاحْتِيَاطِيَّةُ",
|
||||
"install_backup": "تَثْبِيتٌ",
|
||||
"delete_backup": "حَذْفٌ",
|
||||
"create_backup": "نُسْخَةٌ احْتِيَاطِيَّةٌ جَدِيدَةٌ",
|
||||
"last_backup_date": "آخِرُ نُسْخَةٍ احْتِيَاطِيَّةٍ فِي {{date}}",
|
||||
"no_backup_preview": "لَمْ يُعْثَرْ عَلَى أَيِّ أَلْعَابٍ مَحْفُوظَةٍ لِهَذَا الْعُنْوَانِ",
|
||||
"restoring_backup": "جَارٍ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ ({{progress}} مَكْتُومٌ)...",
|
||||
"uploading_backup": "جَارٍ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ...",
|
||||
"no_backups": "لَمْ تَقُمْ بِإِنْشَاءِ أَيِّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ بَعْدُ",
|
||||
"backup_uploaded": "تَمَّ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
||||
"backup_deleted": "تَمَّ حَذْفُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
||||
"backup_restored": "تَمَّ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
||||
"see_all_achievements": "عَرْضُ جَمِيعِ الإِنْجَازَاتِ",
|
||||
"sign_in_to_see_achievements": "سَجِّلِ الدُّخُولَ لِعَرْضِ الإِنْجَازَاتِ",
|
||||
"mapping_method_automatic": "آلِيٌّ",
|
||||
"mapping_method_manual": "يَدَوِيٌّ",
|
||||
"mapping_method_label": "طَرِيقَةُ التَّحْدِيدِ",
|
||||
"files_automatically_mapped": "تَمَّ تَحْدِيدُ الْمَلَفَّاتِ تِلْقَائِيًّا",
|
||||
"no_backups_created": "لَمْ تُنْشَأْ أَيُّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ",
|
||||
"manage_files": "إِدَارَةُ الْمَلَفَّاتِ",
|
||||
"loading_save_preview": "جَارٍ الْبَحْثُ عَنْ أَلْعَابٍ مَحْفُوظَةٍ...",
|
||||
"wine_prefix": "بَادِئَةُ Wine",
|
||||
"wine_prefix_description": "بَادِئَةُ Wine الْمُسْتَخْدَمَةُ لِتَشْغِيلِ هَذِهِ اللُّعْبَةِ",
|
||||
"launch_options": "خِيَارَاتُ الْإِطْلَاقِ",
|
||||
"launch_options_description": "يُمْكِنُ لِلْمُسْتَخْدِمِينَ الْمُتَقَدِّمِينَ إِدْخَالُ تَعْدِيلَاتٍ عَلَى خِيَارَاتِ الْإِطْلَاقِ",
|
||||
"launch_options_placeholder": "لَمْ يُحَدَّدْ أَيُّ مُعَامِلٍ",
|
||||
"no_download_option_info": "لَا تَوْجَدُ مَعْلُومَاتٌ مَتَوَفِّرَةٌ",
|
||||
"backup_deletion_failed": "فَشَلَ فِي حَذْفِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
||||
"max_number_of_artifacts_reached": "تَمَّ بَلُوغُ الْعَدَدِ الْأَقْصَى لِلنُّسَخِ الِاحْتِيَاطِيَّةِ لِهَذِهِ اللُّعْبَةِ",
|
||||
"achievements_not_sync": "تَعَرَّفْ عَلَى كَيْفِيَّةِ مَزْجِ إِنْجَازَاتِكَ",
|
||||
"manage_files_description": "إِدَارَةُ الْمَلَفَّاتِ الَّتِي سَيَتِمُّ نَسْخُهَا احْتِيَاطِيًّا وَاسْتِعَادَتُهَا",
|
||||
"select_folder": "تَحْدِيدُ الْمَجَلَّدِ",
|
||||
"backup_from": "نُسْخَةٌ احْتِيَاطِيَّةٌ مِنْ {{date}}",
|
||||
"custom_backup_location_set": "تَمَّ تَحْدِيدُ مَوْقِعِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ الْمُخَصَّصِ",
|
||||
"no_directory_selected": "لَمْ يُحَدَّدْ أَيُّ دَلِيلٍ"
|
||||
},
|
||||
"activation": {
|
||||
"title": "تفعيل Hydra",
|
||||
"installation_id": "معرف التثبيت:",
|
||||
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا ينبغي أن يكون لديك هذا.",
|
||||
"activate": "فعل",
|
||||
"loading": "تحميل…"
|
||||
"title": "تَفْعِيلُ Hydra",
|
||||
"installation_id": "مُعَرِّفُ التَّثْبِيتِ:",
|
||||
"enter_activation_code": "أَدْخِلْ رَمْزَ التَّفْعِيلِ",
|
||||
"message": "إِذَا كُنْتَ لَا تَعْرِفُ أَيْنَ تَطْلُبُ هَذَا، فَلا يَجِبُ أَنْ تَكُونَ لَدَيْكَ.",
|
||||
"activate": "تَفْعِيلٌ",
|
||||
"loading": "جَارٍ التَّحْمِيلُ..."
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "استئناف",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"eta": "الوقت المتبقي {{eta}}",
|
||||
"paused": "متوقف مؤقتًا",
|
||||
"verifying": "جارٍ التحقق…",
|
||||
"completed": "مكتمل",
|
||||
"removed": "لم يتم تحميلها",
|
||||
"cancel": "إلغاء",
|
||||
"filter": "تصفية الألعاب التي تم تنزيلها",
|
||||
"remove": "إزالة",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية…",
|
||||
"deleting": "جارٍ حذف المثبت…",
|
||||
"delete": "إزالة المثبت",
|
||||
"delete_modal_title": "هل أنت متأكد؟",
|
||||
"delete_modal_description": "سيؤدي هذا إلى إزالة كافة ملفات التثبيت من جهاز الكمبيوتر الخاص بك",
|
||||
"install": "ثَبَّتَ",
|
||||
"download_in_progress": "في تَقَدم",
|
||||
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
||||
"downloads_completed": "مكتمل",
|
||||
"queued": "في قائمة الانتظار",
|
||||
"no_downloads_title": "هذا فارغ",
|
||||
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان بعد للبدء.",
|
||||
"checking_files": "جارٍ فحص الملفات…"
|
||||
"resume": "اسْتِئْنَافٌ",
|
||||
"pause": "إِيقَافٌ",
|
||||
"eta": "الِاكْتِمَالُ {{eta}}",
|
||||
"paused": "مُوْقَفٌ",
|
||||
"verifying": "جَارٍ التَّحَقُّقُ...",
|
||||
"completed": "مَكْتُومٌ",
|
||||
"removed": "لَمْ يُنَزَّلْ",
|
||||
"cancel": "إِلْغَاءٌ",
|
||||
"filter": "تَصْفِيَةُ الْأَلْعَابِ الْمُنَزَّلَةِ",
|
||||
"remove": "إِزَالَةٌ",
|
||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
|
||||
"deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
|
||||
"delete": "حَذْفُ الْمُثَبِّتِ",
|
||||
"delete_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
||||
"delete_modal_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ جَمِيعِ مَلَفَّاتِ التَّثْبِيتِ مِنْ حَاسُوبِكَ",
|
||||
"install": "تَثْبِيتٌ",
|
||||
"download_in_progress": "جَارٍ التَّنْفِيذُ",
|
||||
"queued_downloads": "التَّنْزِيلَاتُ فِي الْانْتِظَارِ",
|
||||
"downloads_completed": "مَكْتُومٌ",
|
||||
"queued": "فِي الْانْتِظَارِ",
|
||||
"no_downloads_title": "فَرَاغٌ تَامٌ",
|
||||
"no_downloads_description": "لَمْ تَقُمْ بِتَنْزِيلِ أَيِّ شَيْءٍ بِاسْتِخْدَامِ Hydra بَعْدُ، لَكِنَّهُ لَيْسَ مُتَأَخِّرًا لِلْبَدْءِ.",
|
||||
"checking_files": "جَارٍ التَّحَقُّقُ مِنَ الْمَلَفَّاتِ...",
|
||||
"seeding": "الْبَذْرُ",
|
||||
"stop_seeding": "إِيقَافُ الْبَذْرِ",
|
||||
"resume_seeding": "اسْتِئْنَافُ الْبَذْرِ",
|
||||
"options": "إِدَارَةٌ"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "مسار التنزيلات",
|
||||
"change": "تحديث",
|
||||
"notifications": "إشعارات",
|
||||
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||
"enable_repack_list_notifications": "عند إضافة حزمة جديدة",
|
||||
"real_debrid_api_token_label": "رمز Real-Debrid API",
|
||||
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق",
|
||||
"launch_with_system": "قم بتشغيل Hydra عند بدء تشغيل النظام",
|
||||
"general": "عام",
|
||||
"behavior": "سلوك",
|
||||
"download_sources": "تحميل المصادر",
|
||||
"language": "لغة",
|
||||
"real_debrid_api_token": "رمز API",
|
||||
"enable_real_debrid": "تمكين ريال ديبريد",
|
||||
"real_debrid_description": "Real-Debrid هو برنامج تنزيل غير مقيد يسمح لك بتنزيل الملفات بسرعة، ولا يقتصر ذلك إلا على سرعة الإنترنت لديك.",
|
||||
"real_debrid_invalid_token": "رمز API غير صالح",
|
||||
"real_debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||
"real_debrid_linked_message": "حساب \"{{username}}\"مرتبط",
|
||||
"save_changes": "حفظ التغييرات",
|
||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||
"download_sources_description": "ستقوم Hydra بجلب روابط التنزيل من هذه المصادر. ",
|
||||
"validate_download_source": "التحقق من صحة",
|
||||
"remove_download_source": "إزالة",
|
||||
"add_download_source": "أضف المصدر",
|
||||
"download_count_zero": "{{countFormatted}} خيارات التنزيل",
|
||||
"download_source_url": "تنزيل عنوان URL المصدر",
|
||||
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
||||
"download_source_up_to_date": "محدث",
|
||||
"download_source_errored": "خطأ",
|
||||
"sync_download_sources": "مصادر المزامنة",
|
||||
"removed_download_source": "تمت إزالة مصدر التنزيل",
|
||||
"added_download_source": "تمت إضافة مصدر التنزيل",
|
||||
"download_sources_synced": "تتم مزامنة جميع مصادر التنزيل",
|
||||
"insert_valid_json_url": "أدخل عنوان URL صالحًا لـ JSON",
|
||||
"found_download_option_zero": "وجد {{countFormatted}} خيارات التنزيل",
|
||||
"import": "يستورد",
|
||||
"public": "عام",
|
||||
"private": "خاص",
|
||||
"friends_only": "الأصدقاء فقط",
|
||||
"privacy": "خصوصية",
|
||||
"profile_visibility": "رؤية الملف الشخصي",
|
||||
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
||||
"required_field": "هذه الخانة مطلوبه",
|
||||
"source_already_exists": "تمت إضافة هذا المصدر بالفعل",
|
||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا",
|
||||
"blocked_users": "المستخدمين المحظورين",
|
||||
"user_unblocked": "تم إلغاء حظر المستخدم",
|
||||
"enable_achievement_notifications": "عندما يتم فتح الإنجاز",
|
||||
"launch_minimized": "تم تصغير إطلاق Hydra",
|
||||
"disable_nsfw_alert": "تعطيل تنبيه NSFW",
|
||||
"show_hidden_achievement_description": "إظهار وصف الإنجازات المخفية قبل فتحها",
|
||||
"download_count_one": "{{countFormatted}} خيار التنزيل",
|
||||
"download_count_two": "{{countFormatted}} خيارات التنزيل",
|
||||
"download_count_few": "{{countFormatted}} خيارات التنزيل",
|
||||
"download_count_many": "{{countFormatted}} خيارات التنزيل",
|
||||
"download_count_other": "{{countFormatted}} خيارات التنزيل",
|
||||
"found_download_option_one": "وجد {{countFormatted}} خيار التنزيل",
|
||||
"found_download_option_two": "وجد {{countFormatted}} خيارات التنزيل",
|
||||
"found_download_option_few": "وجد {{countFormatted}} خيارات التنزيل",
|
||||
"found_download_option_many": "وجد {{countFormatted}} خيارات التنزيل",
|
||||
"found_download_option_other": "وجد {{countFormatted}} خيارات التنزيل"
|
||||
"downloads_path": "مَسَارُ التَّنْزِيلَاتِ",
|
||||
"change": "تَحْدِيثٌ",
|
||||
"notifications": "الإِشْعَارَاتُ",
|
||||
"enable_download_notifications": "عِنْدَ اكْتِمَالِ التَّنْزِيلِ",
|
||||
"enable_repack_list_notifications": "عِنْدَ إِضَافَةِ إِصْدَارٍ مُعَادٍ تَغْلِيفِهِ جَدِيدٍ",
|
||||
"real_debrid_api_token_label": "رَمْزُ واجهة برمجة التطبيقات Real-Debrid",
|
||||
"quit_app_instead_hiding": "لا تُخْفِ Hydra عِنْدَ الإِغْلَاقِ",
|
||||
"launch_with_system": "تَشْغِيلُ Hydra عِنْدَ بَدْءِ النِّظَامِ",
|
||||
"general": "عَامٌ",
|
||||
"behavior": "سُلُوكٌ",
|
||||
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
|
||||
"language": "اللُّغَةُ",
|
||||
"real_debrid_api_token": "رَمْزُ واجهة برمجة التطبيقات",
|
||||
"enable_real_debrid": "تَمْكِينُ Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid هُوَ مُنَزِّلٌ غَيْرُ مَقْيُودٍ يَتِيحُ لَكَ تَنْزِيلَ الْمَلَفَّاتِ بِسُرْعَةٍ، مَحْدُودٌ فَقَطْ بِسُرْعَةِ الْإِنْتَرْنِتِ لَدَيْكَ.",
|
||||
"real_debrid_invalid_token": "رَمْزُ واجهة برمجة التطبيقات غَيْرُ صَالِحٍ",
|
||||
"real_debrid_api_token_hint": "يُمْكِنُكَ الْحُصُولُ عَلَى رَمْزِ واجهة برمجة التطبيقات <0>هُنَا</0>",
|
||||
"real_debrid_free_account_error": "الْحِسَابُ \"{{username}}\" هُوَ حِسَابٌ مَجَّانِيٌّ. يَرْجَى الِاشْتِرَاكُ فِي Real-Debrid",
|
||||
"real_debrid_linked_message": "تَمَّ رَبْطُ الْحِسَابِ \"{{username}}\"",
|
||||
"save_changes": "حِفْظُ التَّغْيِيرَاتِ",
|
||||
"changes_saved": "تَمَّ حِفْظُ التَّغْيِيرَاتِ بِنَجَاحٍ",
|
||||
"download_sources_description": "سَتَقُومُ Hydra بِجَلْبِ رَوَابِطِ التَّنْزِيلِ مِنْ هَذِهِ الْمَصَادِرِ. يَجِبُ أَنْ يَكُونَ عُنْوَانُ URL لِلْمَصْدَرِ رَابِطًا مُبَاشِرًا إِلَى مِلَفٍّ .json يَحْتَوِي عَلَى رَوَابِطِ التَّنْزِيلِ.",
|
||||
"validate_download_source": "تَصْدِيقٌ",
|
||||
"remove_download_source": "إِزَالَةٌ",
|
||||
"add_download_source": "إِضَافَةُ مَصْدَرٍ",
|
||||
"download_count_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
|
||||
"download_count_one": "{{countFormatted}} خِيَارُ تَنْزِيلٍ",
|
||||
"download_count_other": "{{countFormatted}} خِيَارَاتُ تَنْزِيلٍ",
|
||||
"download_source_url": "عُنْوَانُ مَصْدَرِ التَّنْزِيلِ",
|
||||
"add_download_source_description": "أَدْخِلْ عُنْوَانَ URL لِمِلَفٍّ .json",
|
||||
"download_source_up_to_date": "مُحَدَّثٌ",
|
||||
"download_source_errored": "خَطَأٌ",
|
||||
"sync_download_sources": "مَزْجُ الْمَصَادِرِ",
|
||||
"removed_download_source": "تَمَّ إِزَالَةُ مَصْدَرِ التَّنْزِيلِ",
|
||||
"added_download_source": "تَمَّتْ إِضَافَةُ مَصْدَرِ التَّنْزِيلِ",
|
||||
"download_sources_synced": "تَمَّ مَزْجُ جَمِيعِ مَصَادِرِ التَّنْزِيلِ",
|
||||
"insert_valid_json_url": "أَدْخِلْ عُنْوَانَ JSON صَالِحًا",
|
||||
"found_download_option_zero": "لَمْ يُعْثَرْ عَلَى خِيَارِ تَنْزِيلٍ",
|
||||
"found_download_option_one": "عُثِرَ عَلَى {{countFormatted}} خِيَارِ تَنْزِيلٍ",
|
||||
"found_download_option_other": "عُثِرَ عَلَى {{countFormatted}} خِيَارَاتِ تَنْزِيلٍ",
|
||||
"import": "اسْتِيرَادٌ",
|
||||
"public": "عَامٌ",
|
||||
"private": "خَاصٌ",
|
||||
"friends_only": "الْأَصْدِقَاءُ فَقَطْ",
|
||||
"privacy": "الْخُصُوصِيَّةُ",
|
||||
"profile_visibility": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"profile_visibility_description": "اخْتَرْ مَنْ يُمْكِنُهُ رُؤْيَةُ مَلَفِّكَ الشَّخْصِيِّ وَمَكْتَبَتِكَ",
|
||||
"required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
|
||||
"source_already_exists": "تَمَّتْ إِضَافَةُ هَذَا الْمَصْدَرِ مِنْ قَبْلُ",
|
||||
"must_be_valid_url": "يَجِبُ أَنْ يَكُونَ الْمَصْدَرُ عُنْوَانَ URL صَالِحًا",
|
||||
"blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
|
||||
"user_unblocked": "تَمَّ إِزَالَةُ حَظْرِ الْمُسْتَخْدِمِ",
|
||||
"enable_achievement_notifications": "عِنْدَ فَتْحِ إِنْجَازٍ",
|
||||
"launch_minimized": "تَشْغِيلُ Hydra مُصَغَّرًا",
|
||||
"disable_nsfw_alert": "تَعْطِيلُ تَنْبِيهِ الْمُحْتَوَى غَيْرِ اللَّائِقِ",
|
||||
"seed_after_download_complete": "الْبَذْرُ بَعْدَ اكْتِمَالِ التَّنْزِيلِ",
|
||||
"show_hidden_achievement_description": "إِظْهَارُ وَصْفِ الإِنْجَازَاتِ الْمَخْفِيَّةِ قَبْلَ فَتْحِهَا"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "اكتمل التنزيل",
|
||||
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
||||
"repack_list_updated": "تم تحديث قائمة إعادة التعبئة",
|
||||
"new_update_available": "إصدار {{version}} متاح",
|
||||
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
||||
"notification_achievement_unlocked_title": "تم فتح الإنجاز لـ {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} وغيرها {{count}} تم فتحها",
|
||||
"repack_count_zero": "{{count}} تمت إضافة العبوات",
|
||||
"repack_count_one": "{{count}} تمت إضافة أعد حزم",
|
||||
"repack_count_two": "{{count}} تمت إضافة العبوات",
|
||||
"repack_count_few": "{{count}} تمت إضافة العبوات",
|
||||
"repack_count_many": "{{count}} تمت إضافة العبوات",
|
||||
"repack_count_other": "{{count}} تمت إضافة العبوات"
|
||||
"download_complete": "اكْتِمَالُ التَّنْزِيلِ",
|
||||
"game_ready_to_install": "{{title}} جَاهِزٌ لِلتَّثْبِيتِ",
|
||||
"repack_list_updated": "تَمَّ تَحْدِيثُ قَائِمَةِ الإِصْدَارَاتِ الْمُعَادَةِ تَغْلِيفُهَا",
|
||||
"repack_count_one": "{{count}} إِصْدَارٌ مُعَادٌ تَغْلِيفُهُ أُضِيفَ",
|
||||
"repack_count_other": "{{count}} إِصْدَارَاتٌ مُعَادَةٌ تَغْلِيفُهَا أُضِيفَتْ",
|
||||
"new_update_available": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ",
|
||||
"restart_to_install_update": "أَعِدْ تَشْغِيلَ Hydra لِتَثْبِيتِ التَّحْدِيثِ",
|
||||
"notification_achievement_unlocked_title": "تَمَّ فَتْحُ إِنْجَازٍ لِـ {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} وَ{{count}} أُخْرَى تَمَّ فَتْحُهَا"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "افتح Hydra",
|
||||
"quit": "خروج"
|
||||
"open": "فَتْحُ Hydra",
|
||||
"quit": "الْخُرُوجُ"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "لا توجد تنزيلات متاحة"
|
||||
"no_downloads": "لَا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "البرامج غير مثبتة",
|
||||
"description": "لم يتم العثور على الملفات التنفيذية الخاصة بـ Wine أو Lutris على نظامك",
|
||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة Linux لديك حتى تعمل اللعبة بشكل طبيعي"
|
||||
"title": "الْبَرَامِجُ غَيْرُ مُثَبَّتَةٍ",
|
||||
"description": "لَمْ يُعْثَرْ عَلَى مَلَفَّاتٍ تَنْفِيذِيَّةٍ لِـ Wine أَوْ Lutris عَلَى نِظَامِكَ",
|
||||
"instructions": "تَحَقَّقْ مِنَ الطَّرِيقَةِ الصَّحِيحَةِ لِتَثْبِيتِ أَيٍّ مِنْهُمَا عَلَى تَوْزِيعَةِ Linux لَدَيْكَ لِتَعْمَلَ اللُّعْبَةُ بِشَكْلٍ طَبِيعِيٍّ"
|
||||
},
|
||||
"modal": {
|
||||
"close": "زر الإغلاق"
|
||||
"close": "زِرُّ الإِغْلَاقِ"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
||||
"toggle_password_visibility": "تَبْدِيلُ رُؤْيَةِ كَلِمَةِ الْمَرُورِ"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"last_time_played": "لعبت آخر مرة {{period}}",
|
||||
"activity": "النشاط الأخير",
|
||||
"library": "مكتبة",
|
||||
"total_play_time": "إجمالي وقت اللعب",
|
||||
"no_recent_activity_title": "هممم... لا شيء هنا",
|
||||
"no_recent_activity_description": "لم تلعب أي مباراة مؤخرًا. ",
|
||||
"display_name": "اسم العرض",
|
||||
"saving": "توفير",
|
||||
"save": "يحفظ",
|
||||
"edit_profile": "تحرير الملف الشخصي",
|
||||
"saved_successfully": "تم الحفظ بنجاح",
|
||||
"try_again": "من فضلك، حاول مرة أخرى",
|
||||
"sign_out_modal_title": "هل أنت متأكد؟",
|
||||
"cancel": "إلغاء",
|
||||
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
||||
"sign_out": "تسجيل الخروج",
|
||||
"playing_for": "اللعب من أجل {{amount}}",
|
||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. ",
|
||||
"add_friends": "أضف أصدقاء",
|
||||
"add": "يضيف",
|
||||
"friend_code": "رمز الصديق",
|
||||
"see_profile": "انظر الملف الشخصي",
|
||||
"sending": "إرسال",
|
||||
"friend_request_sent": "تم إرسال طلب الصداقة",
|
||||
"friends": "أصدقاء",
|
||||
"friends_list": "قائمة الأصدقاء",
|
||||
"user_not_found": "لم يتم العثور على المستخدم",
|
||||
"block_user": "حظر المستخدم",
|
||||
"add_friend": "إضافة صديق",
|
||||
"request_sent": "تم إرسال الطلب",
|
||||
"request_received": "تم استلام الطلب",
|
||||
"accept_request": "قبول الطلب",
|
||||
"ignore_request": "تجاهل الطلب",
|
||||
"cancel_request": "إلغاء الطلب",
|
||||
"undo_friendship": "التراجع عن الصداقة",
|
||||
"request_accepted": "تم قبول الطلب",
|
||||
"user_blocked_successfully": "تم حظر المستخدم بنجاح",
|
||||
"user_block_modal_text": "هذا سوف يمنع {{displayName}}",
|
||||
"blocked_users": "المستخدمين المحظورين",
|
||||
"unblock": "إلغاء الحظر",
|
||||
"no_friends_added": "ليس لديك أي أصدقاء مضافين",
|
||||
"pending": "قيد الانتظار",
|
||||
"no_pending_invites": "ليس لديك أي دعوات معلقة",
|
||||
"no_blocked_users": "ليس لديك أي مستخدمين محظورين",
|
||||
"friend_code_copied": "تم نسخ رمز الصديق",
|
||||
"undo_friendship_modal_text": "سيؤدي هذا إلى التراجع عن صداقتك معه {{displayName}}",
|
||||
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>إعدادات</0>",
|
||||
"locked_profile": "هذا الملف الشخصي خاص",
|
||||
"image_process_failure": "فشل أثناء معالجة الصورة",
|
||||
"required_field": "هذه الخانة مطلوبه",
|
||||
"displayname_min_length": "يجب أن يتكون اسم العرض من 3 أحرف على الأقل",
|
||||
"displayname_max_length": "يجب ألا يزيد طول اسم العرض عن 50 حرفًا",
|
||||
"report_profile": "الإبلاغ عن هذا الملف الشخصي",
|
||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
||||
"report_description": "معلومات إضافية",
|
||||
"report_description_placeholder": "معلومات إضافية",
|
||||
"report": "تقرير",
|
||||
"report_reason_hate": "خطاب الكراهية",
|
||||
"report_reason_sexual_content": "المحتوى الجنسي",
|
||||
"report_reason_violence": "عنف",
|
||||
"report_reason_spam": "رسائل إلكترونية مزعجة",
|
||||
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
||||
"your_friend_code": "رمز صديقك:",
|
||||
"upload_banner": "تحميل لافتة",
|
||||
"uploading_banner": "جارٍ تحميل البانر…",
|
||||
"background_image_updated": "تم تحديث صورة الخلفية",
|
||||
"report_reason_zero": "آخر",
|
||||
"report_reason_one": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
||||
"report_reason_two": "آخر",
|
||||
"report_reason_few": "آخر",
|
||||
"report_reason_many": "آخر",
|
||||
"report_reason_other": "آخر"
|
||||
"amount_hours": "{{amount}} سَاعَاتٌ",
|
||||
"amount_minutes": "{{amount}} دَقَائِقُ",
|
||||
"last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
|
||||
"activity": "النَّشَاطُ الْأَخِيرُ",
|
||||
"library": "الْمَكْتَبَةُ",
|
||||
"total_play_time": "إِجْمَالِيُّ وَقْتِ اللَّعِبِ",
|
||||
"no_recent_activity_title": "هَمَمْ... لَا شَيْءَ هُنَا",
|
||||
"no_recent_activity_description": "لَمْ تَلْعَبْ أَيَّ أَلْعَابٍ مُؤَخَّرًا. حَانَ الْوَقْتُ لِتَغْيِيرِ ذَلِكَ!",
|
||||
"display_name": "اسْمُ الْعَرْضِ",
|
||||
"saving": "جَارٍ الْحِفْظُ",
|
||||
"save": "حِفْظٌ",
|
||||
"edit_profile": "تَحْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"saved_successfully": "تَمَّ الْحِفْظُ بِنَجَاحٍ",
|
||||
"try_again": "الرَّجَاءُ الْمُحَاوَلَةُ مَرَّةً أُخْرَى",
|
||||
"sign_out_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
||||
"cancel": "إِلْغَاءٌ",
|
||||
"successfully_signed_out": "تَمَّ تَسْجِيلُ الْخُرُوجِ بِنَجَاحٍ",
|
||||
"sign_out": "تَسْجِيلُ الْخُرُوجِ",
|
||||
"playing_for": "جَارِي اللَّعِبُ لِمُدَّةِ {{amount}}",
|
||||
"sign_out_modal_text": "مَكْتَبَتُكَ مُرْتَبِطَةٌ بِحِسَابِكَ الْحَالِيِّ. عِنْدَ تَسْجِيلِ الْخُرُوجِ، لَنْ تَكُونَ مَكْتَبَتُكَ مَرْئِيَّةً بَعْدَ الْآنِ، وَلَنْ يَتِمَّ حِفْظُ أَيِّ تَقَدُّمٍ. هَلْ تُرِيدُ الْمُتَابَعَةَ مَعَ تَسْجِيلِ الْخُرُوجِ؟",
|
||||
"add_friends": "إِضَافَةُ الْأَصْدِقَاءِ",
|
||||
"add": "إِضَافَةٌ",
|
||||
"friend_code": "رَمْزُ الصَّدِيقِ",
|
||||
"see_profile": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"sending": "جَارٍ الْإِرْسَالُ",
|
||||
"friend_request_sent": "تَمَّ إِرْسَالُ طَلَبِ الصَّدَاقَةِ",
|
||||
"friends": "الْأَصْدِقَاءُ",
|
||||
"friends_list": "قَائِمَةُ الْأَصْدِقَاءِ",
|
||||
"user_not_found": "الْمُسْتَخْدِمُ غَيْرُ مَوْجُودٍ",
|
||||
"block_user": "حَظْرُ الْمُسْتَخْدِمِ",
|
||||
"add_friend": "إِضَافَةُ صَدِيقٍ",
|
||||
"request_sent": "تَمَّ إِرْسَالُ الطَّلَبِ",
|
||||
"request_received": "تَمَّ اسْتِقْبَالُ الطَّلَبِ",
|
||||
"accept_request": "قَبُولُ الطَّلَبِ",
|
||||
"ignore_request": "تَجَاهُلُ الطَّلَبِ",
|
||||
"cancel_request": "إِلْغَاءُ الطَّلَبِ",
|
||||
"undo_friendship": "إِلْغَاءُ الصَّدَاقَةِ",
|
||||
"request_accepted": "تَمَّ قَبُولُ الطَّلَبِ",
|
||||
"user_blocked_successfully": "تَمَّ حَظْرُ الْمُسْتَخْدِمِ بِنَجَاحٍ",
|
||||
"user_block_modal_text": "سَيُؤَدِّي هَذَا إِلَى حَظْرِ {{displayName}}",
|
||||
"blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
|
||||
"unblock": "إِزَالَةُ الْحَظْرِ",
|
||||
"no_friends_added": "لَيْسَ لَدَيْكَ أَصْدِقَاءٌ مُضَافُونَ",
|
||||
"pending": "قَيْدُ الْانْتِظَارِ",
|
||||
"no_pending_invites": "لَيْسَ لَدَيْكَ دَعَوَاتٌ قَيْدُ الْانْتِظَارِ",
|
||||
"no_blocked_users": "لَيْسَ لَدَيْكَ مُسْتَخْدِمُونَ مَحْظُورُونَ",
|
||||
"friend_code_copied": "تَمَّ نَسْخُ رَمْزِ الصَّدِيقِ",
|
||||
"undo_friendship_modal_text": "سَيُؤَدِّي هَذَا إِلَى إِلْغَاءِ صَدَاقَتِكَ مَعَ {{displayName}}",
|
||||
"privacy_hint": "لِتَعْدِيلِ مَنْ يُمْكِنُهُ رُؤْيَةُ هَذَا، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
|
||||
"locked_profile": "هَذَا الْمَلَفُّ الشَّخْصِيُّ خَاصٌّ",
|
||||
"image_process_failure": "فَشَلَ أَثْنَاءَ مُعَالَجَةِ الصُّورَةِ",
|
||||
"required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
|
||||
"displayname_min_length": "يَجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَقَلِّ 3 أَحْرُفٍ",
|
||||
"displayname_max_length": "يَجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَكْثَرِ 50 حَرْفًا",
|
||||
"report_profile": "تَقْرِيرٌ عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"report_reason": "لِمَاذَا تُقَدِّمُ تَقْرِيرًا عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ؟",
|
||||
"report_description": "مَعْلُومَاتٌ إِضَافِيَّةٌ",
|
||||
"report_description_placeholder": "مَعْلُومَاتٌ إِضَافِيَّةٌ",
|
||||
"report": "تَقْرِيرٌ",
|
||||
"report_reason_hate": "خِطَابُ الْكُرْهِ",
|
||||
"report_reason_sexual_content": "مُحْتَوًى جِنْسِيٌّ",
|
||||
"report_reason_violence": "عُنْفٌ",
|
||||
"report_reason_spam": "رَاسِلَةٌ عَشْوَائِيَّةٌ",
|
||||
"report_reason_other": "آخَرُ",
|
||||
"profile_reported": "تَمَّ تَقْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"your_friend_code": "رَمْزُ صَدِيقِكَ:",
|
||||
"upload_banner": "رَفْعُ لَافِتَةٍ",
|
||||
"uploading_banner": "جَارٍ رَفْعُ اللَّافِتَةِ...",
|
||||
"background_image_updated": "تَمَّ تَحْدِيثُ صُورَةِ الْخَلْفِيَّةِ",
|
||||
"stats": "الإحْصَائِيَّاتُ",
|
||||
"achievements": "الإِنْجَازَاتُ",
|
||||
"games": "الْأَلْعَابُ",
|
||||
"top_percentile": "الْأَفْضَلُ {{percentile}}%",
|
||||
"ranking_updated_weekly": "التَّرْتِيبُ يُحَدَّثُ أُسْبُوعِيًّا",
|
||||
"playing": "جَارِي اللَّعِبُ {{game}}",
|
||||
"achievements_unlocked": "الإِنْجَازَاتُ الْمَفْتُوحَةُ",
|
||||
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ",
|
||||
"show_achievements_on_profile": "عَرْضُ إِنْجَازَاتِكَ عَلَى مَلَفِّكَ الشَّخْصِيِّ",
|
||||
"show_points_on_profile": "عَرْضُ النَّقَاطِ الْمَكْسُوبَةِ عَلَى مَلَفِّكَ الشَّخْصِيِّ"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "تم فتح الإنجاز",
|
||||
"user_achievements": "{{displayName}}إنجازات",
|
||||
"your_achievements": "إنجازاتك",
|
||||
"unlocked_at": "مقفلة في: {{date}}",
|
||||
"subscription_needed": "مطلوب اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
||||
"new_achievements_unlocked": "مفتوح {{achievementCount}} انجازات جديدة من {{gameCount}} ألعاب",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} الإنجازات",
|
||||
"achievements_unlocked_for_game": "مفتوح {{achievementCount}} انجازات جديدة ل {{gameTitle}}"
|
||||
"achievement_unlocked": "إِنْجَازٌ مَفْتُوحٌ",
|
||||
"user_achievements": "إِنْجَازَاتُ {{displayName}}",
|
||||
"your_achievements": "إِنْجَازَاتُكَ",
|
||||
"unlocked_at": "تَمَّ الْفَتْحُ فِي: {{date}}",
|
||||
"subscription_needed": "يَحْتَاجُ اشْتِرَاكُ Hydra Cloud لِرُؤْيَةِ هَذَا الْمُحْتَوَى",
|
||||
"new_achievements_unlocked": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ مِنْ {{gameCount}} أَلْعَابٍ",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إِنْجَازَاتٍ",
|
||||
"achievements_unlocked_for_game": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ لِـ {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "هَذَا إِنْجَازٌ مَخْفِيٌّ",
|
||||
"achievement_earn_points": "اكْسِبْ {{points}} نَقَاطًا بِهَذَا الإِنْجَازِ",
|
||||
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ:",
|
||||
"available_points": "النَّقَاطُ الْمُتَوَفِّرَةُ:",
|
||||
"how_to_earn_achievements_points": "كَيْفَ تَكْسِبُ نَقَاطَ الإِنْجَازَاتِ؟"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "اشتراك Hydra كلاود",
|
||||
"subscribe_now": "اشترك الآن",
|
||||
"cloud_saving": "الحفظ السحابي",
|
||||
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
||||
"animated_profile_picture": "صور شخصية متحركة",
|
||||
"premium_support": "دعم متميز",
|
||||
"show_and_compare_achievements": "عرض ومقارنة إنجازاتك مع المستخدمين الآخرين",
|
||||
"animated_profile_banner": "لافتة الملف الشخصي المتحركة"
|
||||
"subscription_tour_title": "اشْتِرَاكُ Hydra Cloud",
|
||||
"subscribe_now": "اشْتَرِكِ الْآنَ",
|
||||
"cloud_saving": "حِفْظٌ سَحَابِيٌّ",
|
||||
"cloud_achievements": "حِفْظُ إِنْجَازَاتِكَ فِي السَّحَابَةِ",
|
||||
"animated_profile_picture": "صُورُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
|
||||
"premium_support": "الدَّعْمُ الْمُتَقَدِّمُ",
|
||||
"show_and_compare_achievements": "عَرْضٌ وَمُقَارَنَةُ إِنْجَازَاتِكَ مَعَ مُسْتَخْدِمِينَ آخَرِينَ",
|
||||
"animated_profile_banner": "لَافِتَةُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "لَقَدْ اكْتَشَفْتَ مِيزَةً مِنْ Hydra Cloud!",
|
||||
"learn_more": "تَعَلَّمْ أَكْثَرَ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,20 @@
|
||||
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Следваща страница",
|
||||
"previous_page": "Предишна страница"
|
||||
"search": "Филтър…",
|
||||
"developers": "Разработчици",
|
||||
"genres": "Жанрове",
|
||||
"tags": "Тагове",
|
||||
"publishers": "Издатели",
|
||||
"download_sources": "Източници за изтегляне",
|
||||
"result_count": "{{resultCount}} резултати",
|
||||
"filter_count": "{{filterCount}} налични",
|
||||
"clear_filters": "Изчисти {{filterCount}} избрани"
|
||||
},
|
||||
"game_details": {
|
||||
"launch_options": "Опции за стартиране",
|
||||
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране (экспериментальный)",
|
||||
"launch_options_placeholder": "Няма зададен параметър",
|
||||
"open_download_options": "Варианти за изтегляне",
|
||||
"download_options_zero": "Няма варианти за изтегляне",
|
||||
"download_options_one": "{{count}} варианти за изтегляне",
|
||||
@@ -177,6 +187,10 @@
|
||||
"loading": "Зареждане…"
|
||||
},
|
||||
"downloads": {
|
||||
"seeding": "Сийдване",
|
||||
"stop_seeding": "Спри сийдването",
|
||||
"resume_seeding": "Продължи сийдването",
|
||||
"options": "Управление",
|
||||
"resume": "Продължи",
|
||||
"pause": "Пауза",
|
||||
"eta": "Conclusion {{eta}}",
|
||||
@@ -202,6 +216,8 @@
|
||||
"checking_files": "Проверка на файлове…"
|
||||
},
|
||||
"settings": {
|
||||
"seed_after_download_complete": "Сийд след завършване на изтеглянето",
|
||||
"show_hidden_achievement_description": "Показвай описанието на скритите постижения преди отключването им",
|
||||
"downloads_path": "Инсталационен път",
|
||||
"change": "Актуализиране",
|
||||
"notifications": "Известия",
|
||||
@@ -210,7 +226,7 @@
|
||||
"real_debrid_api_token_label": "Real-Debrid API токен",
|
||||
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
|
||||
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
|
||||
"general": "Общ",
|
||||
"general": "Общи",
|
||||
"behavior": "Поведение",
|
||||
"download_sources": "Източници за изтегляне",
|
||||
"language": "Език",
|
||||
@@ -288,6 +304,16 @@
|
||||
"toggle_password_visibility": "Превключване на видимостта на паролата"
|
||||
},
|
||||
"user_profile": {
|
||||
"stats": "Статистики",
|
||||
"achievements": "Постижения",
|
||||
"games": "Игри",
|
||||
"top_percentile": "Топ {{percentile}}%",
|
||||
"ranking_updated_weekly": "Класацията се актуализира седмично",
|
||||
"playing": "Играе {{game}}",
|
||||
"achievements_unlocked": "Отключени постижения",
|
||||
"earned_points": "Спечелени точки",
|
||||
"show_achievements_on_profile": "Показвай своите постижения в профила",
|
||||
"show_points_on_profile": "Показвай спечелените точки в профила",
|
||||
"amount_hours": "{{amount}} часове",
|
||||
"amount_minutes": "{{amount}} минути",
|
||||
"last_time_played": "Последно играно {{period}}",
|
||||
@@ -359,6 +385,11 @@
|
||||
"background_image_updated": "Обновено фоново изображение"
|
||||
},
|
||||
"achievement": {
|
||||
"hidden_achievement_tooltip": "Това е скрито постижение",
|
||||
"achievement_earn_points": "Спечели {{points}} точки с това постижение",
|
||||
"earned_points": "Спечелени точки:",
|
||||
"available_points": "Налични точки:",
|
||||
"how_to_earn_achievements_points": "Как да спечелиш точки за постижения?",
|
||||
"achievement_unlocked": "Постижението е отключено",
|
||||
"user_achievements": "Постиженията на {{displayName}} ",
|
||||
"your_achievements": "Вашите Постижения",
|
||||
@@ -369,6 +400,9 @@
|
||||
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Открихте функция на Hydra Cloud!",
|
||||
"learn_more": "Научете повече",
|
||||
"subscription_tour_title": "Hydra Cloud Абонамент",
|
||||
"subscribe_now": "Абонирай се сега",
|
||||
"cloud_saving": "Запазване в облака",
|
||||
|
||||
@@ -167,6 +167,9 @@
|
||||
"loading_save_preview": "Searching for save games…",
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "The Wine prefix used to run this game",
|
||||
"launch_options": "Launch Options",
|
||||
"launch_options_description": "Advanced users may choose to enter modifications to their launch options (experimental feature)",
|
||||
"launch_options_placeholder": "No parameter specified",
|
||||
"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",
|
||||
@@ -175,7 +178,13 @@
|
||||
"select_folder": "Select folder",
|
||||
"backup_from": "Backup from {{date}}",
|
||||
"custom_backup_location_set": "Custom backup location set",
|
||||
"no_directory_selected": "No directory selected"
|
||||
"no_directory_selected": "No directory selected",
|
||||
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
|
||||
"reset_achievements": "Reset achievements",
|
||||
"reset_achievements_description": "This will reset all achievements for {{game}}",
|
||||
"reset_achievements_title": "Are you sure?",
|
||||
"reset_achievements_success": "Achievements successfully reset",
|
||||
"reset_achievements_error": "Failed to reset achievements"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
@@ -271,7 +280,23 @@
|
||||
"launch_minimized": "Launch Hydra minimized",
|
||||
"disable_nsfw_alert": "Disable NSFW alert",
|
||||
"seed_after_download_complete": "Seed after download complete",
|
||||
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them"
|
||||
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
|
||||
"account": "Account",
|
||||
"no_users_blocked": "You have no blocked users",
|
||||
"subscription_active_until": "Your Hydra Cloud is active until {{date}}",
|
||||
"manage_subscription": "Manage subscription",
|
||||
"update_email": "Update email",
|
||||
"update_password": "Update password",
|
||||
"current_email": "Current email:",
|
||||
"no_email_account": "You have not set an email yet",
|
||||
"account_data_updated_successfully": "Account data updated successfully",
|
||||
"renew_subscription": "Renew Hydra Cloud",
|
||||
"subscription_expired_at": "Your subscription expired at {{date}}",
|
||||
"no_subscription": "Enjoy Hydra in the best possible way",
|
||||
"become_subscriber": "Be Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||
"bill_sent_until": "Your next bill will be sent until this day"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
||||
@@ -155,6 +155,9 @@
|
||||
"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",
|
||||
"launch_options": "Opções de Inicialização",
|
||||
"launch_options_description": "Usuários avançados podem adicionar opções de inicialização no jogo (experimental)",
|
||||
"launch_options_placeholder": "Nenhum parâmetro informado",
|
||||
"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",
|
||||
@@ -164,7 +167,12 @@
|
||||
"select_folder": "Selecione a pasta",
|
||||
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
||||
"clear": "Limpar",
|
||||
"no_directory_selected": "Nenhum diretório selecionado"
|
||||
"no_directory_selected": "Nenhum diretório selecionado",
|
||||
"reset_achievements": "Resetar conquistas",
|
||||
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
||||
"reset_achievements_title": "Tem certeza?",
|
||||
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
||||
"reset_achievements_error": "Falha ao resetar conquistas"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@@ -260,7 +268,23 @@
|
||||
"launch_minimized": "Iniciar o Hydra minimizado",
|
||||
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
|
||||
"seed_after_download_complete": "Semear após a conclusão do download",
|
||||
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las"
|
||||
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las",
|
||||
"account": "Conta",
|
||||
"no_users_blocked": "Você não bloqueou nenhum usuário",
|
||||
"subscription_active_until": "Sua assinatura Hydra Cloud ficará ativa até {{date}}",
|
||||
"manage_subscription": "Gerenciar assinatura",
|
||||
"update_email": "Atualizar email",
|
||||
"update_password": "Atualizar senha",
|
||||
"current_email": "Email atual:",
|
||||
"no_email_account": "Você ainda não adicionou um email a sua conta",
|
||||
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
|
||||
"renew_subscription": "Renovar Hydra Cloud",
|
||||
"subscription_expired_at": "Sua assinatura expirou em {{date}}",
|
||||
"no_subscription": "Aproveite o Hydra da melhor forma possível",
|
||||
"become_subscriber": "Seja Hydra Cloud",
|
||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
@@ -389,7 +413,7 @@
|
||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
||||
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Está é uma conquista oculta",
|
||||
"hidden_achievement_tooltip": "Esta é uma conquista oculta",
|
||||
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
|
||||
"earned_points": "Pontos ganhos:",
|
||||
"available_points": "Pontos disponíveis:",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"featured": "Рекомендации",
|
||||
"surprise_me": "Удиви меня",
|
||||
"no_results": "Ничего не найдено",
|
||||
"hot": "Сейчас в топе",
|
||||
"start_typing": "Начинаю вводить текст для поиска...",
|
||||
"hot": "Сейчас популярно",
|
||||
"start_typing": "Начинаю вводить текст...",
|
||||
"weekly": "📅 Лучшие игры недели",
|
||||
"achievements": "🏆 Игры, в которых нужно победить"
|
||||
},
|
||||
@@ -167,6 +167,9 @@
|
||||
"loading_save_preview": "Поиск сохранений…",
|
||||
"wine_prefix": "Префикс Wine",
|
||||
"wine_prefix_description": "Префикс Wine, используемый для запуска этой игры",
|
||||
"launch_options": "Параметры запуска",
|
||||
"launch_options_description": "Опытные пользователи могут внести изменения в параметры запуска",
|
||||
"launch_options_placeholder": "Параметр не указан ",
|
||||
"no_download_option_info": "Информация недоступна",
|
||||
"backup_deletion_failed": "Не удалось удалить резервную копию",
|
||||
"max_number_of_artifacts_reached": "Достигнуто максимальное количество резервных копий для этой игры",
|
||||
@@ -175,7 +178,11 @@
|
||||
"select_folder": "Выбрать папку",
|
||||
"backup_from": "Резервная копия от {{date}}",
|
||||
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
||||
"no_directory_selected": "Не выбран каталог"
|
||||
"no_directory_selected": "Не выбран каталог",
|
||||
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
||||
"reset_achievements_title": "Вы уверены?",
|
||||
"reset_achievements_success": "Достижения успешно сброшены",
|
||||
"reset_achievements_error": "Не удалось сбросить достижения"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активировать Hydra",
|
||||
@@ -271,7 +278,23 @@
|
||||
"source_already_exists": "Этот источник уже добавлен",
|
||||
"user_unblocked": "Пользователь разблокирован",
|
||||
"seed_after_download_complete": "Раздавать после завершения загрузки",
|
||||
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением"
|
||||
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
|
||||
"account": "Аккаунт",
|
||||
"no_users_blocked": "У вас нет заблокированных пользователей",
|
||||
"subscription_active_until": "Ваша подписка на Hydra Cloud активна до {{date}}",
|
||||
"manage_subscription": "Управлять подпиской",
|
||||
"update_email": "Обновить электронную почту",
|
||||
"update_password": "Обновить пароль",
|
||||
"current_email": "Текущий email:",
|
||||
"no_email_account": "Вы еще не установили электронную почту",
|
||||
"account_data_updated_successfully": "Данные учетной записи успешно обновлены",
|
||||
"renew_subscription": "Обновить подписку Hydra Cloud",
|
||||
"subscription_expired_at": "Срок действия вашей подписки истек в {{date}}",
|
||||
"no_subscription": "Наслаждайтесь Hydra по максимуму",
|
||||
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
||||
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
|
||||
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Загрузка завершена",
|
||||
@@ -401,7 +424,7 @@
|
||||
"subscribe_now": "Подпишитесь прямо сейчас",
|
||||
"cloud_saving": "Сохранение в облаке",
|
||||
"cloud_achievements": "Сохраняйте свои достижения в облаке",
|
||||
"animated_profile_picture": "Анимированные фотографии профиля",
|
||||
"animated_profile_picture": "Анимированные аватарки",
|
||||
"premium_support": "Премиальная поддержка",
|
||||
"show_and_compare_achievements": "Показывайте и сравнивайте свои достижения с достижениями других пользователей",
|
||||
"animated_profile_banner": "Анимированный баннер профиля",
|
||||
|
||||
@@ -1,131 +1,423 @@
|
||||
{
|
||||
"language_name": "Türkçe",
|
||||
"app": {
|
||||
"successfully_signed_in": "Başarıyla giriş yapıldı"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Öne çıkan",
|
||||
"surprise_me": "Şaşırt beni",
|
||||
"no_results": "Sonuç bulunamadı"
|
||||
"featured": "Öne Çıkanlar",
|
||||
"surprise_me": "Beni Şaşırt",
|
||||
"no_results": "Sonuç bulunamadı",
|
||||
"start_typing": "Aramak için yazmaya başlayın...",
|
||||
"hot": "Şu anda popüler",
|
||||
"weekly": "📅 Haftanın en iyi oyunları",
|
||||
"achievements": "🏆 Tamamlanacak oyunlar"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirmeler",
|
||||
"downloads": "İndirilenler",
|
||||
"settings": "Ayarlar",
|
||||
"my_library": "Kütüphane",
|
||||
"downloading_metadata": "{{title}} (Metadata indiriliyor…)",
|
||||
"paused": "{{title}} (Duraklatıldı)",
|
||||
"my_library": "Kütüphanem",
|
||||
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
|
||||
"paused": "{{title}} (Durduruldu)",
|
||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||
"filter": "Kütüphaneyi filtrele",
|
||||
"home": "Ana menü"
|
||||
"home": "Ana Sayfa",
|
||||
"queued": "{{title}} (Sırada)",
|
||||
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
||||
"sign_in": "Giriş yap",
|
||||
"friends": "Arkadaşlar",
|
||||
"need_help": "Yardıma mı ihtiyacınız var?"
|
||||
},
|
||||
"header": {
|
||||
"search": "Ara",
|
||||
"home": "Ana menü",
|
||||
"search": "Oyunları ara",
|
||||
"home": "Ana Sayfa",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirmeler",
|
||||
"downloads": "İndirilenler",
|
||||
"search_results": "Arama sonuçları",
|
||||
"settings": "Ayarlar"
|
||||
"settings": "Ayarlar",
|
||||
"version_available_install": "Sürüm {{version}} mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
|
||||
"version_available_download": "Sürüm {{version}} mevcut. İndirmek için buraya tıklayın."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "İndirilen bir şey yok",
|
||||
"downloading_metadata": "{{title}} metadatası indiriliyor…",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}"
|
||||
"no_downloads_in_progress": "Devam eden indirme yok",
|
||||
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlama: {{eta}} - Hız: {{speed}}",
|
||||
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Sonraki sayfa",
|
||||
"previous_page": "Önceki sayfa"
|
||||
"search": "Filtrele…",
|
||||
"developers": "Geliştiriciler",
|
||||
"genres": "Türler",
|
||||
"tags": "Etiketler",
|
||||
"publishers": "Yayıncılar",
|
||||
"download_sources": "İndirme kaynakları",
|
||||
"result_count": "{{resultCount}} sonuç",
|
||||
"filter_count": "{{filterCount}} mevcut",
|
||||
"clear_filters": "{{filterCount}} seçili filtreyi temizle"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "İndirme seçeneklerini aç",
|
||||
"download_options_zero": "İndirme seçeneği yok",
|
||||
"download_options_one": "{{count}} indirme seçeneği",
|
||||
"download_options_other": "{{count}} indirme seçeneği",
|
||||
"updated_at": "{{updated_at}} güncellendi",
|
||||
"install": "İndir",
|
||||
"updated_at": "{{updated_at}} tarihinde güncellendi",
|
||||
"install": "Yükle",
|
||||
"resume": "Devam et",
|
||||
"pause": "Duraklat",
|
||||
"pause": "Durdur",
|
||||
"cancel": "İptal et",
|
||||
"remove": "Sil",
|
||||
"space_left_on_disk": "Diskte {{space}} yer kaldı",
|
||||
"eta": "Bitiş {{eta}}",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"filter": "Repackleri filtrele",
|
||||
"remove": "Kaldır",
|
||||
"space_left_on_disk": "Diskte {{space}} boş alan kaldı",
|
||||
"eta": "{{eta}} tahmini bitiş",
|
||||
"calculating_eta": "Kalan süre hesaplanıyor…",
|
||||
"downloading_metadata": "Meta veriler indiriliyor…",
|
||||
"filter": "Paketleri filtrele",
|
||||
"requirements": "Sistem gereksinimleri",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Önerilen",
|
||||
"release_date": "{{date}} tarihinde çıktı",
|
||||
"publisher": "{{publisher}} tarihinde yayınlandı",
|
||||
"hours": "saatler",
|
||||
"minutes": "dakikalar",
|
||||
"paused": "Durduruldu",
|
||||
"release_date": "{{date}} tarihinde yayımlandı",
|
||||
"publisher": "{{publisher}} tarafından yayımlandı",
|
||||
"hours": "saat",
|
||||
"minutes": "dakika",
|
||||
"amount_hours": "{{amount}} saat",
|
||||
"amount_minutes": "{{amount}} dakika",
|
||||
"accuracy": "%{{accuracy}} doğruluk",
|
||||
"accuracy": "{{accuracy}}% doğruluk",
|
||||
"add_to_library": "Kütüphaneye ekle",
|
||||
"remove_from_library": "Kütüphaneden kaldır",
|
||||
"no_downloads": "İndirme yok",
|
||||
"play_time": "{{amount}} oynandı",
|
||||
"last_time_played": "Son oynanan {{period}}",
|
||||
"not_played_yet": "Bu {{title}} hiç oynanmadı",
|
||||
"next_suggestion": "Sıradaki öneri",
|
||||
"no_downloads": "İndirilebilir içerik yok",
|
||||
"play_time": "{{amount}} süre oynandı",
|
||||
"last_time_played": "Son oynama {{period}} önce",
|
||||
"not_played_yet": "{{title}} henüz oynanmadı",
|
||||
"next_suggestion": "Sonraki öneri",
|
||||
"play": "Oyna",
|
||||
"deleting": "Installer siliniyor…",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
"close": "Kapat",
|
||||
"playing_now": "Şimdi oynanıyor",
|
||||
"playing_now": "Şu anda oynanıyor",
|
||||
"change": "Değiştir",
|
||||
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
|
||||
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
|
||||
"download_now": "Şimdi"
|
||||
"repacks_modal_description": "İndirmek istediğiniz paketi seçin",
|
||||
"select_folder_hint": "Varsayılan klasörü değiştirmek için <0>Ayarlar</0> bölümüne gidin",
|
||||
"download_now": "Şimdi indir",
|
||||
"no_shop_details": "Mağaza bilgileri alınamadı.",
|
||||
"download_options": "İndirme seçenekleri",
|
||||
"download_path": "İndirme yolu",
|
||||
"previous_screenshot": "Önceki ekran görüntüsü",
|
||||
"next_screenshot": "Sonraki ekran görüntüsü",
|
||||
"screenshot": "{{number}} ekran görüntüsü",
|
||||
"open_screenshot": "{{number}} ekran görüntüsünü aç",
|
||||
"download_settings": "İndirme ayarları",
|
||||
"downloader": "İndirici",
|
||||
"select_executable": "Seç",
|
||||
"no_executable_selected": "Hiçbir çalıştırılabilir dosya seçilmedi",
|
||||
"open_folder": "Klasörü aç",
|
||||
"open_download_location": "İndirilen dosyaları gör",
|
||||
"create_shortcut": "Masaüstü kısayolu oluştur",
|
||||
"clear": "Temizle",
|
||||
"remove_files": "Dosyaları kaldır",
|
||||
"remove_from_library_title": "Emin misiniz?",
|
||||
"remove_from_library_description": "Bu işlem {{game}} oyununu kütüphanenizden kaldıracaktır",
|
||||
"options": "Seçenekler",
|
||||
"executable_section_title": "Çalıştırılabilir dosya",
|
||||
"executable_section_description": "\"Oyna\" tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"downloads_secion_title": "İndirmeler",
|
||||
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
|
||||
"danger_zone_section_title": "Tehlike bölgesi",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyaları kaldırın",
|
||||
"download_in_progress": "İndirme devam ediyor",
|
||||
"download_paused": "İndirme durduruldu",
|
||||
"last_downloaded_option": "Son indirilen seçenek",
|
||||
"create_shortcut_success": "Kısayol başarıyla oluşturuldu",
|
||||
"create_shortcut_error": "Kısayol oluşturulurken hata oluştu",
|
||||
"nsfw_content_title": "Bu oyun uygunsuz içerik içeriyor",
|
||||
"nsfw_content_description": "{{title}} her yaş için uygun olmayabilecek içeriklere sahiptir. Devam etmek istediğinizden emin misiniz?",
|
||||
"allow_nsfw_content": "Devam et",
|
||||
"refuse_nsfw_content": "Geri dön",
|
||||
"stats": "İstatistikler",
|
||||
"download_count": "İndirme sayısı",
|
||||
"player_count": "Aktif oyuncular",
|
||||
"download_error": "Bu indirme seçeneği mevcut değil",
|
||||
"download": "İndir",
|
||||
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
|
||||
"warning": "Uyarı:",
|
||||
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
|
||||
"achievements": "Başarılar",
|
||||
"achievements_count": "Başarılar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Bulut kaydı",
|
||||
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
|
||||
"backups": "Yedekler",
|
||||
"install_backup": "Yükle",
|
||||
"delete_backup": "Sil",
|
||||
"create_backup": "Yeni yedek oluştur",
|
||||
"last_backup_date": "{{date}} tarihindeki son yedek",
|
||||
"no_backup_preview": "Bu oyun için kayıtlı oyun bulunamadı",
|
||||
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
|
||||
"uploading_backup": "Yedek yükleniyor…",
|
||||
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
|
||||
"backup_uploaded": "Yedek yüklendi",
|
||||
"backup_deleted": "Yedek silindi",
|
||||
"backup_restored": "Yedek geri yüklendi",
|
||||
"see_all_achievements": "Tüm başarıları gör",
|
||||
"sign_in_to_see_achievements": "Başarıları görmek için giriş yapın",
|
||||
"mapping_method_automatic": "Otomatik",
|
||||
"mapping_method_manual": "Manuel",
|
||||
"mapping_method_label": "Eşleme yöntemi",
|
||||
"files_automatically_mapped": "Dosyalar otomatik olarak eşlendi",
|
||||
"no_backups_created": "Bu oyun için yedek oluşturulmadı",
|
||||
"manage_files": "Dosyaları yönet",
|
||||
"loading_save_preview": "Kayıtlı oyunlar aranıyor…",
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
|
||||
"launch_options": "Başlatma Seçenekleri",
|
||||
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine değişiklikler girebilir (deneysel özellik)",
|
||||
"launch_options_placeholder": "Belirtilen bir parametre yok",
|
||||
"no_download_option_info": "Bilgi mevcut değil",
|
||||
"backup_deletion_failed": "Yedek silinemedi",
|
||||
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
|
||||
"achievements_not_sync": "Başarılarınızı senkronize etmeyi öğrenin",
|
||||
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
|
||||
"select_folder": "Klasör seç",
|
||||
"backup_from": "{{date}} tarihinden yedek",
|
||||
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
|
||||
"no_directory_selected": "Bir dizin seçilmedi",
|
||||
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
|
||||
"reset_achievements": "Başarıları sıfırla",
|
||||
"reset_achievements_description": "Bu işlem {{game}} için tüm başarıları sıfırlar",
|
||||
"reset_achievements_title": "Emin misiniz?",
|
||||
"reset_achievements_success": "Başarılar başarıyla sıfırlandı",
|
||||
"reset_achievements_error": "Başarılar sıfırlanamadı"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra'yı aktif et",
|
||||
"installation_id": "Kurulum ID'si:",
|
||||
"enter_activation_code": "Aktifleştirme kodunuzu girin",
|
||||
"message": "Bunu nerede soracağınızı bilmiyorsanız, buna sahip olmamanız gerekiyor.",
|
||||
"activate": "Aktif et",
|
||||
"title": "Hydra'yı Aktive Et",
|
||||
"installation_id": "Kurulum Kimliği:",
|
||||
"enter_activation_code": "Aktivasyon kodunuzu girin",
|
||||
"message": "Bunu nereden soracağınızı bilmiyorsanız, bu sizin için olmamalı.",
|
||||
"activate": "Aktive Et",
|
||||
"loading": "Yükleniyor…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Devam et",
|
||||
"resume": "Devam Et",
|
||||
"pause": "Duraklat",
|
||||
"eta": "Bitiş {{eta}}",
|
||||
"eta": "Tamamlama {{eta}}",
|
||||
"paused": "Duraklatıldı",
|
||||
"verifying": "Doğrulanıyor…",
|
||||
"completed": "Tamamlandı",
|
||||
"cancel": "İptal et",
|
||||
"filter": "Yüklü oyunları filtrele",
|
||||
"removed": "İndirilmedi",
|
||||
"cancel": "İptal Et",
|
||||
"filter": "İndirilen oyunları filtrele",
|
||||
"remove": "Kaldır",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"deleting": "Installer siliniyor…",
|
||||
"delete": "Installer'ı sil",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
"delete": "Yükleyiciyi kaldır",
|
||||
"delete_modal_title": "Emin misiniz?",
|
||||
"delete_modal_description": "Bu bilgisayarınızdan tüm kurulum dosyalarını silecek",
|
||||
"install": "Kur"
|
||||
"delete_modal_description": "Bu işlem, tüm kurulum dosyalarını bilgisayarınızdan kaldıracaktır",
|
||||
"install": "Kur",
|
||||
"download_in_progress": "Devam ediyor",
|
||||
"queued_downloads": "Sıradaki indirmeler",
|
||||
"downloads_completed": "Tamamlananlar",
|
||||
"queued": "Sırada",
|
||||
"no_downloads_title": "Bomboş",
|
||||
"no_downloads_description": "Henüz Hydra ile hiçbir şey indirmediniz, ancak başlamak için asla geç değil.",
|
||||
"checking_files": "Dosyalar kontrol ediliyor…",
|
||||
"seeding": "Paylaşılıyor",
|
||||
"stop_seeding": "Paylaşımı durdur",
|
||||
"resume_seeding": "Paylaşımı sürdür",
|
||||
"options": "Yönet"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "İndirme yolu",
|
||||
"change": "Güncelle",
|
||||
"notifications": "Bildirimler",
|
||||
"enable_download_notifications": "Bir indirme bittiğinde",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde"
|
||||
"enable_download_notifications": "Bir indirme tamamlandığında",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
|
||||
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
|
||||
"quit_app_instead_hiding": "Hydra'yı kapatırken gizlemeyin",
|
||||
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
|
||||
"general": "Genel",
|
||||
"behavior": "Davranış",
|
||||
"download_sources": "İndirme kaynakları",
|
||||
"language": "Dil",
|
||||
"real_debrid_api_token": "API Anahtarı",
|
||||
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
|
||||
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak hızlı dosya indirmenizi sağlayan sınırsız bir indirici.",
|
||||
"real_debrid_invalid_token": "Geçersiz API anahtarı",
|
||||
"real_debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
|
||||
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
|
||||
"real_debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
||||
"save_changes": "Değişiklikleri Kaydet",
|
||||
"changes_saved": "Değişiklikler başarıyla kaydedildi",
|
||||
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacak. Kaynak URL, indirme bağlantılarını içeren bir .json dosyasına doğrudan bir bağlantı olmalıdır.",
|
||||
"validate_download_source": "Doğrula",
|
||||
"remove_download_source": "Kaldır",
|
||||
"add_download_source": "Kaynak ekle",
|
||||
"download_count_zero": "İndirme seçeneği yok",
|
||||
"download_count_one": "{{countFormatted}} indirme seçeneği",
|
||||
"download_count_other": "{{countFormatted}} indirme seçeneği",
|
||||
"download_source_url": "İndirme kaynağı URL'si",
|
||||
"add_download_source_description": ".json dosyasının URL'sini girin",
|
||||
"download_source_up_to_date": "Güncel",
|
||||
"download_source_errored": "Hatalı",
|
||||
"sync_download_sources": "Kaynakları senkronize et",
|
||||
"removed_download_source": "İndirme kaynağı kaldırıldı",
|
||||
"added_download_source": "İndirme kaynağı eklendi",
|
||||
"download_sources_synced": "Tüm indirme kaynakları senkronize edildi",
|
||||
"insert_valid_json_url": "Geçerli bir JSON URL'si girin",
|
||||
"found_download_option_zero": "Hiçbir indirme seçeneği bulunamadı",
|
||||
"found_download_option_one": "{{countFormatted}} indirme seçeneği bulundu",
|
||||
"found_download_option_other": "{{countFormatted}} indirme seçeneği bulundu",
|
||||
"import": "İçe aktar",
|
||||
"public": "Herkese açık",
|
||||
"private": "Gizli",
|
||||
"friends_only": "Sadece arkadaşlar",
|
||||
"privacy": "Gizlilik",
|
||||
"profile_visibility": "Profil görünürlüğü",
|
||||
"profile_visibility_description": "Profilinizi ve kütüphanenizi kimlerin görebileceğini seçin",
|
||||
"required_field": "Bu alan gereklidir",
|
||||
"source_already_exists": "Bu kaynak zaten eklenmiş",
|
||||
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"user_unblocked": "Kullanıcının engeli kaldırıldı",
|
||||
"enable_achievement_notifications": "Bir başarı kilidi açıldığında",
|
||||
"launch_minimized": "Hydra'yı küçültülmüş başlat",
|
||||
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
|
||||
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
|
||||
"show_hidden_achievement_description": "Gizli başarı açıklamalarını kilitlenmeden önce göster"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "İndirme tamamlandı",
|
||||
"game_ready_to_install": "{{title}} kuruluma hazır",
|
||||
"game_ready_to_install": "{{title}} kurulmaya hazır",
|
||||
"repack_list_updated": "Repack listesi güncellendi",
|
||||
"repack_count_one": "{{count}} yeni repack eklendi",
|
||||
"repack_count_other": "{{count}} yeni repack eklendi"
|
||||
"repack_count_one": "{{count}} repack eklendi",
|
||||
"repack_count_other": "{{count}} repack eklendi",
|
||||
"new_update_available": "Sürüm {{version}} mevcut",
|
||||
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra'yı yeniden başlatın",
|
||||
"notification_achievement_unlocked_title": "{{game}} için başarı kilidi açıldı",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarılar açıldı"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra'yı aç",
|
||||
"open": "Hydra'yı Aç",
|
||||
"quit": "Çık"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "İndirme mevcut değil"
|
||||
"no_downloads": "İndirilebilir içerik bulunmuyor"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programlar yüklü değil",
|
||||
"description": "Sisteminizde Wine veya Lutris çalıştırılabiliri bulunamadı",
|
||||
"instructions": "Oyunları düzgün şekilde çalıştırmak için Linux distronuza bunlardan birini nasıl yükleyebileceğinize bakın"
|
||||
"title": "Programlar Yüklü Değil",
|
||||
"description": "Wine veya Lutris çalıştırılabilir dosyaları sisteminizde bulunamadı",
|
||||
"instructions": "Oyunun normal çalışabilmesi için bunlardan herhangi birini Linux dağıtımınıza uygun şekilde nasıl kuracağınızı kontrol edin"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Kapat tuşu"
|
||||
"close": "Kapat düğmesi"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Şifre görünürlüğünü değiştir"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} saat",
|
||||
"amount_minutes": "{{amount}} dakika",
|
||||
"last_time_played": "Son oynanma {{period}}",
|
||||
"activity": "Son Etkinlik",
|
||||
"library": "Kütüphane",
|
||||
"total_play_time": "Toplam oynama süresi",
|
||||
"no_recent_activity_title": "Hmmm… burada bir şey yok",
|
||||
"no_recent_activity_description": "Son zamanlarda hiç oyun oynamamışsınız. Bunu değiştirmenin zamanı geldi!",
|
||||
"display_name": "Görünen isim",
|
||||
"saving": "Kaydediliyor",
|
||||
"save": "Kaydet",
|
||||
"edit_profile": "Profili Düzenle",
|
||||
"saved_successfully": "Başarıyla kaydedildi",
|
||||
"try_again": "Lütfen tekrar deneyin",
|
||||
"sign_out_modal_title": "Emin misiniz?",
|
||||
"cancel": "İptal",
|
||||
"successfully_signed_out": "Başarıyla çıkış yapıldı",
|
||||
"sign_out": "Çıkış yap",
|
||||
"playing_for": "{{amount}} oynanıyor",
|
||||
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaptığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Çıkışa devam etmek istiyor musunuz?",
|
||||
"add_friends": "Arkadaş Ekle",
|
||||
"add": "Ekle",
|
||||
"friend_code": "Arkadaş kodu",
|
||||
"see_profile": "Profili gör",
|
||||
"sending": "Gönderiliyor",
|
||||
"friend_request_sent": "Arkadaşlık isteği gönderildi",
|
||||
"friends": "Arkadaşlar",
|
||||
"friends_list": "Arkadaş listesi",
|
||||
"user_not_found": "Kullanıcı bulunamadı",
|
||||
"block_user": "Kullanıcıyı engelle",
|
||||
"add_friend": "Arkadaş ekle",
|
||||
"request_sent": "İstek gönderildi",
|
||||
"request_received": "İstek alındı",
|
||||
"accept_request": "İsteği kabul et",
|
||||
"ignore_request": "İsteği yok say",
|
||||
"cancel_request": "İsteği iptal et",
|
||||
"undo_friendship": "Arkadaşlığı sonlandır",
|
||||
"request_accepted": "İstek kabul edildi",
|
||||
"user_blocked_successfully": "Kullanıcı başarıyla engellendi",
|
||||
"user_block_modal_text": "Bu işlem {{displayName}} adlı kullanıcıyı engelleyecek",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"unblock": "Engeli kaldır",
|
||||
"no_friends_added": "Hiç arkadaş eklemediniz",
|
||||
"pending": "Bekliyor",
|
||||
"no_pending_invites": "Bekleyen davetiniz yok",
|
||||
"no_blocked_users": "Engellenmiş kullanıcı yok",
|
||||
"friend_code_copied": "Arkadaş kodu kopyalandı",
|
||||
"undo_friendship_modal_text": "Bu işlem {{displayName}} ile arkadaşlığınızı sonlandıracak",
|
||||
"privacy_hint": "Bunu kimin görebileceğini ayarlamak için <0>Ayarlar</0> bölümüne gidin",
|
||||
"locked_profile": "Bu profil gizli",
|
||||
"image_process_failure": "Görüntü işleme başarısız oldu",
|
||||
"required_field": "Bu alan gerekli",
|
||||
"displayname_min_length": "Görünen isim en az 3 karakter uzunluğunda olmalıdır",
|
||||
"displayname_max_length": "Görünen isim en fazla 50 karakter uzunluğunda olabilir",
|
||||
"report_profile": "Bu profili bildir",
|
||||
"report_reason": "Bu profili neden bildiriyorsunuz?",
|
||||
"report_description": "Ek bilgi",
|
||||
"report_description_placeholder": "Ek bilgi",
|
||||
"report": "Bildir",
|
||||
"report_reason_hate": "Nefret söylemi",
|
||||
"report_reason_sexual_content": "Cinsel içerik",
|
||||
"report_reason_violence": "Şiddet",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Diğer",
|
||||
"profile_reported": "Profil bildirildi",
|
||||
"your_friend_code": "Arkadaş kodunuz:",
|
||||
"upload_banner": "Afiş yükle",
|
||||
"uploading_banner": "Afiş yükleniyor…",
|
||||
"background_image_updated": "Arka plan görüntüsü güncellendi",
|
||||
"stats": "İstatistikler",
|
||||
"achievements": "Başarılar",
|
||||
"games": "Oyunlar",
|
||||
"top_percentile": "En üst {{percentile}}%",
|
||||
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
|
||||
"playing": "{{game}} oynanıyor",
|
||||
"achievements_unlocked": "Başarılar açıldı",
|
||||
"earned_points": "Kazanılan puanlar",
|
||||
"show_achievements_on_profile": "Başarılarınızı profilinizde gösterin",
|
||||
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Başarı açıldı",
|
||||
"user_achievements": "{{displayName}}'in Başarıları",
|
||||
"your_achievements": "Başarılarınız",
|
||||
"unlocked_at": "Açılma zamanı: {{date}}",
|
||||
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
|
||||
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarı açıldı",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarı açıldı",
|
||||
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
|
||||
"achievement_earn_points": "Bu başarı ile {{points}} puan kazanın",
|
||||
"earned_points": "Kazanılan puanlar:",
|
||||
"available_points": "Mevcut puanlar:",
|
||||
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud Aboneliği",
|
||||
"subscribe_now": "Şimdi abone olun",
|
||||
"cloud_saving": "Bulut kaydetme",
|
||||
"cloud_achievements": "Başarılarınızı buluta kaydedin",
|
||||
"animated_profile_picture": "Animasyonlu profil resimleri",
|
||||
"premium_support": "Premium Destek",
|
||||
"show_and_compare_achievements": "Başarılarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
|
||||
"animated_profile_banner": "Animasyonlu profil afişi",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
|
||||
"learn_more": "Daha Fazla Bilgi Edinin"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,18 @@ export const defaultDownloadsPath = app.getPath("downloads");
|
||||
|
||||
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
|
||||
|
||||
export const levelDatabasePath = path.join(
|
||||
app.getPath("userData"),
|
||||
`hydra-db${isStaging ? "-staging" : ""}`
|
||||
);
|
||||
|
||||
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
||||
export const databasePath = path.join(
|
||||
databaseDirectory,
|
||||
isStaging ? "hydra_test.db" : "hydra.db"
|
||||
);
|
||||
|
||||
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
|
||||
export const logsPath = path.join(app.getPath("userData"), "hydra", "logs");
|
||||
|
||||
export const seedsPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "seeds")
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { DataSource } from "typeorm";
|
||||
import {
|
||||
DownloadQueue,
|
||||
Game,
|
||||
GameShopCache,
|
||||
UserPreferences,
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
UserSubscription,
|
||||
} from "@main/entity";
|
||||
|
||||
import { databasePath } from "./constants";
|
||||
|
||||
export const dataSource = new DataSource({
|
||||
type: "better-sqlite3",
|
||||
entities: [
|
||||
Game,
|
||||
UserAuth,
|
||||
UserPreferences,
|
||||
UserSubscription,
|
||||
GameShopCache,
|
||||
DownloadQueue,
|
||||
GameAchievement,
|
||||
],
|
||||
synchronize: false,
|
||||
database: databasePath,
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
} from "typeorm";
|
||||
import type { Game } from "./game.entity";
|
||||
|
||||
@Entity("download_queue")
|
||||
export class DownloadQueue {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@OneToOne("Game", "downloadQueue")
|
||||
@JoinColumn()
|
||||
game: Game;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity("game_achievement")
|
||||
export class GameAchievement {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text")
|
||||
objectId: string;
|
||||
|
||||
@Column("text")
|
||||
shop: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
unlockedAchievements: string | null;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
achievements: string | null;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
@Entity("game_shop_cache")
|
||||
export class GameShopCache {
|
||||
@PrimaryColumn("text", { unique: true })
|
||||
objectID: string;
|
||||
|
||||
@Column("text")
|
||||
shop: GameShop;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
serializedData: string;
|
||||
|
||||
/**
|
||||
* @deprecated Use IndexedDB's `howLongToBeatEntries` instead
|
||||
*/
|
||||
@Column("text", { nullable: true })
|
||||
howLongToBeatSerializedData: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
language: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToOne,
|
||||
} from "typeorm";
|
||||
|
||||
import type { GameShop, GameStatus } from "@types";
|
||||
import { Downloader } from "@shared";
|
||||
import type { DownloadQueue } from "./download-queue.entity";
|
||||
|
||||
@Entity("game")
|
||||
export class Game {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text", { unique: true })
|
||||
objectID: string;
|
||||
|
||||
@Column("text", { unique: true, nullable: true })
|
||||
remoteId: string | null;
|
||||
|
||||
@Column("text")
|
||||
title: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
iconUrl: string | null;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
folderName: string | null;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
downloadPath: string | null;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
executablePath: string | null;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
winePrefixPath: string | null;
|
||||
|
||||
@Column("int", { default: 0 })
|
||||
playTimeInMilliseconds: number;
|
||||
|
||||
@Column("text")
|
||||
shop: GameShop;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
status: GameStatus | null;
|
||||
|
||||
@Column("int", { default: Downloader.Torrent })
|
||||
downloader: Downloader;
|
||||
|
||||
/**
|
||||
* Progress is a float between 0 and 1
|
||||
*/
|
||||
@Column("float", { default: 0 })
|
||||
progress: number;
|
||||
|
||||
@Column("int", { default: 0 })
|
||||
bytesDownloaded: number;
|
||||
|
||||
@Column("datetime", { nullable: true })
|
||||
lastTimePlayed: Date | null;
|
||||
|
||||
@Column("float", { default: 0 })
|
||||
fileSize: number;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
uri: string | null;
|
||||
|
||||
@OneToOne("DownloadQueue", "game")
|
||||
downloadQueue: DownloadQueue;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
shouldSeed: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export * from "./game.entity";
|
||||
export * from "./user-auth.entity";
|
||||
export * from "./user-preferences.entity";
|
||||
export * from "./user-subscription.entity";
|
||||
export * from "./game-shop-cache.entity";
|
||||
export * from "./game.entity";
|
||||
export * from "./game-achievements.entity";
|
||||
export * from "./download-queue.entity";
|
||||
@@ -1,45 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToOne,
|
||||
} from "typeorm";
|
||||
import { UserSubscription } from "./user-subscription.entity";
|
||||
|
||||
@Entity("user_auth")
|
||||
export class UserAuth {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
userId: string;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
displayName: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
profileImageUrl: string | null;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
backgroundImageUrl: string | null;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
accessToken: string;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
refreshToken: string;
|
||||
|
||||
@Column("int", { default: 0 })
|
||||
tokenExpirationTimestamp: number;
|
||||
|
||||
@OneToOne("UserSubscription", "user")
|
||||
subscription: UserSubscription | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity("user_preferences")
|
||||
export class UserPreferences {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
downloadsPath: string | null;
|
||||
|
||||
@Column("text", { default: "en" })
|
||||
language: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
realDebridApiToken: string | null;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
downloadNotificationsEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
repackUpdatesNotificationsEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: true })
|
||||
achievementNotificationsEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
runAtStartup: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
startMinimized: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
disableNsfwAlert: boolean;
|
||||
|
||||
@Column("boolean", { default: true })
|
||||
seedAfterDownloadComplete: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
showHiddenAchievementsDescription: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { Auth } from "@types";
|
||||
import { Crypto } from "@main/services";
|
||||
|
||||
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const auth = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
if (!auth) return null;
|
||||
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||
const payload = jwt.decode(
|
||||
Crypto.decrypt(auth.accessToken)
|
||||
) as jwt.JwtPayload;
|
||||
|
||||
if (!payload) return null;
|
||||
|
||||
return payload.sessionId;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import i18next from "i18next";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
import { HydraApi, WindowManager } from "@main/services";
|
||||
import { AuthPage } from "@shared";
|
||||
|
||||
const openAuthWindow = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
WindowManager.openAuthWindow();
|
||||
const openAuthWindow = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
page: AuthPage
|
||||
) => {
|
||||
const searchParams = new URLSearchParams({
|
||||
lng: i18next.language,
|
||||
});
|
||||
|
||||
if ([AuthPage.UpdateEmail, AuthPage.UpdatePassword].includes(page)) {
|
||||
const { accessToken } = await HydraApi.refreshToken().catch(() => {
|
||||
return { accessToken: "" };
|
||||
});
|
||||
searchParams.set("token", accessToken);
|
||||
}
|
||||
|
||||
WindowManager.openAuthWindow(page, searchParams);
|
||||
};
|
||||
|
||||
registerEvent("openAuthWindow", openAuthWindow);
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity";
|
||||
import { PythonRPC } from "@main/services/python-rpc";
|
||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const databaseOperations = dataSource
|
||||
.transaction(async (transactionalEntityManager) => {
|
||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({});
|
||||
|
||||
await transactionalEntityManager.getRepository(Game).delete({});
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(UserAuth)
|
||||
.delete({ id: 1 });
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(UserSubscription)
|
||||
.delete({ id: 1 });
|
||||
})
|
||||
const databaseOperations = db
|
||||
.batch([
|
||||
{
|
||||
type: "del",
|
||||
key: levelKeys.auth,
|
||||
},
|
||||
{
|
||||
type: "del",
|
||||
key: levelKeys.user,
|
||||
},
|
||||
])
|
||||
.then(() => {
|
||||
/* Removes all games being played */
|
||||
gamesPlaytime.clear();
|
||||
|
||||
return Promise.all([gamesSublevel.clear(), downloadsSublevel.clear()]);
|
||||
});
|
||||
|
||||
/* Cancels any ongoing downloads */
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { gameShopCacheRepository } from "@main/repository";
|
||||
import { getSteamAppDetails } from "@main/services";
|
||||
import { getSteamAppDetails, logger } from "@main/services";
|
||||
|
||||
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
||||
import type { ShopDetails, GameShop } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const getLocalizedSteamAppDetails = async (
|
||||
objectId: string,
|
||||
@@ -39,35 +39,27 @@ const getGameShopDetails = async (
|
||||
language: string
|
||||
): Promise<ShopDetails | null> => {
|
||||
if (shop === "steam") {
|
||||
const cachedData = await gameShopCacheRepository.findOne({
|
||||
where: { objectID: objectId, language },
|
||||
});
|
||||
const cachedData = await gamesShopCacheSublevel.get(
|
||||
levelKeys.gameShopCacheItem(shop, objectId, language)
|
||||
);
|
||||
|
||||
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
||||
(result) => {
|
||||
if (result) {
|
||||
gameShopCacheRepository.upsert(
|
||||
{
|
||||
objectID: objectId,
|
||||
shop: "steam",
|
||||
language,
|
||||
serializedData: JSON.stringify(result),
|
||||
},
|
||||
["objectID"]
|
||||
);
|
||||
gamesShopCacheSublevel
|
||||
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
|
||||
.catch((err) => {
|
||||
logger.error("Could not cache game details", err);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
const cachedGame = cachedData?.serializedData
|
||||
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
|
||||
: null;
|
||||
|
||||
if (cachedGame) {
|
||||
if (cachedData) {
|
||||
return {
|
||||
...cachedGame,
|
||||
...cachedData,
|
||||
objectId,
|
||||
} as ShopDetails;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import type { TrendingGame } from "@types";
|
||||
|
||||
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
const language = userPreferences?.language || "en";
|
||||
const language = await db
|
||||
.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
})
|
||||
.then((language) => language || "en");
|
||||
|
||||
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
||||
"/games/trending",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { GameArtifact, GameShop } from "@types";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
import { SubscriptionRequiredError, UserNotLoggedInError } from "@shared";
|
||||
|
||||
const getGameArtifacts = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -22,6 +22,10 @@ const getGameArtifacts = async (
|
||||
return [];
|
||||
}
|
||||
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { GameShop } from "@types";
|
||||
import { Ludusavi } from "@main/services";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const getGameBackupPreview = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID: objectId,
|
||||
shop,
|
||||
},
|
||||
});
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
return Ludusavi.getBackupPreview(shop, objectId, game?.winePrefixPath);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import os from "node:os";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { app } from "electron";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const bundleBackup = async (
|
||||
shop: GameShop,
|
||||
@@ -46,12 +46,7 @@ const uploadSaveGame = async (
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID: objectId,
|
||||
shop,
|
||||
},
|
||||
});
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
const bundleLocation = await bundleBackup(
|
||||
shop,
|
||||
|
||||
15
src/main/events/hardware/check-folder-write-permission.ts
Normal file
15
src/main/events/hardware/check-folder-write-permission.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const checkFolderWritePermission = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
path: string
|
||||
) =>
|
||||
new Promise((resolve) => {
|
||||
fs.access(path, fs.constants.W_OK, (err) => {
|
||||
resolve(!err);
|
||||
});
|
||||
});
|
||||
|
||||
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
||||
@@ -1,10 +1,10 @@
|
||||
import checkDiskSpace from "check-disk-space";
|
||||
import disk from "diskusage";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDiskFreeSpace = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
path: string
|
||||
) => checkDiskSpace(path);
|
||||
) => disk.check(path);
|
||||
|
||||
registerEvent("getDiskFreeSpace", getDiskFreeSpace);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Document as YMLDocument } from "yaml";
|
||||
import { Game } from "@main/entity";
|
||||
import path from "node:path";
|
||||
|
||||
export const generateYML = (game: Game) => {
|
||||
const slugifiedGameTitle = game.title.replace(/\s/g, "-").toLocaleLowerCase();
|
||||
|
||||
const doc = new YMLDocument({
|
||||
name: game.title,
|
||||
game_slug: slugifiedGameTitle,
|
||||
slug: `${slugifiedGameTitle}-installer`,
|
||||
version: "Installer",
|
||||
runner: "wine",
|
||||
script: {
|
||||
game: {
|
||||
prefix: "$GAMEDIR",
|
||||
arch: "win64",
|
||||
working_dir: "$GAMEDIR",
|
||||
},
|
||||
installer: [
|
||||
{
|
||||
task: {
|
||||
name: "create_prefix",
|
||||
arch: "win64",
|
||||
prefix: "$GAMEDIR",
|
||||
},
|
||||
},
|
||||
{
|
||||
task: {
|
||||
executable: path.join(
|
||||
game.downloadPath!,
|
||||
game.folderName!,
|
||||
"setup.exe"
|
||||
),
|
||||
name: "wineexec",
|
||||
prefix: "$GAMEDIR",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return doc.toString();
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { defaultDownloadsPath } from "@main/constants";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
export const getDownloadsPath = async () => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences && userPreferences.downloadsPath)
|
||||
return userPreferences.downloadsPath;
|
||||
|
||||
@@ -11,6 +11,7 @@ import "./catalogue/get-trending-games";
|
||||
import "./catalogue/get-publishers";
|
||||
import "./catalogue/get-developers";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./hardware/check-folder-write-permission";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/create-game-shortcut";
|
||||
import "./library/close-game";
|
||||
@@ -22,13 +23,17 @@ import "./library/open-game-executable-path";
|
||||
import "./library/open-game-installer";
|
||||
import "./library/open-game-installer-path";
|
||||
import "./library/update-executable-path";
|
||||
import "./library/update-launch-options";
|
||||
import "./library/verify-executable-path";
|
||||
import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
import "./library/select-game-wine-prefix";
|
||||
import "./library/reset-game-achievements";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
import "./misc/get-features";
|
||||
import "./misc/show-item-in-folder";
|
||||
import "./torrenting/cancel-game-download";
|
||||
import "./torrenting/pause-game-download";
|
||||
import "./torrenting/resume-game-download";
|
||||
@@ -70,7 +75,6 @@ import "./cloud-save/delete-game-artifact";
|
||||
import "./cloud-save/select-game-backup-path";
|
||||
import "./notifications/publish-new-repacks-notification";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
import "./misc/show-item-in-folder";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => appVersion);
|
||||
|
||||
@@ -1,57 +1,55 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { GameShop } from "@types";
|
||||
import type { Game, GameShop } from "@types";
|
||||
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const addGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
title: string,
|
||||
shop: GameShop
|
||||
title: string
|
||||
) => {
|
||||
return gameRepository
|
||||
.update(
|
||||
{
|
||||
objectID: objectId,
|
||||
},
|
||||
{
|
||||
shop,
|
||||
status: null,
|
||||
isDeleted: false,
|
||||
}
|
||||
)
|
||||
.then(async ({ affected }) => {
|
||||
if (!affected) {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
if (game) {
|
||||
await downloadsSublevel.del(gameKey);
|
||||
|
||||
await gameRepository.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID: objectId,
|
||||
shop,
|
||||
});
|
||||
}
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: { objectID: objectId },
|
||||
});
|
||||
|
||||
updateLocalUnlockedAchivements(game!);
|
||||
|
||||
createGame(game!).catch(() => {});
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isDeleted: false,
|
||||
});
|
||||
} else {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
const game: Game = {
|
||||
title,
|
||||
iconUrl,
|
||||
objectId,
|
||||
shop,
|
||||
remoteId: null,
|
||||
isDeleted: false,
|
||||
playTimeInMilliseconds: 0,
|
||||
lastTimePlayed: null,
|
||||
};
|
||||
|
||||
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
||||
|
||||
updateLocalUnlockedAchivements(game!);
|
||||
|
||||
createGame(game!).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("addGameToLibrary", addGameToLibrary);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { logger } from "@main/services";
|
||||
import sudo from "sudo-prompt";
|
||||
import { app } from "electron";
|
||||
import { PythonRPC } from "@main/services/python-rpc";
|
||||
import { ProcessPayload } from "@main/services/download/types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { GameShop } from "@types";
|
||||
|
||||
const getKillCommand = (pid: number) => {
|
||||
if (process.platform == "win32") {
|
||||
@@ -16,15 +17,14 @@ const getKillCommand = (pid: number) => {
|
||||
|
||||
const closeGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const processes =
|
||||
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
|
||||
[];
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: gameId, isDeleted: false },
|
||||
});
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
if (!game) return;
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
import createDesktopShortcut from "create-desktop-shortcuts";
|
||||
import path from "node:path";
|
||||
import { app } from "electron";
|
||||
import { removeSymbolsFromName } from "@shared";
|
||||
import { GameShop } from "@types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const createGameShortcut = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
id: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
): Promise<boolean> => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id, executablePath: Not(IsNull()) },
|
||||
});
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (game) {
|
||||
const filePath = game.executablePath;
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||
import { logger } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameShop } from "@types";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const deleteGameFolder = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
): Promise<void> => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: [
|
||||
{
|
||||
id: gameId,
|
||||
isDeleted: false,
|
||||
status: "removed",
|
||||
},
|
||||
{
|
||||
id: gameId,
|
||||
progress: 1,
|
||||
isDeleted: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
const downloadKey = levelKeys.game(shop, objectId);
|
||||
|
||||
if (!game) return;
|
||||
const download = await downloadsSublevel.get(downloadKey);
|
||||
|
||||
if (game.folderName) {
|
||||
if (!download) return;
|
||||
|
||||
if (download.folderName) {
|
||||
const folderPath = path.join(
|
||||
game.downloadPath ?? (await getDownloadsPath()),
|
||||
game.folderName
|
||||
download.downloadPath ?? (await getDownloadsPath()),
|
||||
download.folderName
|
||||
);
|
||||
|
||||
if (fs.existsSync(folderPath)) {
|
||||
@@ -52,10 +42,7 @@ const deleteGameFolder = async (
|
||||
}
|
||||
}
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: gameId },
|
||||
{ downloadPath: null, folderName: null, status: null, progress: 0 }
|
||||
);
|
||||
await downloadsSublevel.del(downloadKey);
|
||||
};
|
||||
|
||||
registerEvent("deleteGameFolder", deleteGameFolder);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, downloadsSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const getGameByObjectId = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) =>
|
||||
gameRepository.findOne({
|
||||
where: {
|
||||
objectID: objectId,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const [game, download] = await Promise.all([
|
||||
gamesSublevel.get(gameKey),
|
||||
downloadsSublevel.get(gameKey),
|
||||
]);
|
||||
|
||||
if (!game) return null;
|
||||
|
||||
return { id: gameKey, ...game, download };
|
||||
};
|
||||
|
||||
registerEvent("getGameByObjectId", getGameByObjectId);
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import type { LibraryGame } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { downloadsSublevel, gamesSublevel } from "@main/level";
|
||||
|
||||
const getLibrary = async () =>
|
||||
gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
relations: {
|
||||
downloadQueue: true,
|
||||
},
|
||||
order: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||
return gamesSublevel
|
||||
.iterator()
|
||||
.all()
|
||||
.then((results) => {
|
||||
return Promise.all(
|
||||
results
|
||||
.filter(([_key, game]) => game.isDeleted === false)
|
||||
.map(async ([key, game]) => {
|
||||
const download = await downloadsSublevel.get(key);
|
||||
|
||||
return {
|
||||
id: key,
|
||||
...game,
|
||||
download: download ?? null,
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("getLibrary", getLibrary);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { shell } from "electron";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { GameShop } from "@types";
|
||||
|
||||
const openGameExecutablePath = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: gameId, isDeleted: false },
|
||||
});
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
if (!game || !game.executablePath) return;
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { shell } from "electron";
|
||||
import path from "node:path";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameShop } from "@types";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const openGameInstallerPath = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: gameId, isDeleted: false },
|
||||
});
|
||||
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
if (!game || !game.folderName || !game.downloadPath) return true;
|
||||
if (!download || !download.folderName || !download.downloadPath) return true;
|
||||
|
||||
const gamePath = path.join(
|
||||
game.downloadPath ?? (await getDownloadsPath()),
|
||||
game.folderName!
|
||||
download.downloadPath ?? (await getDownloadsPath()),
|
||||
download.folderName!
|
||||
);
|
||||
|
||||
shell.showItemInFolder(gamePath);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { shell } from "electron";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { spawnSync, exec } from "node:child_process";
|
||||
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { generateYML } from "../helpers/generate-lutris-yaml";
|
||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
import { GameShop } from "@types";
|
||||
|
||||
const executeGameInstaller = (filePath: string) => {
|
||||
if (process.platform === "win32") {
|
||||
@@ -26,21 +24,21 @@ const executeGameInstaller = (filePath: string) => {
|
||||
|
||||
const openGameInstaller = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: gameId, isDeleted: false },
|
||||
});
|
||||
const downloadKey = levelKeys.game(shop, objectId);
|
||||
const download = await downloadsSublevel.get(downloadKey);
|
||||
|
||||
if (!game || !game.folderName) return true;
|
||||
if (!download || !download.folderName) return true;
|
||||
|
||||
const gamePath = path.join(
|
||||
game.downloadPath ?? (await getDownloadsPath()),
|
||||
game.folderName!
|
||||
download.downloadPath ?? (await getDownloadsPath()),
|
||||
download.folderName!
|
||||
);
|
||||
|
||||
if (!fs.existsSync(gamePath)) {
|
||||
await gameRepository.update({ id: gameId }, { status: null });
|
||||
await downloadsSublevel.del(downloadKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -70,13 +68,6 @@ const openGameInstaller = async (
|
||||
);
|
||||
}
|
||||
|
||||
if (spawnSync("which", ["lutris"]).status === 0) {
|
||||
const ymlPath = path.join(gamePath, "setup.yml");
|
||||
await writeFile(ymlPath, generateYML(game));
|
||||
exec(`lutris --install "${ymlPath}"`);
|
||||
return true;
|
||||
}
|
||||
|
||||
shell.openPath(gamePath);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { shell } from "electron";
|
||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { GameShop } from "@types";
|
||||
|
||||
const openGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number,
|
||||
executablePath: string
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
executablePath: string,
|
||||
launchOptions?: string | null
|
||||
) => {
|
||||
// TODO: revisit this for launchOptions
|
||||
const parsedPath = parseExecutablePath(executablePath);
|
||||
|
||||
await gameRepository.update({ id: gameId }, { executablePath: parsedPath });
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
executablePath: parsedPath,
|
||||
launchOptions,
|
||||
});
|
||||
|
||||
shell.openPath(parsedPath);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const removeGameFromLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
gameRepository.update(
|
||||
{ id: gameId },
|
||||
{ isDeleted: true, executablePath: null }
|
||||
);
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
removeRemoveGameFromLibrary(gameId).catch((err) => {
|
||||
logger.error("removeRemoveGameFromLibrary", err);
|
||||
});
|
||||
};
|
||||
if (game) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isDeleted: true,
|
||||
executablePath: null,
|
||||
});
|
||||
|
||||
const removeRemoveGameFromLibrary = async (gameId: number) => {
|
||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
||||
|
||||
if (game?.remoteId) {
|
||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||
if (game?.remoteId) {
|
||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
import { levelKeys, downloadsSublevel } from "@main/level";
|
||||
import { GameShop } from "@types";
|
||||
|
||||
const removeGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
await gameRepository.update(
|
||||
{
|
||||
id: gameId,
|
||||
},
|
||||
{
|
||||
status: "removed",
|
||||
downloadPath: null,
|
||||
bytesDownloaded: 0,
|
||||
progress: 0,
|
||||
}
|
||||
);
|
||||
const downloadKey = levelKeys.game(shop, objectId);
|
||||
await downloadsSublevel.del(downloadKey);
|
||||
};
|
||||
|
||||
registerEvent("removeGame", removeGame);
|
||||
|
||||
68
src/main/events/library/reset-game-achievements.ts
Normal file
68
src/main/events/library/reset-game-achievements.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
|
||||
import fs from "fs";
|
||||
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
|
||||
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
|
||||
import {
|
||||
gameAchievementsSublevel,
|
||||
gamesSublevel,
|
||||
levelKeys,
|
||||
} from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const resetGameAchievements = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
try {
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
if (!game) return;
|
||||
|
||||
const achievementFiles = findAchievementFiles(game);
|
||||
|
||||
if (achievementFiles.length) {
|
||||
for (const achievementFile of achievementFiles) {
|
||||
achievementsLogger.log(`deleting ${achievementFile.filePath}`);
|
||||
await fs.promises.rm(achievementFile.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const levelKey = levelKeys.game(game.shop, game.objectId);
|
||||
|
||||
await gameAchievementsSublevel
|
||||
.get(levelKey)
|
||||
.then(async (gameAchievements) => {
|
||||
if (gameAchievements) {
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
...gameAchievements,
|
||||
unlockedAchievements: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
|
||||
() =>
|
||||
achievementsLogger.log(
|
||||
`Deleted achievements from ${game.remoteId} - ${game.objectId} - ${game.title}`
|
||||
)
|
||||
);
|
||||
|
||||
const gameAchievements = await getUnlockedAchievements(
|
||||
game.objectId,
|
||||
game.shop,
|
||||
true
|
||||
);
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${game.objectId}-${game.shop}`,
|
||||
gameAchievements
|
||||
);
|
||||
} catch (error) {
|
||||
achievementsLogger.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("resetGameAchievements", resetGameAchievements);
|
||||
@@ -1,13 +1,23 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { levelKeys, gamesSublevel } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const selectGameWinePrefix = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
id: number,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefixPath: string | null
|
||||
) => {
|
||||
return gameRepository.update({ id }, { winePrefixPath: winePrefixPath });
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
winePrefixPath: winePrefixPath,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("selectGameWinePrefix", selectGameWinePrefix);
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const updateExecutablePath = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
id: number,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
executablePath: string | null
|
||||
) => {
|
||||
const parsedPath = executablePath
|
||||
? parseExecutablePath(executablePath)
|
||||
: null;
|
||||
|
||||
return gameRepository.update(
|
||||
{
|
||||
id,
|
||||
},
|
||||
{
|
||||
executablePath: parsedPath,
|
||||
}
|
||||
);
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
executablePath: parsedPath,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("updateExecutablePath", updateExecutablePath);
|
||||
|
||||
23
src/main/events/library/update-launch-options.ts
Normal file
23
src/main/events/library/update-launch-options.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { GameShop } from "@types";
|
||||
|
||||
const updateLaunchOptions = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
launchOptions: string | null
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (game) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("updateLaunchOptions", updateLaunchOptions);
|
||||
@@ -1,13 +1,17 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
|
||||
const verifyExecutablePathInUse = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
executablePath: string
|
||||
) => {
|
||||
return gameRepository.findOne({
|
||||
where: { executablePath },
|
||||
});
|
||||
for await (const game of gamesSublevel.values()) {
|
||||
if (game.executablePath === executablePath) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);
|
||||
|
||||
8
src/main/events/misc/get-features.ts
Normal file
8
src/main/events/misc/get-features.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.get<string[]>("/features", null, { needsAuth: false });
|
||||
};
|
||||
|
||||
registerEvent("getFeatures", getFeatures);
|
||||
@@ -1,28 +1,24 @@
|
||||
import { shell } from "electron";
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
userAuthRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { Crypto, HydraApi } from "@main/services";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { Auth } from "@types";
|
||||
|
||||
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const [userAuth, userPreferences] = await Promise.all([
|
||||
userAuthRepository.findOne({ where: { id: 1 } }),
|
||||
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
||||
]);
|
||||
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
if (!userAuth) {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentToken = await HydraApi.post("/auth/payment", {
|
||||
refreshToken: userAuth.refreshToken,
|
||||
refreshToken: Crypto.decrypt(auth.refreshToken),
|
||||
}).then((response) => response.accessToken);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
token: paymentToken,
|
||||
lng: userPreferences?.language || "en",
|
||||
});
|
||||
|
||||
shell.openExternal(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Notification } from "electron";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { t } from "i18next";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
const publishNewRepacksNotification = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -9,9 +10,12 @@ const publishNewRepacksNotification = async (
|
||||
) => {
|
||||
if (newRepacksCount < 1) return;
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
||||
new Notification({
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { GameShop } from "@types";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const cancelGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
await DownloadManager.cancelDownload(gameId);
|
||||
const downloadKey = levelKeys.game(shop, objectId);
|
||||
|
||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
||||
game: { id: gameId },
|
||||
});
|
||||
await DownloadManager.cancelDownload(downloadKey);
|
||||
|
||||
await transactionalEntityManager.getRepository(Game).update(
|
||||
{
|
||||
id: gameId,
|
||||
},
|
||||
{
|
||||
status: "removed",
|
||||
bytesDownloaded: 0,
|
||||
progress: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
await downloadsSublevel.del(downloadKey);
|
||||
};
|
||||
|
||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { GameShop } from "@types";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const pauseGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const download = await downloadsSublevel.get(gameKey);
|
||||
|
||||
if (download) {
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
||||
game: { id: gameId },
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download,
|
||||
status: "paused",
|
||||
});
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ id: gameId }, { status: "paused" });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("pauseGameDownload", pauseGameDownload);
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const pauseGameSeed = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
await gameRepository.update(gameId, {
|
||||
status: "complete",
|
||||
const downloadKey = levelKeys.game(shop, objectId);
|
||||
const download = await downloadsSublevel.get(downloadKey);
|
||||
|
||||
if (!download) return;
|
||||
|
||||
await downloadsSublevel.put(downloadKey, {
|
||||
...download,
|
||||
shouldSeed: false,
|
||||
});
|
||||
|
||||
await DownloadManager.pauseSeeding(gameId);
|
||||
await DownloadManager.pauseSeeding(downloadKey);
|
||||
};
|
||||
|
||||
registerEvent("pauseGameSeed", pauseGameSeed);
|
||||
|
||||
@@ -1,46 +1,36 @@
|
||||
import { Not } from "typeorm";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
import { GameShop } from "@types";
|
||||
|
||||
const resumeGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
id: gameId,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
if (!game) return;
|
||||
const download = await downloadsSublevel.get(gameKey);
|
||||
|
||||
if (game.status === "paused") {
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
await DownloadManager.pauseDownload();
|
||||
if (download?.status === "paused") {
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ status: "active", progress: Not(1) }, { status: "paused" });
|
||||
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||
if (value.status === "active" && value.progress !== 1) {
|
||||
await downloadsSublevel.put(key, {
|
||||
...value,
|
||||
status: "paused",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await DownloadManager.resumeDownload(game);
|
||||
await DownloadManager.resumeDownload(download);
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(DownloadQueue)
|
||||
.delete({ game: { id: gameId } });
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(DownloadQueue)
|
||||
.insert({ game: { id: gameId } });
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ id: gameId }, { status: "active" });
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download,
|
||||
status: "active",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { Downloader } from "@shared";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const resumeGameSeed = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
id: gameId,
|
||||
isDeleted: false,
|
||||
downloader: Downloader.Torrent,
|
||||
progress: 1,
|
||||
},
|
||||
});
|
||||
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
if (!game) return;
|
||||
if (!download) return;
|
||||
|
||||
await gameRepository.update(gameId, {
|
||||
status: "seeding",
|
||||
await downloadsSublevel.put(levelKeys.game(shop, objectId), {
|
||||
...download,
|
||||
shouldSeed: true,
|
||||
});
|
||||
|
||||
await DownloadManager.resumeSeeding(game);
|
||||
await DownloadManager.resumeSeeding(download);
|
||||
};
|
||||
|
||||
registerEvent("resumeGameSeed", resumeGameSeed);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import type { Download, StartGameDownloadPayload } from "@types";
|
||||
import { DownloadManager, HydraApi } from "@main/services";
|
||||
|
||||
import { Not } from "typeorm";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -15,85 +13,84 @@ const startGameDownload = async (
|
||||
) => {
|
||||
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
||||
|
||||
return dataSource.transaction(async (transactionalEntityManager) => {
|
||||
const gameRepository = transactionalEntityManager.getRepository(Game);
|
||||
const downloadQueueRepository =
|
||||
transactionalEntityManager.getRepository(DownloadQueue);
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID: objectId,
|
||||
shop,
|
||||
},
|
||||
});
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await gameRepository.update(
|
||||
{ status: "active", progress: Not(1) },
|
||||
{ status: "paused" }
|
||||
);
|
||||
|
||||
if (game) {
|
||||
await gameRepository.update(
|
||||
{
|
||||
id: game.id,
|
||||
},
|
||||
{
|
||||
status: "active",
|
||||
progress: 0,
|
||||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri,
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID: objectId,
|
||||
downloader,
|
||||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
uri,
|
||||
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||
if (value.status === "active" && value.progress !== 1) {
|
||||
await downloadsSublevel.put(key, {
|
||||
...value,
|
||||
status: "paused",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID: objectId,
|
||||
},
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
/* Delete any previous download */
|
||||
await downloadsSublevel.del(gameKey);
|
||||
|
||||
if (game?.isDeleted) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isDeleted: false,
|
||||
});
|
||||
} else {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||
await gamesSublevel.put(gameKey, {
|
||||
title,
|
||||
iconUrl,
|
||||
objectId,
|
||||
shop,
|
||||
remoteId: null,
|
||||
playTimeInMilliseconds: 0,
|
||||
lastTimePlayed: null,
|
||||
isDeleted: false,
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
createGame(updatedGame!).catch(() => {}),
|
||||
HydraApi.post(
|
||||
"/games/download",
|
||||
{
|
||||
objectId: updatedGame!.objectID,
|
||||
shop: updatedGame!.shop,
|
||||
},
|
||||
{ needsAuth: false }
|
||||
).catch(() => {}),
|
||||
]);
|
||||
});
|
||||
await DownloadManager.cancelDownload(gameKey);
|
||||
|
||||
const download: Download = {
|
||||
shop,
|
||||
objectId,
|
||||
status: "active",
|
||||
progress: 0,
|
||||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri,
|
||||
folderName: null,
|
||||
fileSize: null,
|
||||
shouldSeed: false,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
await downloadsSublevel.put(gameKey, download);
|
||||
|
||||
await DownloadManager.startDownload(download);
|
||||
|
||||
const updatedGame = await gamesSublevel.get(gameKey);
|
||||
|
||||
await Promise.all([
|
||||
createGame(updatedGame!).catch(() => {}),
|
||||
HydraApi.post(
|
||||
"/games/download",
|
||||
{
|
||||
objectId,
|
||||
shop,
|
||||
},
|
||||
{ needsAuth: false }
|
||||
).catch(() => {}),
|
||||
]);
|
||||
};
|
||||
|
||||
registerEvent("startGameDownload", startGameDownload);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
const getUserPreferences = async () =>
|
||||
userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
registerEvent("getUserPreferences", getUserPreferences);
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { UserPreferences } from "@types";
|
||||
import i18next from "i18next";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
|
||||
const updateUserPreferences = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
preferences: Partial<UserPreferences>
|
||||
) => {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{ valueEncoding: "json" }
|
||||
);
|
||||
|
||||
if (preferences.language) {
|
||||
await db.put<string, string>(levelKeys.language, preferences.language, {
|
||||
valueEncoding: "utf-8",
|
||||
});
|
||||
|
||||
i18next.changeLanguage(preferences.language);
|
||||
}
|
||||
|
||||
return userPreferencesRepository.upsert(
|
||||
await db.put<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
id: 1,
|
||||
...userPreferences,
|
||||
...preferences,
|
||||
},
|
||||
["id"]
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ComparedAchievements, GameShop } from "@types";
|
||||
import type { ComparedAchievements, GameShop, UserPreferences } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
|
||||
import { HydraApi } from "@main/services";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
|
||||
const getComparedUnlockedAchievements = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -9,9 +10,12 @@ const getComparedUnlockedAchievements = async (
|
||||
shop: GameShop,
|
||||
userId: string
|
||||
) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
const showHiddenAchievementsDescription =
|
||||
userPreferences?.showHiddenAchievementsDescription || false;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { GameShop, UnlockedAchievement, UserAchievement } from "@types";
|
||||
import type { GameShop, UserAchievement, UserPreferences } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const getUnlockedAchievements = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
useCachedData: boolean
|
||||
): Promise<UserAchievement[]> => {
|
||||
const cachedAchievements = await gameAchievementRepository.findOne({
|
||||
where: { objectId, shop },
|
||||
});
|
||||
const cachedAchievements = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
const showHiddenAchievementsDescription =
|
||||
userPreferences?.showHiddenAchievementsDescription || false;
|
||||
@@ -25,12 +25,10 @@ export const getUnlockedAchievements = async (
|
||||
const achievementsData = await getGameAchievementData(
|
||||
objectId,
|
||||
shop,
|
||||
useCachedData ? cachedAchievements : null
|
||||
useCachedData
|
||||
);
|
||||
|
||||
const unlockedAchievements = JSON.parse(
|
||||
cachedAchievements?.unlockedAchievements || "[]"
|
||||
) as UnlockedAchievement[];
|
||||
const unlockedAchievements = cachedAchievements?.unlockedAchievements ?? [];
|
||||
|
||||
return achievementsData
|
||||
.map((achievementData) => {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { db } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { UserFriends } from "@types";
|
||||
import type { User, UserFriends } from "@types";
|
||||
import { levelKeys } from "@main/level/sublevels";
|
||||
|
||||
export const getUserFriends = async (
|
||||
userId: string,
|
||||
take: number,
|
||||
skip: number
|
||||
): Promise<UserFriends> => {
|
||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||
const user = await db.get<string, User>(levelKeys.user, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
if (loggedUser?.userId === userId) {
|
||||
if (user?.id === userId) {
|
||||
return HydraApi.get(`/profile/friends`, { take, skip });
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,12 @@ import updater from "electron-updater";
|
||||
import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import fs from "node:fs";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import resources from "@locales";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { knexClient, migrationConfig } from "./knex-client";
|
||||
import { databaseDirectory } from "./constants";
|
||||
import { PythonRPC } from "./services/python-rpc";
|
||||
import { Aria2 } from "./services/aria2";
|
||||
import { db, levelKeys } from "./level";
|
||||
|
||||
const { autoUpdater } = updater;
|
||||
|
||||
@@ -50,21 +46,6 @@ if (process.defaultApp) {
|
||||
app.setAsDefaultProtocolClient(PROTOCOL);
|
||||
}
|
||||
|
||||
const runMigrations = async () => {
|
||||
if (!fs.existsSync(databaseDirectory)) {
|
||||
fs.mkdirSync(databaseDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
await knexClient.migrate.list(migrationConfig).then((result) => {
|
||||
logger.log(
|
||||
"Migrations to run:",
|
||||
result[1].map((migration) => migration.name)
|
||||
);
|
||||
});
|
||||
|
||||
await knexClient.migrate.latest(migrationConfig);
|
||||
};
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
@@ -76,31 +57,19 @@ app.whenReady().then(async () => {
|
||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||
});
|
||||
|
||||
await runMigrations()
|
||||
.then(() => {
|
||||
logger.log("Migrations executed successfully");
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.log("Migrations failed to run:", err);
|
||||
});
|
||||
|
||||
await dataSource.initialize();
|
||||
|
||||
await import("./main");
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
const language = await db.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
});
|
||||
|
||||
if (userPreferences?.language) {
|
||||
i18n.changeLanguage(userPreferences.language);
|
||||
}
|
||||
if (language) i18n.changeLanguage(language);
|
||||
|
||||
if (!process.argv.includes("--hidden")) {
|
||||
WindowManager.createMainWindow();
|
||||
}
|
||||
|
||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||
WindowManager.createSystemTray(language || "en");
|
||||
});
|
||||
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
|
||||
@@ -1,51 +1,6 @@
|
||||
import knex, { Knex } from "knex";
|
||||
import knex from "knex";
|
||||
import { databasePath } from "./constants";
|
||||
import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3";
|
||||
import { RepackUris } from "./migrations/20240830143906_RepackUris";
|
||||
import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_language";
|
||||
import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris";
|
||||
import { app } from "electron";
|
||||
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
||||
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";
|
||||
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
|
||||
import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disable_nsfw_alert_column";
|
||||
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
|
||||
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
|
||||
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
|
||||
|
||||
export type HydraMigration = Knex.Migration & { name: string };
|
||||
|
||||
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
getMigrations(): Promise<HydraMigration[]> {
|
||||
return Promise.resolve([
|
||||
Hydra2_0_3,
|
||||
RepackUris,
|
||||
UpdateUserLanguage,
|
||||
EnsureRepackUris,
|
||||
FixMissingColumns,
|
||||
CreateGameAchievement,
|
||||
AddAchievementNotificationPreference,
|
||||
CreateUserSubscription,
|
||||
AddBackgroundImageUrl,
|
||||
AddWinePrefixToGame,
|
||||
AddStartMinimizedColumn,
|
||||
AddDisableNsfwAlertColumn,
|
||||
AddShouldSeedColumn,
|
||||
AddSeedAfterDownloadColumn,
|
||||
AddHiddenAchievementDescriptionColumn,
|
||||
]);
|
||||
}
|
||||
getMigrationName(migration: HydraMigration): string {
|
||||
return migration.name;
|
||||
}
|
||||
getMigration(migration: HydraMigration): Promise<Knex.Migration> {
|
||||
return Promise.resolve(migration);
|
||||
}
|
||||
}
|
||||
|
||||
export const knexClient = knex({
|
||||
debug: !app.isPackaged,
|
||||
@@ -54,7 +9,3 @@ export const knexClient = knex({
|
||||
filename: databasePath,
|
||||
},
|
||||
});
|
||||
|
||||
export const migrationConfig: Knex.MigratorConfig = {
|
||||
migrationSource: new MigrationSource(),
|
||||
};
|
||||
|
||||
3
src/main/level/index.ts
Normal file
3
src/main/level/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { db } from "./level";
|
||||
|
||||
export * from "./sublevels";
|
||||
4
src/main/level/level.ts
Normal file
4
src/main/level/level.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { levelDatabasePath } from "@main/constants";
|
||||
import { Level } from "level";
|
||||
|
||||
export const db = new Level(levelDatabasePath, { valueEncoding: "json" });
|
||||
11
src/main/level/sublevels/downloads.ts
Normal file
11
src/main/level/sublevels/downloads.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Download } from "@types";
|
||||
|
||||
import { db } from "../level";
|
||||
import { levelKeys } from "./keys";
|
||||
|
||||
export const downloadsSublevel = db.sublevel<string, Download>(
|
||||
levelKeys.downloads,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
11
src/main/level/sublevels/game-achievements.ts
Normal file
11
src/main/level/sublevels/game-achievements.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { GameAchievement } from "@types";
|
||||
|
||||
import { db } from "../level";
|
||||
import { levelKeys } from "./keys";
|
||||
|
||||
export const gameAchievementsSublevel = db.sublevel<string, GameAchievement>(
|
||||
levelKeys.gameAchievements,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
11
src/main/level/sublevels/game-shop-cache.ts
Normal file
11
src/main/level/sublevels/game-shop-cache.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ShopDetails } from "@types";
|
||||
|
||||
import { db } from "../level";
|
||||
import { levelKeys } from "./keys";
|
||||
|
||||
export const gamesShopCacheSublevel = db.sublevel<string, ShopDetails>(
|
||||
levelKeys.gameShopCache,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
8
src/main/level/sublevels/games.ts
Normal file
8
src/main/level/sublevels/games.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Game } from "@types";
|
||||
|
||||
import { db } from "../level";
|
||||
import { levelKeys } from "./keys";
|
||||
|
||||
export const gamesSublevel = db.sublevel<string, Game>(levelKeys.games, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
6
src/main/level/sublevels/index.ts
Normal file
6
src/main/level/sublevels/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./downloads";
|
||||
export * from "./games";
|
||||
export * from "./game-shop-cache";
|
||||
export * from "./game-achievements";
|
||||
|
||||
export * from "./keys";
|
||||
16
src/main/level/sublevels/keys.ts
Normal file
16
src/main/level/sublevels/keys.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
export const levelKeys = {
|
||||
games: "games",
|
||||
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
|
||||
user: "user",
|
||||
auth: "auth",
|
||||
gameShopCache: "gameShopCache",
|
||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||
`${shop}:${objectId}:${language}`,
|
||||
gameAchievements: "gameAchievements",
|
||||
downloads: "downloads",
|
||||
userPreferences: "userPreferences",
|
||||
language: "language",
|
||||
sqliteMigrationDone: "sqliteMigrationDone",
|
||||
};
|
||||
173
src/main/main.ts
173
src/main/main.ts
@@ -1,16 +1,19 @@
|
||||
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
gameRepository,
|
||||
userPreferencesRepository,
|
||||
} from "./repository";
|
||||
import { UserPreferences } from "./entity";
|
||||
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
|
||||
import { RealDebridClient } from "./services/download/real-debrid";
|
||||
import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
import { Aria2 } from "./services/aria2";
|
||||
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { Downloader } from "@shared";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
import {
|
||||
gameAchievementsSublevel,
|
||||
gamesSublevel,
|
||||
levelKeys,
|
||||
db,
|
||||
} from "./level";
|
||||
import { Auth, User, type UserPreferences } from "@types";
|
||||
import { knexClient } from "./knex-client";
|
||||
|
||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
import("./events");
|
||||
@@ -27,33 +30,143 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
uploadGamesBatch();
|
||||
});
|
||||
|
||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
relations: {
|
||||
game: true,
|
||||
},
|
||||
});
|
||||
const downloads = await downloadsSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) => {
|
||||
return sortBy(games, "timestamp", "DESC");
|
||||
});
|
||||
|
||||
const seedList = await gameRepository.find({
|
||||
where: {
|
||||
shouldSeed: true,
|
||||
downloader: Downloader.Torrent,
|
||||
progress: 1,
|
||||
uri: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
const [nextItemOnQueue] = downloads;
|
||||
|
||||
await DownloadManager.startRPC(nextQueueItem?.game, seedList);
|
||||
const downloadsToSeed = downloads.filter(
|
||||
(download) =>
|
||||
download.shouldSeed &&
|
||||
download.downloader === Downloader.Torrent &&
|
||||
download.progress === 1 &&
|
||||
download.uri !== null
|
||||
);
|
||||
|
||||
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
||||
|
||||
startMainLoop();
|
||||
};
|
||||
|
||||
userPreferencesRepository
|
||||
.findOne({
|
||||
where: { id: 1 },
|
||||
})
|
||||
.then((userPreferences) => {
|
||||
const migrateFromSqlite = async () => {
|
||||
const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
|
||||
|
||||
if (sqliteMigrationDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const migrateGames = knexClient("game")
|
||||
.select("*")
|
||||
.then((games) => {
|
||||
return gamesSublevel.batch(
|
||||
games.map((game) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(game.shop, game.objectID),
|
||||
value: {
|
||||
objectId: game.objectID,
|
||||
shop: game.shop,
|
||||
title: game.title,
|
||||
iconUrl: game.iconUrl,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
remoteId: game.remoteId,
|
||||
isDeleted: game.isDeleted,
|
||||
},
|
||||
}))
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("Games migrated successfully");
|
||||
});
|
||||
|
||||
const migrateUserPreferences = knexClient("user_preferences")
|
||||
.select("*")
|
||||
.then(async (userPreferences) => {
|
||||
if (userPreferences.length > 0) {
|
||||
await db.put(levelKeys.userPreferences, userPreferences[0]);
|
||||
|
||||
if (userPreferences[0].language) {
|
||||
await db.put(levelKeys.language, userPreferences[0].language);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("User preferences migrated successfully");
|
||||
});
|
||||
|
||||
const migrateAchievements = knexClient("game_achievement")
|
||||
.select("*")
|
||||
.then((achievements) => {
|
||||
return gameAchievementsSublevel.batch(
|
||||
achievements.map((achievement) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(achievement.shop, achievement.objectId),
|
||||
value: {
|
||||
achievements: JSON.parse(achievement.achievements),
|
||||
unlockedAchievements: JSON.parse(achievement.unlockedAchievements),
|
||||
},
|
||||
}))
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("Achievements migrated successfully");
|
||||
});
|
||||
|
||||
const migrateUser = knexClient("user_auth")
|
||||
.select("*")
|
||||
.then(async (users) => {
|
||||
if (users.length > 0) {
|
||||
await db.put<string, User>(
|
||||
levelKeys.user,
|
||||
{
|
||||
id: users[0].userId,
|
||||
displayName: users[0].displayName,
|
||||
profileImageUrl: users[0].profileImageUrl,
|
||||
backgroundImageUrl: users[0].backgroundImageUrl,
|
||||
subscription: users[0].subscription,
|
||||
},
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
await db.put<string, Auth>(
|
||||
levelKeys.auth,
|
||||
{
|
||||
accessToken: users[0].accessToken,
|
||||
refreshToken: users[0].refreshToken,
|
||||
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||
},
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("User data migrated successfully");
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
migrateGames,
|
||||
migrateUserPreferences,
|
||||
migrateAchievements,
|
||||
migrateUser,
|
||||
]);
|
||||
};
|
||||
|
||||
migrateFromSqlite().then(async () => {
|
||||
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
}).then((userPreferences) => {
|
||||
loadState(userPreferences);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const Hydra2_0_3: HydraMigration = {
|
||||
name: "Hydra_2_0_3",
|
||||
up: async (knex: Knex) => {
|
||||
const timestamp = new Date().getTime();
|
||||
|
||||
await knex.schema.hasTable("migrations").then(async (exists) => {
|
||||
if (exists) {
|
||||
await knex.schema.dropTable("migrations");
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasTable("download_source").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.createTable("download_source", (table) => {
|
||||
table.increments("id").primary();
|
||||
table
|
||||
.text("url")
|
||||
.unique({ indexName: "download_source_url_unique_" + timestamp });
|
||||
table.text("name").notNullable();
|
||||
table.text("etag");
|
||||
table.integer("downloadCount").notNullable().defaultTo(0);
|
||||
table.text("status").notNullable().defaultTo(0);
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasTable("repack").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.createTable("repack", (table) => {
|
||||
table.increments("id").primary();
|
||||
table
|
||||
.text("title")
|
||||
.notNullable()
|
||||
.unique({ indexName: "repack_title_unique_" + timestamp });
|
||||
table
|
||||
.text("magnet")
|
||||
.notNullable()
|
||||
.unique({ indexName: "repack_magnet_unique_" + timestamp });
|
||||
table.integer("page");
|
||||
table.text("repacker").notNullable();
|
||||
table.text("fileSize").notNullable();
|
||||
table.datetime("uploadDate").notNullable();
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
table
|
||||
.integer("downloadSourceId")
|
||||
.references("download_source.id")
|
||||
.onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasTable("game").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.createTable("game", (table) => {
|
||||
table.increments("id").primary();
|
||||
table
|
||||
.text("objectID")
|
||||
.notNullable()
|
||||
.unique({ indexName: "game_objectID_unique_" + timestamp });
|
||||
table
|
||||
.text("remoteId")
|
||||
.unique({ indexName: "game_remoteId_unique_" + timestamp });
|
||||
table.text("title").notNullable();
|
||||
table.text("iconUrl");
|
||||
table.text("folderName");
|
||||
table.text("downloadPath");
|
||||
table.text("executablePath");
|
||||
table.integer("playTimeInMilliseconds").notNullable().defaultTo(0);
|
||||
table.text("shop").notNullable();
|
||||
table.text("status");
|
||||
table.integer("downloader").notNullable().defaultTo(1);
|
||||
table.float("progress").notNullable().defaultTo(0);
|
||||
table.integer("bytesDownloaded").notNullable().defaultTo(0);
|
||||
table.datetime("lastTimePlayed");
|
||||
table.float("fileSize").notNullable().defaultTo(0);
|
||||
table.text("uri");
|
||||
table.boolean("isDeleted").notNullable().defaultTo(0);
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
table
|
||||
.integer("repackId")
|
||||
.references("repack.id")
|
||||
.unique("repack_repackId_unique_" + timestamp);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasTable("user_preferences").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.createTable("user_preferences", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("downloadsPath");
|
||||
table.text("language").notNullable().defaultTo("en");
|
||||
table.text("realDebridApiToken");
|
||||
table
|
||||
.boolean("downloadNotificationsEnabled")
|
||||
.notNullable()
|
||||
.defaultTo(0);
|
||||
table
|
||||
.boolean("repackUpdatesNotificationsEnabled")
|
||||
.notNullable()
|
||||
.defaultTo(0);
|
||||
table.boolean("preferQuitInsteadOfHiding").notNullable().defaultTo(0);
|
||||
table.boolean("runAtStartup").notNullable().defaultTo(0);
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasTable("game_shop_cache").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.createTable("game_shop_cache", (table) => {
|
||||
table.text("objectID").primary().notNullable();
|
||||
table.text("shop").notNullable();
|
||||
table.text("serializedData");
|
||||
table.text("howLongToBeatSerializedData");
|
||||
table.text("language");
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasTable("download_queue").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.createTable("download_queue", (table) => {
|
||||
table.increments("id").primary();
|
||||
table
|
||||
.integer("gameId")
|
||||
.references("game.id")
|
||||
.unique("download_queue_gameId_unique_" + timestamp);
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasTable("user_auth").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.createTable("user_auth", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("userId").notNullable().defaultTo("");
|
||||
table.text("displayName").notNullable().defaultTo("");
|
||||
table.text("profileImageUrl");
|
||||
table.text("accessToken").notNullable().defaultTo("");
|
||||
table.text("refreshToken").notNullable().defaultTo("");
|
||||
table.integer("tokenExpirationTimestamp").notNullable().defaultTo(0);
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
await knex.schema.dropTableIfExists("game");
|
||||
await knex.schema.dropTableIfExists("repack");
|
||||
await knex.schema.dropTableIfExists("download_queue");
|
||||
await knex.schema.dropTableIfExists("user_auth");
|
||||
await knex.schema.dropTableIfExists("game_shop_cache");
|
||||
await knex.schema.dropTableIfExists("user_preferences");
|
||||
await knex.schema.dropTableIfExists("download_source");
|
||||
},
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const RepackUris: HydraMigration = {
|
||||
name: "RepackUris",
|
||||
up: async (knex: Knex) => {
|
||||
await knex.schema.alterTable("repack", (table) => {
|
||||
table.text("uris").notNullable().defaultTo("[]");
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
await knex.schema.alterTable("repack", (table) => {
|
||||
table.integer("page");
|
||||
table.dropColumn("uris");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const UpdateUserLanguage: HydraMigration = {
|
||||
name: "UpdateUserLanguage",
|
||||
up: async (knex: Knex) => {
|
||||
await knex("user_preferences")
|
||||
.update("language", "pt-BR")
|
||||
.where("language", "pt");
|
||||
},
|
||||
|
||||
down: async (_knex: Knex) => {},
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const EnsureRepackUris: HydraMigration = {
|
||||
name: "EnsureRepackUris",
|
||||
up: async (knex: Knex) => {
|
||||
await knex.schema.hasColumn("repack", "uris").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.table("repack", (table) => {
|
||||
table.text("uris").notNullable().defaultTo("[]");
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: async (_knex: Knex) => {},
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const FixMissingColumns: HydraMigration = {
|
||||
name: "FixMissingColumns",
|
||||
up: async (knex: Knex) => {
|
||||
const timestamp = new Date().getTime();
|
||||
await knex.schema
|
||||
.hasColumn("repack", "downloadSourceId")
|
||||
.then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.table("repack", (table) => {
|
||||
table
|
||||
.integer("downloadSourceId")
|
||||
.references("download_source.id")
|
||||
.onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasColumn("game", "remoteId").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.table("game", (table) => {
|
||||
table
|
||||
.text("remoteId")
|
||||
.unique({ indexName: "game_remoteId_unique_" + timestamp });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasColumn("game", "uri").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.table("game", (table) => {
|
||||
table.text("uri");
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: async (_knex: Knex) => {},
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const CreateGameAchievement: HydraMigration = {
|
||||
name: "CreateGameAchievement",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.createTable("game_achievement", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("objectId").notNullable();
|
||||
table.text("shop").notNullable();
|
||||
table.text("achievements");
|
||||
table.text("unlockedAchievements");
|
||||
table.unique(["objectId", "shop"]);
|
||||
});
|
||||
},
|
||||
|
||||
down: (knex: Knex) => {
|
||||
return knex.schema.dropTable("game_achievement");
|
||||
},
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
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");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
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");
|
||||
},
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
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");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
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");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddStartMinimizedColumn: HydraMigration = {
|
||||
name: "AddStartMinimizedColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.boolean("startMinimized").notNullable().defaultTo(0);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.dropColumn("startMinimized");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddDisableNsfwAlertColumn: HydraMigration = {
|
||||
name: "AddDisableNsfwAlertColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.boolean("disableNsfwAlert").notNullable().defaultTo(0);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.dropColumn("disableNsfwAlert");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddShouldSeedColumn: HydraMigration = {
|
||||
name: "AddShouldSeedColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("game", (table) => {
|
||||
return table.boolean("shouldSeed").notNullable().defaultTo(true);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("game", (table) => {
|
||||
return table.dropColumn("shouldSeed");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddSeedAfterDownloadColumn: HydraMigration = {
|
||||
name: "AddSeedAfterDownloadColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table
|
||||
.boolean("seedAfterDownloadComplete")
|
||||
.notNullable()
|
||||
.defaultTo(true);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.dropColumn("seedAfterDownloadComplete");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddHiddenAchievementDescriptionColumn: HydraMigration = {
|
||||
name: "AddHiddenAchievementDescriptionColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table
|
||||
.boolean("showHiddenAchievementsDescription")
|
||||
.notNullable()
|
||||
.defaultTo(0);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.dropColumn("showHiddenAchievementsDescription");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const MigrationName: HydraMigration = {
|
||||
name: "MigrationName",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.createTable("table_name", async (table) => {});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {},
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { dataSource } from "./data-source";
|
||||
import {
|
||||
DownloadQueue,
|
||||
Game,
|
||||
GameShopCache,
|
||||
UserPreferences,
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
UserSubscription,
|
||||
} from "@main/entity";
|
||||
|
||||
export const gameRepository = dataSource.getRepository(Game);
|
||||
|
||||
export const userPreferencesRepository =
|
||||
dataSource.getRepository(UserPreferences);
|
||||
|
||||
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
|
||||
|
||||
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||
|
||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||
|
||||
export const userSubscriptionRepository =
|
||||
dataSource.getRepository(UserSubscription);
|
||||
|
||||
export const gameAchievementRepository =
|
||||
dataSource.getRepository(GameAchievement);
|
||||
@@ -1,6 +1,4 @@
|
||||
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 {
|
||||
@@ -9,21 +7,20 @@ import {
|
||||
findAllAchievementFiles,
|
||||
getAlternativeObjectIds,
|
||||
} from "./find-achivement-files";
|
||||
import type { AchievementFile, UnlockedAchievement } from "@types";
|
||||
import type { AchievementFile, Game, UnlockedAchievement } from "@types";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { Cracker } from "@shared";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) => games.filter((game) => !game.isDeleted));
|
||||
|
||||
if (games.length === 0) return;
|
||||
|
||||
@@ -32,7 +29,7 @@ const watchAchievementsWindows = async () => {
|
||||
for (const game of games) {
|
||||
const gameAchievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
|
||||
|
||||
gameAchievementFiles.push(
|
||||
@@ -47,12 +44,12 @@ const watchAchievementsWindows = async () => {
|
||||
};
|
||||
|
||||
const watchAchievementsWithWine = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
winePrefixPath: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) =>
|
||||
games.filter((game) => !game.isDeleted && game.winePrefixPath)
|
||||
);
|
||||
|
||||
for (const game of games) {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
@@ -188,11 +185,10 @@ export class AchievementWatcherManager {
|
||||
};
|
||||
|
||||
private static preSearchAchievementsWindows = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) => games.filter((game) => !game.isDeleted));
|
||||
|
||||
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||
|
||||
@@ -200,7 +196,7 @@ export class AchievementWatcherManager {
|
||||
games.map((game) => {
|
||||
const gameAchievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||
gameAchievementFiles.push(
|
||||
...(gameAchievementFilesMap.get(objectId) || [])
|
||||
);
|
||||
@@ -216,11 +212,10 @@ export class AchievementWatcherManager {
|
||||
};
|
||||
|
||||
private static preSearchAchievementsWithWine = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) => games.filter((game) => !game.isDeleted));
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { app } from "electron";
|
||||
import type { AchievementFile } from "@types";
|
||||
import type { Game, AchievementFile } from "@types";
|
||||
import { Cracker } from "@shared";
|
||||
import { Game } from "@main/entity";
|
||||
import { achievementsLogger } from "../logger";
|
||||
|
||||
const getAppDataPath = () => {
|
||||
@@ -254,7 +253,7 @@ export const findAchievementFiles = (game: Game) => {
|
||||
|
||||
for (const cracker of crackers) {
|
||||
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(
|
||||
game.winePrefixPath ?? "",
|
||||
folderPath,
|
||||
|
||||
@@ -1,40 +1,37 @@
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import type { AchievementData, GameShop } from "@types";
|
||||
import type { GameShop, SteamAchievement } from "@types";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { logger } from "../logger";
|
||||
import { GameAchievement } from "@main/entity";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const getGameAchievementData = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
cachedAchievements: GameAchievement | null
|
||||
useCachedData: boolean
|
||||
) => {
|
||||
if (cachedAchievements && cachedAchievements.achievements) {
|
||||
return JSON.parse(cachedAchievements.achievements) as AchievementData[];
|
||||
}
|
||||
const cachedAchievements = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
if (cachedAchievements && useCachedData)
|
||||
return cachedAchievements.achievements;
|
||||
|
||||
return HydraApi.get<AchievementData[]>("/games/achievements", {
|
||||
const language = await db
|
||||
.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
})
|
||||
.then((language) => language || "en");
|
||||
|
||||
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
|
||||
shop,
|
||||
objectId,
|
||||
language: userPreferences?.language || "en",
|
||||
language,
|
||||
})
|
||||
.then((achievements) => {
|
||||
gameAchievementRepository.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop,
|
||||
achievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
);
|
||||
.then(async (achievements) => {
|
||||
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
||||
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
||||
achievements,
|
||||
});
|
||||
|
||||
return achievements;
|
||||
})
|
||||
@@ -42,15 +39,9 @@ export const getGameAchievementData = async (
|
||||
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[];
|
||||
});
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user