Compare commits

...

42 Commits

Author SHA1 Message Date
Zamitto
7293afb618 Merge branch 'release/v3.8.0'
Some checks failed
Build / build (ubuntu-latest) (push) Has been cancelled
Build / build (windows-2022) (push) Has been cancelled
2026-01-15 08:43:06 -03:00
Zamitto
194e7918ca feat: dont setup ww feedback widget if user has no token
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2026-01-15 08:42:33 -03:00
Zamitto
979958aca6 feat: update ww webRequest interceptor
Some checks failed
Build / build (ubuntu-latest) (push) Has been cancelled
Build / build (windows-2022) (push) Has been cancelled
2026-01-14 19:37:17 -03:00
Zamitto
6e92e0f79f fix: getLibrary throwing error
Some checks failed
Build / build (ubuntu-latest) (push) Has been cancelled
Build / build (windows-2022) (push) Has been cancelled
2026-01-14 00:37:22 -03:00
Zamitto
aef069d4c7 Merge branch 'release/v3.8.1' 2026-01-14 00:07:53 -03:00
Zamitto
1f447cc478 chore: add sentry var to build-renderer action
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2026-01-14 00:05:55 -03:00
Zamitto
5d2dc3616c Merge pull request #1938 from hydralauncher/release/v3.8.1
sync main
2026-01-13 23:43:48 -03:00
Zamitto
1f9972f74e Merge pull request #1937 from hydralauncher/chore/add-sentry
chore: add sentry
2026-01-13 23:43:16 -03:00
Zamitto
3344f68408 feat: add semver for sentry 2026-01-13 23:42:22 -03:00
Zamitto
65be11cc07 chore: add sentry 2026-01-13 23:34:09 -03:00
Chubby Granny Chaser
7e78a0f9f1 chore: update version to 3.8.1 and enhance translations
Some checks failed
Build / build (ubuntu-latest) (push) Has been cancelled
Build / build (windows-2022) (push) Has been cancelled
- Bumped version number in package.json to 3.8.1.
- Added new translation keys for notifications and loading states in Spanish, Portuguese, and Russian.
- Improved UI elements in download group with updated styles for buttons and layout adjustments.
2026-01-11 19:25:11 +00:00
Chubby Granny Chaser
d56cc8695b Merge pull request #1928 from hydralauncher/feat/LBX-367
feat: implement native HTTP downloader option
2026-01-11 18:43:47 +00:00
Moyasee
3f7b9e2a0b fix: merge conflict 2026-01-11 20:42:20 +02:00
Chubby Granny Chaser
3b3926156e Merge pull request #1919 from Stormm232/main
Hungarian Translation of 3.8.0
2026-01-11 18:32:10 +00:00
Chubby Granny Chaser
0089e08ba7 Merge branch 'main' into main 2026-01-11 18:32:04 +00:00
Chubby Granny Chaser
bced12d077 Merge pull request #1934 from hydralauncher/feat/adding-banner-removal
Feat/adding banner removal
2026-01-11 18:31:03 +00:00
Chubby Granny Chaser
f8ba72a0e2 refactor: clean up code formatting and improve readability in DownloadGroup and ProfileHero components
- Adjusted indentation and spacing for better code clarity in DownloadGroup.
- Removed unnecessary blank lines in ProfileHero to streamline the code structure.
- Ensured consistent formatting across both components.
2026-01-11 17:14:24 +00:00
Chubby Granny Chaser
2029f861f6 Merge branch 'main' of https://github.com/hydralauncher/hydra into feat/vikingfile-support 2026-01-11 17:14:07 +00:00
Chubby Granny Chaser
46e248c62a feat: add banner management features and translations
- Introduced new translations for banner actions including "Change banner", "Replace banner", "Remove banner", and confirmation prompts in English, Spanish, Portuguese, and Russian.
- Updated the UploadBackgroundImageButton component to support banner management with options to change, replace, or remove the banner.
- Implemented a confirmation modal for removing the banner.
- Enhanced user experience with animations for dropdown menus and button interactions.
- Removed deprecated Qiwi downloader support and added Rootz downloader integration.
2026-01-11 17:13:54 +00:00
Moyasee
dba8f9fb22 feat: add disabled hint for HTTP downloader setting during active downloads and update z-index for error message 2026-01-11 19:13:51 +02:00
Moyasee
b565ef7f00 Merge branch 'feat/LBX-367' of https://github.com/hydralauncher/hydra into feat/LBX-367 2026-01-11 17:54:05 +02:00
Moyasee
9298d9aa09 fix: enable native HTTP downloader in settings 2026-01-11 17:50:16 +02:00
Moyase
5b05fc2644 Merge branch 'main' into feat/LBX-367 2026-01-11 16:13:19 +02:00
Moyase
605d064ec0 Merge pull request #1924 from hydralauncher/fix/friends-box-display
feat: add empty state for friends box and new translation key
2026-01-11 16:13:07 +02:00
Moyase
c0956b1bc1 Merge branch 'main' into fix/friends-box-display 2026-01-11 16:06:20 +02:00
Moyasee
2e152d321e refactor: remove HttpMultiLinkDownloader and update download handling logic 2026-01-11 16:00:39 +02:00
Moyasee
a553b049ba Merge branch 'feat/LBX-367' of https://github.com/hydralauncher/hydra into feat/LBX-367 2026-01-11 15:37:13 +02:00
Moyasee
467b27baa3 refactor: remove unused JsMultiLinkDownloader and ensure aria2 spawning on startup 2026-01-11 15:36:16 +02:00
Moyase
4342a4a5d5 Merge branch 'main' into feat/LBX-367 2026-01-11 15:30:17 +02:00
Chubby Granny Chaser
d9d443ee6d Merge pull request #1932 from hydralauncher/dependabot/npm_and_yarn/npm_and_yarn-e9d8c310be
Some checks failed
Build / build (ubuntu-latest) (push) Has been cancelled
Build / build (windows-2022) (push) Has been cancelled
chore(deps): bump @smithy/config-resolver from 4.3.1 to 4.4.5 in the npm_and_yarn group across 1 directory
2026-01-11 02:38:10 +00:00
Chubby Granny Chaser
a912b57ccc Merge branch 'main' into dependabot/npm_and_yarn/npm_and_yarn-e9d8c310be 2026-01-11 02:20:44 +00:00
Moyase
447c146035 Merge pull request #1923 from hydralauncher/fix/archive-extraction
fix: archives with password doesn't extract properly
2026-01-11 04:12:01 +02:00
Moyase
39ff44f9d1 Merge branch 'main' into fix/archive-extraction 2026-01-11 04:09:00 +02:00
Chubby Granny Chaser
dbe101b7df Merge pull request #1927 from Sneezedip/main
Fix translation for hydra_cloud_feature_found (pt-PT)
2026-01-11 02:08:39 +00:00
dependabot[bot]
e7a62c16fa chore(deps): bump @smithy/config-resolver
Bumps the npm_and_yarn group with 1 update in the / directory: [@smithy/config-resolver](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/config-resolver).


Updates `@smithy/config-resolver` from 4.3.1 to 4.4.5
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/config-resolver/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/config-resolver@4.4.5/packages/config-resolver)

---
updated-dependencies:
- dependency-name: "@smithy/config-resolver"
  dependency-version: 4.4.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-08 23:29:01 +00:00
Sneezedip
f37ccbb4c0 Fix translation for hydra_cloud_feature_found 2026-01-06 12:33:14 -01:00
Moyasee
feb8d78e01 fix: update password index initialization in tryPassword function for correct behavior 2026-01-04 21:10:37 +02:00
Kiwo.2
44f39d94c4 Added new lines* 2026-01-04 14:07:34 +01:00
Kiwo.2
e618d313b3 Merge branch 'main' of https://github.com/Stormm232/hydra 2026-01-04 13:56:46 +01:00
Kiwo.2
7eec87c192 Added new lines 2026-01-04 13:56:32 +01:00
Zamitto
96140e614c Merge pull request #1917 from hydralauncher/fix/friends-box-display
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
hotfix: add empty state for friends box and new translation key
2026-01-04 02:59:53 -03:00
Moyasee
2ccc93ea61 feat: add empty state for friends box and new translation key 2026-01-04 04:23:59 +02:00
48 changed files with 936 additions and 649 deletions

View File

@@ -42,6 +42,7 @@ jobs:
run: yarn build
env:
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- name: Deploy to Cloudflare Pages
env:

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.8.0",
"version": "3.8.1",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -40,6 +40,7 @@
"@primer/octicons-react": "^19.9.0",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@reduxjs/toolkit": "^2.2.3",
"@sentry/react": "^10.33.0",
"@tiptap/extension-bold": "^3.6.2",
"@tiptap/extension-italic": "^3.6.2",
"@tiptap/extension-link": "^3.6.2",

View File

@@ -1,151 +0,0 @@
import aria2p
from aria2p.client import ClientException as DownloadNotFound
class HttpMultiLinkDownloader:
def __init__(self):
self.downloads = []
self.completed_downloads = []
self.total_size = None
self.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
def start_download(self, urls: list[str], save_path: str, header: str = None, out: str = None, total_size: int = None):
"""Add multiple URLs to download queue with same options"""
options = {"dir": save_path}
if header:
options["header"] = header
if out:
options["out"] = out
# Clear any existing downloads first
self.cancel_download()
self.completed_downloads = []
self.total_size = total_size
for url in urls:
try:
added_downloads = self.aria2.add(url, options=options)
self.downloads.extend(added_downloads)
except Exception as e:
print(f"Error adding download for URL {url}: {str(e)}")
def pause_download(self):
"""Pause all active downloads"""
if self.downloads:
try:
self.aria2.pause(self.downloads)
except Exception as e:
print(f"Error pausing downloads: {str(e)}")
def cancel_download(self):
"""Cancel and remove all downloads"""
if self.downloads:
try:
# First try to stop the downloads
self.aria2.remove(self.downloads)
except Exception as e:
print(f"Error removing downloads: {str(e)}")
finally:
# Clear the downloads list regardless of success/failure
self.downloads = []
self.completed_downloads = []
def get_download_status(self):
"""Get status for all tracked downloads, auto-remove completed/failed ones"""
if not self.downloads and not self.completed_downloads:
return []
total_completed = 0
current_download_speed = 0
active_downloads = []
to_remove = []
# First calculate sizes from completed downloads
for completed in self.completed_downloads:
total_completed += completed['size']
# Then check active downloads
for download in self.downloads:
try:
current_download = self.aria2.get_download(download.gid)
# Skip downloads that are not properly initialized
if not current_download or not current_download.files:
to_remove.append(download)
continue
# Add to completed size and speed calculations
total_completed += current_download.completed_length
current_download_speed += current_download.download_speed
# If download is complete, move it to completed_downloads
if current_download.status == 'complete':
self.completed_downloads.append({
'name': current_download.name,
'size': current_download.total_length
})
to_remove.append(download)
else:
active_downloads.append({
'name': current_download.name,
'size': current_download.total_length,
'completed': current_download.completed_length,
'speed': current_download.download_speed
})
except DownloadNotFound:
to_remove.append(download)
continue
except Exception as e:
print(f"Error getting download status: {str(e)}")
continue
# Clean up completed/removed downloads from active list
for download in to_remove:
try:
if download in self.downloads:
self.downloads.remove(download)
except ValueError:
pass
# Return aggregate status
if self.total_size or active_downloads or self.completed_downloads:
# Use the first active download's name as the folder name, or completed if none active
folder_name = None
if active_downloads:
folder_name = active_downloads[0]['name']
elif self.completed_downloads:
folder_name = self.completed_downloads[0]['name']
if folder_name and '/' in folder_name:
folder_name = folder_name.split('/')[0]
# Use provided total size if available, otherwise sum from downloads
total_size = self.total_size
if not total_size:
total_size = sum(d['size'] for d in active_downloads) + sum(d['size'] for d in self.completed_downloads)
# Calculate completion status based on total downloaded vs total size
is_complete = len(active_downloads) == 0 and total_completed >= (total_size * 0.99) # Allow 1% margin for size differences
# If all downloads are complete, clear the completed_downloads list to prevent status updates
if is_complete:
self.completed_downloads = []
return [{
'folderName': folder_name,
'fileSize': total_size,
'progress': total_completed / total_size if total_size > 0 else 0,
'downloadSpeed': current_download_speed,
'numPeers': 0,
'numSeeds': 0,
'status': 'complete' if is_complete else 'active',
'bytesDownloaded': total_completed,
}]
return []

View File

@@ -3,7 +3,6 @@ import sys, json, urllib.parse, psutil
from torrent_downloader import TorrentDownloader
from http_downloader import HttpDownloader
from profile_image_processor import ProfileImageProcessor
from http_multi_link_downloader import HttpMultiLinkDownloader
import libtorrent as lt
app = Flask(__name__)
@@ -25,15 +24,7 @@ if start_download_payload:
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
downloading_game_id = initial_download['game_id']
if isinstance(initial_download['url'], list):
# Handle multiple URLs using HttpMultiLinkDownloader
http_multi_downloader = HttpMultiLinkDownloader()
downloads[initial_download['game_id']] = http_multi_downloader
try:
http_multi_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
except Exception as e:
print("Error starting multi-link download", e)
elif initial_download['url'].startswith('magnet'):
if initial_download['url'].startswith('magnet'):
torrent_downloader = TorrentDownloader(torrent_session)
downloads[initial_download['game_id']] = torrent_downloader
try:
@@ -78,14 +69,6 @@ def status():
if not status:
return jsonify(None)
if isinstance(status, list):
if not status: # Empty list
return jsonify(None)
# For multi-link downloader, use the aggregated status
# The status will already be aggregated by the HttpMultiLinkDownloader
return jsonify(status[0]), 200
return jsonify(status), 200
@app.route("/seed-status", methods=["GET"])
@@ -104,21 +87,7 @@ def seed_status():
if not response:
continue
if isinstance(response, list):
# For multi-link downloader, check if all files are complete
if response and all(item['status'] == 'complete' for item in response):
seed_status.append({
'gameId': game_id,
'status': 'complete',
'folderName': response[0]['folderName'],
'fileSize': sum(item['fileSize'] for item in response),
'bytesDownloaded': sum(item['bytesDownloaded'] for item in response),
'downloadSpeed': 0,
'numPeers': 0,
'numSeeds': 0,
'progress': 1.0
})
elif response.get('status') == 5: # Original torrent seeding check
if response.get('status') == 5: # Torrent seeding check
seed_status.append({
'gameId': game_id,
**response,
@@ -180,15 +149,7 @@ def action():
existing_downloader = downloads.get(game_id)
if isinstance(url, list):
# Handle multiple URLs using HttpMultiLinkDownloader
if existing_downloader and isinstance(existing_downloader, HttpMultiLinkDownloader):
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
else:
http_multi_downloader = HttpMultiLinkDownloader()
downloads[game_id] = http_multi_downloader
http_multi_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
elif url.startswith('magnet'):
if url.startswith('magnet'):
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
existing_downloader.start_download(url, data['save_path'])
else:

View File

@@ -600,7 +600,8 @@
"autoplay_trailers_on_game_page": "Automatically start playing trailers on game page",
"hide_to_tray_on_game_start": "Hide Hydra to tray on game startup",
"downloads": "Downloads",
"use_native_http_downloader": "Use native HTTP downloader (experimental)"
"use_native_http_downloader": "Use native HTTP downloader (experimental)",
"cannot_change_downloader_while_downloading": "Cannot change this setting while a download is in progress"
},
"notifications": {
"download_complete": "Download complete",
@@ -695,6 +696,7 @@
"blocked_users": "Blocked users",
"unblock": "Unblock",
"no_friends_added": "You have no added friends",
"no_friends_yet": "You haven't added any friends yet",
"view_all": "View all",
"load_more": "Load more",
"loading": "Loading",
@@ -722,8 +724,15 @@
"profile_reported": "Profile reported",
"your_friend_code": "Your friend code:",
"copy_friend_code": "Copy friend code",
"copied": "Copied!",
"upload_banner": "Upload banner",
"uploading_banner": "Uploading banner…",
"change_banner": "Change banner",
"replace_banner": "Replace banner",
"remove_banner": "Remove banner",
"remove_banner_modal_title": "Remove banner?",
"remove_banner_confirmation": "Are you sure you want to remove your banner? You can always pick a new one when you want.",
"remove": "Remove",
"background_image_updated": "Background image updated",
"stats": "Stats",
"achievements": "achievements",
@@ -744,9 +753,7 @@
"user_reviews": "Reviews",
"delete_review": "Delete Review",
"loading_reviews": "Loading reviews...",
"wrapped_2025": "Wrapped 2025",
"view_my_wrapped_button": "View My Wrapped 2025",
"view_wrapped_button": "View {{displayName}}'s Wrapped 2025"
"wrapped_2025": "Wrapped 2025"
},
"library": {
"library": "Library",

View File

@@ -26,6 +26,7 @@
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
"sign_in": "Iniciar Sesión",
"friends": "Amigos",
"notifications": "Notificaciones",
"need_help": "¿Necesitás ayuda?",
"favorites": "Favoritos",
"playable_button_title": "Solo mostrar juegos que podés jugar en este momento",
@@ -115,6 +116,7 @@
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Restante {{eta}} - {{speed}}",
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Comprobando tiempo restante…",
"checking_files": "Revisando archivos de {{title}}… ({{percentage}} completado)",
"extracting": "Extrayendo {{title}}… ({{percentage}} completado)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Instalación completada",
"installation_complete_message": "Common redistributables instalados correctamente"
@@ -173,6 +175,7 @@
"repacks_modal_description": "Elegí el repack que querés descargar",
"select_folder_hint": "Si querés cambiar la carpeta por defecto, andá a <0>Ajustes</0>",
"download_now": "Descargar ahora",
"loading": "Cargando...",
"no_shop_details": "No se pudieron obtener detalles de la tienda.",
"download_options": "Opciones de descarga",
"download_path": "Ruta de descarga",
@@ -206,6 +209,7 @@
"danger_zone_section_description": "Remover este juego de tu librería o los archivos descargados por Hydra",
"download_in_progress": "Descarga en progreso",
"download_paused": "Descarga pausada",
"extracting": "Extrayendo",
"last_downloaded_option": "Última opción de descarga",
"new_download_option": "Nuevo",
"create_steam_shortcut": "Crear atajo de Steam",
@@ -400,6 +404,10 @@
"completed": "Completado",
"removed": "No descargado",
"cancel": "Cancelar",
"cancel_download": "¿Cancelar descarga?",
"cancel_download_description": "¿Estás seguro de que querés cancelar esta descarga? Todos los archivos descargados serán eliminados.",
"keep_downloading": "No, seguir descargando",
"yes_cancel": "Sí, cancelar",
"filter": "Filtrar juegos descargados",
"remove": "Remover",
"downloading_metadata": "Descargando metadatos…",
@@ -420,7 +428,13 @@
"resume_seeding": "Continuar sembrando",
"options": "Administrar",
"extract": "Extraer archivos",
"extracting": "Extrayendo archivos…"
"extracting": "Extrayendo archivos…",
"delete_archive_title": "¿Querés eliminar {{fileName}}?",
"delete_archive_description": "El archivo se extrajo exitosamente y ya no es necesario.",
"yes": "Sí",
"no": "No",
"network": "RED",
"peak": "PICO"
},
"settings": {
"downloads_path": "Ruta de descarga",
@@ -544,6 +558,7 @@
"show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo",
"extract_files_by_default": "Extraer archivos por defecto después de descargar",
"enable_steam_achievements": "Habilitar búsqueda de logros de Steam",
"enable_new_download_options_badges": "Mostrar badges de nuevas opciones de descarga",
"achievement_custom_notification_position": "Posición de notificación de logros",
"top-left": "Superior Izquierda",
"top-center": "Superior Centro",
@@ -570,20 +585,10 @@
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
"change_achievement_sound": "Cambiar sonido de logro",
"download_source_already_exists": "Esta fuente de descarga URL ya existe.",
"download_source_failed": "Error",
"download_source_matched": "Actualizado",
"download_source_matching": "Actualizando",
"download_source_no_information": "Sin información disponible",
"download_source_pending_matching": "Actualizando pronto",
"download_sources_synced_successfully": "Todas las fuentes de descarga están sincronizadas",
"failed_add_download_source": "Error al añadir la fuente de descarga. Por favor intentá de nuevo.",
"hydra_cloud": "Hydra Cloud",
"preview_sound": "Vista previa de sonido",
"remove_achievement_sound": "Eliminar sonido de logros",
"removed_all_download_sources": "Todas las fuentes de descarga eliminadas",
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego",
"downloads": "Descargas",
"use_native_http_downloader": "Usar descargador HTTP nativo (experimental)",
"cannot_change_downloader_while_downloading": "No se puede cambiar esta configuración mientras una descarga está en progreso"
},
"notifications": {
"download_complete": "Descarga completada",
@@ -675,6 +680,7 @@
"blocked_users": "Usuarios bloqueados",
"unblock": "Desbloquear",
"no_friends_added": "No tenés amistades añadidas",
"no_friends_yet": "Aún no has agregado ningún amigo",
"view_all": "Ver todo",
"load_more": "Cargar más",
"loading": "Cargando",
@@ -702,8 +708,15 @@
"profile_reported": "Perfil reportado",
"your_friend_code": "Tu código de amistad:",
"copy_friend_code": "Copiar código de amistad",
"copied": "¡Copiado!",
"upload_banner": "Subir banner",
"uploading_banner": "Subiendo banner…",
"change_banner": "Cambiar banner",
"replace_banner": "Reemplazar banner",
"remove_banner": "Eliminar banner",
"remove_banner_modal_title": "¿Eliminar banner?",
"remove_banner_confirmation": "¿Estás seguro de que querés eliminar tu banner? Siempre podés elegir uno nuevo cuando quieras.",
"remove": "Eliminar",
"background_image_updated": "Imagen de fondo actualizada",
"stats": "Estadísticas",
"achievements": "logros",
@@ -727,8 +740,6 @@
"user_reviews": "Reseñas",
"loading_reviews": "Cargando reseñas...",
"wrapped_2025": "Wrapped 2025",
"view_my_wrapped_button": "Ver Mi Wrapped 2025",
"view_wrapped_button": "Ver Wrapped 2025 de {{displayName}}",
"no_reviews": "Sin reseñas aún",
"delete_review": "Eliminar reseña"
},

View File

@@ -26,6 +26,7 @@
"game_has_no_executable": "A játékhoz nincs tallózva futtatható fájl",
"sign_in": "Bejelentkezés",
"friends": "Barátok",
"notifications": "Értesítések",
"need_help": "Elakadtál?",
"favorites": "Kedvenc Játékaim",
"playable_button_title": "Csak az azonnal játszható játékokat mutasd",
@@ -174,6 +175,7 @@
"repacks_modal_description": "Válaszd ki a repacket amit leszeretnél tölteni",
"select_folder_hint": "A letöltési mappát a <0>Beállításokban</0> változtathatod meg",
"download_now": "Letöltés",
"loading": "Töltés...",
"no_shop_details": "A bolt adatai nem érhetőek el.",
"download_options": "Letöltési opciók",
"download_path": "Letöltési hely",
@@ -182,7 +184,13 @@
"screenshot": "Screenshot {{number}}",
"open_screenshot": "{{number}} Screenshot megnyitása ",
"download_settings": "Letöltési beállítások",
"downloader": "Letöltési mód",
"downloader": "Letöltő",
"downloader_online": "Elérhető",
"downloader_not_configured": "Elérhető de nincs beállítva",
"downloader_offline": "A link nem elérhető",
"downloader_not_available": "Nem elérhető",
"recommended": "Ajánlott",
"go_to_settings": "Beállítások megnyitása",
"select_executable": "Tallózás",
"no_executable_selected": "Nincs futtatható fájl tallózva",
"open_folder": "Mappa megnyitása",
@@ -418,9 +426,11 @@
"extract": "Fájlok kibontása",
"extracting": "Fájlok kibontása…",
"delete_archive_title": "Szeretnéd törölni ezt a fájlt? {{fileName}}",
"delete_archive_description": "A tömörített fájl ki lett csomagolva, s többé nincs rá szükség. ",
"delete_archive_description": "A tömörített fájl ki lett csomagolva és többé nincs rá szükség.",
"yes": "Igen",
"no": "Nem"
"no": "Nem",
"network": "HÁLÓZAT",
"peak": "CSÚCS"
},
"settings": {
"downloads_path": "Letöltési útvonalak",
@@ -444,7 +454,7 @@
"debrid_linked_message": "Fiók összekapcsolva: \"{{username}}\" ",
"save_changes": "Változtatások mentése",
"changes_saved": "Változtatások sikeresen mentve",
"download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból. Az URL Forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
"download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból, ennek az URL Forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
"validate_download_source": "Érvényesítés",
"remove_download_source": "Eltávolítás",
"add_download_source": "Forrás hozáadása",
@@ -556,6 +566,7 @@
"show_download_speed_in_megabytes": "Letöltési sebesség megabájt/másodpercben lévő megjelenítése",
"extract_files_by_default": "Fájlok kicsomagolása letöltés után",
"enable_steam_achievements": "Steam-achievementek utáni keresés engedélyezése",
"enable_new_download_options_badges": "Új letöltési helyek",
"achievement_custom_notification_position": "Achievement-értesítések egyéni elhelyezése",
"top-left": "Bal felső sarok",
"top-center": "Felső közép",
@@ -636,9 +647,9 @@
"sort_by": "Rendezés:",
"achievements_earned": "Elért achievementek",
"played_recently": "Nemrég játszva",
"playtime": "Játszottidő",
"total_play_time": "Teljes játszottidő",
"manual_playtime_tooltip": "Ez a játszottidő manuálisan lett frissítve",
"playtime": "Játékidő",
"total_play_time": "Teljes játékidő",
"manual_playtime_tooltip": "Ez a játékidő manuálisan lett frissítve",
"no_recent_activity_title": "Hmmm… itt semmi sincs",
"no_recent_activity_description": "Mostanában nem játszottál semmivel. Hát ideje ezt megváltoztatni!",
"display_name": "Profilnév",
@@ -660,6 +671,7 @@
"sending": "Küldés..",
"friend_request_sent": "Barátfelkérés elküldve",
"friends": "Barátok",
"badges": "Kitűzők",
"friends_list": "Barát lista",
"user_not_found": "Felhasználó nem találva",
"block_user": "Felhasználó letiltása",
@@ -670,12 +682,16 @@
"ignore_request": "Kérés ignorálása",
"cancel_request": "Kérés visszavonása",
"undo_friendship": "Barát eltávolítása",
"friendship_removed": "Barát eltávolítva",
"request_accepted": "Barátfelkérés elfogadva",
"user_blocked_successfully": "Felhasználó sikeresen letiltva",
"user_block_modal_text": "Ez által letiltod őt: {{displayName}}",
"blocked_users": "Letiltott felhasználók",
"unblock": "Tiltás feloldása",
"no_friends_added": "Nincs bejelölt barátod",
"view_all": "Összes megtekintése",
"load_more": "Több betöltése",
"loading": "Töltés..",
"pending": "Függőben",
"no_pending_invites": "Nincs függőben lévő barátfelkérésed",
"no_blocked_users": "Nincs letiltott felhasználó",
@@ -699,6 +715,7 @@
"report_reason_other": "Egyéb",
"profile_reported": "Profil bejelentve",
"your_friend_code": "A barát kódod:",
"copy_friend_code": "Barátkód kimásolása",
"upload_banner": "Borítókép feltöltése",
"uploading_banner": "Borítókép feltöltése…",
"background_image_updated": "Borítókép frissítve",
@@ -720,7 +737,10 @@
"karma_count": "karma",
"user_reviews": "Vélemények",
"delete_review": "Vélemény Törlése",
"loading_reviews": "Vélemények betöltése..."
"loading_reviews": "Vélemények betöltése...",
"wrapped_2025": "Wrapped 2025",
"view_my_wrapped_button": "Wrapped 2025 megtekintése",
"view_wrapped_button": "{{displayName}} Wrapped 2025 megtekintése"
},
"library": {
"library": "Könyvtár",
@@ -738,7 +758,7 @@
"amount_minutes": "{{amount}} perc",
"amount_hours_short": "{{amount}}ó",
"amount_minutes_short": "{{amount}}p",
"manual_playtime_tooltip": "Ez a játszottidő manuálisan lett frissítve",
"manual_playtime_tooltip": "Ez a játékidő manuálisan lett frissítve",
"all_games": "Összes Játék",
"recently_played": "Nemrég Játszva",
"favorites": "Kedvencek"
@@ -771,5 +791,41 @@
"hydra_cloud_feature_found": "Épp felfedeztél egy Hydra Cloud funkciót!",
"learn_more": "Tudj meg többet",
"debrid_description": "Akár 4x gyorsabb letöltés a Nimbusszal"
},
"notifications_page": {
"title": "Értesítések",
"mark_all_as_read": "Megjelölés olvasottként",
"clear_all": "Összes Törlése",
"loading": "Töltés..",
"empty_title": "Nincsenek értesítések",
"empty_description": "Már mindet láttad! Nézz vissza később az újdonságokért.",
"empty_filter_description": "Nincs értesítés ami megfelel ennek a szűrőnek.",
"filter_all": "Összes",
"filter_unread": "Olvasatlan",
"filter_friends": "Barátok",
"filter_badges": "Kitűzők",
"filter_upvotes": "Felpontok",
"filter_local": "Helyi",
"load_more": "Több betöltése",
"dismiss": "Eltüntetés",
"accept": "Elfogad",
"refuse": "Elutasít",
"notification": "Értesítés",
"friend_request_received_title": "Új barátkérelem!",
"friend_request_received_description": "{{displayName}} a barátod szeretne lenni",
"friend_request_accepted_title": "Barátkérelem elfogadva!",
"friend_request_accepted_description": "{{displayName}} elfogadta a barátkérelmed",
"badge_received_title": "Kaptál egy új kitűzőt!",
"badge_received_description": "{{badgeName}}",
"review_upvote_title": "A véleményed a(z) {{gameTitle}} játékhoz felpont-ot kapott!",
"review_upvote_description": "A véleményed {{count}} új felpontot kapott",
"marked_all_as_read": "Összes értesítés olvasottnak jelölve",
"failed_to_mark_as_read": "Az értesítések olvasottnak jelölése nem sikerült",
"cleared_all": "Összes értesítés eltüntetve",
"failed_to_clear": "Az értesítések eltüntetése nem sikerült",
"failed_to_load": "Az értesítések betöltése nem sikerült",
"failed_to_dismiss": "Értesítés eltüntetése nem sikerült",
"friend_request_accepted": "Barátfelkérés elfogadva",
"friend_request_refused": "Barátfelkérés elutasítva"
}
}

View File

@@ -26,6 +26,7 @@
"game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login",
"friends": "Amigos",
"notifications": "Notificações",
"need_help": "Precisa de ajuda?",
"favorites": "Favoritos",
"playable_button_title": "Mostrar apenas jogos que você pode jogar agora",
@@ -163,6 +164,7 @@
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
"select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes</0>",
"download_now": "Iniciar download",
"loading": "Carregando...",
"no_shop_details": "Não foi possível obter os detalhes da loja.",
"download_options": "Opções de download",
"download_path": "Diretório de download",
@@ -368,6 +370,7 @@
"show_translation": "Mostrar tradução",
"show_original_translated_from": "Mostrar original (traduzido do {{language}})",
"hide_original": "Ocultar original",
"vote_failed": "Falha ao registrar seu voto. Por favor, tente novamente.",
"rating_count": "Avaliação",
"review_from_blocked_user": "Avaliação de usuário bloqueado",
"show": "Mostrar",
@@ -390,6 +393,10 @@
"completed": "Concluído",
"removed": "Cancelado",
"cancel": "Cancelar",
"cancel_download": "Cancelar download?",
"cancel_download_description": "Tem certeza de que deseja cancelar este download? Todos os arquivos baixados serão excluídos.",
"keep_downloading": "Não, continuar baixando",
"yes_cancel": "Sim, cancelar",
"filter": "Filtrar jogos baixados",
"remove": "Remover",
"downloading_metadata": "Baixando metadados…",
@@ -463,6 +470,7 @@
"download_sources_synced_successfully": "Fontes de download sincronizadas",
"removed_download_source": "Fonte removida",
"removed_download_sources": "Fontes removidas",
"removed_all_download_sources": "Todas as fontes de download removidas",
"cancel_button_confirmation_delete_all_sources": "Não",
"confirm_button_confirmation_delete_all_sources": "Sim, excluir tudo",
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
@@ -488,6 +496,7 @@
"blocked_users": "Usuários bloqueados",
"user_unblocked": "Usuário desbloqueado",
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
"hydra_cloud": "Hydra Cloud",
"launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
"seed_after_download_complete": "Semear após a conclusão do download",
@@ -550,6 +559,7 @@
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
"enable_steam_achievements": "Habilitar busca por conquistas da Steam",
"enable_new_download_options_badges": "Mostrar badges de novas opções de download",
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
"top-left": "Superior esquerdo",
"top-center": "Superior central",
@@ -567,6 +577,9 @@
"test_notification": "Testar notificação",
"achievement_sound_volume": "Volume do som de conquista",
"select_achievement_sound": "Selecionar som de conquista",
"change_achievement_sound": "Alterar som de conquista",
"remove_achievement_sound": "Remover som de conquista",
"preview_sound": "Reproduzir som",
"select": "Selecionar",
"preview": "Reproduzir",
"remove": "Remover",
@@ -574,7 +587,10 @@
"notification_preview": "Prévia da Notificação de Conquistas",
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
"autoplay_trailers_on_game_page": "Reproduzir trailers automaticamente na página do jogo",
"hide_to_tray_on_game_start": "Ocultar o Hydra na bandeja ao iniciar um jogo"
"hide_to_tray_on_game_start": "Ocultar o Hydra na bandeja ao iniciar um jogo",
"downloads": "Downloads",
"use_native_http_downloader": "Usar downloader HTTP nativo (experimental)",
"cannot_change_downloader_while_downloading": "Não é possível alterar esta configuração enquanto um download estiver em andamento"
},
"notifications": {
"download_complete": "Download concluído",
@@ -680,6 +696,7 @@
"blocked_users": "Usuários bloqueados",
"unblock": "Desbloquear",
"no_friends_added": "Você ainda não possui amigos adicionados",
"no_friends_yet": "Você ainda não adicionou nenhum amigo",
"view_all": "Ver todos",
"load_more": "Carregar mais",
"loading": "Carregando",
@@ -707,8 +724,15 @@
"profile_reported": "Perfil reportado",
"your_friend_code": "Seu código de amigo:",
"copy_friend_code": "Copiar código de amigo",
"copied": "Copiado!",
"upload_banner": "Carregar banner",
"uploading_banner": "Carregando banner…",
"change_banner": "Alterar banner",
"replace_banner": "Substituir banner",
"remove_banner": "Remover banner",
"remove_banner_modal_title": "Remover banner?",
"remove_banner_confirmation": "Tem certeza de que deseja remover seu banner? Você sempre pode escolher um novo quando quiser.",
"remove": "Remover",
"background_image_updated": "Imagem de fundo salva",
"stats": "Estatísticas",
"achievements": "conquistas",
@@ -736,8 +760,6 @@
"user_reviews": "Avaliações",
"loading_reviews": "Carregando avaliações...",
"wrapped_2025": "Wrapped 2025",
"view_my_wrapped_button": "Ver Meu Wrapped 2025",
"view_wrapped_button": "Ver Wrapped 2025 de {{displayName}}",
"no_reviews": "Ainda não há avaliações",
"delete_review": "Excluir avaliação"
},

View File

@@ -508,7 +508,7 @@
"show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores",
"animated_profile_banner": "Banner animado no perfil",
"cloud_saving": "Progresso dos jogos na nuvem",
"hydra_cloud_feature_found": "Descubriste uma funcionalidade Hydra Cloud!",
"hydra_cloud_feature_found": "Descobriste uma funcionalidade Hydra Cloud!",
"learn_more": "Saber mais"
}
}

View File

@@ -26,6 +26,7 @@
"game_has_no_executable": "Файл запуска игры не выбран",
"sign_in": "Войти",
"friends": "Друзья",
"notifications": "Уведомления",
"need_help": "Нужна помощь?",
"favorites": "Избранное",
"playable_button_title": "Показать только установленные игры.",
@@ -115,6 +116,7 @@
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}",
"calculating_eta": "Загрузка {{title}}… ({{percentage}} завершено) - Подсчёт оставшегося времени…",
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)",
"extracting": "Распаковка {{title}}… ({{percentage}} завершено)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Установка завершена",
"installation_complete_message": "Библиотеки успешно установлены"
@@ -173,6 +175,7 @@
"repacks_modal_description": "Выберите репак для загрузки",
"select_folder_hint": "Чтобы изменить папку загрузок по умолчанию, откройте <0>Настройки</0>",
"download_now": "Загрузить сейчас",
"loading": "Загрузка...",
"no_shop_details": "Не удалось получить описание",
"download_options": "Источники",
"download_path": "Путь для загрузок",
@@ -208,6 +211,7 @@
"danger_zone_section_description": "Вы можете удалить эту игру из вашей библиотеки или файлы скачанные из Hydra",
"download_in_progress": "Идёт загрузка",
"download_paused": "Загрузка приостановлена",
"extracting": "Распаковка",
"last_downloaded_option": "Последний вариант загрузки",
"new_download_option": "Новый",
"create_steam_shortcut": "Создать ярлык Steam",
@@ -400,6 +404,10 @@
"completed": "Завершено",
"removed": "Не скачано",
"cancel": "Отмена",
"cancel_download": "Отменить загрузку?",
"cancel_download_description": "Вы уверены, что хотите отменить эту загрузку? Все загруженные файлы будут удалены.",
"keep_downloading": "Нет, продолжить загрузку",
"yes_cancel": "Да, отменить",
"filter": "Поиск загруженных игр",
"remove": "Удалить",
"downloading_metadata": "Загрузка метаданных…",
@@ -420,7 +428,13 @@
"resume_seeding": "Продолжить раздачу",
"options": "Управлять",
"extract": "Распаковать файлы",
"extracting": "Распаковка файлов…"
"extracting": "Распаковка файлов…",
"delete_archive_title": "Хотите удалить {{fileName}}?",
"delete_archive_description": "Файл был успешно распакован и больше не нужен.",
"yes": "Да",
"no": "Нет",
"network": "СЕТЬ",
"peak": "ПИК"
},
"settings": {
"downloads_path": "Путь загрузок",
@@ -556,6 +570,7 @@
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
"enable_steam_achievements": "Включить поиск достижений Steam",
"enable_new_download_options_badges": "Показывать значки новых вариантов загрузки",
"achievement_custom_notification_position": "Позиция уведомлений достижений",
"top-left": "Верхний левый угол",
"top-center": "Верхний центр",
@@ -573,6 +588,9 @@
"test_notification": "Тестовое уведомление",
"achievement_sound_volume": "Громкость звука достижения",
"select_achievement_sound": "Выбрать звук достижения",
"change_achievement_sound": "Изменить звук достижения",
"remove_achievement_sound": "Удалить звук достижения",
"preview_sound": "Предпросмотр звука",
"select": "Выбрать",
"preview": "Предпросмотр",
"remove": "Удалить",
@@ -580,7 +598,10 @@
"notification_preview": "Предварительный просмотр уведомления о достижении",
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру",
"autoplay_trailers_on_game_page": "Автоматически начинать воспроизведение трейлеров на странице игры",
"hide_to_tray_on_game_start": "Скрывать Hydra в трей при запуске игры"
"hide_to_tray_on_game_start": "Скрывать Hydra в трей при запуске игры",
"downloads": "Загрузки",
"use_native_http_downloader": "Использовать встроенный HTTP-загрузчик (экспериментально)",
"cannot_change_downloader_while_downloading": "Нельзя изменить эту настройку во время загрузки"
},
"notifications": {
"download_complete": "Загрузка завершена",
@@ -675,6 +696,7 @@
"blocked_users": "Заблокированные пользователи",
"unblock": "Разблокировать",
"no_friends_added": "Вы ещё не добавили ни одного друга",
"no_friends_yet": "Вы ещё не добавили ни одного друга",
"view_all": "Показать все",
"load_more": "Загрузить еще",
"loading": "Загрузка",
@@ -702,8 +724,15 @@
"profile_reported": "Жалоба на профиль отправлена",
"your_friend_code": "Код вашего друга:",
"copy_friend_code": "Копировать код друга",
"copied": "Скопировано!",
"upload_banner": "Загрузить баннер",
"uploading_banner": "Загрузка баннера...",
"change_banner": "Изменить баннер",
"replace_banner": "Заменить баннер",
"remove_banner": "Удалить баннер",
"remove_banner_modal_title": "Удалить баннер?",
"remove_banner_confirmation": "Вы уверены, что хотите удалить свой баннер? Вы всегда можете выбрать новый, когда захотите.",
"remove": "Удалить",
"background_image_updated": "Фоновое изображение обновлено",
"stats": "Статистика",
"achievements": "Достижения",
@@ -724,8 +753,6 @@
"user_reviews": "Отзывы",
"loading_reviews": "Загрузка отзывов...",
"wrapped_2025": "Wrapped 2025",
"view_my_wrapped_button": "Просмотреть мой Wrapped 2025",
"view_wrapped_button": "Просмотреть Wrapped 2025 {{displayName}}",
"no_reviews": "Пока нет отзывов",
"delete_review": "Удалить отзыв"
},

View File

@@ -0,0 +1,59 @@
import path from "node:path";
import fs from "node:fs";
import { getDownloadsPath } from "../helpers/get-downloads-path";
import { registerEvent } from "../register-event";
import { downloadsSublevel, levelKeys } from "@main/level";
import { GameShop } from "@types";
const getGameInstallerActionType = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
): Promise<"install" | "open-folder"> => {
const downloadKey = levelKeys.game(shop, objectId);
const download = await downloadsSublevel.get(downloadKey);
if (!download?.folderName) return "open-folder";
const gamePath = path.join(
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
if (!fs.existsSync(gamePath)) {
await downloadsSublevel.del(downloadKey);
return "open-folder";
}
// macOS always opens folder
if (process.platform === "darwin") {
return "open-folder";
}
// If path is a file, it will show in folder (open-folder behavior)
if (fs.lstatSync(gamePath).isFile()) {
return "open-folder";
}
// Check for setup.exe
const setupPath = path.join(gamePath, "setup.exe");
if (fs.existsSync(setupPath)) {
return "install";
}
// Check if there's exactly one .exe file
const gamePathFileNames = fs.readdirSync(gamePath);
const gamePathExecutableFiles = gamePathFileNames.filter(
(fileName: string) => path.extname(fileName).toLowerCase() === ".exe"
);
if (gamePathExecutableFiles.length === 1) {
return "install";
}
// Otherwise, opens folder
return "open-folder";
};
registerEvent("getGameInstallerActionType", getGameInstallerActionType);

View File

@@ -25,7 +25,7 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
const achievements = await gameAchievementsSublevel.get(key);
unlockedAchievementCount =
achievements?.unlockedAchievements.length ?? 0;
achievements?.unlockedAchievements?.length ?? 0;
}
return {

View File

@@ -13,6 +13,7 @@ import "./delete-game-folder";
import "./extract-game-download";
import "./get-default-wine-prefix-selection-path";
import "./get-game-by-object-id";
import "./get-game-installer-action-type";
import "./get-library";
import "./open-game-executable-path";
import "./open-game-installer-path";

View File

@@ -51,22 +51,30 @@ const updateProfile = async (
"backgroundImageUrl",
]);
if (updateProfile.profileImageUrl) {
const profileImageUrl = await uploadImage(
"profile-image",
updateProfile.profileImageUrl
).catch(() => undefined);
if (updateProfile.profileImageUrl !== undefined) {
if (updateProfile.profileImageUrl === null) {
payload["profileImageUrl"] = null;
} else {
const profileImageUrl = await uploadImage(
"profile-image",
updateProfile.profileImageUrl
).catch(() => undefined);
payload["profileImageUrl"] = profileImageUrl;
payload["profileImageUrl"] = profileImageUrl;
}
}
if (updateProfile.backgroundImageUrl) {
const backgroundImageUrl = await uploadImage(
"background-image",
updateProfile.backgroundImageUrl
).catch(() => undefined);
if (updateProfile.backgroundImageUrl !== undefined) {
if (updateProfile.backgroundImageUrl === null) {
payload["backgroundImageUrl"] = null;
} else {
const backgroundImageUrl = await uploadImage(
"background-image",
updateProfile.backgroundImageUrl
).catch(() => undefined);
payload["backgroundImageUrl"] = backgroundImageUrl;
payload["backgroundImageUrl"] = backgroundImageUrl;
}
}
return patchUserProfile(payload);

View File

@@ -34,11 +34,7 @@ export const loadState = async () => {
await import("./events");
// Only spawn aria2 if user explicitly disabled native HTTP downloader
// Default is to use native HTTP downloader (aria2 is opt-in)
if (userPreferences?.useNativeHttpDownloader === false) {
Aria2.spawn();
}
Aria2.spawn();
if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences.realDebridApiToken);

View File

@@ -46,7 +46,7 @@ export class SevenZip {
onProgress?: (progress: ExtractionProgress) => void
): Promise<ExtractionResult> {
return new Promise((resolve, reject) => {
const tryPassword = (index = -1) => {
const tryPassword = (index = 0) => {
const password = passwords[index] ?? "";
logger.info(
`Trying password "${password || "(empty)"}" on ${filePath}`
@@ -115,7 +115,7 @@ export class SevenZip {
});
};
tryPassword();
tryPassword(0);
});
}

View File

@@ -4,10 +4,11 @@ import { publishDownloadCompleteNotification } from "../notifications";
import type { Download, DownloadProgress, UserPreferences } from "@types";
import {
GofileApi,
QiwiApi,
DatanodesApi,
MediafireApi,
PixelDrainApi,
VikingFileApi,
RootzApi,
} from "../hosters";
import { PythonRPC } from "../python-rpc";
import {
@@ -24,11 +25,7 @@ import { sortBy } from "lodash-es";
import { TorBoxClient } from "./torbox";
import { GameFilesManager } from "../game-files-manager";
import { HydraDebridClient } from "./hydra-debrid";
import {
BuzzheavierApi,
FuckingFastApi,
VikingFileApi,
} from "@main/services/hosters";
import { BuzzheavierApi, FuckingFastApi } from "@main/services/hosters";
import { JsHttpDownloader } from "./js-http-downloader";
export class DownloadManager {
@@ -546,8 +543,6 @@ export class DownloadManager {
return this.getGofileDownloadOptions(download, resumingFilename);
case Downloader.PixelDrain:
return this.getPixelDrainDownloadOptions(download, resumingFilename);
case Downloader.Qiwi:
return this.getQiwiDownloadOptions(download, resumingFilename);
case Downloader.Datanodes:
return this.getDatanodesDownloadOptions(download, resumingFilename);
case Downloader.Buzzheavier:
@@ -564,6 +559,8 @@ export class DownloadManager {
return this.getHydraDownloadOptions(download, resumingFilename);
case Downloader.VikingFile:
return this.getVikingFileDownloadOptions(download, resumingFilename);
case Downloader.Rootz:
return this.getRootzDownloadOptions(download, resumingFilename);
default:
return null;
}
@@ -608,23 +605,6 @@ export class DownloadManager {
);
}
private static async getQiwiDownloadOptions(
download: Download,
resumingFilename?: string
) {
const downloadUrl = await QiwiApi.getDownloadUrl(download.uri);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getDatanodesDownloadOptions(
download: Download,
resumingFilename?: string
@@ -768,6 +748,23 @@ export class DownloadManager {
);
}
private static async getRootzDownloadOptions(
download: Download,
resumingFilename?: string
) {
const downloadUrl = await RootzApi.getDownloadUrl(download.uri);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getDownloadPayload(download: Download) {
const downloadId = levelKeys.game(download.shop, download.objectId);
@@ -799,15 +796,6 @@ export class DownloadManager {
save_path: download.downloadPath,
};
}
case Downloader.Qiwi: {
const downloadUrl = await QiwiApi.getDownloadUrl(download.uri);
return {
action: "start",
game_id: downloadId,
url: downloadUrl,
save_path: download.downloadPath,
};
}
case Downloader.Datanodes: {
const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri);
return {
@@ -925,6 +913,15 @@ export class DownloadManager {
download.downloadPath
);
}
case Downloader.Rootz: {
const downloadUrl = await RootzApi.getDownloadUrl(download.uri);
return {
action: "start",
game_id: downloadId,
url: downloadUrl,
save_path: download.downloadPath,
};
}
default:
return undefined;
}

View File

@@ -2,4 +2,3 @@ export * from "./download-manager";
export * from "./real-debrid";
export * from "./torbox";
export * from "./js-http-downloader";
export * from "./js-multi-link-downloader";

View File

@@ -1,201 +0,0 @@
import { JsHttpDownloader, JsHttpDownloaderStatus } from "./js-http-downloader";
import { logger } from "../logger";
export interface JsMultiLinkDownloaderOptions {
urls: string[];
savePath: string;
headers?: Record<string, string>;
totalSize?: number;
}
interface CompletedDownload {
name: string;
size: number;
}
export class JsMultiLinkDownloader {
private downloader: JsHttpDownloader | null = null;
private currentOptions: JsMultiLinkDownloaderOptions | null = null;
private currentUrlIndex = 0;
private completedDownloads: CompletedDownload[] = [];
private totalSize: number | null = null;
private isDownloading = false;
private isPaused = false;
async startDownload(options: JsMultiLinkDownloaderOptions): Promise<void> {
this.currentOptions = options;
this.currentUrlIndex = 0;
this.completedDownloads = [];
this.totalSize = options.totalSize ?? null;
this.isDownloading = true;
this.isPaused = false;
await this.downloadNextUrl();
}
private async downloadNextUrl(): Promise<void> {
if (!this.currentOptions || this.isPaused) {
return;
}
const { urls, savePath, headers } = this.currentOptions;
if (this.currentUrlIndex >= urls.length) {
logger.log("[JsMultiLinkDownloader] All downloads complete");
this.isDownloading = false;
return;
}
const url = urls[this.currentUrlIndex];
logger.log(
`[JsMultiLinkDownloader] Starting download ${this.currentUrlIndex + 1}/${urls.length}`
);
this.downloader = new JsHttpDownloader();
try {
await this.downloader.startDownload({
url,
savePath,
headers,
});
const status = this.downloader.getDownloadStatus();
if (status?.status === "complete") {
this.completedDownloads.push({
name: status.folderName,
size: status.fileSize,
});
}
this.currentUrlIndex++;
this.downloader = null;
if (!this.isPaused) {
await this.downloadNextUrl();
}
} catch (err) {
logger.error("[JsMultiLinkDownloader] Download error:", err);
throw err;
}
}
pauseDownload(): void {
logger.log("[JsMultiLinkDownloader] Pausing download");
this.isPaused = true;
if (this.downloader) {
this.downloader.pauseDownload();
}
}
async resumeDownload(): Promise<void> {
if (!this.currentOptions) {
throw new Error("No download options available for resume");
}
logger.log("[JsMultiLinkDownloader] Resuming download");
this.isPaused = false;
this.isDownloading = true;
if (this.downloader) {
await this.downloader.startDownload({
url: this.currentOptions.urls[this.currentUrlIndex],
savePath: this.currentOptions.savePath,
headers: this.currentOptions.headers,
});
const status = this.downloader.getDownloadStatus();
if (status?.status === "complete") {
this.completedDownloads.push({
name: status.folderName,
size: status.fileSize,
});
this.currentUrlIndex++;
this.downloader = null;
await this.downloadNextUrl();
}
} else {
await this.downloadNextUrl();
}
}
cancelDownload(): void {
logger.log("[JsMultiLinkDownloader] Cancelling download");
this.isPaused = true;
this.isDownloading = false;
if (this.downloader) {
this.downloader.cancelDownload();
this.downloader = null;
}
this.reset();
}
getDownloadStatus(): JsHttpDownloaderStatus | null {
if (!this.currentOptions && this.completedDownloads.length === 0) {
return null;
}
let totalBytesDownloaded = 0;
let currentDownloadSpeed = 0;
let currentFolderName = "";
let currentStatus: "active" | "paused" | "complete" | "error" = "active";
for (const completed of this.completedDownloads) {
totalBytesDownloaded += completed.size;
}
if (this.downloader) {
const status = this.downloader.getDownloadStatus();
if (status) {
totalBytesDownloaded += status.bytesDownloaded;
currentDownloadSpeed = status.downloadSpeed;
currentFolderName = status.folderName;
currentStatus = status.status;
}
} else if (this.completedDownloads.length > 0) {
currentFolderName = this.completedDownloads[0].name;
}
if (currentFolderName?.includes("/")) {
currentFolderName = currentFolderName.split("/")[0];
}
const totalFileSize =
this.totalSize ||
this.completedDownloads.reduce((sum, d) => sum + d.size, 0) +
(this.downloader?.getDownloadStatus()?.fileSize || 0);
const allComplete =
!this.isDownloading &&
this.currentOptions &&
this.currentUrlIndex >= this.currentOptions.urls.length;
if (allComplete) {
currentStatus = "complete";
} else if (this.isPaused) {
currentStatus = "paused";
}
return {
folderName: currentFolderName,
fileSize: totalFileSize,
progress: totalFileSize > 0 ? totalBytesDownloaded / totalFileSize : 0,
downloadSpeed: currentDownloadSpeed,
numPeers: 0,
numSeeds: 0,
status: currentStatus,
bytesDownloaded: totalBytesDownloaded,
};
}
private reset(): void {
this.currentOptions = null;
this.currentUrlIndex = 0;
this.completedDownloads = [];
this.totalSize = null;
this.isDownloading = false;
this.isPaused = false;
}
}

View File

@@ -1,8 +1,8 @@
export * from "./gofile";
export * from "./qiwi";
export * from "./datanodes";
export * from "./mediafire";
export * from "./pixeldrain";
export * from "./buzzheavier";
export * from "./fuckingfast";
export * from "./vikingfile";
export * from "./rootz";

View File

@@ -1,15 +0,0 @@
import { requestWebPage } from "@main/helpers";
export class QiwiApi {
public static async getDownloadUrl(url: string) {
const document = await requestWebPage(url);
const fileName = document.querySelector("h1")?.textContent;
const slug = url.split("/").pop();
const extension = fileName?.split(".").pop();
const downloadUrl = `https://spyderrock.com/${slug}.${extension}`;
return downloadUrl;
}
}

View File

@@ -0,0 +1,58 @@
import axios, { AxiosError } from "axios";
import { logger } from "../logger";
interface RootzApiResponse {
success: boolean;
data?: {
url: string;
fileName: string;
size: number;
mimeType: string;
expiresIn: number;
expiresAt: string | null;
downloads: number;
canDelete: boolean;
fileId: string;
isMirrored: boolean;
sourceService: string | null;
adsEnabled: boolean;
};
error?: string;
}
export class RootzApi {
public static async getDownloadUrl(uri: string): Promise<string> {
try {
const url = new URL(uri);
const pathSegments = url.pathname.split("/").filter(Boolean);
if (pathSegments.length < 2 || pathSegments[0] !== "d") {
throw new Error("Invalid rootz URL format");
}
const id = pathSegments[1];
const apiUrl = `https://www.rootz.so/api/files/download-by-short/${id}`;
const response = await axios.get<RootzApiResponse>(apiUrl);
if (response.data.success && response.data.data?.url) {
return response.data.data.url;
}
throw new Error("Failed to get download URL from rootz API");
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<RootzApiResponse>;
if (axiosError.response?.status === 404) {
const errorMessage =
axiosError.response.data?.error || "File not found";
logger.error(`[Rootz] ${errorMessage}`);
throw new Error(errorMessage);
}
}
logger.error("[Rootz] Error fetching download URL:", error);
throw error;
}
}
}

View File

@@ -138,12 +138,21 @@ export class WindowManager {
(details, callback) => {
if (
details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot") ||
details.url.includes("workwonders")
details.url.includes("chatwoot")
) {
return callback(details);
}
if (details.url.includes("workwonders")) {
return callback({
...details,
requestHeaders: {
Origin: "https://workwonders.app",
...details.requestHeaders,
},
});
}
const userAgent = new UserAgent();
callback({

View File

@@ -206,6 +206,8 @@ contextBridge.exposeInMainWorld("electron", {
refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"),
openGameInstaller: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("openGameInstaller", shop, objectId),
getGameInstallerActionType: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("getGameInstallerActionType", shop, objectId),
openGameInstallerPath: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("openGameInstallerPath", shop, objectId),
openGameExecutablePath: (shop: GameShop, objectId: string) =>

View File

@@ -134,7 +134,10 @@ export function App() {
await workwondersRef.current.initChangelogWidget();
workwondersRef.current.initChangelogWidgetMini();
workwondersRef.current.initFeedbackWidget();
if (token) {
workwondersRef.current.initFeedbackWidget();
}
},
[workwondersRef]
);

View File

@@ -8,6 +8,7 @@
min-width: 200px;
flex-direction: column;
align-items: center;
animation: dropdown-menu-fade-in 0.2s ease-out;
}
&__group {
@@ -66,3 +67,14 @@
justify-content: center;
}
}
@keyframes dropdown-menu-fade-in {
0% {
opacity: 0;
transform: translateY(-8px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -224,21 +224,6 @@ export function Header() {
setActiveIndex(-1);
};
useEffect(() => {
const prevPath = sessionStorage.getItem("prevPath");
const currentPath = location.pathname;
if (
prevPath?.startsWith("/catalogue") &&
!currentPath.startsWith("/catalogue") &&
catalogueSearchValue
) {
dispatch(setFilters({ title: "" }));
}
sessionStorage.setItem("prevPath", currentPath);
}, [location.pathname, catalogueSearchValue, dispatch]);
useEffect(() => {
if (!isDropdownVisible) return;

View File

@@ -7,7 +7,6 @@ export const DOWNLOADER_NAME = {
[Downloader.Torrent]: "Torrent",
[Downloader.Gofile]: "Gofile",
[Downloader.PixelDrain]: "PixelDrain",
[Downloader.Qiwi]: "Qiwi",
[Downloader.Datanodes]: "Datanodes",
[Downloader.Mediafire]: "Mediafire",
[Downloader.Buzzheavier]: "Buzzheavier",
@@ -15,6 +14,7 @@ export const DOWNLOADER_NAME = {
[Downloader.TorBox]: "TorBox",
[Downloader.Hydra]: "Nimbus",
[Downloader.VikingFile]: "VikingFile",
[Downloader.Rootz]: "Rootz",
};
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;

View File

@@ -167,6 +167,10 @@ declare global {
getLibrary: () => Promise<LibraryGame[]>;
refreshLibraryAssets: () => Promise<void>;
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
getGameInstallerActionType: (
shop: GameShop,
objectId: string
) => Promise<"install" | "open-folder">;
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>;
openGameExecutablePath: (shop: GameShop, objectId: string) => Promise<void>;
openGame: (

View File

@@ -21,6 +21,7 @@ import resources from "@locales";
import { logger } from "./logger";
import { addCookieInterceptor } from "./cookies";
import * as Sentry from "@sentry/react";
import { levelDBService } from "./services/leveldb.service";
import Catalogue from "./pages/catalogue/catalogue";
import Home from "./pages/home/home";
@@ -36,6 +37,18 @@ import { AchievementNotification } from "./pages/achievements/notification/achie
console.log = logger.log;
Sentry.init({
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
tracesSampleRate: 0.5,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
release: "hydra-launcher@" + (await window.electron.getVersion()),
});
const isStaging = await window.electron.isStaging();
addCookieInterceptor(isStaging);

View File

@@ -427,7 +427,7 @@
padding: 0;
cursor: pointer;
text-align: left;
width: 100%;
width: fit-content;
transition: opacity 0.2s ease;
&:focus,
@@ -509,6 +509,15 @@
&__simple-menu-btn {
padding: calc(globals.$spacing-unit);
min-height: unset;
border-radius: 8px;
}
&__simple-action-btn {
padding: calc(globals.$spacing-unit);
min-height: unset;
gap: calc(globals.$spacing-unit);
min-width: 120px;
border-radius: 8px;
}
&__progress-wrapper {

View File

@@ -32,12 +32,12 @@ import {
FileDirectoryIcon,
LinkIcon,
PlayIcon,
ThreeBarsIcon,
TrashIcon,
UnlinkIcon,
XCircleIcon,
GraphIcon,
} from "@primer/octicons-react";
import { MoreVertical, Folder } from "lucide-react";
import { average } from "color.js";
interface AnimatedPercentageProps {
@@ -452,6 +452,7 @@ export function DownloadGroup({
seedingStatus,
}: Readonly<DownloadGroupProps>) {
const { t } = useTranslation("downloads");
const { t: tGameDetails } = useTranslation("game_details");
const navigate = useNavigate();
const userPreferences = useAppSelector(
@@ -530,6 +531,9 @@ export function DownloadGroup({
const [gameToCancelObjectId, setGameToCancelObjectId] = useState<
string | null
>(null);
const [gameActionTypes, setGameActionTypes] = useState<
Record<string, "install" | "open-folder">
>({});
const extractDominantColor = useCallback(
async (imageUrl: string, gameId: string) => {
@@ -694,14 +698,6 @@ export function DownloadGroup({
if (game.download?.progress === 1) {
const actions = [
{
label: t("install"),
disabled: deleting,
onClick: () => {
openGameInstaller(game.shop, game.objectId);
},
icon: <DownloadIcon />,
},
{
label: t("extract"),
disabled: game.download.extracting,
@@ -805,6 +801,37 @@ export function DownloadGroup({
]
);
// Fetch action types for completed games
useEffect(() => {
const fetchActionTypes = async () => {
const completedGames = library.filter(
(game) => game.download?.progress === 1
);
const actionTypesPromises = completedGames.map(async (game) => {
try {
const actionType = await window.electron.getGameInstallerActionType(
game.shop,
game.objectId
);
return { gameId: game.id, actionType };
} catch {
return { gameId: game.id, actionType: "open-folder" as const };
}
});
const results = await Promise.all(actionTypesPromises);
const newActionTypes: Record<string, "install" | "open-folder"> = {};
results.forEach(({ gameId, actionType }) => {
newActionTypes[gameId] = actionType;
});
setGameActionTypes((prev) => ({ ...prev, ...newActionTypes }));
};
fetchActionTypes();
}, [library]);
if (!library.length) return null;
const isDownloadingGroup = title === t("download_in_progress");
@@ -959,18 +986,35 @@ export function DownloadGroup({
)}
<div className="download-group__simple-actions">
{game.download?.progress === 1 && (
<Button
theme="primary"
onClick={() =>
openGameInstaller(game.shop, game.objectId)
}
disabled={isGameDeleting(game.id)}
className="download-group__simple-menu-btn"
>
<PlayIcon size={16} />
</Button>
)}
{game.download?.progress === 1 &&
(() => {
const actionType =
gameActionTypes[game.id] || "open-folder";
const isInstall = actionType === "install";
return (
<Button
theme="primary"
onClick={() =>
openGameInstaller(game.shop, game.objectId)
}
disabled={isGameDeleting(game.id)}
className="download-group__simple-action-btn"
>
{isInstall ? (
<>
<DownloadIcon size={16} />
{t("install")}
</>
) : (
<>
<Folder size={16} />
{tGameDetails("open_folder")}
</>
)}
</Button>
);
})()}
{isQueuedGroup && game.download?.progress !== 1 && (
<Button
theme="primary"
@@ -986,7 +1030,7 @@ export function DownloadGroup({
theme="outline"
className="download-group__simple-menu-btn"
>
<ThreeBarsIcon />
<MoreVertical size={16} />
</Button>
</DropdownMenu>
</div>

View File

@@ -4,6 +4,20 @@
&__box {
padding: calc(globals.$spacing-unit * 2);
position: relative;
&--empty {
display: flex;
flex-direction: column;
align-items: center;
gap: calc(globals.$spacing-unit * 2);
}
}
&__empty-text {
color: globals.$muted-color;
font-size: globals.$small-font-size;
margin: 0;
text-align: center;
}
&__add-friend-button {

View File

@@ -19,6 +19,7 @@ export function FriendsBox() {
const [showAddFriendModal, setShowAddFriendModal] = useState(false);
const isMe = userDetails?.id === userProfile?.id;
const hasFriends = userProfile?.friends && userProfile.friends.length > 0;
const getGameImage = (game: { iconUrl: string | null; title: string }) => {
if (game.iconUrl) {
@@ -35,7 +36,15 @@ export function FriendsBox() {
return <SteamLogo width={16} height={16} />;
};
if (!userProfile?.friends.length) return null;
if (!hasFriends) {
if (!isMe) return null;
return (
<div className="friends-box__box friends-box__box--empty">
<p className="friends-box__empty-text">{t("no_friends_yet")}</p>
</div>
);
}
const visibleFriends = userProfile.friends.slice(0, MAX_VISIBLE_FRIENDS);
const totalFriends = userProfile.friends.length;

View File

@@ -376,7 +376,7 @@ export function ProfileContent() {
const hasAnyGames = hasGames || hasPinnedGames;
const shouldShowRightContent =
hasAnyGames || userProfile.friends.length > 0;
hasAnyGames || userProfile.friends.length > 0 || isMe;
return (
<section className="profile-content__section">
@@ -444,7 +444,7 @@ export function ProfileContent() {
<RecentGamesBox />
</ProfileSection>
)}
{userProfile?.friends.length > 0 && (
{(userProfile?.friends.length > 0 || isMe) && (
<ProfileSection
title={t("friends")}
count={userStats?.friendsCount || userProfile.friends.length}

View File

@@ -29,6 +29,12 @@
background-color: rgba(255, 255, 255, 0.15);
text-decoration: none;
}
&--wrapped {
&:hover {
background-color: transparent;
}
}
}
&__list-title {
@@ -70,4 +76,15 @@
font-size: 0.75rem;
line-height: 1.4;
}
&__wrapped-link {
background: none;
border: none;
padding: 0;
text-align: start;
color: globals.$body-color;
font-size: 0.875rem;
cursor: pointer;
text-decoration: underline;
}
}

View File

@@ -1,4 +1,4 @@
import { useCallback, useContext } from "react";
import { useCallback, useContext, useState } from "react";
import { userProfileContext } from "@renderer/context";
import { useTranslation } from "react-i18next";
import { useFormat, useUserDetails } from "@renderer/hooks";
@@ -7,9 +7,11 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useSubscription } from "@renderer/hooks/use-subscription";
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
import { Award } from "lucide-react";
import { WrappedFullscreenModal } from "./wrapped-tab";
import "./user-stats-box.scss";
export function UserStatsBox() {
const [showWrappedModal, setShowWrappedModal] = useState(false);
const { showHydraCloudModal } = useSubscription();
const { userStats, isMe, userProfile } = useContext(userProfileContext);
const { userDetails } = useUserDetails();
@@ -41,6 +43,18 @@ export function UserStatsBox() {
return (
<div className="user-stats__box">
<ul className="user-stats__list">
{userProfile?.hasCompletedWrapped2025 && (
<li className="user-stats__list-item user-stats__list-item--wrapped">
<button
type="button"
onClick={() => setShowWrappedModal(true)}
className="user-stats__wrapped-link"
>
Wrapped 2025
</button>
</li>
)}
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
<li className="user-stats__list-item">
<h3 className="user-stats__list-title">
@@ -126,6 +140,14 @@ export function UserStatsBox() {
</li>
)}
</ul>
{userProfile && (
<WrappedFullscreenModal
userId={userProfile.id}
isOpen={showWrappedModal}
onClose={() => setShowWrappedModal(false)}
/>
)}
</div>
);
}

View File

@@ -144,11 +144,6 @@
}
}
&__left-actions {
display: flex;
gap: globals.$spacing-unit;
}
&__actions {
display: flex;
gap: globals.$spacing-unit;
@@ -160,35 +155,5 @@
&--outline {
border-color: globals.$body-color;
}
&--wrapped {
background: linear-gradient(
120deg,
#2a57ff 0%,
#2951e6 11%,
#2f5bff 16%,
#2c56e8 29%,
#244acc 34%,
#2245c2 40%,
#3a6bff 45%,
#3766f2 50%,
#2444b8 56%,
#122a73 82%,
#2348b3 86%,
#1f429e 87%,
#10286a 93%,
#0e2a63 100%
);
background-color: #2a57ff;
background-size: 105% 100%;
background-position: 100% 50%;
border: none;
color: white;
transition: background-position 0.4s ease;
&:hover {
background-position: 0% 50%;
}
}
}
}

View File

@@ -7,7 +7,6 @@ import {
PencilIcon,
PersonAddIcon,
SignOutIcon,
TrophyIcon,
XCircleFillIcon,
} from "@primer/octicons-react";
import { buildGameDetailsPath } from "@renderer/helpers";
@@ -30,7 +29,6 @@ import { motion } from "framer-motion";
import type { FriendRequestAction } from "@types";
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
import { WrappedFullscreenModal } from "../profile-content/wrapped-tab";
import Skeleton from "react-loading-skeleton";
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
import "./profile-hero.scss";
@@ -41,10 +39,10 @@ type FriendAction =
export function ProfileHero() {
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const [showWrappedModal, setShowWrappedModal] = useState(false);
const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false);
const [isPerformingAction, setIsPerformingAction] = useState(false);
const [isCopyButtonHovered, setIsCopyButtonHovered] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } =
useContext(userProfileContext);
@@ -261,9 +259,23 @@ export function ProfileHero() {
const copyFriendCode = useCallback(() => {
if (userProfile?.id) {
navigator.clipboard.writeText(userProfile.id);
showSuccessToast(t("friend_code_copied"));
setIsCopied(true);
const startTime = performance.now();
const duration = 1200; // 1.2 seconds
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
if (elapsed < duration) {
requestAnimationFrame(animate);
} else {
setIsCopied(false);
}
};
requestAnimationFrame(animate);
}
}, [userProfile, showSuccessToast, t]);
}, [userProfile]);
const currentGame = useMemo(() => {
if (isMe) {
@@ -286,13 +298,6 @@ export function ProfileHero() {
onClose={() => setShowEditProfileModal(false)}
/>
{userProfile && (
<WrappedFullscreenModal
userId={userProfile.id}
isOpen={showWrappedModal}
onClose={() => setShowWrappedModal(false)}
/>
)}
<FullscreenMediaModal
visible={showFullscreenAvatar}
onClose={() => setShowFullscreenAvatar(false)}
@@ -348,7 +353,7 @@ export function ProfileHero() {
onMouseLeave={() => setIsCopyButtonHovered(false)}
initial={{ width: 28 }}
animate={{
width: isCopyButtonHovered ? 105 : 28,
width: isCopyButtonHovered || isCopied ? 105 : 28,
}}
transition={{ duration: 0.2, ease: "easeInOut" }}
>
@@ -356,12 +361,12 @@ export function ProfileHero() {
className="profile-hero__friend-code"
initial={{ opacity: 0, marginRight: 0 }}
animate={{
opacity: isCopyButtonHovered ? 1 : 0,
marginRight: isCopyButtonHovered ? 8 : 0,
opacity: isCopyButtonHovered || isCopied ? 1 : 0,
marginRight: isCopyButtonHovered || isCopied ? 8 : 0,
}}
transition={{ duration: 0.2, ease: "easeInOut" }}
>
{userProfile?.id}
{isCopied ? t("copied") : userProfile?.id}
</motion.span>
<CopyIcon size={16} />
</motion.button>
@@ -410,22 +415,6 @@ export function ProfileHero() {
background: !backgroundImage ? heroBackground : undefined,
}}
>
{userProfile?.hasCompletedWrapped2025 && (
<div className="profile-hero__left-actions">
<Button
theme="outline"
onClick={() => setShowWrappedModal(true)}
className="profile-hero__button--wrapped"
>
<TrophyIcon />
{isMe
? t("view_my_wrapped_button")
: t("view_wrapped_button", {
displayName: userProfile.displayName,
})}
</Button>
</div>
)}
<div className="profile-hero__actions">{profileActions}</div>
</div>
</section>

View File

@@ -1,11 +1,86 @@
@use "../../../scss/globals.scss";
.upload-background-image-button {
position: absolute;
top: 16px;
right: 16px;
&__wrapper {
position: absolute;
top: 16px;
right: 16px;
}
border-color: globals.$body-color;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(20px);
&__menu {
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
min-width: 180px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
z-index: 1000;
padding: 4px;
display: flex;
flex-direction: column;
animation: menu-fade-in 0.2s ease-out;
&--closing {
animation: menu-fade-out 0.15s ease-in;
}
}
&__menu-item {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 8px;
border-radius: 4px;
padding: 5px 12px;
cursor: pointer;
transition: background-color 0.1s ease-in-out;
font-size: 14px;
background: none;
border: none;
color: globals.$body-color;
text-align: left;
&:hover:not(:disabled) {
background-color: rgba(255, 255, 255, 0.1);
}
&:disabled {
cursor: default;
opacity: 0.6;
}
&:focus {
background-color: rgba(255, 255, 255, 0.1);
outline: none;
}
}
}
@keyframes menu-fade-in {
0% {
opacity: 0;
transform: translateY(-8px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes menu-fade-out {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-8px);
}
}

View File

@@ -1,6 +1,8 @@
import { UploadIcon } from "@primer/octicons-react";
import { Button } from "@renderer/components";
import { useContext, useState } from "react";
import { TrashIcon, UploadIcon } from "@primer/octicons-react";
import { MoreVertical } from "lucide-react";
import { Button, ConfirmationModal } from "@renderer/components";
import { createPortal } from "react-dom";
import { useContext, useEffect, useRef, useState } from "react";
import { userProfileContext } from "@renderer/context";
import { useToast, useUserDetails } from "@renderer/hooks";
import { useTranslation } from "react-i18next";
@@ -9,16 +11,33 @@ import "./upload-background-image-button.scss";
export function UploadBackgroundImageButton() {
const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] =
useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isMenuClosing, setIsMenuClosing] = useState(false);
const [showRemoveBannerModal, setShowRemoveBannerModal] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const { hasActiveSubscription } = useUserDetails();
const { t } = useTranslation("user_profile");
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
const { isMe, setSelectedBackgroundImage, userProfile, getUserProfile } =
useContext(userProfileContext);
const { patchUser, fetchUserDetails } = useUserDetails();
const { showSuccessToast } = useToast();
const handleChangeCoverClick = async () => {
const hasBanner = !!userProfile?.backgroundImageUrl;
const closeMenu = () => {
setIsMenuClosing(true);
setTimeout(() => {
setIsMenuOpen(false);
setIsMenuClosing(false);
}, 150);
};
const handleReplaceBanner = async () => {
closeMenu();
try {
const { filePaths } = await window.electron.showOpenDialog({
properties: ["openFile"],
@@ -40,23 +59,159 @@ export function UploadBackgroundImageButton() {
showSuccessToast(t("background_image_updated"));
await fetchUserDetails();
await getUserProfile();
}
} finally {
setIsUploadingBackgorundImage(false);
}
};
const handleRemoveBannerClick = () => {
closeMenu();
setShowRemoveBannerModal(true);
};
const handleRemoveBannerConfirm = async () => {
setShowRemoveBannerModal(false);
try {
setIsUploadingBackgorundImage(true);
setSelectedBackgroundImage("");
await patchUser({ backgroundImageUrl: null });
showSuccessToast(t("background_image_updated"));
await fetchUserDetails();
await getUserProfile();
} finally {
setIsUploadingBackgorundImage(false);
}
};
// Handle click outside, scroll, and escape key to close menu
useEffect(() => {
if (!isMenuOpen) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
if (
menuRef.current &&
!menuRef.current.contains(target) &&
buttonRef.current &&
!buttonRef.current.contains(target)
) {
closeMenu();
}
};
const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closeMenu();
}
};
const handleScroll = () => {
closeMenu();
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleEscape);
window.addEventListener("scroll", handleScroll, true);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleEscape);
window.removeEventListener("scroll", handleScroll, true);
};
}, [isMenuOpen]);
if (!isMe || !hasActiveSubscription) return null;
return (
<Button
theme="outline"
className="upload-background-image-button"
onClick={handleChangeCoverClick}
disabled={isUploadingBackgroundImage}
// If no banner exists, show the original upload button
if (!hasBanner) {
return (
<div className="upload-background-image-button__wrapper">
<Button
theme="outline"
className="upload-background-image-button"
onClick={handleReplaceBanner}
disabled={isUploadingBackgroundImage}
>
<UploadIcon />
{isUploadingBackgroundImage
? t("uploading_banner")
: t("upload_banner")}
</Button>
</div>
);
}
// Calculate menu position
const getMenuPosition = () => {
if (!buttonRef.current) return { top: 0, right: 0 };
const rect = buttonRef.current.getBoundingClientRect();
return {
top: rect.bottom + 5,
right: window.innerWidth - rect.right,
};
};
const menuPosition = isMenuOpen ? getMenuPosition() : { top: 0, right: 0 };
const menuContent = isMenuOpen && (
<div
ref={menuRef}
className={`upload-background-image-button__menu ${
isMenuClosing ? "upload-background-image-button__menu--closing" : ""
}`}
style={{
position: "fixed",
top: `${menuPosition.top}px`,
right: `${menuPosition.right}px`,
}}
>
<UploadIcon />
{isUploadingBackgroundImage ? t("uploading_banner") : t("upload_banner")}
</Button>
<button
type="button"
className="upload-background-image-button__menu-item"
onClick={handleReplaceBanner}
disabled={isUploadingBackgroundImage}
>
<UploadIcon size={16} />
{t("replace_banner")}
</button>
<button
type="button"
className="upload-background-image-button__menu-item"
onClick={handleRemoveBannerClick}
disabled={isUploadingBackgroundImage}
>
<TrashIcon size={16} />
{t("remove_banner")}
</button>
</div>
);
return (
<>
<div ref={buttonRef} className="upload-background-image-button__wrapper">
<Button
theme="outline"
className="upload-background-image-button"
onClick={() => setIsMenuOpen(!isMenuOpen)}
disabled={isUploadingBackgroundImage}
>
{t("change_banner")}
<MoreVertical size={16} />
</Button>
</div>
{createPortal(menuContent, document.body)}
<ConfirmationModal
visible={showRemoveBannerModal}
title={t("remove_banner_modal_title")}
descriptionText={t("remove_banner_confirmation")}
onClose={() => setShowRemoveBannerModal(false)}
onConfirm={handleRemoveBannerConfirm}
cancelButtonLabel={t("cancel")}
confirmButtonLabel={t("remove")}
buttonsIsDisabled={isUploadingBackgroundImage}
/>
</>
);
}

View File

@@ -14,10 +14,6 @@
&__section {
display: flex;
flex-direction: column;
&:not(:last-child) {
margin-bottom: calc(globals.$spacing-unit * 2);
}
}
&__section-header {

View File

@@ -18,6 +18,13 @@
align-self: flex-start;
}
&__disabled-hint {
font-size: 13px;
color: globals.$muted-color;
margin-top: calc(globals.$spacing-unit * -1);
font-style: italic;
}
&__volume-control {
display: flex;
flex-direction: column;

View File

@@ -37,6 +37,12 @@ export function SettingsGeneral() {
(state) => state.userPreferences.value
);
const lastPacket = useAppSelector((state) => state.download.lastPacket);
const hasActiveDownload =
lastPacket !== null &&
lastPacket.progress < 1 &&
!lastPacket.isDownloadingMetadata;
const [canInstallCommonRedist, setCanInstallCommonRedist] = useState(false);
const [installingCommonRedist, setInstallingCommonRedist] = useState(false);
@@ -53,7 +59,7 @@ export function SettingsGeneral() {
achievementSoundVolume: 15,
language: "",
customStyles: window.localStorage.getItem("customStyles") || "",
useNativeHttpDownloader: false,
useNativeHttpDownloader: true,
});
const [languageOptions, setLanguageOptions] = useState<LanguageOption[]>([]);
@@ -133,7 +139,7 @@ export function SettingsGeneral() {
userPreferences.friendStartGameNotificationsEnabled ?? true,
language: language ?? "en",
useNativeHttpDownloader:
userPreferences.useNativeHttpDownloader ?? false,
userPreferences.useNativeHttpDownloader ?? true,
}));
}
}, [userPreferences, defaultDownloadsPath]);
@@ -256,6 +262,7 @@ export function SettingsGeneral() {
<CheckboxField
label={t("use_native_http_downloader")}
checked={form.useNativeHttpDownloader}
disabled={hasActiveDownload}
onChange={() =>
handleChange({
useNativeHttpDownloader: !form.useNativeHttpDownloader,
@@ -263,6 +270,12 @@ export function SettingsGeneral() {
}
/>
{hasActiveDownload && (
<p className="settings-general__disabled-hint">
{t("cannot_change_downloader_while_downloading")}
</p>
)}
<h2 className="settings-general__section-title">{t("notifications")}</h2>
<CheckboxField

View File

@@ -18,7 +18,7 @@ $active-opacity: 0.7;
$spacing-unit: 8px;
$toast-z-index: 5;
$toast-z-index: 150;
$bottom-panel-z-index: 3;
$title-bar-z-index: 4;
$backdrop-z-index: 4;

View File

@@ -3,7 +3,6 @@ export enum Downloader {
Torrent,
Gofile,
PixelDrain,
Qiwi,
Datanodes,
Mediafire,
TorBox,
@@ -11,6 +10,7 @@ export enum Downloader {
Buzzheavier,
FuckingFast,
VikingFile,
Rootz,
}
export enum DownloadSourceStatus {

View File

@@ -110,7 +110,6 @@ export const getDownloadersForUri = (uri: string) => {
if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
if (uri.startsWith("https://qiwi.gg")) return [Downloader.Qiwi];
if (uri.startsWith("https://datanodes.to")) return [Downloader.Datanodes];
if (uri.startsWith("https://www.mediafire.com"))
return [Downloader.Mediafire];
@@ -127,6 +126,9 @@ export const getDownloadersForUri = (uri: string) => {
if (uri.startsWith("https://vikingfile.com")) {
return [Downloader.VikingFile];
}
if (uri.startsWith("https://www.rootz.so")) {
return [Downloader.Rootz];
}
if (realDebridHosts.some((host) => uri.startsWith(host)))
return [Downloader.RealDebrid];

117
yarn.lock
View File

@@ -2174,6 +2174,60 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz#5b2dd648a960b8fa00d76f2cc4eea2f03daa80f4"
integrity sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==
"@sentry-internal/browser-utils@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-10.33.0.tgz#4a5d98352267b63fcc449efe14627c0fc082089e"
integrity sha512-nDJFHAfiFifBfJB0OF6DV6BIsIV5uah4lDsV4UBAgPBf+YAHclO10y1gi2U/JMh58c+s4lXi9p+PI1TFXZ0c6w==
dependencies:
"@sentry/core" "10.33.0"
"@sentry-internal/feedback@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-10.33.0.tgz#5865b4a68d607bb48d8159a100464ae640a638e7"
integrity sha512-sN/VLWtEf0BeV6w6wldIpTxUQxNVc9o9tjLRQa8je1ZV2FCgXA124Iff/zsowsz82dLqtg7qp6GA5zYXVq+JMA==
dependencies:
"@sentry/core" "10.33.0"
"@sentry-internal/replay-canvas@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-10.33.0.tgz#9ea15b320618ad220e5d8f7c804a0d9ca55b04af"
integrity sha512-MTmP6uoAVzw4CCPeqCgCLsRSiOfGLxgyMFjGTCW3E7t62MJ9S0H5sLsQ34sHxXUa1gFU9UNAjEvRRpZ0JvWrPw==
dependencies:
"@sentry-internal/replay" "10.33.0"
"@sentry/core" "10.33.0"
"@sentry-internal/replay@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-10.33.0.tgz#8cfe3a353731fcd81e7afb646b6befeb0f9feb0f"
integrity sha512-UOU9PYxuXnPop3HoQ3l4Q7SZUXJC3Vmfm0Adgad8U03UcrThWIHYc5CxECSrVzfDFNOT7w9o7HQgRAgWxBPMXg==
dependencies:
"@sentry-internal/browser-utils" "10.33.0"
"@sentry/core" "10.33.0"
"@sentry/browser@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.33.0.tgz#33284952a1cdf43cdac15ac144c85e81e7cbaa93"
integrity sha512-iWiPjik9zetM84jKfk01UveW1J0+X7w8XmJ8+IrhTyNDBVUWCRJWD8FrksiN1dRSg5mFWgfMRzKMz27hAScRwg==
dependencies:
"@sentry-internal/browser-utils" "10.33.0"
"@sentry-internal/feedback" "10.33.0"
"@sentry-internal/replay" "10.33.0"
"@sentry-internal/replay-canvas" "10.33.0"
"@sentry/core" "10.33.0"
"@sentry/core@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.33.0.tgz#ea4964fbec290503b419ccaf1a313924d30ad1c8"
integrity sha512-ehH1VSUclIHZKEZVdv+klofsFIh8FFzqA6AAV23RtLepptzA8wqQzUGraEuSN25sYcNmYJ0jti5U0Ys+WZv5Dw==
"@sentry/react@^10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-10.33.0.tgz#89a3be88d43e49de90943ad2ac86ee1664048097"
integrity sha512-iMdC2Iw54ibAccatJ5TjoLlIy3VotFteied7JFvOudgj1/2eBBeWthRobZ5p6/nAOpj4p9vJk0DeLrc012sd2g==
dependencies:
"@sentry/browser" "10.33.0"
"@sentry/core" "10.33.0"
"@sindresorhus/is@^4.0.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
@@ -2203,14 +2257,15 @@
tslib "^2.6.2"
"@smithy/config-resolver@^4.3.0", "@smithy/config-resolver@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.3.1.tgz#f1a0ed6faa52377909440002e1632be9fc901840"
integrity sha512-tWDwrWy37CDVGeaP8AIGZPFL2RoFtmd5Y+nTzLw5qroXNedT2S66EY2d+XzB1zxulCd6nfDXnAQu4auq90aj5Q==
version "4.4.5"
resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.4.5.tgz#35e792b6db00887bdd029df9b41780ca005d064b"
integrity sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==
dependencies:
"@smithy/node-config-provider" "^4.3.1"
"@smithy/types" "^4.7.0"
"@smithy/node-config-provider" "^4.3.7"
"@smithy/types" "^4.11.0"
"@smithy/util-config-provider" "^4.2.0"
"@smithy/util-middleware" "^4.2.1"
"@smithy/util-endpoints" "^3.2.7"
"@smithy/util-middleware" "^4.2.7"
tslib "^2.6.2"
"@smithy/core@^3.15.0", "@smithy/core@^3.16.0":
@@ -2421,6 +2476,16 @@
"@smithy/types" "^4.7.0"
tslib "^2.6.2"
"@smithy/node-config-provider@^4.3.7":
version "4.3.7"
resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz#c023fa857b008c314f621fb5b124724c157b2fd3"
integrity sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==
dependencies:
"@smithy/property-provider" "^4.2.7"
"@smithy/shared-ini-file-loader" "^4.4.2"
"@smithy/types" "^4.11.0"
tslib "^2.6.2"
"@smithy/node-http-handler@^4.3.0", "@smithy/node-http-handler@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.0.tgz#e1f6ae4a90cd7257699263bf8e06e653ff0e5f83"
@@ -2440,6 +2505,14 @@
"@smithy/types" "^4.7.0"
tslib "^2.6.2"
"@smithy/property-provider@^4.2.7":
version "4.2.7"
resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.2.7.tgz#cd0044e13495cf4064b3a6ed3299e5f549ba7513"
integrity sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==
dependencies:
"@smithy/types" "^4.11.0"
tslib "^2.6.2"
"@smithy/protocol-http@^5.3.0", "@smithy/protocol-http@^5.3.1":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.3.1.tgz#add01f73290f1e8fd49d7102b63e3fe53a5e6e18"
@@ -2480,6 +2553,14 @@
"@smithy/types" "^4.7.0"
tslib "^2.6.2"
"@smithy/shared-ini-file-loader@^4.4.2":
version "4.4.2"
resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz#8fa1b459de485b11185fe8c64182e3205a280ba9"
integrity sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==
dependencies:
"@smithy/types" "^4.11.0"
tslib "^2.6.2"
"@smithy/signature-v4@^5.3.0":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.3.1.tgz#c3d711c29d37f3db4daf51750eea75204c4f51d4"
@@ -2507,6 +2588,13 @@
"@smithy/util-stream" "^4.5.1"
tslib "^2.6.2"
"@smithy/types@^4.11.0":
version "4.11.0"
resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.11.0.tgz#c02f6184dcb47c4f0b387a32a7eca47956cc09f1"
integrity sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==
dependencies:
tslib "^2.6.2"
"@smithy/types@^4.6.0", "@smithy/types@^4.7.0":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.7.0.tgz#42d707276d9184aef705f04e04615cd1979d044f"
@@ -2601,6 +2689,15 @@
"@smithy/types" "^4.7.0"
tslib "^2.6.2"
"@smithy/util-endpoints@^3.2.7":
version "3.2.7"
resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz#78cd5dd4aac8d9977f49d256d1e3418a09cade72"
integrity sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==
dependencies:
"@smithy/node-config-provider" "^4.3.7"
"@smithy/types" "^4.11.0"
tslib "^2.6.2"
"@smithy/util-hex-encoding@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz#1c22ea3d1e2c3a81ff81c0a4f9c056a175068a7b"
@@ -2616,6 +2713,14 @@
"@smithy/types" "^4.7.0"
tslib "^2.6.2"
"@smithy/util-middleware@^4.2.7":
version "4.2.7"
resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-4.2.7.tgz#1cae2c4fd0389ac858d29f7170c33b4443e83524"
integrity sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==
dependencies:
"@smithy/types" "^4.11.0"
tslib "^2.6.2"
"@smithy/util-retry@^4.2.0", "@smithy/util-retry@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.2.1.tgz#8336368586a458cdce86fc92d6fb11fd1db41521"