mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
resolved conflicts and prettier
This commit is contained in:
commit
bc31a28eee
208 changed files with 4271 additions and 3914 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -14,3 +14,5 @@ aria2/
|
||||||
|
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.env.sentry-build-plugin
|
.env.sentry-build-plugin
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
|
|
@ -38,6 +38,13 @@ export default defineConfig(({ mode }) => {
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
api: "modern",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@renderer": resolve("src/renderer/src"),
|
"@renderer": resolve("src/renderer/src"),
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"better-sqlite3": "^11.7.0",
|
"better-sqlite3": "^11.7.0",
|
||||||
|
"classic-level": "^2.0.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
|
@ -74,7 +75,6 @@
|
||||||
"sound-play": "^1.1.0",
|
"sound-play": "^1.1.0",
|
||||||
"sudo-prompt": "^9.2.1",
|
"sudo-prompt": "^9.2.1",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"typeorm": "^0.3.20",
|
|
||||||
"user-agents": "^1.1.387",
|
"user-agents": "^1.1.387",
|
||||||
"yaml": "^2.6.1",
|
"yaml": "^2.6.1",
|
||||||
"yup": "^1.5.0",
|
"yup": "^1.5.0",
|
||||||
|
|
|
@ -11,11 +11,12 @@ class HttpDownloader:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def start_download(self, url: str, save_path: str, header: str):
|
def start_download(self, url: str, save_path: str, header: str, out: str = None):
|
||||||
if self.download:
|
if self.download:
|
||||||
self.aria2.resume([self.download])
|
self.aria2.resume([self.download])
|
||||||
else:
|
else:
|
||||||
downloads = self.aria2.add(url, options={"header": header, "dir": save_path})
|
downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})
|
||||||
|
|
||||||
self.download = downloads[0]
|
self.download = downloads[0]
|
||||||
|
|
||||||
def pause_download(self):
|
def pause_download(self):
|
||||||
|
|
|
@ -28,14 +28,14 @@ if start_download_payload:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session)
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
downloads[initial_download['game_id']] = torrent_downloader
|
downloads[initial_download['game_id']] = torrent_downloader
|
||||||
try:
|
try:
|
||||||
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'], "")
|
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting torrent download", e)
|
print("Error starting torrent download", e)
|
||||||
else:
|
else:
|
||||||
http_downloader = HttpDownloader()
|
http_downloader = HttpDownloader()
|
||||||
downloads[initial_download['game_id']] = http_downloader
|
downloads[initial_download['game_id']] = http_downloader
|
||||||
try:
|
try:
|
||||||
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'))
|
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting http download", e)
|
print("Error starting http download", e)
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ if start_seeding_payload:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||||
downloads[seed['game_id']] = torrent_downloader
|
downloads[seed['game_id']] = torrent_downloader
|
||||||
try:
|
try:
|
||||||
torrent_downloader.start_download(seed['url'], seed['save_path'], "")
|
torrent_downloader.start_download(seed['url'], seed['save_path'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting seeding", e)
|
print("Error starting seeding", e)
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ def seed_status():
|
||||||
|
|
||||||
@app.route("/healthcheck", methods=["GET"])
|
@app.route("/healthcheck", methods=["GET"])
|
||||||
def healthcheck():
|
def healthcheck():
|
||||||
return "", 200
|
return "ok", 200
|
||||||
|
|
||||||
@app.route("/process-list", methods=["GET"])
|
@app.route("/process-list", methods=["GET"])
|
||||||
def process_list():
|
def process_list():
|
||||||
|
@ -140,18 +140,18 @@ def action():
|
||||||
|
|
||||||
if url.startswith('magnet'):
|
if url.startswith('magnet'):
|
||||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||||
existing_downloader.start_download(url, data['save_path'], "")
|
existing_downloader.start_download(url, data['save_path'])
|
||||||
else:
|
else:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session)
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
downloads[game_id] = torrent_downloader
|
downloads[game_id] = torrent_downloader
|
||||||
torrent_downloader.start_download(url, data['save_path'], "")
|
torrent_downloader.start_download(url, data['save_path'])
|
||||||
else:
|
else:
|
||||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||||
existing_downloader.start_download(url, data['save_path'], data.get('header'))
|
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
else:
|
else:
|
||||||
http_downloader = HttpDownloader()
|
http_downloader = HttpDownloader()
|
||||||
downloads[game_id] = http_downloader
|
downloads[game_id] = http_downloader
|
||||||
http_downloader.start_download(url, data['save_path'], data.get('header'))
|
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
|
|
||||||
downloading_game_id = game_id
|
downloading_game_id = game_id
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ def action():
|
||||||
elif action == 'resume_seeding':
|
elif action == 'resume_seeding':
|
||||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||||
downloads[game_id] = torrent_downloader
|
downloads[game_id] = torrent_downloader
|
||||||
torrent_downloader.start_download(data['url'], data['save_path'], "")
|
torrent_downloader.start_download(data['url'], data['save_path'])
|
||||||
elif action == 'pause_seeding':
|
elif action == 'pause_seeding':
|
||||||
downloader = downloads.get(game_id)
|
downloader = downloads.get(game_id)
|
||||||
if downloader:
|
if downloader:
|
||||||
|
|
|
@ -102,7 +102,7 @@ class TorrentDownloader:
|
||||||
"http://bvarf.tracker.sh:2086/announce",
|
"http://bvarf.tracker.sh:2086/announce",
|
||||||
]
|
]
|
||||||
|
|
||||||
def start_download(self, magnet: str, save_path: str, header: str):
|
def start_download(self, magnet: str, save_path: str):
|
||||||
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers, 'flags': self.flags}
|
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers, 'flags': self.flags}
|
||||||
self.torrent_handle = self.session.add_torrent(params)
|
self.torrent_handle = self.session.add_torrent(params)
|
||||||
self.torrent_handle.resume()
|
self.torrent_handle.resume()
|
||||||
|
|
|
@ -1,423 +1,445 @@
|
||||||
{
|
{
|
||||||
"language_name": "اَلْعَرَبِيَّةُ",
|
"language_name": "العربية",
|
||||||
"app": {
|
"app": {
|
||||||
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
|
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "مُتَمَيِّز",
|
"featured": "مميز",
|
||||||
"surprise_me": "فَاجِئْنِي",
|
"surprise_me": "مفاجئني",
|
||||||
"no_results": "لَمْ يُعْثَرْ عَلَى نَتائِج",
|
"no_results": "لم يتم العثور على نتائج",
|
||||||
"start_typing": "اِبْدَأْ بِالْكِتَابَةِ لِلْبَحْثِ...",
|
"start_typing": "ابدأ الكتابة للبحث...",
|
||||||
"hot": "اَلْأَكْثَرُ شُيُوعًا الْآن",
|
"hot": "الأكثر شيوعًا الآن",
|
||||||
"weekly": "📅 أَفْضَلُ أَلْعَابِ الْأُسْبُوعِ",
|
"weekly": "📅 أفضل ألعاب الأسبوع",
|
||||||
"achievements": "🏆 أَلْعَابٌ لِلتَّغَلُّبِ عَلَيْهَا"
|
"achievements": "🏆 ألعاب للتغلب عليها"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "الْفِهْرِسُ",
|
"catalogue": "الكـتالوج",
|
||||||
"downloads": "التَّنْزِيلَاتُ",
|
"downloads": "التنزيلات",
|
||||||
"settings": "الإعْدَادَاتُ",
|
"settings": "الإعدادات",
|
||||||
"my_library": "مَكْتَبَتِي",
|
"my_library": "مكتبتي",
|
||||||
"downloading_metadata": "{{title}} (جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...)",
|
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
||||||
"paused": "{{title}} (مُوْقَفٌ)",
|
"paused": "{{title}} (معلّق)",
|
||||||
"downloading": "{{title}} ({{percentage}} - جَارٍ التَّنْزِيلُ...)",
|
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
||||||
"filter": "تَصْفِيَةُ الْمَكْتَبَةِ",
|
"filter": "تصفية المكتبة",
|
||||||
"home": "الرَّئِيسِيَّةُ",
|
"home": "الرئيسية",
|
||||||
"queued": "{{title}} (فِي الْانْتِظَارِ)",
|
"queued": "{{title}} (في قائمة الانتظار)",
|
||||||
"game_has_no_executable": "اللُّعْبَةُ لَيْسَ لَدَيْهَا مِلَفٌّ تَنْفِيذِيٌّ مُحَدَّدٌ",
|
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
||||||
"sign_in": "تَسْجِيلُ الدُّخُولِ",
|
"sign_in": "تسجيل الدخول",
|
||||||
"friends": "الْأَصْدِقَاءُ",
|
"friends": "الأصدقاء",
|
||||||
"need_help": "هَلْ تَحْتَاجُ إِلَى مُسَاعَدَةٍ؟"
|
"need_help": "تحتاج مساعدة؟"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "بَحْثُ الْأَلْعَابِ",
|
"search": "ابحث عن الألعاب",
|
||||||
"home": "الرَّئِيسِيَّةُ",
|
"home": "الرئيسية",
|
||||||
"catalogue": "الْفِهْرِسُ",
|
"catalogue": "الكـتالوج",
|
||||||
"downloads": "التَّنْزِيلَاتُ",
|
"downloads": "التنزيلات",
|
||||||
"search_results": "نَتائِجُ الْبَحْثِ",
|
"search_results": "نتائج البحث",
|
||||||
"settings": "الإعْدَادَاتُ",
|
"settings": "الإعدادات",
|
||||||
"version_available_install": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِإِعَادَةِ التَّشْغِيلِ وَالتَّثْبِيتِ.",
|
"version_available_install": "الإصدار {{version}} متوفر. انقر هنا لإعادة التشغيل والتثبيت.",
|
||||||
"version_available_download": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِلتَّنْزِيلِ."
|
"version_available_download": "الإصدار {{version}} متوفر. انقر هنا للتنزيل."
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "لَا تَوْجَدُ تَنْزِيلَاتٌ جَارِيَةٌ",
|
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
||||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ لِـ {{title}}...",
|
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية لـ {{title}}...",
|
||||||
"downloading": "جَارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - الِاكْتِمَالُ {{eta}} - {{speed}}",
|
"downloading": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||||
"calculating_eta": "جَارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
|
"calculating_eta": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - جاري حساب الوقت المتبقي...",
|
||||||
"checking_files": "جَارٍ التَّحَقُّقُ مِنْ مَلَفَّاتِ {{title}}... ({{percentage}} مَكْتُومٌ)"
|
"checking_files": "جارٍ فحص ملفات {{title}}... ({{percentage}} اكتمال)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"search": "تَصْفِيَةٌ...",
|
"search": "تصفية...",
|
||||||
"developers": "الْمُطَوِّرُونَ",
|
"developers": "المطورون",
|
||||||
"genres": "الْأَنْوَاعُ",
|
"genres": "الأنواع",
|
||||||
"tags": "الْعَلَامَاتُ",
|
"tags": "العلامات",
|
||||||
"publishers": "النَّاشِرُونَ",
|
"publishers": "الناشرون",
|
||||||
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
|
"download_sources": "مصادر التنزيل",
|
||||||
"result_count": "{{resultCount}} نَتائِجُ",
|
"result_count": "{{resultCount}} نتيجة",
|
||||||
"filter_count": "{{filterCount}} مَتَوَفِّرٌ",
|
"filter_count": "{{filterCount}} متاح",
|
||||||
"clear_filters": "مَسْحُ {{filterCount}} الْمُخْتَارَةِ"
|
"clear_filters": "مسح {{filterCount}} المحددة"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "فَتْحُ خِيَارَاتِ التَّنْزِيلِ",
|
"open_download_options": "فتح خيارات التنزيل",
|
||||||
"download_options_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
|
"download_options_zero": "لا توجد خيارات تنزيل",
|
||||||
"download_options_one": "{{count}} خِيَارُ تَنْزِيلٍ",
|
"download_options_one": "خيار تنزيل واحد",
|
||||||
"download_options_other": "{{count}} خِيَارَاتُ تَنْزِيلٍ",
|
"download_options_other": "{{count}} خيارات تنزيل",
|
||||||
"updated_at": "تَمَّ التَّحْدِيثُ فِي {{updated_at}}",
|
"updated_at": "تم التحديث في {{updated_at}}",
|
||||||
"install": "تَثْبِيتٌ",
|
"install": "تثبيت",
|
||||||
"resume": "اسْتِئْنَافٌ",
|
"resume": "استئناف",
|
||||||
"pause": "إِيقَافٌ",
|
"pause": "إيقاف مؤقت",
|
||||||
"cancel": "إِلْغَاءٌ",
|
"cancel": "إلغاء",
|
||||||
"remove": "إِزَالَةٌ",
|
"remove": "إزالة",
|
||||||
"space_left_on_disk": "{{space}} مُتَبَقٍّ عَلَى الْقُرْصِ",
|
"space_left_on_disk": "{{space}} متبقي على القرص",
|
||||||
"eta": "الِاكْتِمَالُ {{eta}}",
|
"eta": "الانتهاء {{eta}}",
|
||||||
"calculating_eta": "جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
|
"calculating_eta": "جارٍ حساب الوقت المتبقي...",
|
||||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
|
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||||
"filter": "تَصْفِيَةُ الْإِصْدَارَاتِ الْمُعَادِ تَغْلِيفُهَا",
|
"filter": "تصفية الحزم المعاد تعبئتها",
|
||||||
"requirements": "مُتَطَلَّبَاتُ النِّظَامِ",
|
"requirements": "متطلبات النظام",
|
||||||
"minimum": "الْأَدْنَى",
|
"minimum": "الحد الأدنى",
|
||||||
"recommended": "الْمُوَصَّى بِهِ",
|
"recommended": "مُوصى به",
|
||||||
"paused": "مُوْقَفٌ",
|
"paused": "معلّق",
|
||||||
"release_date": "تَمَّ الْإِصْدَارُ فِي {{date}}",
|
"release_date": "تاريخ الإصدار {{date}}",
|
||||||
"publisher": "نُشِرَ بِوَاسِطَةِ {{publisher}}",
|
"publisher": "نشر بواسطة {{publisher}}",
|
||||||
"hours": "سَاعَاتٌ",
|
"hours": "ساعات",
|
||||||
"minutes": "دَقَائِقُ",
|
"minutes": "دقائق",
|
||||||
"amount_hours": "{{amount}} سَاعَاتٌ",
|
"amount_hours": "{{amount}} ساعات",
|
||||||
"amount_minutes": "{{amount}} دَقَائِقُ",
|
"amount_minutes": "{{amount}} دقائق",
|
||||||
"accuracy": "دِقَّةٌ {{accuracy}}%",
|
"accuracy": "دقة {{accuracy}}%",
|
||||||
"add_to_library": "إِضَافَةٌ إِلَى الْمَكْتَبَةِ",
|
"add_to_library": "إضافة إلى المكتبة",
|
||||||
"remove_from_library": "إِزَالَةٌ مِنَ الْمَكْتَبَةِ",
|
"remove_from_library": "إزالة من المكتبة",
|
||||||
"no_downloads": "لَا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ",
|
"no_downloads": "لا توجد تنزيلات متاحة",
|
||||||
"play_time": "لُعِبَ لِمُدَّةِ {{amount}}",
|
"play_time": "لعب لمدة {{amount}}",
|
||||||
"last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
|
"last_time_played": "آخر تشغيل {{period}}",
|
||||||
"not_played_yet": "لَمْ تَلْعَبْ {{title}} بَعْدُ",
|
"not_played_yet": "لم تلعب {{title}} بعد",
|
||||||
"next_suggestion": "الِاقْتِرَاحُ التَّالِي",
|
"next_suggestion": "الاقتراح التالي",
|
||||||
"play": "لَعِبٌ",
|
"play": "تشغيل",
|
||||||
"deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
|
"deleting": "جارٍ حذف المثبت...",
|
||||||
"close": "إِغْلَاقٌ",
|
"close": "إغلاق",
|
||||||
"playing_now": "جَارِي اللَّعِبُ الْآن",
|
"playing_now": "يتم التشغيل الآن",
|
||||||
"change": "تَغْيِيرٌ",
|
"change": "تغيير",
|
||||||
"repacks_modal_description": "اخْتَرِ الْإِصْدَارَ الْمُعَادَ تَغْلِيفُهُ الَّذِي تُرِيدُ تَنْزِيلَهُ",
|
"repacks_modal_description": "اختر الحزمة المعاد تعبئتها التي تريد تنزيلها",
|
||||||
"select_folder_hint": "لِتَغْيِيرِ الْمَجَلَّدِ الافْتِرَاضِيِّ، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
|
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
||||||
"download_now": "تَنْزِيلٌ الْآن",
|
"download_now": "تنزيل الآن",
|
||||||
"no_shop_details": "لَمْ يَتَمَكَّنْ مِنْ اسْتِرْدَادِ تَفَاصِيلِ الْمَتْجَرِ.",
|
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
||||||
"download_options": "خِيَارَاتُ التَّنْزِيلِ",
|
"download_options": "خيارات التنزيل",
|
||||||
"download_path": "مَسَارُ التَّنْزِيلِ",
|
"download_path": "مسار التنزيل",
|
||||||
"previous_screenshot": "لَقْطَةُ الشَّاشَةِ السَّابِقَةُ",
|
"previous_screenshot": "لقطة الشاشة السابقة",
|
||||||
"next_screenshot": "لَقْطَةُ الشَّاشَةِ التَّالِيَةُ",
|
"next_screenshot": "لقطة الشاشة التالية",
|
||||||
"screenshot": "لَقْطَةُ الشَّاشَةِ {{number}}",
|
"screenshot": "لقطة الشاشة {{number}}",
|
||||||
"open_screenshot": "فَتْحُ لَقْطَةِ الشَّاشَةِ {{number}}",
|
"open_screenshot": "فتح لقطة الشاشة {{number}}",
|
||||||
"download_settings": "إعْدَادَاتُ التَّنْزِيلِ",
|
"download_settings": "إعدادات التنزيل",
|
||||||
"downloader": "الْمُنَزِّلُ",
|
"downloader": "أداة التنزيل",
|
||||||
"select_executable": "تَحْدِيدٌ",
|
"select_executable": "تحديد",
|
||||||
"no_executable_selected": "لَمْ يُحَدَّدْ مِلَفٌّ تَنْفِيذِيٌّ",
|
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
||||||
"open_folder": "فَتْحُ الْمَجَلَّدِ",
|
"open_folder": "فتح المجلد",
|
||||||
"open_download_location": "مُشَاهَدَةُ الْمَلَفَّاتِ الْمُنَزَّلَةِ",
|
"open_download_location": "عرض الملفات المحملة",
|
||||||
"create_shortcut": "إِنْشَاءُ طَرِيقٍ مُخْتَصَرٍ عَلَى سَطْحِ الْمَكْتَبِ",
|
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
||||||
"clear": "مَسْحٌ",
|
"clear": "مسح",
|
||||||
"remove_files": "إِزَالَةُ الْمَلَفَّاتِ",
|
"remove_files": "إزالة الملفات",
|
||||||
"remove_from_library_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
"remove_from_library_title": "هل أنت متأكد؟",
|
||||||
"remove_from_library_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ {{game}} مِنْ مَكْتَبَتِكَ",
|
"remove_from_library_description": "سيؤدي هذا إلى إزالة {{game}} من مكتبتك",
|
||||||
"options": "خِيَارَاتٌ",
|
"options": "خيارات",
|
||||||
"executable_section_title": "الْمِلَفُّ التَّنْفِيذِيُّ",
|
"executable_section_title": "ملف التشغيل",
|
||||||
"executable_section_description": "مَسَارُ الْمِلَفِّ الَّذِي سَيَتِمُّ تَنْفِيذُهُ عِنْدَ النَّقْرِ عَلَى \"لَعِبٌ\"",
|
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
||||||
"downloads_secion_title": "التَّنْزِيلَاتُ",
|
"downloads_secion_title": "التنزيلات",
|
||||||
"downloads_section_description": "تَحَقَّقْ مِنَ التَّحْدِيثَاتِ أَوِ الْإِصْدَارَاتِ الْأُخْرَى لِهَذِهِ اللُّعْبَةِ",
|
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
||||||
"danger_zone_section_title": "مِنْطَقَةُ الْخَطَرِ",
|
"danger_zone_section_title": "منطقة الخطر",
|
||||||
"danger_zone_section_description": "إِزَالَةُ هَذِهِ اللُّعْبَةِ مِنْ مَكْتَبَتِكَ أَوِ الْمَلَفَّاتِ الْمُنَزَّلَةِ بِوَاسِطَةِ Hydra",
|
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||||
"download_in_progress": "جَارٍ التَّنْزِيلُ",
|
"download_in_progress": "تنزيل قيد التقدم",
|
||||||
"download_paused": "التَّنْزِيلُ مُوْقَفٌ",
|
"download_paused": "التنزيل معلق",
|
||||||
"last_downloaded_option": "خِيَارُ التَّنْزِيلِ الْأَخِيرُ",
|
"last_downloaded_option": "خيار التنزيل الأخير",
|
||||||
"create_shortcut_success": "تَمَّ إِنْشَاءُ الطَّرِيقِ الْمُخْتَصَرِ بِنَجَاحٍ",
|
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
||||||
"create_shortcut_error": "خَطَأٌ فِي إِنْشَاءِ الطَّرِيقِ الْمُخْتَصَرِ",
|
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
||||||
"nsfw_content_title": "هَذِهِ اللُّعْبَةُ تَحْتَوِي عَلَى مُحْتَوًى غَيْرِ لَائِقٍ",
|
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
||||||
"nsfw_content_description": "{{title}} تَحْتَوِي عَلَى مُحْتَوًى قَدْ لَا يَكُونُ مُنَاسِبًا لِجَمِيعِ الْأَعْمَارِ. هَلْ أَنْتَ مُتَأَكِّدٌ مِنْ أَنَّكَ تُرِيدُ الْمُتَابَعَةَ؟",
|
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يناسب جميع الأعمار. هل تريد المتابعة؟",
|
||||||
"allow_nsfw_content": "الْمُتَابَعَةُ",
|
"allow_nsfw_content": "متابعة",
|
||||||
"refuse_nsfw_content": "الرُّجُوعُ",
|
"refuse_nsfw_content": "رجوع",
|
||||||
"stats": "الإحْصَائِيَّاتُ",
|
"stats": "الإحصائيات",
|
||||||
"download_count": "التَّنْزِيلَاتُ",
|
"download_count": "مرات التنزيل",
|
||||||
"player_count": "اللَّاعِبُونَ النَّشِطُونَ",
|
"player_count": "اللاعبون النشطون",
|
||||||
"download_error": "هَذَا خِيَارُ التَّنْزِيلِ غَيْرُ مَتَوَفِّرٍ",
|
"download_error": "خيار التنزيل هذا غير متاح",
|
||||||
"download": "تَنْزِيلٌ",
|
"download": "تنزيل",
|
||||||
"executable_path_in_use": "الْمِلَفُّ التَّنْفِيذِيُّ مُسْتَخْدَمٌ بِوَاسِطَةِ \"{{game}}\"",
|
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
||||||
"warning": "تَنْبِيهٌ:",
|
"warning": "تحذير:",
|
||||||
"hydra_needs_to_remain_open": "لِهَذَا التَّنْزِيلِ، يَجِبُ أَنْ يَبْقَى Hydra مَفْتُوحًا حَتَّى يَتِمَّ الِاكْتِمَالُ. إِذَا أُغْلِقَ Hydra قَبْلَ الِاكْتِمَالِ، سَتَفْقِدُ تَقَدُّمَكَ.",
|
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يبقى Hydra مفتوحًا حتى اكتماله. إذا أغلق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||||
"achievements": "الإِنْجَازَاتُ",
|
"achievements": "الإنجازات",
|
||||||
"achievements_count": "الإِنْجَازَاتُ {{unlockedCount}}/{{achievementsCount}}",
|
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "حِفْظٌ سَحَابِيٌّ",
|
"cloud_save": "حفظ سحابي",
|
||||||
"cloud_save_description": "احْفَظْ تَقَدُّمَكَ فِي السَّحَابَةِ وَاسْتَمِرَّ فِي اللَّعِبِ عَلَى أَيِّ جِهَازٍ",
|
"cloud_save_description": "احفظ تقدمك على السحابة واستمر في اللعب من أي جهاز",
|
||||||
"backups": "الْنُسَخُ الِاحْتِيَاطِيَّةُ",
|
"backups": "النسخ الاحتياطية",
|
||||||
"install_backup": "تَثْبِيتٌ",
|
"install_backup": "تثبيت",
|
||||||
"delete_backup": "حَذْفٌ",
|
"delete_backup": "حذف",
|
||||||
"create_backup": "نُسْخَةٌ احْتِيَاطِيَّةٌ جَدِيدَةٌ",
|
"create_backup": "نسخة احتياطية جديدة",
|
||||||
"last_backup_date": "آخِرُ نُسْخَةٍ احْتِيَاطِيَّةٍ فِي {{date}}",
|
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
||||||
"no_backup_preview": "لَمْ يُعْثَرْ عَلَى أَيِّ أَلْعَابٍ مَحْفُوظَةٍ لِهَذَا الْعُنْوَانِ",
|
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
||||||
"restoring_backup": "جَارٍ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ ({{progress}} مَكْتُومٌ)...",
|
"restoring_backup": "جارٍ استعادة النسخة الاحتياطية ({{progress}} اكتمال)...",
|
||||||
"uploading_backup": "جَارٍ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ...",
|
"uploading_backup": "جارٍ رفع النسخة الاحتياطية...",
|
||||||
"no_backups": "لَمْ تَقُمْ بِإِنْشَاءِ أَيِّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ بَعْدُ",
|
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
||||||
"backup_uploaded": "تَمَّ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
||||||
"backup_deleted": "تَمَّ حَذْفُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
||||||
"backup_restored": "تَمَّ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
"backup_restored": "تم استعادة النسخة الاحتياطية",
|
||||||
"see_all_achievements": "عَرْضُ جَمِيعِ الإِنْجَازَاتِ",
|
"see_all_achievements": "عرض جميع الإنجازات",
|
||||||
"sign_in_to_see_achievements": "سَجِّلِ الدُّخُولَ لِعَرْضِ الإِنْجَازَاتِ",
|
"sign_in_to_see_achievements": "سجل الدخول لعرض الإنجازات",
|
||||||
"mapping_method_automatic": "آلِيٌّ",
|
"mapping_method_automatic": "تلقائي",
|
||||||
"mapping_method_manual": "يَدَوِيٌّ",
|
"mapping_method_manual": "يدوي",
|
||||||
"mapping_method_label": "طَرِيقَةُ التَّحْدِيدِ",
|
"mapping_method_label": "طريقة التعيين",
|
||||||
"files_automatically_mapped": "تَمَّ تَحْدِيدُ الْمَلَفَّاتِ تِلْقَائِيًّا",
|
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
||||||
"no_backups_created": "لَمْ تُنْشَأْ أَيُّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ",
|
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
||||||
"manage_files": "إِدَارَةُ الْمَلَفَّاتِ",
|
"manage_files": "إدارة الملفات",
|
||||||
"loading_save_preview": "جَارٍ الْبَحْثُ عَنْ أَلْعَابٍ مَحْفُوظَةٍ...",
|
"loading_save_preview": "جارٍ البحث عن حفظات الألعاب...",
|
||||||
"wine_prefix": "بَادِئَةُ Wine",
|
"wine_prefix": "بادئة Wine",
|
||||||
"wine_prefix_description": "بَادِئَةُ Wine الْمُسْتَخْدَمَةُ لِتَشْغِيلِ هَذِهِ اللُّعْبَةِ",
|
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
||||||
"launch_options": "خِيَارَاتُ الْإِطْلَاقِ",
|
"launch_options": "خيارات التشغيل",
|
||||||
"launch_options_description": "يُمْكِنُ لِلْمُسْتَخْدِمِينَ الْمُتَقَدِّمِينَ إِدْخَالُ تَعْدِيلَاتٍ عَلَى خِيَارَاتِ الْإِطْلَاقِ",
|
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
||||||
"launch_options_placeholder": "لَمْ يُحَدَّدْ أَيُّ مُعَامِلٍ",
|
"launch_options_placeholder": "لم يتم تحديد أي معاملات",
|
||||||
"no_download_option_info": "لَا تَوْجَدُ مَعْلُومَاتٌ مَتَوَفِّرَةٌ",
|
"no_download_option_info": "لا توجد معلومات متاحة",
|
||||||
"backup_deletion_failed": "فَشَلَ فِي حَذْفِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
"backup_deletion_failed": "فشل حذف النسخة الاحتياطية",
|
||||||
"max_number_of_artifacts_reached": "تَمَّ بَلُوغُ الْعَدَدِ الْأَقْصَى لِلنُّسَخِ الِاحْتِيَاطِيَّةِ لِهَذِهِ اللُّعْبَةِ",
|
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة",
|
||||||
"achievements_not_sync": "تَعَرَّفْ عَلَى كَيْفِيَّةِ مَزْجِ إِنْجَازَاتِكَ",
|
"achievements_not_sync": "تعرف على كيفية مزامنة إنجازاتك",
|
||||||
"manage_files_description": "إِدَارَةُ الْمَلَفَّاتِ الَّتِي سَيَتِمُّ نَسْخُهَا احْتِيَاطِيًّا وَاسْتِعَادَتُهَا",
|
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
||||||
"select_folder": "تَحْدِيدُ الْمَجَلَّدِ",
|
"select_folder": "حدد المجلد",
|
||||||
"backup_from": "نُسْخَةٌ احْتِيَاطِيَّةٌ مِنْ {{date}}",
|
"backup_from": "نسخة احتياطية من {{date}}",
|
||||||
"custom_backup_location_set": "تَمَّ تَحْدِيدُ مَوْقِعِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ الْمُخَصَّصِ",
|
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
||||||
"no_directory_selected": "لَمْ يُحَدَّدْ أَيُّ دَلِيلٍ"
|
"no_directory_selected": "لم يتم تحديد مجلد",
|
||||||
|
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا لمعرفة المزيد.",
|
||||||
|
"reset_achievements": "إعادة تعيين الإنجازات",
|
||||||
|
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
||||||
|
"reset_achievements_title": "هل أنت متأكد؟",
|
||||||
|
"reset_achievements_success": "تم إعادة تعيين الإنجازات بنجاح",
|
||||||
|
"reset_achievements_error": "فشل إعادة تعيين الإنجازات"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "تَفْعِيلُ Hydra",
|
"title": "تفعيل Hydra",
|
||||||
"installation_id": "مُعَرِّفُ التَّثْبِيتِ:",
|
"installation_id": "معرف التثبيت:",
|
||||||
"enter_activation_code": "أَدْخِلْ رَمْزَ التَّفْعِيلِ",
|
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
||||||
"message": "إِذَا كُنْتَ لَا تَعْرِفُ أَيْنَ تَطْلُبُ هَذَا، فَلا يَجِبُ أَنْ تَكُونَ لَدَيْكَ.",
|
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا يجب أن يكون لديك هذا.",
|
||||||
"activate": "تَفْعِيلٌ",
|
"activate": "تفعيل",
|
||||||
"loading": "جَارٍ التَّحْمِيلُ..."
|
"loading": "جارٍ التحميل..."
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "اسْتِئْنَافٌ",
|
"resume": "استئناف",
|
||||||
"pause": "إِيقَافٌ",
|
"pause": "إيقاف مؤقت",
|
||||||
"eta": "الِاكْتِمَالُ {{eta}}",
|
"eta": "الانتهاء {{eta}}",
|
||||||
"paused": "مُوْقَفٌ",
|
"paused": "معلّق",
|
||||||
"verifying": "جَارٍ التَّحَقُّقُ...",
|
"verifying": "جارٍ التحقق...",
|
||||||
"completed": "مَكْتُومٌ",
|
"completed": "مكتمل",
|
||||||
"removed": "لَمْ يُنَزَّلْ",
|
"removed": "غير محمل",
|
||||||
"cancel": "إِلْغَاءٌ",
|
"cancel": "إلغاء",
|
||||||
"filter": "تَصْفِيَةُ الْأَلْعَابِ الْمُنَزَّلَةِ",
|
"filter": "تصفية الألعاب المحملة",
|
||||||
"remove": "إِزَالَةٌ",
|
"remove": "إزالة",
|
||||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
|
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||||
"deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
|
"deleting": "جارٍ حذف المثبت...",
|
||||||
"delete": "حَذْفُ الْمُثَبِّتِ",
|
"delete": "إزالة المثبت",
|
||||||
"delete_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
"delete_modal_title": "هل أنت متأكد؟",
|
||||||
"delete_modal_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ جَمِيعِ مَلَفَّاتِ التَّثْبِيتِ مِنْ حَاسُوبِكَ",
|
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
||||||
"install": "تَثْبِيتٌ",
|
"install": "تثبيت",
|
||||||
"download_in_progress": "جَارٍ التَّنْفِيذُ",
|
"download_in_progress": "قيد التقدم",
|
||||||
"queued_downloads": "التَّنْزِيلَاتُ فِي الْانْتِظَارِ",
|
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
||||||
"downloads_completed": "مَكْتُومٌ",
|
"downloads_completed": "مكتمل",
|
||||||
"queued": "فِي الْانْتِظَارِ",
|
"queued": "في قائمة الانتظار",
|
||||||
"no_downloads_title": "فَرَاغٌ تَامٌ",
|
"no_downloads_title": "فارغ جدًا",
|
||||||
"no_downloads_description": "لَمْ تَقُمْ بِتَنْزِيلِ أَيِّ شَيْءٍ بِاسْتِخْدَامِ Hydra بَعْدُ، لَكِنَّهُ لَيْسَ مُتَأَخِّرًا لِلْبَدْءِ.",
|
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
||||||
"checking_files": "جَارٍ التَّحَقُّقُ مِنَ الْمَلَفَّاتِ...",
|
"checking_files": "جارٍ فحص الملفات...",
|
||||||
"seeding": "الْبَذْرُ",
|
"seeding": "التوزيع",
|
||||||
"stop_seeding": "إِيقَافُ الْبَذْرِ",
|
"stop_seeding": "إيقاف التوزيع",
|
||||||
"resume_seeding": "اسْتِئْنَافُ الْبَذْرِ",
|
"resume_seeding": "استئناف التوزيع",
|
||||||
"options": "إِدَارَةٌ"
|
"options": "إدارة"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "مَسَارُ التَّنْزِيلَاتِ",
|
"downloads_path": "مسار التنزيلات",
|
||||||
"change": "تَحْدِيثٌ",
|
"change": "تحديث",
|
||||||
"notifications": "الإِشْعَارَاتُ",
|
"notifications": "الإشعارات",
|
||||||
"enable_download_notifications": "عِنْدَ اكْتِمَالِ التَّنْزِيلِ",
|
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||||
"enable_repack_list_notifications": "عِنْدَ إِضَافَةِ إِصْدَارٍ مُعَادٍ تَغْلِيفِهِ جَدِيدٍ",
|
"enable_repack_list_notifications": "عند إضافة حزمة معاد تعبئتها جديدة",
|
||||||
"real_debrid_api_token_label": "رَمْزُ واجهة برمجة التطبيقات Real-Debrid",
|
"real_debrid_api_token_label": "رمز واجهة برمجة تطبيقات Real-Debrid",
|
||||||
"quit_app_instead_hiding": "لا تُخْفِ Hydra عِنْدَ الإِغْلَاقِ",
|
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق",
|
||||||
"launch_with_system": "تَشْغِيلُ Hydra عِنْدَ بَدْءِ النِّظَامِ",
|
"launch_with_system": "تشغيل Hydra مع بدء النظام",
|
||||||
"general": "عَامٌ",
|
"general": "عام",
|
||||||
"behavior": "سُلُوكٌ",
|
"behavior": "السلوك",
|
||||||
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
|
"download_sources": "مصادر التنزيل",
|
||||||
"language": "اللُّغَةُ",
|
"language": "اللغة",
|
||||||
"real_debrid_api_token": "رَمْزُ واجهة برمجة التطبيقات",
|
"api_token": "رمز API",
|
||||||
"enable_real_debrid": "تَمْكِينُ Real-Debrid",
|
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid هُوَ مُنَزِّلٌ غَيْرُ مَقْيُودٍ يَتِيحُ لَكَ تَنْزِيلَ الْمَلَفَّاتِ بِسُرْعَةٍ، مَحْدُودٌ فَقَطْ بِسُرْعَةِ الْإِنْتَرْنِتِ لَدَيْكَ.",
|
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
||||||
"real_debrid_invalid_token": "رَمْزُ واجهة برمجة التطبيقات غَيْرُ صَالِحٍ",
|
"debrid_invalid_token": "رمز API غير صالح",
|
||||||
"real_debrid_api_token_hint": "يُمْكِنُكَ الْحُصُولُ عَلَى رَمْزِ واجهة برمجة التطبيقات <0>هُنَا</0>",
|
"debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||||
"real_debrid_free_account_error": "الْحِسَابُ \"{{username}}\" هُوَ حِسَابٌ مَجَّانِيٌّ. يَرْجَى الِاشْتِرَاكُ فِي Real-Debrid",
|
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||||
"real_debrid_linked_message": "تَمَّ رَبْطُ الْحِسَابِ \"{{username}}\"",
|
"debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
||||||
"save_changes": "حِفْظُ التَّغْيِيرَاتِ",
|
"save_changes": "حفظ التغييرات",
|
||||||
"changes_saved": "تَمَّ حِفْظُ التَّغْيِيرَاتِ بِنَجَاحٍ",
|
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||||
"download_sources_description": "سَتَقُومُ Hydra بِجَلْبِ رَوَابِطِ التَّنْزِيلِ مِنْ هَذِهِ الْمَصَادِرِ. يَجِبُ أَنْ يَكُونَ عُنْوَانُ URL لِلْمَصْدَرِ رَابِطًا مُبَاشِرًا إِلَى مِلَفٍّ .json يَحْتَوِي عَلَى رَوَابِطِ التَّنْزِيلِ.",
|
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||||
"validate_download_source": "تَصْدِيقٌ",
|
"validate_download_source": "تحقق",
|
||||||
"remove_download_source": "إِزَالَةٌ",
|
"remove_download_source": "إزالة",
|
||||||
"removed_download_sources": "تمت إزالة مصادر التنزيل",
|
"removed_download_sources": "تمت إزالة مصادر التنزيل",
|
||||||
"cancel_button_confirmation_delete_all_sources": "لا",
|
"cancel_button_confirmation_delete_all_sources": "لا",
|
||||||
"confirm_button_confirmation_delete_all_sources": "نعم، احذف كل شيء",
|
"confirm_button_confirmation_delete_all_sources": "نعم، احذف كل شيء",
|
||||||
"description_confirmation_delete_all_sources": "سوف تقوم بحذف جميع مصادر التنزيل",
|
"description_confirmation_delete_all_sources": "سوف تقوم بحذف جميع مصادر التنزيل",
|
||||||
"title_confirmation_delete_all_sources": "احذف جميع مصادر التنزيل",
|
"title_confirmation_delete_all_sources": "احذف جميع مصادر التنزيل",
|
||||||
"button_delete_all_sources": "قم بإزالة جميع مصادر التنزيل",
|
"button_delete_all_sources": "قم بإزالة جميع مصادر التنزيل",
|
||||||
"add_download_source": "إِضَافَةُ مَصْدَرٍ",
|
"add_download_source": "إضافة مصدر",
|
||||||
"download_count_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
|
"download_count_zero": "لا توجد خيارات تنزيل",
|
||||||
"download_count_one": "{{countFormatted}} خِيَارُ تَنْزِيلٍ",
|
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
||||||
"download_count_other": "{{countFormatted}} خِيَارَاتُ تَنْزِيلٍ",
|
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
||||||
"download_source_url": "عُنْوَانُ مَصْدَرِ التَّنْزِيلِ",
|
"download_source_url": "عنوان URL لمصدر التنزيل",
|
||||||
"add_download_source_description": "أَدْخِلْ عُنْوَانَ URL لِمِلَفٍّ .json",
|
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
||||||
"download_source_up_to_date": "مُحَدَّثٌ",
|
"download_source_up_to_date": "محدث",
|
||||||
"download_source_errored": "خَطَأٌ",
|
"download_source_errored": "خطأ",
|
||||||
"sync_download_sources": "مَزْجُ الْمَصَادِرِ",
|
"sync_download_sources": "مزامنة المصادر",
|
||||||
"removed_download_source": "تَمَّ إِزَالَةُ مَصْدَرِ التَّنْزِيلِ",
|
"removed_download_source": "تمت إزالة مصدر التنزيل",
|
||||||
"added_download_source": "تَمَّتْ إِضَافَةُ مَصْدَرِ التَّنْزِيلِ",
|
"added_download_source": "تمت إضافة مصدر التنزيل",
|
||||||
"download_sources_synced": "تَمَّ مَزْجُ جَمِيعِ مَصَادِرِ التَّنْزِيلِ",
|
"download_sources_synced": "تمت مزامنة جميع مصادر التنزيل",
|
||||||
"insert_valid_json_url": "أَدْخِلْ عُنْوَانَ JSON صَالِحًا",
|
"insert_valid_json_url": "أدخل عنوان JSON صالح",
|
||||||
"found_download_option_zero": "لَمْ يُعْثَرْ عَلَى خِيَارِ تَنْزِيلٍ",
|
"found_download_option_zero": "لم يتم العثور على خيارات تنزيل",
|
||||||
"found_download_option_one": "عُثِرَ عَلَى {{countFormatted}} خِيَارِ تَنْزِيلٍ",
|
"found_download_option_one": "تم العثور على {{countFormatted}} خيار تنزيل",
|
||||||
"found_download_option_other": "عُثِرَ عَلَى {{countFormatted}} خِيَارَاتِ تَنْزِيلٍ",
|
"found_download_option_other": "تم العثور على {{countFormatted}} خيارات تنزيل",
|
||||||
"import": "اسْتِيرَادٌ",
|
"import": "استيراد",
|
||||||
"public": "عَامٌ",
|
"public": "عام",
|
||||||
"private": "خَاصٌ",
|
"private": "خاص",
|
||||||
"friends_only": "الْأَصْدِقَاءُ فَقَطْ",
|
"friends_only": "الأصدقاء فقط",
|
||||||
"privacy": "الْخُصُوصِيَّةُ",
|
"privacy": "الخصوصية",
|
||||||
"profile_visibility": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
|
"profile_visibility": "رؤية الملف الشخصي",
|
||||||
"profile_visibility_description": "اخْتَرْ مَنْ يُمْكِنُهُ رُؤْيَةُ مَلَفِّكَ الشَّخْصِيِّ وَمَكْتَبَتِكَ",
|
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
||||||
"required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
|
"required_field": "هذا الحقل مطلوب",
|
||||||
"source_already_exists": "تَمَّتْ إِضَافَةُ هَذَا الْمَصْدَرِ مِنْ قَبْلُ",
|
"source_already_exists": "تمت إضافة هذا المصدر مسبقًا",
|
||||||
"must_be_valid_url": "يَجِبُ أَنْ يَكُونَ الْمَصْدَرُ عُنْوَانَ URL صَالِحًا",
|
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا",
|
||||||
"blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
|
"blocked_users": "المستخدمون المحظورون",
|
||||||
"user_unblocked": "تَمَّ إِزَالَةُ حَظْرِ الْمُسْتَخْدِمِ",
|
"user_unblocked": "تم إلغاء حظر المستخدم",
|
||||||
"enable_achievement_notifications": "عِنْدَ فَتْحِ إِنْجَازٍ",
|
"enable_achievement_notifications": "عند فتح إنجاز",
|
||||||
"launch_minimized": "تَشْغِيلُ Hydra مُصَغَّرًا",
|
"launch_minimized": "تشغيل Hydra مصغرًا",
|
||||||
"disable_nsfw_alert": "تَعْطِيلُ تَنْبِيهِ الْمُحْتَوَى غَيْرِ اللَّائِقِ",
|
"disable_nsfw_alert": "تعطيل تنبيه المحتوى غير اللائق",
|
||||||
"seed_after_download_complete": "الْبَذْرُ بَعْدَ اكْتِمَالِ التَّنْزِيلِ",
|
"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": "البريد الإلكتروني الحالي:",
|
||||||
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "اكْتِمَالُ التَّنْزِيلِ",
|
"download_complete": "اكتمل التنزيل",
|
||||||
"game_ready_to_install": "{{title}} جَاهِزٌ لِلتَّثْبِيتِ",
|
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
||||||
"repack_list_updated": "تَمَّ تَحْدِيثُ قَائِمَةِ الإِصْدَارَاتِ الْمُعَادَةِ تَغْلِيفُهَا",
|
"repack_list_updated": "تم تحديث قائمة الحزم المعاد تعبئتها",
|
||||||
"repack_count_one": "{{count}} إِصْدَارٌ مُعَادٌ تَغْلِيفُهُ أُضِيفَ",
|
"repack_count_one": "تمت إضافة {{count}} حزمة معاد تعبئتها",
|
||||||
"repack_count_other": "{{count}} إِصْدَارَاتٌ مُعَادَةٌ تَغْلِيفُهَا أُضِيفَتْ",
|
"repack_count_other": "تمت إضافة {{count}} حزم معاد تعبئتها",
|
||||||
"new_update_available": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ",
|
"new_update_available": "الإصدار {{version}} متوفر",
|
||||||
"restart_to_install_update": "أَعِدْ تَشْغِيلَ Hydra لِتَثْبِيتِ التَّحْدِيثِ",
|
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
||||||
"notification_achievement_unlocked_title": "تَمَّ فَتْحُ إِنْجَازٍ لِـ {{game}}",
|
"notification_achievement_unlocked_title": "تم فتح إنجاز لـ {{game}}",
|
||||||
"notification_achievement_unlocked_body": "{{achievement}} وَ{{count}} أُخْرَى تَمَّ فَتْحُهَا"
|
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} آخرين تم فتحهم"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "فَتْحُ Hydra",
|
"open": "فتح Hydra",
|
||||||
"quit": "الْخُرُوجُ"
|
"quit": "خروج"
|
||||||
},
|
},
|
||||||
"game_card": {
|
"game_card": {
|
||||||
"no_downloads": "لَا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ"
|
"no_downloads": "لا توجد تنزيلات متاحة"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "الْبَرَامِجُ غَيْرُ مُثَبَّتَةٍ",
|
"title": "البرامج غير مثبتة",
|
||||||
"description": "لَمْ يُعْثَرْ عَلَى مَلَفَّاتٍ تَنْفِيذِيَّةٍ لِـ Wine أَوْ Lutris عَلَى نِظَامِكَ",
|
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
||||||
"instructions": "تَحَقَّقْ مِنَ الطَّرِيقَةِ الصَّحِيحَةِ لِتَثْبِيتِ أَيٍّ مِنْهُمَا عَلَى تَوْزِيعَةِ Linux لَدَيْكَ لِتَعْمَلَ اللُّعْبَةُ بِشَكْلٍ طَبِيعِيٍّ"
|
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة لينكس الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "زِرُّ الإِغْلَاقِ"
|
"close": "زر الإغلاق"
|
||||||
},
|
},
|
||||||
"forms": {
|
"forms": {
|
||||||
"toggle_password_visibility": "تَبْدِيلُ رُؤْيَةِ كَلِمَةِ الْمَرُورِ"
|
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"amount_hours": "{{amount}} سَاعَاتٌ",
|
"amount_hours": "{{amount}} ساعات",
|
||||||
"amount_minutes": "{{amount}} دَقَائِقُ",
|
"amount_minutes": "{{amount}} دقائق",
|
||||||
"last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
|
"last_time_played": "آخر تشغيل {{period}}",
|
||||||
"activity": "النَّشَاطُ الْأَخِيرُ",
|
"activity": "النشاط الأخير",
|
||||||
"library": "الْمَكْتَبَةُ",
|
"library": "المكتبة",
|
||||||
"total_play_time": "إِجْمَالِيُّ وَقْتِ اللَّعِبِ",
|
"total_play_time": "إجمالي وقت اللعب",
|
||||||
"no_recent_activity_title": "هَمَمْ... لَا شَيْءَ هُنَا",
|
"no_recent_activity_title": "همم... لا شيء هنا",
|
||||||
"no_recent_activity_description": "لَمْ تَلْعَبْ أَيَّ أَلْعَابٍ مُؤَخَّرًا. حَانَ الْوَقْتُ لِتَغْيِيرِ ذَلِكَ!",
|
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
||||||
"display_name": "اسْمُ الْعَرْضِ",
|
"display_name": "اسم العرض",
|
||||||
"saving": "جَارٍ الْحِفْظُ",
|
"saving": "جارٍ الحفظ",
|
||||||
"save": "حِفْظٌ",
|
"save": "حفظ",
|
||||||
"edit_profile": "تَحْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
|
"edit_profile": "تعديل الملف الشخصي",
|
||||||
"saved_successfully": "تَمَّ الْحِفْظُ بِنَجَاحٍ",
|
"saved_successfully": "تم الحفظ بنجاح",
|
||||||
"try_again": "الرَّجَاءُ الْمُحَاوَلَةُ مَرَّةً أُخْرَى",
|
"try_again": "يرجى المحاولة مرة أخرى",
|
||||||
"sign_out_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
"sign_out_modal_title": "هل أنت متأكد؟",
|
||||||
"cancel": "إِلْغَاءٌ",
|
"cancel": "إلغاء",
|
||||||
"successfully_signed_out": "تَمَّ تَسْجِيلُ الْخُرُوجِ بِنَجَاحٍ",
|
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
||||||
"sign_out": "تَسْجِيلُ الْخُرُوجِ",
|
"sign_out": "تسجيل الخروج",
|
||||||
"playing_for": "جَارِي اللَّعِبُ لِمُدَّةِ {{amount}}",
|
"playing_for": "يلعب لمدة {{amount}}",
|
||||||
"sign_out_modal_text": "مَكْتَبَتُكَ مُرْتَبِطَةٌ بِحِسَابِكَ الْحَالِيِّ. عِنْدَ تَسْجِيلِ الْخُرُوجِ، لَنْ تَكُونَ مَكْتَبَتُكَ مَرْئِيَّةً بَعْدَ الْآنِ، وَلَنْ يَتِمَّ حِفْظُ أَيِّ تَقَدُّمٍ. هَلْ تُرِيدُ الْمُتَابَعَةَ مَعَ تَسْجِيلِ الْخُرُوجِ؟",
|
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية بعد الآن، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||||
"add_friends": "إِضَافَةُ الْأَصْدِقَاءِ",
|
"add_friends": "إضافة أصدقاء",
|
||||||
"add": "إِضَافَةٌ",
|
"add": "إضافة",
|
||||||
"friend_code": "رَمْزُ الصَّدِيقِ",
|
"friend_code": "رمز الصديق",
|
||||||
"see_profile": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
|
"see_profile": "عرض الملف الشخصي",
|
||||||
"sending": "جَارٍ الْإِرْسَالُ",
|
"sending": "جارٍ الإرسال",
|
||||||
"friend_request_sent": "تَمَّ إِرْسَالُ طَلَبِ الصَّدَاقَةِ",
|
"friend_request_sent": "تم إرسال طلب الصداقة",
|
||||||
"friends": "الْأَصْدِقَاءُ",
|
"friends": "الأصدقاء",
|
||||||
"friends_list": "قَائِمَةُ الْأَصْدِقَاءِ",
|
"friends_list": "قائمة الأصدقاء",
|
||||||
"user_not_found": "الْمُسْتَخْدِمُ غَيْرُ مَوْجُودٍ",
|
"user_not_found": "المستخدم غير موجود",
|
||||||
"block_user": "حَظْرُ الْمُسْتَخْدِمِ",
|
"block_user": "حظر المستخدم",
|
||||||
"add_friend": "إِضَافَةُ صَدِيقٍ",
|
"add_friend": "إضافة صديق",
|
||||||
"request_sent": "تَمَّ إِرْسَالُ الطَّلَبِ",
|
"request_sent": "تم إرسال الطلب",
|
||||||
"request_received": "تَمَّ اسْتِقْبَالُ الطَّلَبِ",
|
"request_received": "تم استلام الطلب",
|
||||||
"accept_request": "قَبُولُ الطَّلَبِ",
|
"accept_request": "قبول الطلب",
|
||||||
"ignore_request": "تَجَاهُلُ الطَّلَبِ",
|
"ignore_request": "تجاهل الطلب",
|
||||||
"cancel_request": "إِلْغَاءُ الطَّلَبِ",
|
"cancel_request": "إلغاء الطلب",
|
||||||
"undo_friendship": "إِلْغَاءُ الصَّدَاقَةِ",
|
"undo_friendship": "إلغاء الصداقة",
|
||||||
"request_accepted": "تَمَّ قَبُولُ الطَّلَبِ",
|
"request_accepted": "تم قبول الطلب",
|
||||||
"user_blocked_successfully": "تَمَّ حَظْرُ الْمُسْتَخْدِمِ بِنَجَاحٍ",
|
"user_blocked_successfully": "تم حظر المستخدم بنجاح",
|
||||||
"user_block_modal_text": "سَيُؤَدِّي هَذَا إِلَى حَظْرِ {{displayName}}",
|
"user_block_modal_text": "سيؤدي هذا إلى حظر {{displayName}}",
|
||||||
"blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
|
"blocked_users": "المستخدمون المحظورون",
|
||||||
"unblock": "إِزَالَةُ الْحَظْرِ",
|
"unblock": "إلغاء الحظر",
|
||||||
"no_friends_added": "لَيْسَ لَدَيْكَ أَصْدِقَاءٌ مُضَافُونَ",
|
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
||||||
"pending": "قَيْدُ الْانْتِظَارِ",
|
"pending": "قيد الانتظار",
|
||||||
"no_pending_invites": "لَيْسَ لَدَيْكَ دَعَوَاتٌ قَيْدُ الْانْتِظَارِ",
|
"no_pending_invites": "ليس لديك دعوات معلقة",
|
||||||
"no_blocked_users": "لَيْسَ لَدَيْكَ مُسْتَخْدِمُونَ مَحْظُورُونَ",
|
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
||||||
"friend_code_copied": "تَمَّ نَسْخُ رَمْزِ الصَّدِيقِ",
|
"friend_code_copied": "تم نسخ رمز الصديق",
|
||||||
"undo_friendship_modal_text": "سَيُؤَدِّي هَذَا إِلَى إِلْغَاءِ صَدَاقَتِكَ مَعَ {{displayName}}",
|
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
||||||
"privacy_hint": "لِتَعْدِيلِ مَنْ يُمْكِنُهُ رُؤْيَةُ هَذَا، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
|
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
||||||
"locked_profile": "هَذَا الْمَلَفُّ الشَّخْصِيُّ خَاصٌّ",
|
"locked_profile": "هذا الملف الشخصي خاص",
|
||||||
"image_process_failure": "فَشَلَ أَثْنَاءَ مُعَالَجَةِ الصُّورَةِ",
|
"image_process_failure": "فشل معالجة الصورة",
|
||||||
"required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
|
"required_field": "هذا الحقل مطلوب",
|
||||||
"displayname_min_length": "يَجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَقَلِّ 3 أَحْرُفٍ",
|
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
||||||
"displayname_max_length": "يَجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَكْثَرِ 50 حَرْفًا",
|
"displayname_max_length": "يجب ألا يتجاوز اسم العرض 50 حرفًا",
|
||||||
"report_profile": "تَقْرِيرٌ عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ",
|
"report_profile": "الإبلاغ عن هذا الملف الشخصي",
|
||||||
"report_reason": "لِمَاذَا تُقَدِّمُ تَقْرِيرًا عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ؟",
|
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
||||||
"report_description": "مَعْلُومَاتٌ إِضَافِيَّةٌ",
|
"report_description": "معلومات إضافية",
|
||||||
"report_description_placeholder": "مَعْلُومَاتٌ إِضَافِيَّةٌ",
|
"report_description_placeholder": "معلومات إضافية",
|
||||||
"report": "تَقْرِيرٌ",
|
"report": "الإبلاغ",
|
||||||
"report_reason_hate": "خِطَابُ الْكُرْهِ",
|
"report_reason_hate": "خطاب كراهية",
|
||||||
"report_reason_sexual_content": "مُحْتَوًى جِنْسِيٌّ",
|
"report_reason_sexual_content": "محتوى جنسي",
|
||||||
"report_reason_violence": "عُنْفٌ",
|
"report_reason_violence": "عنف",
|
||||||
"report_reason_spam": "رَاسِلَةٌ عَشْوَائِيَّةٌ",
|
"report_reason_spam": "بريد عشوائي",
|
||||||
"report_reason_other": "آخَرُ",
|
"report_reason_other": "أخرى",
|
||||||
"profile_reported": "تَمَّ تَقْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
|
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
||||||
"your_friend_code": "رَمْزُ صَدِيقِكَ:",
|
"your_friend_code": "رمز صديقك:",
|
||||||
"upload_banner": "رَفْعُ لَافِتَةٍ",
|
"upload_banner": "تحميل بانر",
|
||||||
"uploading_banner": "جَارٍ رَفْعُ اللَّافِتَةِ...",
|
"uploading_banner": "جارٍ تحميل البانر...",
|
||||||
"background_image_updated": "تَمَّ تَحْدِيثُ صُورَةِ الْخَلْفِيَّةِ",
|
"background_image_updated": "تم تحديث صورة الخلفية",
|
||||||
"stats": "الإحْصَائِيَّاتُ",
|
"stats": "الإحصائيات",
|
||||||
"achievements": "الإِنْجَازَاتُ",
|
"achievements": "إنجازات",
|
||||||
"games": "الْأَلْعَابُ",
|
"games": "الألعاب",
|
||||||
"top_percentile": "الْأَفْضَلُ {{percentile}}%",
|
"top_percentile": "ال{{percentile}}% الأعلى",
|
||||||
"ranking_updated_weekly": "التَّرْتِيبُ يُحَدَّثُ أُسْبُوعِيًّا",
|
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
||||||
"playing": "جَارِي اللَّعِبُ {{game}}",
|
"playing": "يلعب {{game}}",
|
||||||
"achievements_unlocked": "الإِنْجَازَاتُ الْمَفْتُوحَةُ",
|
"achievements_unlocked": "الإنجازات المفتوحة",
|
||||||
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ",
|
"earned_points": "النقاط المكتسبة",
|
||||||
"show_achievements_on_profile": "عَرْضُ إِنْجَازَاتِكَ عَلَى مَلَفِّكَ الشَّخْصِيِّ",
|
"show_achievements_on_profile": "عرض إنجازاتك على ملفك الشخصي",
|
||||||
"show_points_on_profile": "عَرْضُ النَّقَاطِ الْمَكْسُوبَةِ عَلَى مَلَفِّكَ الشَّخْصِيِّ"
|
"show_points_on_profile": "عرض نقاطك المكتسبة على ملفك الشخصي"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "إِنْجَازٌ مَفْتُوحٌ",
|
"achievement_unlocked": "تم فتح الإنجاز",
|
||||||
"user_achievements": "إِنْجَازَاتُ {{displayName}}",
|
"user_achievements": "إنجازات {{displayName}}",
|
||||||
"your_achievements": "إِنْجَازَاتُكَ",
|
"your_achievements": "إنجازاتك",
|
||||||
"unlocked_at": "تَمَّ الْفَتْحُ فِي: {{date}}",
|
"unlocked_at": "تم الفتح في: {{date}}",
|
||||||
"subscription_needed": "يَحْتَاجُ اشْتِرَاكُ Hydra Cloud لِرُؤْيَةِ هَذَا الْمُحْتَوَى",
|
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
||||||
"new_achievements_unlocked": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ مِنْ {{gameCount}} أَلْعَابٍ",
|
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
||||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إِنْجَازَاتٍ",
|
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
||||||
"achievements_unlocked_for_game": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ لِـ {{gameTitle}}",
|
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
||||||
"hidden_achievement_tooltip": "هَذَا إِنْجَازٌ مَخْفِيٌّ",
|
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
||||||
"achievement_earn_points": "اكْسِبْ {{points}} نَقَاطًا بِهَذَا الإِنْجَازِ",
|
"achievement_earn_points": "اكسب {{points}} نقطة مع هذا الإنجاز",
|
||||||
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ:",
|
"earned_points": "النقاط المكتسبة:",
|
||||||
"available_points": "النَّقَاطُ الْمُتَوَفِّرَةُ:",
|
"available_points": "النقاط المتاحة:",
|
||||||
"how_to_earn_achievements_points": "كَيْفَ تَكْسِبُ نَقَاطَ الإِنْجَازَاتِ؟"
|
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
||||||
},
|
},
|
||||||
"hydra_cloud": {
|
"hydra_cloud": {
|
||||||
"subscription_tour_title": "اشْتِرَاكُ Hydra Cloud",
|
"subscription_tour_title": "اشتراك Hydra Cloud",
|
||||||
"subscribe_now": "اشْتَرِكِ الْآنَ",
|
"subscribe_now": "اشترك الآن",
|
||||||
"cloud_saving": "حِفْظٌ سَحَابِيٌّ",
|
"cloud_saving": "حفظ سحابي",
|
||||||
"cloud_achievements": "حِفْظُ إِنْجَازَاتِكَ فِي السَّحَابَةِ",
|
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
||||||
"animated_profile_picture": "صُورُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
|
"animated_profile_picture": "صورة ملف شخصي متحركة",
|
||||||
"premium_support": "الدَّعْمُ الْمُتَقَدِّمُ",
|
"premium_support": "دعم ممتاز",
|
||||||
"show_and_compare_achievements": "عَرْضٌ وَمُقَارَنَةُ إِنْجَازَاتِكَ مَعَ مُسْتَخْدِمِينَ آخَرِينَ",
|
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
||||||
"animated_profile_banner": "لَافِتَةُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
|
"animated_profile_banner": "بانر ملف شخصي متحرك",
|
||||||
"hydra_cloud": "Hydra Cloud",
|
"hydra_cloud": "Hydra Cloud",
|
||||||
"hydra_cloud_feature_found": "لَقَدْ اكْتَشَفْتَ مِيزَةً مِنْ Hydra Cloud!",
|
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
||||||
"learn_more": "تَعَلَّمْ أَكْثَرَ"
|
"learn_more": "معرفة المزيد"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,13 +230,13 @@
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Източници за изтегляне",
|
"download_sources": "Източници за изтегляне",
|
||||||
"language": "Език",
|
"language": "Език",
|
||||||
"real_debrid_api_token": "API Токен",
|
"api_token": "API Токен",
|
||||||
"enable_real_debrid": "Включи Real-Debrid",
|
"enable_real_debrid": "Включи Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
|
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
|
||||||
"real_debrid_invalid_token": "Невалиден API токен",
|
"debrid_invalid_token": "Невалиден API токен",
|
||||||
"real_debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
|
"debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
|
||||||
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
|
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
|
||||||
"real_debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
|
"debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
|
||||||
"save_changes": "Запази промените",
|
"save_changes": "Запази промените",
|
||||||
"changes_saved": "Промените са успешно запазни",
|
"changes_saved": "Промените са успешно запазни",
|
||||||
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
|
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
|
||||||
|
|
|
@ -161,13 +161,13 @@
|
||||||
"behavior": "Comportament",
|
"behavior": "Comportament",
|
||||||
"download_sources": "Fonts de descàrrega",
|
"download_sources": "Fonts de descàrrega",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Testimoni API",
|
"api_token": "Testimoni API",
|
||||||
"enable_real_debrid": "Activa el Real Debrid",
|
"enable_real_debrid": "Activa el Real Debrid",
|
||||||
"real_debrid_description": "Real-Debrid és un programa de descàrrega sense restriccions que us permet descarregar fitxers a l'instant i al màxim de la vostra velocitat d'Internet.",
|
"real_debrid_description": "Real-Debrid és un programa de descàrrega sense restriccions que us permet descarregar fitxers a l'instant i al màxim de la vostra velocitat d'Internet.",
|
||||||
"real_debrid_invalid_token": "Invalida el testimoni de l'API",
|
"debrid_invalid_token": "Invalida el testimoni de l'API",
|
||||||
"real_debrid_api_token_hint": "Pots obtenir la teva clau de l'API <0>aquí</0>.",
|
"debrid_api_token_hint": "Pots obtenir la teva clau de l'API <0>aquí</0>.",
|
||||||
"real_debrid_free_account_error": "L'usuari \"{{username}}\" és un compte gratuït. Si us plau subscriu-te a Real-Debrid",
|
"real_debrid_free_account_error": "L'usuari \"{{username}}\" és un compte gratuït. Si us plau subscriu-te a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Compte \"{{username}}\" vinculat",
|
"debrid_linked_message": "Compte \"{{username}}\" vinculat",
|
||||||
"save_changes": "Desa els canvis",
|
"save_changes": "Desa els canvis",
|
||||||
"changes_saved": "Els canvis s'han desat correctament",
|
"changes_saved": "Els canvis s'han desat correctament",
|
||||||
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
|
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
|
||||||
|
|
|
@ -214,13 +214,13 @@
|
||||||
"behavior": "Chování",
|
"behavior": "Chování",
|
||||||
"download_sources": "Zdroje stahování",
|
"download_sources": "Zdroje stahování",
|
||||||
"language": "Jazyk",
|
"language": "Jazyk",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Povolit Real-Debrid",
|
"enable_real_debrid": "Povolit Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid je neomezený správce stahování, který umožňuje stahovat soubory v nejvyšší rychlosti vašeho internetu.",
|
"real_debrid_description": "Real-Debrid je neomezený správce stahování, který umožňuje stahovat soubory v nejvyšší rychlosti vašeho internetu.",
|
||||||
"real_debrid_invalid_token": "Neplatný API token",
|
"debrid_invalid_token": "Neplatný API token",
|
||||||
"real_debrid_api_token_hint": "API token můžeš sehnat <0>zde</0>",
|
"debrid_api_token_hint": "API token můžeš sehnat <0>zde</0>",
|
||||||
"real_debrid_free_account_error": "Účet \"{{username}}\" má základní úroveň. Prosím předplaťte si Real-Debrid",
|
"real_debrid_free_account_error": "Účet \"{{username}}\" má základní úroveň. Prosím předplaťte si Real-Debrid",
|
||||||
"real_debrid_linked_message": "Účet \"{{username}}\" je propojen",
|
"debrid_linked_message": "Účet \"{{username}}\" je propojen",
|
||||||
"save_changes": "Uložit změny",
|
"save_changes": "Uložit změny",
|
||||||
"changes_saved": "Změny úspěšně uloženy",
|
"changes_saved": "Změny úspěšně uloženy",
|
||||||
"download_sources_description": "Hydra bude odsud sbírat soubory. Zdrojový odkaz musí být .json soubor obsahující odkazy na soubory.",
|
"download_sources_description": "Hydra bude odsud sbírat soubory. Zdrojový odkaz musí být .json soubor obsahující odkazy na soubory.",
|
||||||
|
|
|
@ -177,13 +177,13 @@
|
||||||
"behavior": "Opførsel",
|
"behavior": "Opførsel",
|
||||||
"download_sources": "Download kilder",
|
"download_sources": "Download kilder",
|
||||||
"language": "Sprog",
|
"language": "Sprog",
|
||||||
"real_debrid_api_token": "API nøgle",
|
"api_token": "API nøgle",
|
||||||
"enable_real_debrid": "Slå Real-Debrid til",
|
"enable_real_debrid": "Slå Real-Debrid til",
|
||||||
"real_debrid_description": "Real-Debrid er en ubegrænset downloader der gør det muligt for dig at downloade filer med det samme og med den bedste udnyttelse af din internet hastighed.",
|
"real_debrid_description": "Real-Debrid er en ubegrænset downloader der gør det muligt for dig at downloade filer med det samme og med den bedste udnyttelse af din internet hastighed.",
|
||||||
"real_debrid_invalid_token": "Ugyldig API nøgle",
|
"debrid_invalid_token": "Ugyldig API nøgle",
|
||||||
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
"debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
||||||
"real_debrid_free_account_error": "Brugeren \"{{username}}\" er en gratis bruger. Venligst abbonér på Real-Debrid",
|
"real_debrid_free_account_error": "Brugeren \"{{username}}\" er en gratis bruger. Venligst abbonér på Real-Debrid",
|
||||||
"real_debrid_linked_message": "Brugeren \"{{username}}\" er forbundet",
|
"debrid_linked_message": "Brugeren \"{{username}}\" er forbundet",
|
||||||
"save_changes": "Gem ændringer",
|
"save_changes": "Gem ændringer",
|
||||||
"changes_saved": "Ændringer gemt successfuldt",
|
"changes_saved": "Ændringer gemt successfuldt",
|
||||||
"download_sources_description": "Hydra vil hente download links fra disse kilder. Kilde URLen skal være et direkte link til en .json fil der indeholder download linkene.",
|
"download_sources_description": "Hydra vil hente download links fra disse kilder. Kilde URLen skal være et direkte link til en .json fil der indeholder download linkene.",
|
||||||
|
|
|
@ -161,13 +161,13 @@
|
||||||
"behavior": "Verhalten",
|
"behavior": "Verhalten",
|
||||||
"download_sources": "Download-Quellen",
|
"download_sources": "Download-Quellen",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Real-Debrid aktivieren",
|
"enable_real_debrid": "Real-Debrid aktivieren",
|
||||||
"real_debrid_description": "Real-Debrid ist ein unrestriktiver Downloader, der es dir ermöglicht Dateien sofort und mit deiner maximalen Internetgeschwindigkeit herunterzuladen.",
|
"real_debrid_description": "Real-Debrid ist ein unrestriktiver Downloader, der es dir ermöglicht Dateien sofort und mit deiner maximalen Internetgeschwindigkeit herunterzuladen.",
|
||||||
"real_debrid_invalid_token": "API token nicht gültig",
|
"debrid_invalid_token": "API token nicht gültig",
|
||||||
"real_debrid_api_token_hint": "<0>Hier</0> kannst du dir deinen API Token holen",
|
"debrid_api_token_hint": "<0>Hier</0> kannst du dir deinen API Token holen",
|
||||||
"real_debrid_free_account_error": "Das Konto \"{{username}}\" ist ein gratis account. Bitte abonniere Real-Debrid",
|
"real_debrid_free_account_error": "Das Konto \"{{username}}\" ist ein gratis account. Bitte abonniere Real-Debrid",
|
||||||
"real_debrid_linked_message": "Konto \"{{username}}\" verknüpft",
|
"debrid_linked_message": "Konto \"{{username}}\" verknüpft",
|
||||||
"save_changes": "Änderungen speichern",
|
"save_changes": "Änderungen speichern",
|
||||||
"changes_saved": "Änderungen erfolgreich gespeichert",
|
"changes_saved": "Änderungen erfolgreich gespeichert",
|
||||||
"download_sources_description": "Hydra wird die Download-Links von diesen Quellen abrufen. Die Quell-URL muss ein direkter Link zu einer .json Datei, welche die Download-Links enthält, sein.",
|
"download_sources_description": "Hydra wird die Download-Links von diesen Quellen abrufen. Die Quell-URL muss ein direkter Link zu einer .json Datei, welche die Download-Links enthält, sein.",
|
||||||
|
|
|
@ -184,7 +184,11 @@
|
||||||
"reset_achievements_description": "This will reset all achievements for {{game}}",
|
"reset_achievements_description": "This will reset all achievements for {{game}}",
|
||||||
"reset_achievements_title": "Are you sure?",
|
"reset_achievements_title": "Are you sure?",
|
||||||
"reset_achievements_success": "Achievements successfully reset",
|
"reset_achievements_success": "Achievements successfully reset",
|
||||||
"reset_achievements_error": "Failed to reset achievements"
|
"reset_achievements_error": "Failed to reset achievements",
|
||||||
|
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||||
|
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
@ -236,13 +240,13 @@
|
||||||
"behavior": "Behavior",
|
"behavior": "Behavior",
|
||||||
"download_sources": "Download sources",
|
"download_sources": "Download sources",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Enable Real-Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to quickly download files, only limited by your internet speed.",
|
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to quickly download files, only limited by your internet speed.",
|
||||||
"real_debrid_invalid_token": "Invalid API token",
|
"debrid_invalid_token": "Invalid API token",
|
||||||
"real_debrid_api_token_hint": "You can get your API token <0>here</0>",
|
"debrid_api_token_hint": "You can get your API token <0>here</0>",
|
||||||
"real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid",
|
"real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid",
|
||||||
"real_debrid_linked_message": "Account \"{{username}}\" linked",
|
"debrid_linked_message": "Account \"{{username}}\" linked",
|
||||||
"save_changes": "Save changes",
|
"save_changes": "Save changes",
|
||||||
"changes_saved": "Changes successfully saved",
|
"changes_saved": "Changes successfully saved",
|
||||||
"download_sources_description": "Hydra will fetch the download links from these sources. The source URL must be a direct link to a .json file containing the download links.",
|
"download_sources_description": "Hydra will fetch the download links from these sources. The source URL must be a direct link to a .json file containing the download links.",
|
||||||
|
@ -302,7 +306,11 @@
|
||||||
"become_subscriber": "Be Hydra Cloud",
|
"become_subscriber": "Be Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||||
"bill_sent_until": "Your next bill will be sent until this day"
|
"bill_sent_until": "Your next bill will be sent until this day",
|
||||||
|
"enable_torbox": "Enable Torbox",
|
||||||
|
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||||
|
"torbox_account_linked": "TorBox account linked",
|
||||||
|
"real_debrid_account_linked": "Real-Debrid account linked"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|
|
@ -175,7 +175,16 @@
|
||||||
"backup_from": "Copia de seguridad de {{date}}",
|
"backup_from": "Copia de seguridad de {{date}}",
|
||||||
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
||||||
"clear": "Limpiar",
|
"clear": "Limpiar",
|
||||||
"no_directory_selected": "No se seleccionó un directorio"
|
"no_directory_selected": "No se seleccionó un directorio",
|
||||||
|
"launch_options": "Opciones de Inicio",
|
||||||
|
"launch_options_description": "Los usuarios avanzados pueden introducir sus propias modificaciones de opciones de inicio (característica experimental)",
|
||||||
|
"launch_options_placeholder": "Sin parámetro específicado",
|
||||||
|
"no_write_permission": "No se puede descargar en este directorio. Presiona aquí para aprender más.",
|
||||||
|
"reset_achievements": "Reiniciar logros",
|
||||||
|
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
||||||
|
"reset_achievements_title": "¿Estás seguro?",
|
||||||
|
"reset_achievements_success": "Logros reiniciados exitosamente",
|
||||||
|
"reset_achievements_error": "Se produjo un error al reiniciar los logros"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
|
@ -227,13 +236,13 @@
|
||||||
"behavior": "Otros",
|
"behavior": "Otros",
|
||||||
"download_sources": "Fuentes de descarga",
|
"download_sources": "Fuentes de descarga",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"enable_real_debrid": "Activar Real-Debrid",
|
"enable_real_debrid": "Activar Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.",
|
"real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
"debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
||||||
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid",
|
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
|
"debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
|
||||||
"save_changes": "Guardar cambios",
|
"save_changes": "Guardar cambios",
|
||||||
"changes_saved": "Ajustes guardados exitosamente",
|
"changes_saved": "Ajustes guardados exitosamente",
|
||||||
"download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga",
|
"download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga",
|
||||||
|
@ -277,7 +286,23 @@
|
||||||
"launch_minimized": "Iniciar Hydra minimizado",
|
"launch_minimized": "Iniciar Hydra minimizado",
|
||||||
"disable_nsfw_alert": "Desactivar alerta NSFW",
|
"disable_nsfw_alert": "Desactivar alerta NSFW",
|
||||||
"seed_after_download_complete": "Realizar seeding después de que se completa la descarga",
|
"seed_after_download_complete": "Realizar seeding después de que se completa la descarga",
|
||||||
"show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos"
|
"show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos",
|
||||||
|
"account": "Cuenta",
|
||||||
|
"account_data_updated_successfully": "Datos de la cuenta actualizados",
|
||||||
|
"bill_sent_until": "Tú próxima factura se enviará el {{date}}",
|
||||||
|
"current_email": "Correo actual:",
|
||||||
|
"manage_subscription": "Gestionar suscripción",
|
||||||
|
"no_email_account": "No has configurado un correo aún",
|
||||||
|
"no_subscription": "Disfruta Hydra de la mejor manera",
|
||||||
|
"no_users_blocked": "No tienes usuarios bloqueados",
|
||||||
|
"notifications": "Notificaciones",
|
||||||
|
"renew_subscription": "Renovar Hydra Cloud",
|
||||||
|
"subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}",
|
||||||
|
"subscription_expired_at": "Tú suscripción expiró el {{date}}",
|
||||||
|
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
||||||
|
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
||||||
|
"update_email": "Actualizar correo",
|
||||||
|
"update_password": "Actualizar contraseña"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
|
|
|
@ -213,13 +213,13 @@
|
||||||
"behavior": "Käitumine",
|
"behavior": "Käitumine",
|
||||||
"download_sources": "Allalaadimise allikad",
|
"download_sources": "Allalaadimise allikad",
|
||||||
"language": "Keel",
|
"language": "Keel",
|
||||||
"real_debrid_api_token": "API Võti",
|
"api_token": "API Võti",
|
||||||
"enable_real_debrid": "Luba Real-Debrid",
|
"enable_real_debrid": "Luba Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid on piiranguteta allalaadija, mis võimaldab sul faile alla laadida koheselt ja sinu internetiühenduse parima kiirusega.",
|
"real_debrid_description": "Real-Debrid on piiranguteta allalaadija, mis võimaldab sul faile alla laadida koheselt ja sinu internetiühenduse parima kiirusega.",
|
||||||
"real_debrid_invalid_token": "Vigane API võti",
|
"debrid_invalid_token": "Vigane API võti",
|
||||||
"real_debrid_api_token_hint": "Sa saad oma API võtme <0>siit</0>",
|
"debrid_api_token_hint": "Sa saad oma API võtme <0>siit</0>",
|
||||||
"real_debrid_free_account_error": "Konto \"{{username}}\" on tasuta konto. Palun telli Real-Debrid",
|
"real_debrid_free_account_error": "Konto \"{{username}}\" on tasuta konto. Palun telli Real-Debrid",
|
||||||
"real_debrid_linked_message": "Konto \"{{username}}\" ühendatud",
|
"debrid_linked_message": "Konto \"{{username}}\" ühendatud",
|
||||||
"save_changes": "Salvesta muudatused",
|
"save_changes": "Salvesta muudatused",
|
||||||
"changes_saved": "Muudatused edukalt salvestatud",
|
"changes_saved": "Muudatused edukalt salvestatud",
|
||||||
"download_sources_description": "Hydra laeb allalaadimise lingid nendest allikatest. Allika URL peab olema otsene link .json failile, mis sisaldab allalaadimise linke.",
|
"download_sources_description": "Hydra laeb allalaadimise lingid nendest allikatest. Allika URL peab olema otsene link .json failile, mis sisaldab allalaadimise linke.",
|
||||||
|
|
|
@ -116,7 +116,7 @@
|
||||||
"removed_download_sources": "منابع دانلود حذف شد",
|
"removed_download_sources": "منابع دانلود حذف شد",
|
||||||
"button_delete_all_sources": "تمام منابع دانلود را حذف کنید",
|
"button_delete_all_sources": "تمام منابع دانلود را حذف کنید",
|
||||||
"enable_real_debrid": "فعالسازی Real-Debrid",
|
"enable_real_debrid": "فعالسازی Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
"debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
||||||
"save_changes": "ذخیره تغییرات"
|
"save_changes": "ذخیره تغییرات"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -161,13 +161,13 @@
|
||||||
"behavior": "Perilaku",
|
"behavior": "Perilaku",
|
||||||
"download_sources": "Sumber unduhan",
|
"download_sources": "Sumber unduhan",
|
||||||
"language": "Bahasa",
|
"language": "Bahasa",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"enable_real_debrid": "Aktifkan Real-Debrid",
|
"enable_real_debrid": "Aktifkan Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid adalah downloader tanpa batas yang memungkinkan kamu untuk mengunduh file dengan cepat dan pada kecepatan terbaik dari Internet kamu.",
|
"real_debrid_description": "Real-Debrid adalah downloader tanpa batas yang memungkinkan kamu untuk mengunduh file dengan cepat dan pada kecepatan terbaik dari Internet kamu.",
|
||||||
"real_debrid_invalid_token": "Token API tidak valid",
|
"debrid_invalid_token": "Token API tidak valid",
|
||||||
"real_debrid_api_token_hint": "Kamu bisa dapatkan token API di <0>sini</0>",
|
"debrid_api_token_hint": "Kamu bisa dapatkan token API di <0>sini</0>",
|
||||||
"real_debrid_free_account_error": "Akun \"{{username}}\" adalah akun gratis. Silakan berlangganan Real-Debrid",
|
"real_debrid_free_account_error": "Akun \"{{username}}\" adalah akun gratis. Silakan berlangganan Real-Debrid",
|
||||||
"real_debrid_linked_message": "Akun \"{{username}}\" terhubung",
|
"debrid_linked_message": "Akun \"{{username}}\" terhubung",
|
||||||
"save_changes": "Simpan perubahan",
|
"save_changes": "Simpan perubahan",
|
||||||
"changes_saved": "Perubahan disimpan berhasil",
|
"changes_saved": "Perubahan disimpan berhasil",
|
||||||
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
|
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
|
||||||
|
|
|
@ -124,7 +124,7 @@
|
||||||
"title_confirmation_delete_all_sources": "Elimina tutte le fonti di download",
|
"title_confirmation_delete_all_sources": "Elimina tutte le fonti di download",
|
||||||
"removed_download_sources": "Fonti di download rimosse",
|
"removed_download_sources": "Fonti di download rimosse",
|
||||||
"button_delete_all_sources": "Rimuovi tutte le fonti di download",
|
"button_delete_all_sources": "Rimuovi tutte le fonti di download",
|
||||||
"real_debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
"debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
||||||
"save_changes": "Salva modifiche"
|
"save_changes": "Salva modifiche"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -159,13 +159,13 @@
|
||||||
"behavior": "Мінез-құлық",
|
"behavior": "Мінез-құлық",
|
||||||
"download_sources": "Жүктеу көздері",
|
"download_sources": "Жүктеу көздері",
|
||||||
"language": "Тіл",
|
"language": "Тіл",
|
||||||
"real_debrid_api_token": "API Кілті",
|
"api_token": "API Кілті",
|
||||||
"enable_real_debrid": "Real-Debrid-ті қосу",
|
"enable_real_debrid": "Real-Debrid-ті қосу",
|
||||||
"real_debrid_description": "Real-Debrid - бұл шектеусіз жүктеуші, ол интернетте орналастырылған файлдарды тез жүктеуге немесе жеке желі арқылы кез келген блоктарды айналып өтіп, оларды бірден плеерге беруге мүмкіндік береді.",
|
"real_debrid_description": "Real-Debrid - бұл шектеусіз жүктеуші, ол интернетте орналастырылған файлдарды тез жүктеуге немесе жеке желі арқылы кез келген блоктарды айналып өтіп, оларды бірден плеерге беруге мүмкіндік береді.",
|
||||||
"real_debrid_invalid_token": "Қате API кілті",
|
"debrid_invalid_token": "Қате API кілті",
|
||||||
"real_debrid_api_token_hint": "API кілтін <0>осы жерден</0> алуға болады",
|
"debrid_api_token_hint": "API кілтін <0>осы жерден</0> алуға болады",
|
||||||
"real_debrid_free_account_error": "\"{{username}}\" аккаунты жазылымға ие емес. Real-Debrid жазылымын алыңыз",
|
"real_debrid_free_account_error": "\"{{username}}\" аккаунты жазылымға ие емес. Real-Debrid жазылымын алыңыз",
|
||||||
"real_debrid_linked_message": "\"{{username}}\" аккаунты байланған",
|
"debrid_linked_message": "\"{{username}}\" аккаунты байланған",
|
||||||
"save_changes": "Өзгерістерді сақтау",
|
"save_changes": "Өзгерістерді сақтау",
|
||||||
"changes_saved": "Өзгерістер сәтті сақталды",
|
"changes_saved": "Өзгерістер сәтті сақталды",
|
||||||
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
|
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
|
||||||
|
|
|
@ -116,7 +116,7 @@
|
||||||
"removed_download_sources": "제거된 글꼴",
|
"removed_download_sources": "제거된 글꼴",
|
||||||
"button_delete_all_sources": "모든 다운로드 소스 제거",
|
"button_delete_all_sources": "모든 다운로드 소스 제거",
|
||||||
"enable_real_debrid": "Real-Debrid 활성화",
|
"enable_real_debrid": "Real-Debrid 활성화",
|
||||||
"real_debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
"debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
||||||
"save_changes": "변경 사항 저장"
|
"save_changes": "변경 사항 저장"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -177,13 +177,13 @@
|
||||||
"behavior": "Oppførsel",
|
"behavior": "Oppførsel",
|
||||||
"download_sources": "Nedlastingskilder",
|
"download_sources": "Nedlastingskilder",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
"real_debrid_api_token": "API nøkkel",
|
"api_token": "API nøkkel",
|
||||||
"enable_real_debrid": "Slå på Real-Debrid",
|
"enable_real_debrid": "Slå på Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid er en ubegrenset nedlaster som gør det mulig for deg å laste ned filer med en gang og med den beste utnyttelsen av internethastigheten din.",
|
"real_debrid_description": "Real-Debrid er en ubegrenset nedlaster som gør det mulig for deg å laste ned filer med en gang og med den beste utnyttelsen av internethastigheten din.",
|
||||||
"real_debrid_invalid_token": "Ugyldig API nøkkel",
|
"debrid_invalid_token": "Ugyldig API nøkkel",
|
||||||
"real_debrid_api_token_hint": "Du kan få API nøkkelen din <0>her</0>",
|
"debrid_api_token_hint": "Du kan få API nøkkelen din <0>her</0>",
|
||||||
"real_debrid_free_account_error": "Brukeren \"{{username}}\" er en gratis bruker. Vennligst abboner på Real-Debrid",
|
"real_debrid_free_account_error": "Brukeren \"{{username}}\" er en gratis bruker. Vennligst abboner på Real-Debrid",
|
||||||
"real_debrid_linked_message": "Brukeren \"{{username}}\" er forbunnet",
|
"debrid_linked_message": "Brukeren \"{{username}}\" er forbunnet",
|
||||||
"save_changes": "Lagre endringer",
|
"save_changes": "Lagre endringer",
|
||||||
"changes_saved": "Lagring av endringer vellykket",
|
"changes_saved": "Lagring av endringer vellykket",
|
||||||
"download_sources_description": "Hydra vil hente nedlastingslenker fra disse kildene. Kilde URLen skal være en direkte lenke til en .json fil som inneholder nedlastingslenkene.",
|
"download_sources_description": "Hydra vil hente nedlastingslenker fra disse kildene. Kilde URLen skal være en direkte lenke til en .json fil som inneholder nedlastingslenkene.",
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
"removed_download_sources": "Downloadbronnen verwijderd",
|
"removed_download_sources": "Downloadbronnen verwijderd",
|
||||||
"button_delete_all_sources": "Verwijder alle downloadbronnen",
|
"button_delete_all_sources": "Verwijder alle downloadbronnen",
|
||||||
"enable_real_debrid": "Enable Real-Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
"debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
||||||
"save_changes": "Wijzigingen opslaan"
|
"save_changes": "Wijzigingen opslaan"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -124,7 +124,7 @@
|
||||||
"title_confirmation_delete_all_sources": "Usuń wszystkie źródła pobierania",
|
"title_confirmation_delete_all_sources": "Usuń wszystkie źródła pobierania",
|
||||||
"button_delete_all_sources": "Usuń wszystkie źródła pobierania",
|
"button_delete_all_sources": "Usuń wszystkie źródła pobierania",
|
||||||
"enable_real_debrid": "Włącz Real-Debrid",
|
"enable_real_debrid": "Włącz Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
"debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
||||||
"save_changes": "Zapisz zmiany"
|
"save_changes": "Zapisz zmiany"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -172,7 +172,12 @@
|
||||||
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
||||||
"reset_achievements_title": "Tem certeza?",
|
"reset_achievements_title": "Tem certeza?",
|
||||||
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
||||||
"reset_achievements_error": "Falha ao resetar conquistas"
|
"reset_achievements_error": "Falha ao resetar conquistas",
|
||||||
|
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais.",
|
||||||
|
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||||
|
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -224,13 +229,13 @@
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"download_sources": "Fontes de download",
|
"download_sources": "Fontes de download",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token de API",
|
"api_token": "Token de API",
|
||||||
"enable_real_debrid": "Habilitar Real-Debrid",
|
"enable_real_debrid": "Habilitar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
"debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
||||||
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite baixar arquivos instantaneamente e com a melhor velocidade da sua Internet.",
|
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite baixar arquivos instantaneamente e com a melhor velocidade da sua Internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid",
|
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
"debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
||||||
"save_changes": "Salvar mudanças",
|
"save_changes": "Salvar mudanças",
|
||||||
"changes_saved": "Ajustes salvos com sucesso",
|
"changes_saved": "Ajustes salvos com sucesso",
|
||||||
"download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
"download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
||||||
|
@ -290,7 +295,11 @@
|
||||||
"become_subscriber": "Seja Hydra Cloud",
|
"become_subscriber": "Seja Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
|
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
||||||
|
"enable_torbox": "Habilitar Torbox",
|
||||||
|
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||||
|
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||||
|
"real_debrid_account_linked": "Conta Real-Debrid associada"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
|
|
@ -205,13 +205,13 @@
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"download_sources": "Fontes de transferência",
|
"download_sources": "Fontes de transferência",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token de API",
|
"api_token": "Token de API",
|
||||||
"enable_real_debrid": "Ativar Real-Debrid",
|
"enable_real_debrid": "Ativar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Podes obter o teu token de API <0>aqui</0>",
|
"debrid_api_token_hint": "Podes obter o teu token de API <0>aqui</0>",
|
||||||
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite descarregar ficheiros instantaneamente e com a melhor velocidade da tua Internet.",
|
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite descarregar ficheiros instantaneamente e com a melhor velocidade da tua Internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreve o Real-Debrid",
|
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreve o Real-Debrid",
|
||||||
"real_debrid_linked_message": "Conta \"{{username}}\" associada",
|
"debrid_linked_message": "Conta \"{{username}}\" associada",
|
||||||
"save_changes": "Guardar alterações",
|
"save_changes": "Guardar alterações",
|
||||||
"changes_saved": "Alterações guardadas com sucesso",
|
"changes_saved": "Alterações guardadas com sucesso",
|
||||||
"download_sources_description": "O Hydra vai procurar links de download em todas as fontes ativadas. O URL da fonte deve ser um link direto para um ficheiro .json que contenha uma lista de links.",
|
"download_sources_description": "O Hydra vai procurar links de download em todas as fontes ativadas. O URL da fonte deve ser um link direto para um ficheiro .json que contenha uma lista de links.",
|
||||||
|
|
|
@ -124,7 +124,7 @@
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"behavior": "Comportament",
|
"behavior": "Comportament",
|
||||||
"language": "Limbă",
|
"language": "Limbă",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"cancel_button_confirmation_delete_all_sources": "Nu",
|
"cancel_button_confirmation_delete_all_sources": "Nu",
|
||||||
"confirm_button_confirmation_delete_all_sources": "Da, șterge totul",
|
"confirm_button_confirmation_delete_all_sources": "Da, șterge totul",
|
||||||
"description_confirmation_delete_all_sources": "Veți șterge toate sursele de descărcare",
|
"description_confirmation_delete_all_sources": "Veți șterge toate sursele de descărcare",
|
||||||
|
@ -133,10 +133,10 @@
|
||||||
"button_delete_all_sources": "Eliminați toate sursele de descărcare",
|
"button_delete_all_sources": "Eliminați toate sursele de descărcare",
|
||||||
"enable_real_debrid": "Activează Real-Debrid",
|
"enable_real_debrid": "Activează Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
|
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
|
||||||
"real_debrid_invalid_token": "Token API invalid",
|
"debrid_invalid_token": "Token API invalid",
|
||||||
"real_debrid_api_token_hint": "Poți obține token-ul tău API <0>aici</0>",
|
"debrid_api_token_hint": "Poți obține token-ul tău API <0>aici</0>",
|
||||||
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
||||||
"real_debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
"debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
||||||
"save_changes": "Salvează modificările",
|
"save_changes": "Salvează modificările",
|
||||||
"changes_saved": "Modificările au fost salvate cu succes"
|
"changes_saved": "Modificările au fost salvate cu succes"
|
||||||
},
|
},
|
||||||
|
|
|
@ -237,13 +237,13 @@
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Источники загрузки",
|
"download_sources": "Источники загрузки",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"real_debrid_api_token": "API Ключ",
|
"api_token": "API Ключ",
|
||||||
"enable_real_debrid": "Включить Real-Debrid",
|
"enable_real_debrid": "Включить Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid - это неограниченный загрузчик, который позволяет быстро скачивать файлы, размещенные в Интернете, или мгновенно передавать их в плеер через частную сеть, позволяющую обходить любые блокировки.",
|
"real_debrid_description": "Real-Debrid - это неограниченный загрузчик, который позволяет быстро скачивать файлы, размещенные в Интернете, или мгновенно передавать их в плеер через частную сеть, позволяющую обходить любые блокировки.",
|
||||||
"real_debrid_invalid_token": "Неверный API ключ",
|
"debrid_invalid_token": "Неверный API ключ",
|
||||||
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>",
|
"debrid_api_token_hint": "API ключ можно получить <0>здесь</0>",
|
||||||
"real_debrid_free_account_error": "Аккаунт \"{{username}}\" - не имеет подписки. Пожалуйста, оформите подписку на Real-Debrid",
|
"real_debrid_free_account_error": "Аккаунт \"{{username}}\" - не имеет подписки. Пожалуйста, оформите подписку на Real-Debrid",
|
||||||
"real_debrid_linked_message": "Привязан аккаунт \"{{username}}\"",
|
"debrid_linked_message": "Привязан аккаунт \"{{username}}\"",
|
||||||
"save_changes": "Сохранить изменения",
|
"save_changes": "Сохранить изменения",
|
||||||
"changes_saved": "Изменения успешно сохранены",
|
"changes_saved": "Изменения успешно сохранены",
|
||||||
"download_sources_description": "Hydra будет получать ссылки на загрузки из этих источников. URL должна содержать прямую ссылку на .json-файл с ссылками для загрузок.",
|
"download_sources_description": "Hydra будет получать ссылки на загрузки из этих источников. URL должна содержать прямую ссылку на .json-файл с ссылками для загрузок.",
|
||||||
|
|
|
@ -236,13 +236,13 @@
|
||||||
"behavior": "Davranış",
|
"behavior": "Davranış",
|
||||||
"download_sources": "İndirme kaynakları",
|
"download_sources": "İndirme kaynakları",
|
||||||
"language": "Dil",
|
"language": "Dil",
|
||||||
"real_debrid_api_token": "API Anahtarı",
|
"api_token": "API Anahtarı",
|
||||||
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
|
"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_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ı",
|
"debrid_invalid_token": "Geçersiz API anahtarı",
|
||||||
"real_debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
|
"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_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
|
||||||
"real_debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
"debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
||||||
"save_changes": "Değişiklikleri Kaydet",
|
"save_changes": "Değişiklikleri Kaydet",
|
||||||
"changes_saved": "Değişiklikler başarıyla kaydedildi",
|
"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.",
|
"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.",
|
||||||
|
|
|
@ -180,13 +180,13 @@
|
||||||
"title_confirmation_delete_all_sources": "Видалити всі джерела завантаження",
|
"title_confirmation_delete_all_sources": "Видалити всі джерела завантаження",
|
||||||
"removed_download_sources": "Джерела завантажень видалено",
|
"removed_download_sources": "Джерела завантажень видалено",
|
||||||
"button_delete_all_sources": "Видаліть усі джерела завантаження",
|
"button_delete_all_sources": "Видаліть усі джерела завантаження",
|
||||||
"real_debrid_api_token": "API-токен",
|
"api_token": "API-токен",
|
||||||
"real_debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
"debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
||||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||||
"real_debrid_description": "Real-Debrid — це необмежений завантажувач, який дозволяє швидко завантажувати файли, розміщені в Інтернеті, або миттєво передавати їх у плеєр через приватну мережу, що дозволяє обходити будь-які блокування.",
|
"real_debrid_description": "Real-Debrid — це необмежений завантажувач, який дозволяє швидко завантажувати файли, розміщені в Інтернеті, або миттєво передавати їх у плеєр через приватну мережу, що дозволяє обходити будь-які блокування.",
|
||||||
"real_debrid_free_account_error": "Акаунт \"{{username}}\" - не має наявної підписки. Будь ласка, оформіть підписку на Real-Debrid",
|
"real_debrid_free_account_error": "Акаунт \"{{username}}\" - не має наявної підписки. Будь ласка, оформіть підписку на Real-Debrid",
|
||||||
"real_debrid_invalid_token": "Невірний API-токен",
|
"debrid_invalid_token": "Невірний API-токен",
|
||||||
"real_debrid_linked_message": "Акаунт \"{{username}}\" привязаний",
|
"debrid_linked_message": "Акаунт \"{{username}}\" привязаний",
|
||||||
"remove_download_source": "Видалити",
|
"remove_download_source": "Видалити",
|
||||||
"removed_download_source": "Джерело завантажень було видалено",
|
"removed_download_source": "Джерело завантажень було видалено",
|
||||||
"save_changes": "Зберегти зміни",
|
"save_changes": "Зберегти зміни",
|
||||||
|
|
|
@ -213,13 +213,13 @@
|
||||||
"behavior": "行为",
|
"behavior": "行为",
|
||||||
"download_sources": "下载源",
|
"download_sources": "下载源",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"real_debrid_api_token": "API 令牌",
|
"api_token": "API 令牌",
|
||||||
"enable_real_debrid": "启用 Real-Debrid",
|
"enable_real_debrid": "启用 Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid 是一个无限制的下载器,允许您以最快的互联网速度即时下载文件。",
|
"real_debrid_description": "Real-Debrid 是一个无限制的下载器,允许您以最快的互联网速度即时下载文件。",
|
||||||
"real_debrid_invalid_token": "无效的 API 令牌",
|
"debrid_invalid_token": "无效的 API 令牌",
|
||||||
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
"debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
||||||
"real_debrid_free_account_error": "账户 \"{{username}}\" 是免费账户。请订阅 Real-Debrid",
|
"real_debrid_free_account_error": "账户 \"{{username}}\" 是免费账户。请订阅 Real-Debrid",
|
||||||
"real_debrid_linked_message": "账户 \"{{username}}\" 已链接",
|
"debrid_linked_message": "账户 \"{{username}}\" 已链接",
|
||||||
"save_changes": "保存更改",
|
"save_changes": "保存更改",
|
||||||
"changes_saved": "更改已成功保存",
|
"changes_saved": "更改已成功保存",
|
||||||
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
|
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
|
||||||
|
|
|
@ -7,13 +7,18 @@ export const defaultDownloadsPath = app.getPath("downloads");
|
||||||
|
|
||||||
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
|
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 databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
||||||
export const databasePath = path.join(
|
export const databasePath = path.join(
|
||||||
databaseDirectory,
|
databaseDirectory,
|
||||||
isStaging ? "hydra_test.db" : "hydra.db"
|
isStaging ? "hydra_test.db" : "hydra.db"
|
||||||
);
|
);
|
||||||
|
|
||||||
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
|
export const logsPath = path.join(app.getPath("userData"), "logs");
|
||||||
|
|
||||||
export const seedsPath = app.isPackaged
|
export const seedsPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, "seeds")
|
? 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,90 +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 })
|
|
||||||
launchOptions: 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,19 @@
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { userAuthRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
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 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;
|
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;
|
if (!payload) return null;
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,29 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity";
|
|
||||||
import { PythonRPC } from "@main/services/python-rpc";
|
|
||||||
|
|
||||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const databaseOperations = dataSource
|
const databaseOperations = db
|
||||||
.transaction(async (transactionalEntityManager) => {
|
.batch([
|
||||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({});
|
{
|
||||||
|
type: "del",
|
||||||
await transactionalEntityManager.getRepository(Game).delete({});
|
key: levelKeys.auth,
|
||||||
|
},
|
||||||
await transactionalEntityManager
|
{
|
||||||
.getRepository(UserAuth)
|
type: "del",
|
||||||
.delete({ id: 1 });
|
key: levelKeys.user,
|
||||||
|
},
|
||||||
await transactionalEntityManager
|
])
|
||||||
.getRepository(UserSubscription)
|
|
||||||
.delete({ id: 1 });
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
/* Removes all games being played */
|
/* Removes all games being played */
|
||||||
gamesPlaytime.clear();
|
gamesPlaytime.clear();
|
||||||
|
|
||||||
|
return Promise.all([gamesSublevel.clear(), downloadsSublevel.clear()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Cancels any ongoing downloads */
|
/* Cancels any ongoing downloads */
|
||||||
DownloadManager.cancelDownload();
|
DownloadManager.cancelDownload();
|
||||||
|
|
||||||
/* Disconnects libtorrent */
|
|
||||||
PythonRPC.kill();
|
|
||||||
|
|
||||||
HydraApi.handleSignOut();
|
HydraApi.handleSignOut();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
|
@ -1,47 +1,8 @@
|
||||||
import type { AppUpdaterEvent } from "@types";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import updater, { UpdateInfo } from "electron-updater";
|
import { UpdateManager } from "@main/services/update-manager";
|
||||||
import { WindowManager } from "@main/services";
|
|
||||||
import { app } from "electron";
|
|
||||||
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
|
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
|
||||||
|
|
||||||
const sendEvent = (event: AppUpdaterEvent) => {
|
|
||||||
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendEventsForDebug = false;
|
|
||||||
|
|
||||||
const isAutoInstallAvailable =
|
|
||||||
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
|
|
||||||
|
|
||||||
const mockValuesForDebug = () => {
|
|
||||||
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
|
||||||
sendEvent({ type: "update-downloaded" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const newVersionInfo = { version: "" };
|
|
||||||
|
|
||||||
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
autoUpdater
|
return UpdateManager.checkForUpdates();
|
||||||
.once("update-available", (info: UpdateInfo) => {
|
|
||||||
sendEvent({ type: "update-available", info });
|
|
||||||
newVersionInfo.version = info.version;
|
|
||||||
})
|
|
||||||
.once("update-downloaded", () => {
|
|
||||||
sendEvent({ type: "update-downloaded" });
|
|
||||||
publishNotificationUpdateReadyToInstall(newVersionInfo.version);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
|
||||||
autoUpdater.autoDownload = isAutoInstallAvailable;
|
|
||||||
autoUpdater.checkForUpdates();
|
|
||||||
} else if (sendEventsForDebug) {
|
|
||||||
mockValuesForDebug();
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAutoInstallAvailable;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("checkForUpdates", checkForUpdates);
|
registerEvent("checkForUpdates", checkForUpdates);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { gameShopCacheRepository } from "@main/repository";
|
import { getSteamAppDetails, logger } from "@main/services";
|
||||||
import { getSteamAppDetails } from "@main/services";
|
|
||||||
|
|
||||||
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
import type { ShopDetails, GameShop } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
|
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const getLocalizedSteamAppDetails = async (
|
const getLocalizedSteamAppDetails = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
|
@ -39,35 +39,27 @@ const getGameShopDetails = async (
|
||||||
language: string
|
language: string
|
||||||
): Promise<ShopDetails | null> => {
|
): Promise<ShopDetails | null> => {
|
||||||
if (shop === "steam") {
|
if (shop === "steam") {
|
||||||
const cachedData = await gameShopCacheRepository.findOne({
|
const cachedData = await gamesShopCacheSublevel.get(
|
||||||
where: { objectID: objectId, language },
|
levelKeys.gameShopCacheItem(shop, objectId, language)
|
||||||
});
|
);
|
||||||
|
|
||||||
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
gameShopCacheRepository.upsert(
|
gamesShopCacheSublevel
|
||||||
{
|
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
|
||||||
objectID: objectId,
|
.catch((err) => {
|
||||||
shop: "steam",
|
logger.error("Could not cache game details", err);
|
||||||
language,
|
});
|
||||||
serializedData: JSON.stringify(result),
|
|
||||||
},
|
|
||||||
["objectID"]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const cachedGame = cachedData?.serializedData
|
if (cachedData) {
|
||||||
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (cachedGame) {
|
|
||||||
return {
|
return {
|
||||||
...cachedGame,
|
...cachedData,
|
||||||
objectId,
|
objectId,
|
||||||
} as ShopDetails;
|
} as ShopDetails;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import type { TrendingGame } from "@types";
|
import type { TrendingGame } from "@types";
|
||||||
|
|
||||||
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const language = await db
|
||||||
where: { id: 1 },
|
.get<string, string>(levelKeys.language, {
|
||||||
});
|
valueEncoding: "utf-8",
|
||||||
|
})
|
||||||
const language = userPreferences?.language || "en";
|
.then((language) => language || "en");
|
||||||
|
|
||||||
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
||||||
"/games/trending",
|
"/games/trending",
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { Ludusavi } from "@main/services";
|
import { Ludusavi } from "@main/services";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const getGameBackupPreview = async (
|
const getGameBackupPreview = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: {
|
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ludusavi.getBackupPreview(shop, objectId, game?.winePrefixPath);
|
return Ludusavi.getBackupPreview(shop, objectId, game?.winePrefixPath);
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,7 @@ import os from "node:os";
|
||||||
import { backupsPath } from "@main/constants";
|
import { backupsPath } from "@main/constants";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { normalizePath } from "@main/helpers";
|
import { normalizePath } from "@main/helpers";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const bundleBackup = async (
|
const bundleBackup = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
@ -46,12 +46,7 @@ const uploadSaveGame = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
downloadOptionTitle: string | null
|
downloadOptionTitle: string | null
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: {
|
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bundleLocation = await bundleBackup(
|
const bundleLocation = await bundleBackup(
|
||||||
shop,
|
shop,
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const checkFolderWritePermission = async (
|
const checkFolderWritePermission = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
path: string
|
testPath: string
|
||||||
) =>
|
) => {
|
||||||
new Promise((resolve) => {
|
const testFilePath = path.join(testPath, ".hydra-write-test");
|
||||||
fs.access(path, fs.constants.W_OK, (err) => {
|
|
||||||
resolve(!err);
|
try {
|
||||||
});
|
fs.writeFileSync(testFilePath, "");
|
||||||
});
|
fs.rmSync(testFilePath);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
||||||
|
|
|
@ -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,15 +1,16 @@
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { defaultDownloadsPath } from "@main/constants";
|
import { defaultDownloadsPath } from "@main/constants";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
export const getDownloadsPath = async () => {
|
export const getDownloadsPath = async () => {
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
where: {
|
levelKeys.userPreferences,
|
||||||
id: 1,
|
{
|
||||||
},
|
valueEncoding: "json",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (userPreferences && userPreferences.downloadsPath)
|
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||||
return userPreferences.downloadsPath;
|
|
||||||
|
|
||||||
return defaultDownloadsPath;
|
return defaultDownloadsPath;
|
||||||
};
|
};
|
||||||
|
|
7
src/main/events/helpers/parse-launch-options.ts
Normal file
7
src/main/events/helpers/parse-launch-options.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const parseLaunchOptions = (params?: string | null): string[] => {
|
||||||
|
if (!params) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.split(" ");
|
||||||
|
};
|
|
@ -46,6 +46,7 @@ import "./user-preferences/auto-launch";
|
||||||
import "./autoupdater/check-for-updates";
|
import "./autoupdater/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
import "./user-preferences/authenticate-real-debrid";
|
import "./user-preferences/authenticate-real-debrid";
|
||||||
|
import "./user-preferences/authenticate-torbox";
|
||||||
import "./download-sources/put-download-source";
|
import "./download-sources/put-download-source";
|
||||||
import "./auth/sign-out";
|
import "./auth/sign-out";
|
||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
|
|
|
@ -1,57 +1,55 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { Game, GameShop } from "@types";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements";
|
import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements";
|
||||||
|
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
title: string,
|
title: string
|
||||||
shop: GameShop
|
|
||||||
) => {
|
) => {
|
||||||
return gameRepository
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
.update(
|
const game = await gamesSublevel.get(gameKey);
|
||||||
{
|
|
||||||
objectID: objectId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
shop,
|
|
||||||
status: null,
|
|
||||||
isDeleted: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(async ({ affected }) => {
|
|
||||||
if (!affected) {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
if (game) {
|
||||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
await downloadsSublevel.del(gameKey);
|
||||||
: null;
|
|
||||||
|
|
||||||
await gameRepository.insert({
|
await gamesSublevel.put(gameKey, {
|
||||||
title,
|
...game,
|
||||||
iconUrl,
|
isDeleted: false,
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
|
||||||
where: { objectID: objectId },
|
|
||||||
});
|
|
||||||
|
|
||||||
updateLocalUnlockedAchivements(game!);
|
|
||||||
|
|
||||||
createGame(game!).catch(() => {});
|
|
||||||
});
|
});
|
||||||
|
} 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);
|
registerEvent("addGameToLibrary", addGameToLibrary);
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { logger } from "@main/services";
|
import { logger } from "@main/services";
|
||||||
import sudo from "sudo-prompt";
|
import sudo from "sudo-prompt";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { PythonRPC } from "@main/services/python-rpc";
|
import { PythonRPC } from "@main/services/python-rpc";
|
||||||
import { ProcessPayload } from "@main/services/download/types";
|
import { ProcessPayload } from "@main/services/download/types";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const getKillCommand = (pid: number) => {
|
const getKillCommand = (pid: number) => {
|
||||||
if (process.platform == "win32") {
|
if (process.platform == "win32") {
|
||||||
|
@ -16,15 +17,14 @@ const getKillCommand = (pid: number) => {
|
||||||
|
|
||||||
const closeGame = async (
|
const closeGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const processes =
|
const processes =
|
||||||
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
|
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
|
||||||
[];
|
[];
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: { id: gameId, isDeleted: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { IsNull, Not } from "typeorm";
|
|
||||||
import createDesktopShortcut from "create-desktop-shortcuts";
|
import createDesktopShortcut from "create-desktop-shortcuts";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { removeSymbolsFromName } from "@shared";
|
import { removeSymbolsFromName } from "@shared";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const createGameShortcut = async (
|
const createGameShortcut = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
id: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const game = await gameRepository.findOne({
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
where: { id, executablePath: Not(IsNull()) },
|
const game = await gamesSublevel.get(gameKey);
|
||||||
});
|
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
const filePath = game.executablePath;
|
const filePath = game.executablePath;
|
||||||
|
|
|
@ -1,37 +1,27 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||||
import { logger } from "@main/services";
|
import { logger } from "@main/services";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const deleteGameFolder = async (
|
const deleteGameFolder = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const game = await gameRepository.findOne({
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
where: [
|
|
||||||
{
|
|
||||||
id: gameId,
|
|
||||||
isDeleted: false,
|
|
||||||
status: "removed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: gameId,
|
|
||||||
progress: 1,
|
|
||||||
isDeleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
|
|
||||||
if (game.folderName) {
|
if (!download) return;
|
||||||
|
|
||||||
|
if (download.folderName) {
|
||||||
const folderPath = path.join(
|
const folderPath = path.join(
|
||||||
game.downloadPath ?? (await getDownloadsPath()),
|
download.downloadPath ?? (await getDownloadsPath()),
|
||||||
game.folderName
|
download.folderName
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fs.existsSync(folderPath)) {
|
if (fs.existsSync(folderPath)) {
|
||||||
|
@ -52,10 +42,7 @@ const deleteGameFolder = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await gameRepository.update(
|
await downloadsSublevel.del(downloadKey);
|
||||||
{ id: gameId },
|
|
||||||
{ downloadPath: null, folderName: null, status: null, progress: 0 }
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("deleteGameFolder", deleteGameFolder);
|
registerEvent("deleteGameFolder", deleteGameFolder);
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const getGameByObjectId = async (
|
const getGameByObjectId = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
objectId: string
|
objectId: string
|
||||||
) =>
|
) => {
|
||||||
gameRepository.findOne({
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
where: {
|
const [game, download] = await Promise.all([
|
||||||
objectID: objectId,
|
gamesSublevel.get(gameKey),
|
||||||
isDeleted: false,
|
downloadsSublevel.get(gameKey),
|
||||||
},
|
]);
|
||||||
});
|
|
||||||
|
if (!game || game.isDeleted) return null;
|
||||||
|
|
||||||
|
return { id: gameKey, ...game, download };
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("getGameByObjectId", getGameByObjectId);
|
registerEvent("getGameByObjectId", getGameByObjectId);
|
||||||
|
|
|
@ -1,17 +1,26 @@
|
||||||
import { gameRepository } from "@main/repository";
|
import type { LibraryGame } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { downloadsSublevel, gamesSublevel } from "@main/level";
|
||||||
|
|
||||||
const getLibrary = async () =>
|
const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||||
gameRepository.find({
|
return gamesSublevel
|
||||||
where: {
|
.iterator()
|
||||||
isDeleted: false,
|
.all()
|
||||||
},
|
.then((results) => {
|
||||||
relations: {
|
return Promise.all(
|
||||||
downloadQueue: true,
|
results
|
||||||
},
|
.filter(([_key, game]) => game.isDeleted === false)
|
||||||
order: {
|
.map(async ([key, game]) => {
|
||||||
createdAt: "desc",
|
const download = await downloadsSublevel.get(key);
|
||||||
},
|
|
||||||
});
|
return {
|
||||||
|
id: key,
|
||||||
|
...game,
|
||||||
|
download: download ?? null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("getLibrary", getLibrary);
|
registerEvent("getLibrary", getLibrary);
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const openGameExecutablePath = async (
|
const openGameExecutablePath = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: { id: gameId, isDeleted: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game || !game.executablePath) return;
|
if (!game || !game.executablePath) return;
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const openGameInstallerPath = async (
|
const openGameInstallerPath = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: { id: gameId, isDeleted: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game || !game.folderName || !game.downloadPath) return true;
|
if (!download || !download.folderName || !download.downloadPath) return true;
|
||||||
|
|
||||||
const gamePath = path.join(
|
const gamePath = path.join(
|
||||||
game.downloadPath ?? (await getDownloadsPath()),
|
download.downloadPath ?? (await getDownloadsPath()),
|
||||||
game.folderName!
|
download.folderName!
|
||||||
);
|
);
|
||||||
|
|
||||||
shell.showItemInFolder(gamePath);
|
shell.showItemInFolder(gamePath);
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { writeFile } from "node:fs/promises";
|
|
||||||
import { spawnSync, exec } from "node:child_process";
|
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 { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const executeGameInstaller = (filePath: string) => {
|
const executeGameInstaller = (filePath: string) => {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
|
@ -26,21 +24,21 @@ const executeGameInstaller = (filePath: string) => {
|
||||||
|
|
||||||
const openGameInstaller = async (
|
const openGameInstaller = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
where: { id: gameId, isDeleted: false },
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
});
|
|
||||||
|
|
||||||
if (!game || !game.folderName) return true;
|
if (!download?.folderName) return true;
|
||||||
|
|
||||||
const gamePath = path.join(
|
const gamePath = path.join(
|
||||||
game.downloadPath ?? (await getDownloadsPath()),
|
download.downloadPath ?? (await getDownloadsPath()),
|
||||||
game.folderName!
|
download.folderName
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(gamePath)) {
|
if (!fs.existsSync(gamePath)) {
|
||||||
await gameRepository.update({ id: gameId }, { status: null });
|
await downloadsSublevel.del(downloadKey);
|
||||||
return true;
|
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);
|
shell.openPath(gamePath);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,39 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
|
import { spawn } from "child_process";
|
||||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { parseLaunchOptions } from "../helpers/parse-launch-options";
|
||||||
|
|
||||||
const openGame = async (
|
const openGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number,
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
executablePath: string,
|
executablePath: string,
|
||||||
launchOptions: string | null
|
launchOptions?: string | null
|
||||||
) => {
|
) => {
|
||||||
// TODO: revisit this for launchOptions
|
|
||||||
const parsedPath = parseExecutablePath(executablePath);
|
const parsedPath = parseExecutablePath(executablePath);
|
||||||
|
const parsedParams = parseLaunchOptions(launchOptions);
|
||||||
|
|
||||||
await gameRepository.update(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
{ id: gameId },
|
|
||||||
{ executablePath: parsedPath, launchOptions }
|
|
||||||
);
|
|
||||||
|
|
||||||
shell.openPath(parsedPath);
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
executablePath: parsedPath,
|
||||||
|
launchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedParams.length === 0) {
|
||||||
|
shell.openPath(parsedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(parsedPath, parsedParams, { shell: false, detached: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("openGame", openGame);
|
registerEvent("openGame", openGame);
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
import { HydraApi } from "@main/services";
|
||||||
import { HydraApi, logger } from "@main/services";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const removeGameFromLibrary = async (
|
const removeGameFromLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
gameRepository.update(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
{ id: gameId },
|
const game = await gamesSublevel.get(gameKey);
|
||||||
{ isDeleted: true, executablePath: null }
|
|
||||||
);
|
|
||||||
|
|
||||||
removeRemoveGameFromLibrary(gameId).catch((err) => {
|
if (game) {
|
||||||
logger.error("removeRemoveGameFromLibrary", err);
|
await gamesSublevel.put(gameKey, {
|
||||||
});
|
...game,
|
||||||
};
|
isDeleted: true,
|
||||||
|
executablePath: null,
|
||||||
|
});
|
||||||
|
|
||||||
const removeRemoveGameFromLibrary = async (gameId: number) => {
|
if (game?.remoteId) {
|
||||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
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 { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
import { levelKeys, downloadsSublevel } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const removeGame = async (
|
const removeGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
await gameRepository.update(
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
{
|
await downloadsSublevel.del(downloadKey);
|
||||||
id: gameId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: "removed",
|
|
||||||
downloadPath: null,
|
|
||||||
bytesDownloaded: 0,
|
|
||||||
progress: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("removeGame", removeGame);
|
registerEvent("removeGame", removeGame);
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
|
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
|
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
|
||||||
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
|
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
|
||||||
|
import {
|
||||||
|
gameAchievementsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
levelKeys,
|
||||||
|
} from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const resetGameAchievements = async (
|
const resetGameAchievements = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
|
@ -23,28 +29,34 @@ const resetGameAchievements = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await gameAchievementRepository.update(
|
const levelKey = levelKeys.game(game.shop, game.objectId);
|
||||||
{ objectId: game.objectID },
|
|
||||||
{
|
await gameAchievementsSublevel
|
||||||
unlockedAchievements: null,
|
.get(levelKey)
|
||||||
}
|
.then(async (gameAchievements) => {
|
||||||
);
|
if (gameAchievements) {
|
||||||
|
await gameAchievementsSublevel.put(levelKey, {
|
||||||
|
...gameAchievements,
|
||||||
|
unlockedAchievements: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
|
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
|
||||||
() =>
|
() =>
|
||||||
achievementsLogger.log(
|
achievementsLogger.log(
|
||||||
`Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}`
|
`Deleted achievements from ${game.remoteId} - ${game.objectId} - ${game.title}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const gameAchievements = await getUnlockedAchievements(
|
const gameAchievements = await getUnlockedAchievements(
|
||||||
game.objectID,
|
game.objectId,
|
||||||
game.shop,
|
game.shop,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
`on-update-achievements-${game.objectID}-${game.shop}`,
|
`on-update-achievements-${game.objectId}-${game.shop}`,
|
||||||
gameAchievements
|
gameAchievements
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { levelKeys, gamesSublevel } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const selectGameWinePrefix = async (
|
const selectGameWinePrefix = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
id: number,
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
winePrefixPath: string | null
|
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);
|
registerEvent("selectGameWinePrefix", selectGameWinePrefix);
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const updateExecutablePath = async (
|
const updateExecutablePath = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
id: number,
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) => {
|
) => {
|
||||||
const parsedPath = executablePath
|
const parsedPath = executablePath
|
||||||
? parseExecutablePath(executablePath)
|
? parseExecutablePath(executablePath)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return gameRepository.update(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
{
|
|
||||||
id,
|
const game = await gamesSublevel.get(gameKey);
|
||||||
},
|
if (!game) return;
|
||||||
{
|
|
||||||
executablePath: parsedPath,
|
await gamesSublevel.put(gameKey, {
|
||||||
}
|
...game,
|
||||||
);
|
executablePath: parsedPath,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateExecutablePath", updateExecutablePath);
|
registerEvent("updateExecutablePath", updateExecutablePath);
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const updateLaunchOptions = async (
|
const updateLaunchOptions = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
id: number,
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
launchOptions: string | null
|
launchOptions: string | null
|
||||||
) => {
|
) => {
|
||||||
return gameRepository.update(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
{
|
|
||||||
id,
|
const game = await gamesSublevel.get(gameKey);
|
||||||
},
|
|
||||||
{
|
if (game) {
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
|
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateLaunchOptions", updateLaunchOptions);
|
registerEvent("updateLaunchOptions", updateLaunchOptions);
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel } from "@main/level";
|
||||||
|
|
||||||
const verifyExecutablePathInUse = async (
|
const verifyExecutablePathInUse = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
executablePath: string
|
executablePath: string
|
||||||
) => {
|
) => {
|
||||||
return gameRepository.findOne({
|
for await (const game of gamesSublevel.values()) {
|
||||||
where: { executablePath },
|
if (game.executablePath === executablePath) {
|
||||||
});
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);
|
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { Crypto, HydraApi } from "@main/services";
|
||||||
import { HydraApi } from "@main/services";
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { Auth } from "@types";
|
||||||
|
|
||||||
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
|
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
if (!userAuth) {
|
if (!auth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentToken = await HydraApi.post("/auth/payment", {
|
const paymentToken = await HydraApi.post("/auth/payment", {
|
||||||
refreshToken: userAuth.refreshToken,
|
refreshToken: Crypto.decrypt(auth.refreshToken),
|
||||||
}).then((response) => response.accessToken);
|
}).then((response) => response.accessToken);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Notification } from "electron";
|
import { Notification } from "electron";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
const publishNewRepacksNotification = async (
|
const publishNewRepacksNotification = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -9,9 +10,12 @@ const publishNewRepacksNotification = async (
|
||||||
) => {
|
) => {
|
||||||
if (newRepacksCount < 1) return;
|
if (newRepacksCount < 1) return;
|
||||||
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
where: { id: 1 },
|
levelKeys.userPreferences,
|
||||||
});
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
||||||
new Notification({
|
new Notification({
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { omit } from "lodash-es";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { fileTypeFromFile } from "file-type";
|
import { fileTypeFromFile } from "file-type";
|
||||||
|
|
||||||
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
export const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
||||||
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,19 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { GameShop } from "@types";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const cancelGameDownload = async (
|
const cancelGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
await DownloadManager.cancelDownload(gameId);
|
|
||||||
|
|
||||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
await DownloadManager.cancelDownload(downloadKey);
|
||||||
game: { id: gameId },
|
|
||||||
});
|
|
||||||
|
|
||||||
await transactionalEntityManager.getRepository(Game).update(
|
await downloadsSublevel.del(downloadKey);
|
||||||
{
|
|
||||||
id: gameId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: "removed",
|
|
||||||
bytesDownloaded: 0,
|
|
||||||
progress: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { GameShop } from "@types";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const pauseGameDownload = async (
|
const pauseGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
await DownloadManager.pauseDownload();
|
|
||||||
|
|
||||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
const download = await downloadsSublevel.get(gameKey);
|
||||||
game: { id: gameId },
|
|
||||||
|
if (download) {
|
||||||
|
await DownloadManager.pauseDownload(gameKey);
|
||||||
|
|
||||||
|
await downloadsSublevel.put(gameKey, {
|
||||||
|
...download,
|
||||||
|
status: "paused",
|
||||||
|
queued: false,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
await transactionalEntityManager
|
|
||||||
.getRepository(Game)
|
|
||||||
.update({ id: gameId }, { status: "paused" });
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("pauseGameDownload", pauseGameDownload);
|
registerEvent("pauseGameDownload", pauseGameDownload);
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { gameRepository } from "@main/repository";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const pauseGameSeed = async (
|
const pauseGameSeed = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
await gameRepository.update(gameId, {
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
status: "complete",
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
|
|
||||||
|
if (!download) return;
|
||||||
|
|
||||||
|
await downloadsSublevel.put(downloadKey, {
|
||||||
|
...download,
|
||||||
shouldSeed: false,
|
shouldSeed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await DownloadManager.pauseSeeding(gameId);
|
await DownloadManager.pauseSeeding(downloadKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("pauseGameSeed", pauseGameSeed);
|
registerEvent("pauseGameSeed", pauseGameSeed);
|
||||||
|
|
|
@ -1,46 +1,37 @@
|
||||||
import { Not } from "typeorm";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
|
||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const resumeGameDownload = async (
|
const resumeGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
where: {
|
|
||||||
id: gameId,
|
|
||||||
isDeleted: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
const download = await downloadsSublevel.get(gameKey);
|
||||||
|
|
||||||
if (game.status === "paused") {
|
if (download?.status === "paused") {
|
||||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
await DownloadManager.pauseDownload();
|
||||||
await DownloadManager.pauseDownload();
|
|
||||||
|
|
||||||
await transactionalEntityManager
|
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||||
.getRepository(Game)
|
if (value.status === "active" && value.progress !== 1) {
|
||||||
.update({ status: "active", progress: Not(1) }, { status: "paused" });
|
await downloadsSublevel.put(key, {
|
||||||
|
...value,
|
||||||
|
status: "paused",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await DownloadManager.resumeDownload(game);
|
await DownloadManager.resumeDownload(download);
|
||||||
|
|
||||||
await transactionalEntityManager
|
await downloadsSublevel.put(gameKey, {
|
||||||
.getRepository(DownloadQueue)
|
...download,
|
||||||
.delete({ game: { id: gameId } });
|
status: "active",
|
||||||
|
timestamp: Date.now(),
|
||||||
await transactionalEntityManager
|
queued: true,
|
||||||
.getRepository(DownloadQueue)
|
|
||||||
.insert({ game: { id: gameId } });
|
|
||||||
|
|
||||||
await transactionalEntityManager
|
|
||||||
.getRepository(Game)
|
|
||||||
.update({ id: gameId }, { status: "active" });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,29 +1,23 @@
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { Downloader } from "@shared";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const resumeGameSeed = async (
|
const resumeGameSeed = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: {
|
|
||||||
id: gameId,
|
|
||||||
isDeleted: false,
|
|
||||||
downloader: Downloader.Torrent,
|
|
||||||
progress: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
if (!download) return;
|
||||||
|
|
||||||
await gameRepository.update(gameId, {
|
await downloadsSublevel.put(levelKeys.game(shop, objectId), {
|
||||||
status: "seeding",
|
...download,
|
||||||
shouldSeed: true,
|
shouldSeed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await DownloadManager.resumeSeeding(game);
|
await DownloadManager.resumeSeeding(download);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("resumeGameSeed", resumeGameSeed);
|
registerEvent("resumeGameSeed", resumeGameSeed);
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { Download, StartGameDownloadPayload } from "@types";
|
||||||
import { DownloadManager, HydraApi } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { Not } from "typeorm";
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { Downloader, DownloadError, steamUrlBuilder } from "@shared";
|
||||||
import { dataSource } from "@main/data-source";
|
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -15,85 +14,117 @@ const startGameDownload = async (
|
||||||
) => {
|
) => {
|
||||||
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
||||||
|
|
||||||
return dataSource.transaction(async (transactionalEntityManager) => {
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
const gameRepository = transactionalEntityManager.getRepository(Game);
|
|
||||||
const downloadQueueRepository =
|
|
||||||
transactionalEntityManager.getRepository(DownloadQueue);
|
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
await DownloadManager.pauseDownload();
|
||||||
where: {
|
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await DownloadManager.pauseDownload();
|
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||||
|
if (value.status === "active" && value.progress !== 1) {
|
||||||
await gameRepository.update(
|
await downloadsSublevel.put(key, {
|
||||||
{ status: "active", progress: Not(1) },
|
...value,
|
||||||
{ status: "paused" }
|
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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedGame = await gameRepository.findOne({
|
const game = await gamesSublevel.get(gameKey);
|
||||||
where: {
|
|
||||||
objectID: objectId,
|
/* 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);
|
const iconUrl = steamGame?.clientIcon
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||||
|
: null;
|
||||||
|
|
||||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
await gamesSublevel.put(gameKey, {
|
||||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
title,
|
||||||
|
iconUrl,
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
remoteId: null,
|
||||||
|
playTimeInMilliseconds: 0,
|
||||||
|
lastTimePlayed: null,
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
queued: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DownloadManager.startDownload(download).then(() => {
|
||||||
|
return downloadsSublevel.put(gameKey, download);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedGame = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
createGame(updatedGame!).catch(() => {}),
|
createGame(updatedGame!).catch(() => {}),
|
||||||
HydraApi.post(
|
HydraApi.post(
|
||||||
"/games/download",
|
"/games/download",
|
||||||
{
|
{
|
||||||
objectId: updatedGame!.objectID,
|
objectId,
|
||||||
shop: updatedGame!.shop,
|
shop,
|
||||||
},
|
},
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
).catch(() => {}),
|
).catch(() => {}),
|
||||||
]);
|
]);
|
||||||
});
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
logger.error("Failed to start download", err);
|
||||||
|
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
if (err.response?.status === 429 && downloader === Downloader.Gofile) {
|
||||||
|
return { ok: false, error: DownloadError.GofileQuotaExceeded };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
err.response?.status === 403 &&
|
||||||
|
downloader === Downloader.RealDebrid
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: DownloadError.RealDebridAccountNotAuthorized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloader === Downloader.TorBox) {
|
||||||
|
return { ok: false, error: err.response?.data?.detail };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return { ok: false, error: err.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("startGameDownload", startGameDownload);
|
registerEvent("startGameDownload", startGameDownload);
|
||||||
|
|
14
src/main/events/user-preferences/authenticate-torbox.ts
Normal file
14
src/main/events/user-preferences/authenticate-torbox.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { TorBoxClient } from "@main/services/download/torbox";
|
||||||
|
|
||||||
|
const authenticateTorBox = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
apiToken: string
|
||||||
|
) => {
|
||||||
|
TorBoxClient.authorize(apiToken);
|
||||||
|
|
||||||
|
const user = await TorBoxClient.getUser();
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("authenticateTorBox", authenticateTorBox);
|
|
@ -1,9 +1,27 @@
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import { Crypto } from "@main/services";
|
||||||
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
const getUserPreferences = async () =>
|
const getUserPreferences = async () =>
|
||||||
userPreferencesRepository.findOne({
|
db
|
||||||
where: { id: 1 },
|
.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
});
|
valueEncoding: "json",
|
||||||
|
})
|
||||||
|
.then((userPreferences) => {
|
||||||
|
if (userPreferences?.realDebridApiToken) {
|
||||||
|
userPreferences.realDebridApiToken = Crypto.decrypt(
|
||||||
|
userPreferences.realDebridApiToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userPreferences?.torBoxApiToken) {
|
||||||
|
userPreferences.torBoxApiToken = Crypto.decrypt(
|
||||||
|
userPreferences.torBoxApiToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userPreferences;
|
||||||
|
});
|
||||||
|
|
||||||
registerEvent("getUserPreferences", getUserPreferences);
|
registerEvent("getUserPreferences", getUserPreferences);
|
||||||
|
|
|
@ -1,23 +1,52 @@
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import { Crypto } from "@main/services";
|
||||||
|
import { patchUserProfile } from "../profile/update-profile";
|
||||||
|
|
||||||
const updateUserPreferences = async (
|
const updateUserPreferences = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
preferences: Partial<UserPreferences>
|
preferences: Partial<UserPreferences>
|
||||||
) => {
|
) => {
|
||||||
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{ valueEncoding: "json" }
|
||||||
|
);
|
||||||
|
|
||||||
if (preferences.language) {
|
if (preferences.language) {
|
||||||
|
await db.put<string, string>(levelKeys.language, preferences.language, {
|
||||||
|
valueEncoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
i18next.changeLanguage(preferences.language);
|
i18next.changeLanguage(preferences.language);
|
||||||
|
patchUserProfile({ language: preferences.language }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
return userPreferencesRepository.upsert(
|
if (preferences.realDebridApiToken) {
|
||||||
|
preferences.realDebridApiToken = Crypto.encrypt(
|
||||||
|
preferences.realDebridApiToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.torBoxApiToken) {
|
||||||
|
preferences.torBoxApiToken = Crypto.encrypt(preferences.torBoxApiToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preferences.downloadsPath) {
|
||||||
|
preferences.downloadsPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.put<string, UserPreferences>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
id: 1,
|
...userPreferences,
|
||||||
...preferences,
|
...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 { registerEvent } from "../register-event";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const getComparedUnlockedAchievements = async (
|
const getComparedUnlockedAchievements = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -9,9 +10,12 @@ const getComparedUnlockedAchievements = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
userId: string
|
userId: string
|
||||||
) => {
|
) => {
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
where: { id: 1 },
|
levelKeys.userPreferences,
|
||||||
});
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const showHiddenAchievementsDescription =
|
const showHiddenAchievementsDescription =
|
||||||
userPreferences?.showHiddenAchievementsDescription || false;
|
userPreferences?.showHiddenAchievementsDescription || false;
|
||||||
|
@ -21,7 +25,7 @@ const getComparedUnlockedAchievements = async (
|
||||||
{
|
{
|
||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
language: userPreferences?.language || "en",
|
language: userPreferences?.language ?? "en",
|
||||||
}
|
}
|
||||||
).then((achievements) => {
|
).then((achievements) => {
|
||||||
const sortedAchievements = achievements.achievements
|
const sortedAchievements = achievements.achievements
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import type { GameShop, UnlockedAchievement, UserAchievement } from "@types";
|
import type { GameShop, UserAchievement, UserPreferences } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import {
|
|
||||||
gameAchievementRepository,
|
|
||||||
userPreferencesRepository,
|
|
||||||
} from "@main/repository";
|
|
||||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||||
|
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
export const getUnlockedAchievements = async (
|
export const getUnlockedAchievements = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
useCachedData: boolean
|
useCachedData: boolean
|
||||||
): Promise<UserAchievement[]> => {
|
): Promise<UserAchievement[]> => {
|
||||||
const cachedAchievements = await gameAchievementRepository.findOne({
|
const cachedAchievements = await gameAchievementsSublevel.get(
|
||||||
where: { objectId, shop },
|
levelKeys.game(shop, objectId)
|
||||||
});
|
);
|
||||||
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
where: { id: 1 },
|
levelKeys.userPreferences,
|
||||||
});
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const showHiddenAchievementsDescription =
|
const showHiddenAchievementsDescription =
|
||||||
userPreferences?.showHiddenAchievementsDescription || false;
|
userPreferences?.showHiddenAchievementsDescription || false;
|
||||||
|
@ -25,12 +25,10 @@ export const getUnlockedAchievements = async (
|
||||||
const achievementsData = await getGameAchievementData(
|
const achievementsData = await getGameAchievementData(
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
useCachedData ? cachedAchievements : null
|
useCachedData
|
||||||
);
|
);
|
||||||
|
|
||||||
const unlockedAchievements = JSON.parse(
|
const unlockedAchievements = cachedAchievements?.unlockedAchievements ?? [];
|
||||||
cachedAchievements?.unlockedAchievements || "[]"
|
|
||||||
) as UnlockedAchievement[];
|
|
||||||
|
|
||||||
return achievementsData
|
return achievementsData
|
||||||
.map((achievementData) => {
|
.map((achievementData) => {
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { db } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
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 (
|
export const getUserFriends = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
): Promise<UserFriends> => {
|
): 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 });
|
return HydraApi.get(`/profile/friends`, { take, skip });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,13 @@ import updater from "electron-updater";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import url from "node:url";
|
import url from "node:url";
|
||||||
import fs from "node:fs";
|
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
import { logger, WindowManager } from "@main/services";
|
import { logger, WindowManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
|
||||||
import resources from "@locales";
|
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 { PythonRPC } from "./services/python-rpc";
|
||||||
import { Aria2 } from "./services/aria2";
|
import { Aria2 } from "./services/aria2";
|
||||||
|
import { db, levelKeys } from "./level";
|
||||||
|
import { loadState } from "./main";
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
const { autoUpdater } = updater;
|
||||||
|
|
||||||
|
@ -50,21 +47,6 @@ if (process.defaultApp) {
|
||||||
app.setAsDefaultProtocolClient(PROTOCOL);
|
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
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
|
@ -76,31 +58,19 @@ app.whenReady().then(async () => {
|
||||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
await runMigrations()
|
await loadState();
|
||||||
.then(() => {
|
|
||||||
logger.log("Migrations executed successfully");
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.log("Migrations failed to run:", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
await dataSource.initialize();
|
const language = await db.get<string, string>(levelKeys.language, {
|
||||||
|
valueEncoding: "utf-8",
|
||||||
await import("./main");
|
|
||||||
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userPreferences?.language) {
|
if (language) i18n.changeLanguage(language);
|
||||||
i18n.changeLanguage(userPreferences.language);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.argv.includes("--hidden")) {
|
if (!process.argv.includes("--hidden")) {
|
||||||
WindowManager.createMainWindow();
|
WindowManager.createMainWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
WindowManager.createSystemTray(language || "en");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
|
|
|
@ -1,53 +1,6 @@
|
||||||
import knex, { Knex } from "knex";
|
import knex from "knex";
|
||||||
import { databasePath } from "./constants";
|
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 { 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 ";
|
|
||||||
import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game";
|
|
||||||
|
|
||||||
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,
|
|
||||||
AddLaunchOptionsColumnToGame,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
getMigrationName(migration: HydraMigration): string {
|
|
||||||
return migration.name;
|
|
||||||
}
|
|
||||||
getMigration(migration: HydraMigration): Promise<Knex.Migration> {
|
|
||||||
return Promise.resolve(migration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const knexClient = knex({
|
export const knexClient = knex({
|
||||||
debug: !app.isPackaged,
|
debug: !app.isPackaged,
|
||||||
|
@ -56,7 +9,3 @@ export const knexClient = knex({
|
||||||
filename: databasePath,
|
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";
|
6
src/main/level/level.ts
Normal file
6
src/main/level/level.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { levelDatabasePath } from "@main/constants";
|
||||||
|
import { ClassicLevel } from "classic-level";
|
||||||
|
|
||||||
|
export const db = new ClassicLevel(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",
|
||||||
|
};
|
223
src/main/main.ts
223
src/main/main.ts
|
@ -1,24 +1,50 @@
|
||||||
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
|
|
||||||
import {
|
import {
|
||||||
downloadQueueRepository,
|
Crypto,
|
||||||
gameRepository,
|
DownloadManager,
|
||||||
userPreferencesRepository,
|
logger,
|
||||||
} from "./repository";
|
Ludusavi,
|
||||||
import { UserPreferences } from "./entity";
|
startMainLoop,
|
||||||
|
} from "./services";
|
||||||
import { RealDebridClient } from "./services/download/real-debrid";
|
import { RealDebridClient } from "./services/download/real-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
import { Aria2 } from "./services/aria2";
|
import { Aria2 } from "./services/aria2";
|
||||||
|
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||||
|
import { sortBy } from "lodash-es";
|
||||||
import { Downloader } from "@shared";
|
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";
|
||||||
|
import { TorBoxClient } from "./services/download/torbox";
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
export const loadState = async () => {
|
||||||
import("./events");
|
const userPreferences = await migrateFromSqlite().then(async () => {
|
||||||
|
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
|
return db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await import("./events");
|
||||||
|
|
||||||
Aria2.spawn();
|
Aria2.spawn();
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
RealDebridClient.authorize(
|
||||||
|
Crypto.decrypt(userPreferences.realDebridApiToken)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userPreferences?.torBoxApiToken) {
|
||||||
|
TorBoxClient.authorize(Crypto.decrypt(userPreferences.torBoxApiToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
@ -27,33 +53,162 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
});
|
});
|
||||||
|
|
||||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
const downloads = await downloadsSublevel
|
||||||
order: {
|
.values()
|
||||||
id: "DESC",
|
.all()
|
||||||
},
|
.then((games) => {
|
||||||
relations: {
|
return sortBy(
|
||||||
game: true,
|
games.filter((game) => game.queued),
|
||||||
},
|
"timestamp",
|
||||||
});
|
"DESC"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const seedList = await gameRepository.find({
|
const [nextItemOnQueue] = downloads;
|
||||||
where: {
|
|
||||||
shouldSeed: true,
|
|
||||||
downloader: Downloader.Torrent,
|
|
||||||
progress: 1,
|
|
||||||
uri: Not(IsNull()),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
startMainLoop();
|
||||||
};
|
};
|
||||||
|
|
||||||
userPreferencesRepository
|
const migrateFromSqlite = async () => {
|
||||||
.findOne({
|
const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
|
||||||
where: { id: 1 },
|
|
||||||
})
|
if (sqliteMigrationDone) {
|
||||||
.then((userPreferences) => {
|
return;
|
||||||
loadState(userPreferences);
|
}
|
||||||
});
|
|
||||||
|
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,
|
||||||
|
winePrefixPath: game.winePrefixPath,
|
||||||
|
launchOptions: game.launchOptions,
|
||||||
|
executablePath: game.executablePath,
|
||||||
|
isDeleted: game.isDeleted === 1,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info("Games migrated successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
const migrateUserPreferences = knexClient("user_preferences")
|
||||||
|
.select("*")
|
||||||
|
.then(async (userPreferences) => {
|
||||||
|
if (userPreferences.length > 0) {
|
||||||
|
const { realDebridApiToken, ...rest } = userPreferences[0];
|
||||||
|
|
||||||
|
await db.put<string, UserPreferences>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{
|
||||||
|
...rest,
|
||||||
|
realDebridApiToken: realDebridApiToken
|
||||||
|
? Crypto.encrypt(realDebridApiToken)
|
||||||
|
: null,
|
||||||
|
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||||
|
runAtStartup: rest.runAtStartup === 1,
|
||||||
|
startMinimized: rest.startMinimized === 1,
|
||||||
|
disableNsfwAlert: rest.disableNsfwAlert === 1,
|
||||||
|
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
|
||||||
|
showHiddenAchievementsDescription:
|
||||||
|
rest.showHiddenAchievementsDescription === 1,
|
||||||
|
downloadNotificationsEnabled:
|
||||||
|
rest.downloadNotificationsEnabled === 1,
|
||||||
|
repackUpdatesNotificationsEnabled:
|
||||||
|
rest.repackUpdatesNotificationsEnabled === 1,
|
||||||
|
achievementNotificationsEnabled:
|
||||||
|
rest.achievementNotificationsEnabled === 1,
|
||||||
|
},
|
||||||
|
{ valueEncoding: "json" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rest.language) {
|
||||||
|
await db.put(levelKeys.language, rest.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: Crypto.encrypt(users[0].accessToken),
|
||||||
|
refreshToken: Crypto.encrypt(users[0].refreshToken),
|
||||||
|
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info("User data migrated successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.allSettled([
|
||||||
|
migrateGames,
|
||||||
|
migrateUserPreferences,
|
||||||
|
migrateAchievements,
|
||||||
|
migrateUser,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
|
@ -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) => {},
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue