Compare commits

..

3 Commits

Author SHA1 Message Date
Chubby Granny Chaser
e578047929 feat: add translation key for Hydra Wrapped 2025 in multiple languages and implement sidebar route with marquee effect
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
2025-12-08 15:22:05 +00:00
Chubby Granny Chaser
e49d885b30 chore: update package.json to use yarn commands for type checking and building
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
2025-11-30 15:21:50 +00:00
Chubby Granny Chaser
cb01301a0d feat: add new translation keys for network statistics in multiple languages 2025-11-30 15:07:32 +00:00
46 changed files with 625 additions and 1432 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "hydralauncher", "name": "hydralauncher",
"version": "3.7.6", "version": "3.7.5",
"description": "Hydra", "description": "Hydra",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Los Broxas", "author": "Los Broxas",
@@ -19,12 +19,12 @@
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web", "typecheck": "yarn run typecheck:node && yarn run typecheck:web",
"start": "electron-vite preview", "start": "electron-vite preview",
"dev": "electron-vite dev", "dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build", "build": "yarn run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps && node ./scripts/postinstall.cjs", "postinstall": "electron-builder install-app-deps && node ./scripts/postinstall.cjs",
"build:unpack": "npm run build && electron-builder --dir", "build:unpack": "yarn run build && electron-builder --dir",
"build:win": "electron-vite build && electron-builder --win", "build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac", "build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux", "build:linux": "electron-vite build && electron-builder --linux",
@@ -70,7 +70,6 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"node-7z": "^3.0.0",
"parse-torrent": "^11.0.18", "parse-torrent": "^11.0.18",
"rc-virtual-list": "^3.18.3", "rc-virtual-list": "^3.18.3",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",

2
proto

Submodule proto updated: 6f11c99c57...7a23620f93

View File

@@ -16,6 +16,7 @@
"library": "Library", "library": "Library",
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Settings", "settings": "Settings",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Available",
"my_library": "My library", "my_library": "My library",
"downloading_metadata": "{{title}} (Downloading metadata…)", "downloading_metadata": "{{title}} (Downloading metadata…)",
"paused": "{{title}} (Paused)", "paused": "{{title}} (Paused)",
@@ -115,7 +116,6 @@
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}", "downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}",
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…", "calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
"checking_files": "Checking {{title}} files… ({{percentage}} complete)", "checking_files": "Checking {{title}} files… ({{percentage}} complete)",
"extracting": "Extracting {{title}}… ({{percentage}} complete)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Installation complete", "installation_complete": "Installation complete",
"installation_complete_message": "Common redistributables installed successfully" "installation_complete_message": "Common redistributables installed successfully"
@@ -203,7 +203,6 @@
"danger_zone_section_description": "Remove this game from your library or the files downloaded by Hydra", "danger_zone_section_description": "Remove this game from your library or the files downloaded by Hydra",
"download_in_progress": "Download in progress", "download_in_progress": "Download in progress",
"download_paused": "Download paused", "download_paused": "Download paused",
"extracting": "Extracting",
"last_downloaded_option": "Last downloaded option", "last_downloaded_option": "Last downloaded option",
"new_download_option": "New", "new_download_option": "New",
"create_steam_shortcut": "Create Steam shortcut", "create_steam_shortcut": "Create Steam shortcut",
@@ -417,12 +416,10 @@
"options": "Manage", "options": "Manage",
"extract": "Extract files", "extract": "Extract files",
"extracting": "Extracting files…", "extracting": "Extracting files…",
"delete_archive_title": "Would you like to delete {{fileName}}?", "network": "Network",
"delete_archive_description": "The file has been successfully extracted and it's no longer needed.", "peak": "Peak",
"yes": "Yes", "seeds": "Seeds",
"no": "No", "peers": "Peers"
"network": "NETWORK",
"peak": "PEAK"
}, },
"settings": { "settings": {
"downloads_path": "Downloads path", "downloads_path": "Downloads path",

View File

@@ -16,6 +16,7 @@
"library": "Librería", "library": "Librería",
"downloads": "Descargas", "downloads": "Descargas",
"settings": "Ajustes", "settings": "Ajustes",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Disponible",
"my_library": "Mi Librería", "my_library": "Mi Librería",
"downloading_metadata": "{{title}} (Descargando metadatos…)", "downloading_metadata": "{{title}} (Descargando metadatos…)",
"paused": "{{title}} (Pausado)", "paused": "{{title}} (Pausado)",
@@ -414,7 +415,11 @@
"resume_seeding": "Continuar sembrando", "resume_seeding": "Continuar sembrando",
"options": "Administrar", "options": "Administrar",
"extract": "Extraer archivos", "extract": "Extraer archivos",
"extracting": "Extrayendo archivos…" "extracting": "Extrayendo archivos…",
"network": "Red",
"peak": "Pico",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Ruta de descarga", "downloads_path": "Ruta de descarga",
@@ -458,7 +463,6 @@
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas", "description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
"button_delete_all_sources": "Eliminar todo", "button_delete_all_sources": "Eliminar todo",
"added_download_source": "Añadir fuente de descarga", "added_download_source": "Añadir fuente de descarga",
"adding": "Añadiendo…",
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas", "download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
"insert_valid_json_url": "Introducí una URL de json válida", "insert_valid_json_url": "Introducí una URL de json válida",
"found_download_option_zero": "Sin opciones de descargas encontrada", "found_download_option_zero": "Sin opciones de descargas encontrada",
@@ -564,19 +568,6 @@
"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.", "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", "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", "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"
}, },
"notifications": { "notifications": {

View File

@@ -27,69 +27,7 @@
"friends": "Amis", "friends": "Amis",
"need_help": "Besoin d'aide ?", "need_help": "Besoin d'aide ?",
"favorites": "Favoris", "favorites": "Favoris",
"playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant", "playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant"
"library": "Bibliothèque",
"add_custom_game_tooltip": "Ajouter un jeu personnalisé",
"show_playable_only_tooltip": "Afficher uniquement les jeux jouables",
"custom_game_modal": "Ajouter un jeu personnalisé",
"custom_game_modal_description": "Ajoutez un jeu personnalisé à votre bibliothèque en sélectionnant un fichier exécutable",
"custom_game_modal_executable_path": "Chemin de l'exécutable",
"custom_game_modal_select_executable": "Sélectionner un fichier exécutable",
"custom_game_modal_title": "Titre",
"custom_game_modal_enter_title": "Entrer le titre",
"custom_game_modal_browse": "Parcourir",
"custom_game_modal_cancel": "Annuler",
"custom_game_modal_add": "Ajouter le jeu",
"custom_game_modal_adding": "Ajout du jeu…",
"custom_game_modal_success": "Jeu personnalisé ajouté avec succès",
"custom_game_modal_failed": "Échec de lajout du jeu personnalisé",
"custom_game_modal_executable": "Exécutable",
"edit_game_modal": "Personnaliser les ressources",
"edit_game_modal_description": "Personnalisez les ressources et les détails du jeu",
"edit_game_modal_title": "Titre",
"edit_game_modal_enter_title": "Entrer le titre",
"edit_game_modal_image": "Image",
"edit_game_modal_select_image": "Sélectionner une image",
"edit_game_modal_browse": "Parcourir",
"edit_game_modal_image_preview": "Aperçu de limage",
"edit_game_modal_icon": "Icône",
"edit_game_modal_select_icon": "Sélectionner une icône",
"edit_game_modal_icon_preview": "Aperçu de licône",
"edit_game_modal_logo": "Logo",
"edit_game_modal_select_logo": "Sélectionner un logo",
"edit_game_modal_logo_preview": "Aperçu du logo",
"edit_game_modal_hero": "Bannière de la bibliothèque",
"edit_game_modal_select_hero": "Sélectionner limage de bannière",
"edit_game_modal_hero_preview": "Aperçu de la bannière",
"edit_game_modal_cancel": "Annuler",
"edit_game_modal_update": "Mettre à jour",
"edit_game_modal_updating": "Mise à jour…",
"edit_game_modal_fill_required": "Veuillez remplir tous les champs requis",
"edit_game_modal_success": "Ressources mises à jour avec succès",
"edit_game_modal_failed": "Échec de la mise à jour des ressources",
"edit_game_modal_image_filter": "Image",
"edit_game_modal_icon_resolution": "Résolution recommandée : 256x256px",
"edit_game_modal_logo_resolution": "Résolution recommandée : 640x360px",
"edit_game_modal_hero_resolution": "Résolution recommandée : 1920x620px",
"edit_game_modal_assets": "Ressources",
"edit_game_modal_drop_icon_image_here": "Déposez limage de licône ici",
"edit_game_modal_drop_logo_image_here": "Déposez limage du logo ici",
"edit_game_modal_drop_hero_image_here": "Déposez limage de la bannière ici",
"edit_game_modal_drop_to_replace_icon": "Déposez pour remplacer licône",
"edit_game_modal_drop_to_replace_logo": "Déposez pour remplacer le logo",
"edit_game_modal_drop_to_replace_hero": "Déposez pour remplacer la bannière",
"install_decky_plugin": "Installer le plugin Decky",
"update_decky_plugin": "Mettre à jour le plugin Decky",
"decky_plugin_installed_version": "Plugin Decky (v{{version}})",
"install_decky_plugin_title": "Installer le plugin Decky Hydra",
"install_decky_plugin_message": "Cela téléchargera et installera le plugin Hydra pour Decky Loader. Des permissions élevées peuvent être requises. Continuer ?",
"update_decky_plugin_title": "Mettre à jour le plugin Decky Hydra",
"update_decky_plugin_message": "Une nouvelle version du plugin Decky Hydra est disponible. Souhaitez-vous la mettre à jour maintenant ?",
"decky_plugin_installed": "Plugin Decky v{{version}} installé avec succès",
"decky_plugin_installation_failed": "Échec de linstallation du plugin Decky : {{error}}",
"decky_plugin_installation_error": "Erreur lors de linstallation du plugin Decky : {{error}}",
"confirm": "Confirmer",
"cancel": "Annuler"
}, },
"header": { "header": {
"search": "Rechercher", "search": "Rechercher",
@@ -99,15 +37,7 @@
"search_results": "Résultats de la recherche", "search_results": "Résultats de la recherche",
"settings": "Paramètres", "settings": "Paramètres",
"version_available_install": "Version {{version}} disponible. Cliquez ici pour redémarrer et installer.", "version_available_install": "Version {{version}} disponible. Cliquez ici pour redémarrer et installer.",
"version_available_download": "Version {{version}} disponible. Cliquez ici pour télécharger.", "version_available_download": "Version {{version}} disponible. Cliquez ici pour télécharger."
"search_library": "Rechercher dans la bibliothèque",
"recent_searches": "Recherches récentes",
"suggestions": "Suggestions",
"clear_history": "Effacer",
"remove_from_history": "Supprimer de l'historique",
"loading": "Chargement…",
"no_results": "Aucun résultat",
"library": "Bibliothèque"
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "Aucun téléchargement en cours", "no_downloads_in_progress": "Aucun téléchargement en cours",
@@ -117,8 +47,7 @@
"checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)", "checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Installation terminée", "installation_complete": "Installation terminée",
"installation_complete_message": "Redistribuables communs installés avec succès", "installation_complete_message": "Redistribuables communs installés avec succès"
"extracting": "Extraction de {{title}}… ({{percentage}} terminé)"
}, },
"catalogue": { "catalogue": {
"search": "Filtrer…", "search": "Filtrer…",
@@ -269,113 +198,7 @@
"download_error_not_cached_on_hydra": "Ce téléchargement n'est pas disponible sur Nimbus.", "download_error_not_cached_on_hydra": "Ce téléchargement n'est pas disponible sur Nimbus.",
"game_removed_from_favorites": "Jeu retiré des favoris", "game_removed_from_favorites": "Jeu retiré des favoris",
"game_added_to_favorites": "Jeu ajouté aux favoris", "game_added_to_favorites": "Jeu ajouté aux favoris",
"automatically_extract_downloaded_files": "Extraire automatiquement les fichiers téléchargés", "automatically_extract_downloaded_files": "Extraire automatiquement les fichiers téléchargés"
"already_in_library": "Déjà dans la bibliothèque",
"create_shortcut_simple": "Créer un raccourci",
"properties": "Propriétés",
"extracting": "Extraction en cours",
"new_download_option": "Nouveau",
"create_steam_shortcut": "Créer un raccourci Steam",
"you_might_need_to_restart_steam": "Vous devrez peut-être redémarrer Steam pour voir les changements",
"add_to_favorites": "Ajouter aux favoris",
"remove_from_favorites": "Retirer des favoris",
"failed_update_favorites": "Échec de la mise à jour des favoris",
"game_removed_from_library": "Jeu retiré de la bibliothèque",
"failed_remove_from_library": "Échec de la suppression du jeu de la bibliothèque",
"files_removed_success": "Fichiers supprimés avec succès",
"failed_remove_files": "Échec de la suppression des fichiers",
"rating_count": "Évaluations",
"show_more": "Afficher plus",
"show_less": "Afficher moins",
"reviews": "Avis",
"review_played_for": "Temps de jeu",
"leave_a_review": "Laisser un avis",
"write_review_placeholder": "Partagez votre avis sur ce jeu…",
"sort_newest": "Les plus récents",
"sort_oldest": "Les plus anciens",
"sort_highest_score": "Meilleure note",
"sort_lowest_score": "Note la plus basse",
"sort_most_voted": "Les plus votés",
"no_reviews_yet": "Aucun avis pour le moment",
"be_first_to_review": "Soyez le premier à donner votre avis !",
"rating": "Note",
"rating_stats": "Évaluation",
"rating_very_negative": "Très négatif",
"rating_negative": "Négatif",
"rating_neutral": "Neutre",
"rating_positive": "Positif",
"rating_very_positive": "Très positif",
"submit_review": "Envoyer",
"submitting": "Envoi…",
"review_submitted_successfully": "Avis envoyé avec succès !",
"review_submission_failed": "Échec de lenvoi de lavis. Veuillez réessayer.",
"review_cannot_be_empty": "Le champ de lavis ne peut pas être vide.",
"review_deleted_successfully": "Avis supprimé avec succès.",
"review_deletion_failed": "Échec de la suppression de lavis.",
"loading_reviews": "Chargement des avis…",
"loading_more_reviews": "Chargement de plus davis…",
"load_more_reviews": "Charger plus davis",
"you_seemed_to_enjoy_this_game": "Vous semblez avoir apprécié ce jeu",
"would_you_recommend_this_game": "Souhaitez-vous laisser un avis sur ce jeu ?",
"yes": "Oui",
"maybe_later": "Peut-être plus tard",
"backup_failed": "Échec de la sauvegarde",
"update_playtime_title": "Mettre à jour le temps de jeu",
"update_playtime_description": "Mettre à jour manuellement le temps de jeu pour {{game}}",
"update_playtime": "Mettre à jour le temps de jeu",
"update_playtime_success": "Temps de jeu mis à jour avec succès",
"update_playtime_error": "Échec de la mise à jour du temps de jeu",
"update_game_playtime": "Mettre à jour le temps de jeu",
"manual_playtime_warning": "Vos heures seront marquées comme modifiées manuellement et cela ne peut pas être annulé.",
"manual_playtime_tooltip": "Ce temps de jeu a été modifié manuellement",
"game_removed_from_pinned": "Jeu retiré des épinglés",
"game_added_to_pinned": "Jeu ajouté aux épinglés",
"create_start_menu_shortcut": "Créer un raccourci dans le menu Démarrer",
"invalid_wine_prefix_path": "Chemin du préfixe Wine invalide",
"invalid_wine_prefix_path_description": "Le chemin du préfixe Wine est invalide. Veuillez vérifier et réessayer.",
"missing_wine_prefix": "Un préfixe Wine est requis pour créer une sauvegarde sous Linux",
"artifact_renamed": "Sauvegarde renommée avec succès",
"rename_artifact": "Renommer la sauvegarde",
"rename_artifact_description": "Renommez la sauvegarde avec un nom plus descriptif",
"artifact_name_label": "Nom de la sauvegarde",
"artifact_name_placeholder": "Entrez un nom pour la sauvegarde",
"save_changes": "Enregistrer les modifications",
"required_field": "Ce champ est requis",
"max_length_field": "Ce champ doit contenir moins de {{length}} caractères",
"freeze_backup": "Épingler pour éviter lécrasement automatique",
"unfreeze_backup": "Désépingler",
"backup_frozen": "Sauvegarde épinglée",
"backup_unfrozen": "Sauvegarde désépinglée",
"backup_freeze_failed": "Échec de lépinglage de la sauvegarde",
"backup_freeze_failed_description": "Vous devez laisser au moins un emplacement libre pour les sauvegardes automatiques",
"edit_game_modal_button": "Personnaliser les ressources du jeu",
"game_details": "Détails du jeu",
"prices": "Prix",
"no_prices_found": "Aucun prix trouvé",
"view_all_prices": "Cliquer pour voir tous les prix",
"retail_price": "Prix officiel",
"keyshop_price": "Prix Keyshop",
"historical_retail": "Historique officiel",
"historical_keyshop": "Historique Keyshop",
"language": "Langue",
"caption": "Sous-titres",
"audio": "Audio",
"filter_by_source": "Filtrer par source",
"no_repacks_found": "Aucune source trouvée pour ce jeu",
"delete_review": "Supprimer lavis",
"remove_review": "Retirer lavis",
"delete_review_modal_title": "Voulez-vous vraiment supprimer votre avis ?",
"delete_review_modal_description": "Cette action est irréversible.",
"delete_review_modal_delete_button": "Supprimer",
"delete_review_modal_cancel_button": "Annuler",
"vote_failed": "Échec de lenregistrement de votre vote. Veuillez réessayer.",
"show_original": "Afficher loriginal",
"show_translation": "Afficher la traduction",
"show_original_translated_from": "Afficher loriginal (traduit depuis {{language}})",
"hide_original": "Masquer loriginal",
"review_from_blocked_user": "Avis dun utilisateur bloqué",
"show": "Afficher",
"hide": "Masquer"
}, },
"activation": { "activation": {
"title": "Activer Hydra", "title": "Activer Hydra",
@@ -414,11 +237,7 @@
"resume_seeding": "Reprendre le partage", "resume_seeding": "Reprendre le partage",
"options": "Gérer", "options": "Gérer",
"extract": "Extraire les fichiers", "extract": "Extraire les fichiers",
"extracting": "Extraction des fichiers…", "extracting": "Extraction des fichiers…"
"delete_archive_title": "Voulez-vous supprimer {{fileName}} ?",
"delete_archive_description": "Le fichier a été extrait avec succès et nest plus nécessaire.",
"yes": "Oui",
"no": "Non"
}, },
"settings": { "settings": {
"downloads_path": "Chemin des téléchargements", "downloads_path": "Chemin des téléchargements",
@@ -547,40 +366,7 @@
"bottom-left": "En bas à gauche", "bottom-left": "En bas à gauche",
"bottom-center": "En bas au centre", "bottom-center": "En bas au centre",
"bottom-right": "En bas à droite", "bottom-right": "En bas à droite",
"enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu", "enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu"
"adding": "Ajout…",
"failed_add_download_source": "Échec de lajout de la source de téléchargement. Veuillez réessayer.",
"download_source_already_exists": "Cette URL de source existe déjà",
"download_source_pending_matching": "Mise à jour imminente",
"download_source_matched": "À jour",
"download_source_matching": "Mise à jour",
"download_source_failed": "Erreur",
"download_source_no_information": "Aucune information disponible",
"removed_all_download_sources": "Toutes les sources de téléchargement supprimées",
"download_sources_synced_successfully": "Toutes les sources de téléchargement ont été synchronisées",
"importing": "Importation…",
"hydra_cloud": "Hydra Cloud",
"debrid": "Debrid",
"enable_steam_achievements": "Activer la recherche de succès Steam",
"alignment": "Alignement",
"variation": "Variation",
"default": "Par défaut",
"rare": "Rare",
"platinum": "Platine",
"hidden": "Caché",
"test_notification": "Notification de test",
"achievement_sound_volume": "Volume du son de succès",
"select_achievement_sound": "Sélectionner un son de succès",
"change_achievement_sound": "Changer le son de succès",
"remove_achievement_sound": "Supprimer le son de succès",
"preview_sound": "Prévisualiser le son",
"select": "Sélectionner",
"preview": "Aperçu",
"remove": "Supprimer",
"no_sound_file_selected": "Aucun fichier sonore sélectionné",
"notification_preview": "Aperçu de la notification de succès",
"autoplay_trailers_on_game_page": "Lire automatiquement les bandes-annonces sur la page du jeu",
"hide_to_tray_on_game_start": "Réduire Hydra dans la barre système au lancement dun jeu"
}, },
"notifications": { "notifications": {
"download_complete": "Téléchargement terminé", "download_complete": "Téléchargement terminé",

View File

@@ -22,7 +22,7 @@
"downloading": "{{title}} ({{percentage}} - Letöltés…)", "downloading": "{{title}} ({{percentage}} - Letöltés…)",
"filter": "Könyvtár szűrése", "filter": "Könyvtár szűrése",
"home": "Főoldal", "home": "Főoldal",
"queued": "{{title}} (Várakozásban)", "queued": "A(z) {{title}} (Várakozósorban van)",
"game_has_no_executable": "A játékhoz nincs tallózva futtatható fájl", "game_has_no_executable": "A játékhoz nincs tallózva futtatható fájl",
"sign_in": "Bejelentkezés", "sign_in": "Bejelentkezés",
"friends": "Barátok", "friends": "Barátok",
@@ -94,12 +94,6 @@
"header": { "header": {
"search": "Keresés", "search": "Keresés",
"search_library": "Könyvtár böngészése", "search_library": "Könyvtár böngészése",
"recent_searches": "Korábbi Keresések",
"suggestions": "Találatok",
"clear_history": "Törlés",
"remove_from_history": "Törlés az előzményekből",
"loading": "Töltés...",
"no_results": "Nincs találat",
"home": "Főoldal", "home": "Főoldal",
"catalogue": "Katalógus", "catalogue": "Katalógus",
"library": "Könyvtár", "library": "Könyvtár",
@@ -115,7 +109,6 @@
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}", "downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}",
"calculating_eta": "{{title}} letöltése… ({{percentage}} kész) - Hátralévő idő…", "calculating_eta": "{{title}} letöltése… ({{percentage}} kész) - Hátralévő idő…",
"checking_files": "A(z) {{title}} fájljaiból… ({{percentage}} kész)", "checking_files": "A(z) {{title}} fájljaiból… ({{percentage}} kész)",
"extracting": "{{title}} kicsomagolása… ({{percentage}} kicsomagolva)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Telepítés befejezve", "installation_complete": "Telepítés befejezve",
"installation_complete_message": "A(z) Alapvető segédprogramok sikeresen telepítve" "installation_complete_message": "A(z) Alapvető segédprogramok sikeresen telepítve"
@@ -172,7 +165,7 @@
"playing_now": "Játékban: ", "playing_now": "Játékban: ",
"change": "Változtatás", "change": "Változtatás",
"repacks_modal_description": "Válaszd ki a repacket amit leszeretnél tölteni", "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", "select_folder_hint": "A letöltési mappát a <0>Beállítások</0> menüjében változtathatod meg",
"download_now": "Letöltés", "download_now": "Letöltés",
"no_shop_details": "A bolt adatai nem érhetőek el.", "no_shop_details": "A bolt adatai nem érhetőek el.",
"download_options": "Letöltési opciók", "download_options": "Letöltési opciók",
@@ -203,7 +196,6 @@
"danger_zone_section_description": "Itt eltávolítható a játék a könyvtáradból, vagy a fájlok amelyek a Hydra által lettek letöltve", "danger_zone_section_description": "Itt eltávolítható a játék a könyvtáradból, vagy a fájlok amelyek a Hydra által lettek letöltve",
"download_in_progress": "Letöltés folyamatban", "download_in_progress": "Letöltés folyamatban",
"download_paused": "Letöltés szüneteltetve", "download_paused": "Letöltés szüneteltetve",
"extracting": "Kicsomagolás",
"last_downloaded_option": "Utoljára letöltött", "last_downloaded_option": "Utoljára letöltött",
"new_download_option": "Új", "new_download_option": "Új",
"create_steam_shortcut": "Steam parancsikon létrehozása", "create_steam_shortcut": "Steam parancsikon létrehozása",
@@ -405,7 +397,7 @@
"delete_modal_description": "Ez eltávolítja a telepítési fájlokat a számítógépedről", "delete_modal_description": "Ez eltávolítja a telepítési fájlokat a számítógépedről",
"install": "Telepít", "install": "Telepít",
"download_in_progress": "Folyamatban lévő", "download_in_progress": "Folyamatban lévő",
"queued_downloads": "Várakozásban lévő letöltések", "queued_downloads": "Várakozósoron lévő letöltések",
"downloads_completed": "Befejezett", "downloads_completed": "Befejezett",
"queued": "Várakozásban", "queued": "Várakozásban",
"no_downloads_title": "Oly üres..", "no_downloads_title": "Oly üres..",
@@ -416,11 +408,7 @@
"resume_seeding": "Seedelés folytatása", "resume_seeding": "Seedelés folytatása",
"options": "Kezelés", "options": "Kezelés",
"extract": "Fájlok kibontása", "extract": "Fájlok kibontása",
"extracting": "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. ",
"yes": "Igen",
"no": "Nem"
}, },
"settings": { "settings": {
"downloads_path": "Letöltési útvonalak", "downloads_path": "Letöltési útvonalak",
@@ -681,7 +669,7 @@
"no_blocked_users": "Nincs letiltott felhasználó", "no_blocked_users": "Nincs letiltott felhasználó",
"friend_code_copied": "Barát kód kimásolva", "friend_code_copied": "Barát kód kimásolva",
"undo_friendship_modal_text": "Ezáltal megszünteted a barátságod vele: {{displayName}}", "undo_friendship_modal_text": "Ezáltal megszünteted a barátságod vele: {{displayName}}",
"privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállításokba</0>", "privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállítások</0> menüjébe",
"locked_profile": "Ez a profil privát", "locked_profile": "Ez a profil privát",
"image_process_failure": "Hiba a kép feldolgozása közben", "image_process_failure": "Hiba a kép feldolgozása közben",
"required_field": "Ez a mező kötelező", "required_field": "Ez a mező kötelező",

View File

@@ -16,6 +16,7 @@
"library": "Biblioteca", "library": "Biblioteca",
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Ajustes", "settings": "Ajustes",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Já disponível",
"my_library": "Biblioteca", "my_library": "Biblioteca",
"downloading_metadata": "{{title}} (Baixando metadados…)", "downloading_metadata": "{{title}} (Baixando metadados…)",
"paused": "{{title}} (Pausado)", "paused": "{{title}} (Pausado)",
@@ -115,7 +116,6 @@
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}", "downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…", "calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…",
"checking_files": "Verificando arquivos de {{title}}…", "checking_files": "Verificando arquivos de {{title}}…",
"extracting": "Extraindo {{title}}… ({{percentage}} concluído)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Instalação concluída", "installation_complete": "Instalação concluída",
"installation_complete_message": "Componentes recomendados instalados com sucesso" "installation_complete_message": "Componentes recomendados instalados com sucesso"
@@ -191,7 +191,6 @@
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os arquivos que foram baixados pelo Hydra", "danger_zone_section_description": "Remova o jogo da sua biblioteca ou os arquivos que foram baixados pelo Hydra",
"download_in_progress": "Download em andamento", "download_in_progress": "Download em andamento",
"download_paused": "Download pausado", "download_paused": "Download pausado",
"extracting": "Extraindo",
"last_downloaded_option": "Última opção baixada", "last_downloaded_option": "Última opção baixada",
"new_download_option": "Novo", "new_download_option": "Novo",
"create_steam_shortcut": "Criar atalho na Steam", "create_steam_shortcut": "Criar atalho na Steam",
@@ -405,12 +404,10 @@
"options": "Gerenciar", "options": "Gerenciar",
"extract": "Extrair arquivos", "extract": "Extrair arquivos",
"extracting": "Extraindo arquivos…", "extracting": "Extraindo arquivos…",
"delete_archive_title": "Deseja deletar {{fileName}}?", "network": "Rede",
"delete_archive_description": "O arquivo foi extraído com sucesso e não é mais necessário.", "peak": "Pico",
"yes": "Sim", "seeds": "Seeds",
"no": "Não", "peers": "Peers"
"network": "REDE",
"peak": "PICO"
}, },
"settings": { "settings": {
"downloads_path": "Diretório dos downloads", "downloads_path": "Diretório dos downloads",

View File

@@ -15,6 +15,7 @@
"catalogue": "Catálogo", "catalogue": "Catálogo",
"downloads": "Transferências", "downloads": "Transferências",
"settings": "Definições", "settings": "Definições",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Já disponível",
"my_library": "Biblioteca", "my_library": "Biblioteca",
"downloading_metadata": "{{title}} (A transferir metadados…)", "downloading_metadata": "{{title}} (A transferir metadados…)",
"paused": "{{title}} (Em pausa)", "paused": "{{title}} (Em pausa)",
@@ -229,7 +230,13 @@
"seeding": "A semear", "seeding": "A semear",
"stop_seeding": "Parar de semear", "stop_seeding": "Parar de semear",
"resume_seeding": "Semear", "resume_seeding": "Semear",
"options": "Opções" "options": "Opções",
"extract": "Extrair ficheiros",
"extracting": "A extrair ficheiros…",
"network": "Rede",
"peak": "Pico",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Local das transferências", "downloads_path": "Local das transferências",

View File

@@ -16,6 +16,7 @@
"library": "Библиотека", "library": "Библиотека",
"downloads": "Загрузки", "downloads": "Загрузки",
"settings": "Настройки", "settings": "Настройки",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Доступно",
"my_library": "Библиотека", "my_library": "Библиотека",
"downloading_metadata": "{{title}} (Загрузка метаданных…)", "downloading_metadata": "{{title}} (Загрузка метаданных…)",
"paused": "{{title}} (Приостановлено)", "paused": "{{title}} (Приостановлено)",
@@ -414,7 +415,11 @@
"resume_seeding": "Продолжить раздачу", "resume_seeding": "Продолжить раздачу",
"options": "Управлять", "options": "Управлять",
"extract": "Распаковать файлы", "extract": "Распаковать файлы",
"extracting": "Распаковка файлов…" "extracting": "Распаковка файлов…",
"network": "Сеть",
"peak": "Пик",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Путь загрузок", "downloads_path": "Путь загрузок",

View File

@@ -1,23 +0,0 @@
import fs from "node:fs";
import { registerEvent } from "../register-event";
import { logger } from "@main/services";
const deleteArchive = async (
_event: Electron.IpcMainInvokeEvent,
filePath: string
) => {
try {
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
logger.info(`Deleted archive: ${filePath}`);
return true;
}
return true;
} catch (err) {
logger.error(`Failed to delete archive: ${filePath}`, err);
return false;
}
};
registerEvent("deleteArchive", deleteArchive);

View File

@@ -22,7 +22,6 @@ const extractGameDownload = async (
await downloadsSublevel.put(gameKey, { await downloadsSublevel.put(gameKey, {
...download, ...download,
extracting: true, extracting: true,
extractionProgress: 0,
}); });
const gameFilesManager = new GameFilesManager(shop, objectId); const gameFilesManager = new GameFilesManager(shop, objectId);

View File

@@ -8,7 +8,6 @@ import "./close-game";
import "./copy-custom-game-asset"; import "./copy-custom-game-asset";
import "./create-game-shortcut"; import "./create-game-shortcut";
import "./create-steam-shortcut"; import "./create-steam-shortcut";
import "./delete-archive";
import "./delete-game-folder"; import "./delete-game-folder";
import "./extract-game-download"; import "./extract-game-download";
import "./get-default-wine-prefix-selection-path"; import "./get-default-wine-prefix-selection-path";

View File

@@ -82,7 +82,6 @@ const startGameDownload = async (
queued: true, queued: true,
extracting: false, extracting: false,
automaticallyExtract, automaticallyExtract,
extractionProgress: 0,
}; };
try { try {

View File

@@ -33,7 +33,9 @@ export const loadState = async () => {
await import("./events"); await import("./events");
Aria2.spawn(); if (process.platform !== "darwin") {
Aria2.spawn();
}
if (userPreferences?.realDebridApiToken) { if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences.realDebridApiToken); RealDebridClient.authorize(userPreferences.realDebridApiToken);

View File

@@ -1,5 +1,5 @@
import { app } from "electron"; import { app } from "electron";
import Seven, { CommandLineSwitches } from "node-7z"; import cp from "node:child_process";
import path from "node:path"; import path from "node:path";
import { logger } from "./logger"; import { logger } from "./logger";
@@ -9,17 +9,6 @@ export const binaryName = {
win32: "7z.exe", win32: "7z.exe",
}; };
export interface ExtractionProgress {
percent: number;
fileCount: number;
file: string;
}
export interface ExtractionResult {
success: boolean;
extractedFiles: string[];
}
export class SevenZip { export class SevenZip {
private static readonly binaryPath = app.isPackaged private static readonly binaryPath = app.isPackaged
? path.join(process.resourcesPath, binaryName[process.platform]) ? path.join(process.resourcesPath, binaryName[process.platform])
@@ -43,109 +32,43 @@ export class SevenZip {
cwd?: string; cwd?: string;
passwords?: string[]; passwords?: string[];
}, },
onProgress?: (progress: ExtractionProgress) => void successCb: () => void,
): Promise<ExtractionResult> { errorCb: () => void
return new Promise((resolve, reject) => { ) {
const tryPassword = (index = -1) => { const tryPassword = (index = -1) => {
const password = passwords[index] ?? ""; const password = passwords[index] ?? "";
logger.info( logger.info(`Trying password ${password} on ${filePath}`);
`Trying password "${password || "(empty)"}" on ${filePath}`
);
const extractedFiles: string[] = []; const args = ["x", filePath, "-y", "-p" + password];
let fileCount = 0;
const options: CommandLineSwitches = { if (outputPath) {
$bin: this.binaryPath, args.push("-o" + outputPath);
$progress: true, }
yes: true,
password: password || undefined,
};
if (outputPath) { const child = cp.execFile(this.binaryPath, args, {
options.outputDir = outputPath; cwd,
});
child.once("exit", (code) => {
if (code === 0) {
successCb();
return;
} }
const stream = Seven.extractFull(filePath, outputPath || cwd || ".", { if (index < passwords.length - 1) {
...options,
$spawnOptions: cwd ? { cwd } : undefined,
});
stream.on("progress", (progress) => {
if (onProgress) {
onProgress({
percent: progress.percent,
fileCount: fileCount,
file: progress.fileCount?.toString() || "",
});
}
});
stream.on("data", (data) => {
if (data.file) {
extractedFiles.push(data.file);
fileCount++;
}
});
stream.on("end", () => {
logger.info( logger.info(
`Successfully extracted ${filePath} (${extractedFiles.length} files)` `Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
); );
resolve({
success: true,
extractedFiles,
});
});
stream.on("error", (err) => { tryPassword(index + 1);
logger.error(`Extraction error for ${filePath}:`, err); } else {
logger.info(`Failed to extract file: ${filePath}`);
if (index < passwords.length - 1) { errorCb();
logger.info(
`Failed to extract file: ${filePath} with password: "${password}". Trying next password...`
);
tryPassword(index + 1);
} else {
logger.error(
`Failed to extract file: ${filePath} after trying all passwords`
);
reject(new Error(`Failed to extract file: ${filePath}`));
}
});
};
tryPassword();
});
}
public static listFiles(
filePath: string,
password?: string
): Promise<string[]> {
return new Promise((resolve, reject) => {
const files: string[] = [];
const options: CommandLineSwitches = {
$bin: this.binaryPath,
password: password || undefined,
};
const stream = Seven.list(filePath, options);
stream.on("data", (data) => {
if (data.file) {
files.push(data.file);
} }
}); });
};
stream.on("end", () => { tryPassword();
resolve(files);
});
stream.on("error", (err) => {
reject(err);
});
});
} }
} }

View File

@@ -7,12 +7,9 @@ export class Aria2 {
private static process: cp.ChildProcess | null = null; private static process: cp.ChildProcess | null = null;
public static spawn() { public static spawn() {
const binaryPath = const binaryPath = app.isPackaged
process.platform === "darwin" ? path.join(process.resourcesPath, "aria2c")
? "aria2c" : path.join(__dirname, "..", "..", "binaries", "aria2c");
: app.isPackaged
? path.join(process.resourcesPath, "aria2c")
: path.join(__dirname, "..", "..", "binaries", "aria2c");
this.process = cp.spawn( this.process = cp.spawn(
binaryPath, binaryPath,

View File

@@ -74,16 +74,21 @@ export class DeckyPlugin {
await fs.promises.mkdir(extractPath, { recursive: true }); await fs.promises.mkdir(extractPath, { recursive: true });
try { return new Promise((resolve, reject) => {
await SevenZip.extractFile({ SevenZip.extractFile(
filePath: zipPath, {
outputPath: extractPath, filePath: zipPath,
}); outputPath: extractPath,
logger.log(`Plugin extracted to: ${extractPath}`); },
return extractPath; () => {
} catch { logger.log(`Plugin extracted to: ${extractPath}`);
throw new Error("Failed to extract plugin"); resolve(extractPath);
} },
() => {
reject(new Error("Failed to extract plugin"));
}
);
});
} }
private static needsSudo(): boolean { private static needsSudo(): boolean {

View File

@@ -126,10 +126,21 @@ export class DownloadManager {
} }
); );
if (WindowManager.mainWindow && download) {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(
JSON.stringify({
...status,
game,
})
)
);
}
const shouldExtract = download.automaticallyExtract; const shouldExtract = download.automaticallyExtract;
// Handle download completion BEFORE sending progress to renderer
// This ensures extraction starts and DB is updated before UI reacts
if (progress === 1 && download) { if (progress === 1 && download) {
publishDownloadCompleteNotification(game); publishDownloadCompleteNotification(game);
@@ -143,7 +154,6 @@ export class DownloadManager {
shouldSeed: true, shouldSeed: true,
queued: false, queued: false,
extracting: shouldExtract, extracting: shouldExtract,
extractionProgress: shouldExtract ? 0 : download.extractionProgress,
}); });
} else { } else {
await downloadsSublevel.put(gameId, { await downloadsSublevel.put(gameId, {
@@ -152,22 +162,12 @@ export class DownloadManager {
shouldSeed: false, shouldSeed: false,
queued: false, queued: false,
extracting: shouldExtract, extracting: shouldExtract,
extractionProgress: shouldExtract ? 0 : download.extractionProgress,
}); });
this.cancelDownload(gameId); this.cancelDownload(gameId);
} }
if (shouldExtract) { if (shouldExtract) {
// Send initial extraction progress BEFORE download progress
// This ensures the UI shows extraction immediately
WindowManager.mainWindow?.webContents.send(
"on-extraction-progress",
game.shop,
game.objectId,
0
);
const gameFilesManager = new GameFilesManager( const gameFilesManager = new GameFilesManager(
game.shop, game.shop,
game.objectId game.objectId
@@ -209,18 +209,6 @@ export class DownloadManager {
this.downloadingGameId = null; this.downloadingGameId = null;
} }
} }
// Send progress to renderer after completion handling
if (WindowManager.mainWindow && download) {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
WindowManager.mainWindow.webContents.send(
"on-download-progress",
structuredClone({
...status,
game,
})
);
}
} }
} }

View File

@@ -3,58 +3,24 @@ import fs from "node:fs";
import type { GameShop } from "@types"; import type { GameShop } from "@types";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared"; import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
import { SevenZip, ExtractionProgress } from "./7zip"; import { SevenZip } from "./7zip";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { publishExtractionCompleteNotification } from "./notifications"; import { publishExtractionCompleteNotification } from "./notifications";
import { logger } from "./logger"; import { logger } from "./logger";
const PROGRESS_THROTTLE_MS = 1000;
export class GameFilesManager { export class GameFilesManager {
private lastProgressUpdate = 0;
constructor( constructor(
private readonly shop: GameShop, private readonly shop: GameShop,
private readonly objectId: string private readonly objectId: string
) {} ) {}
private get gameKey() {
return levelKeys.game(this.shop, this.objectId);
}
private async updateExtractionProgress(progress: number, force = false) {
const now = Date.now();
if (!force && now - this.lastProgressUpdate < PROGRESS_THROTTLE_MS) {
return;
}
this.lastProgressUpdate = now;
const download = await downloadsSublevel.get(this.gameKey);
if (!download) return;
await downloadsSublevel.put(this.gameKey, {
...download,
extractionProgress: progress,
});
WindowManager.mainWindow?.webContents.send(
"on-extraction-progress",
this.shop,
this.objectId,
progress
);
}
private async clearExtractionState() { private async clearExtractionState() {
const download = await downloadsSublevel.get(this.gameKey); const gameKey = levelKeys.game(this.shop, this.objectId);
if (!download) return; const download = await downloadsSublevel.get(gameKey);
await downloadsSublevel.put(this.gameKey, { await downloadsSublevel.put(gameKey, {
...download, ...download!,
extracting: false, extracting: false,
extractionProgress: 0,
}); });
WindowManager.mainWindow?.webContents.send( WindowManager.mainWindow?.webContents.send(
@@ -64,10 +30,6 @@ export class GameFilesManager {
); );
} }
private readonly handleProgress = (progress: ExtractionProgress) => {
this.updateExtractionProgress(progress.percent / 100);
};
async extractFilesInDirectory(directoryPath: string) { async extractFilesInDirectory(directoryPath: string) {
if (!fs.existsSync(directoryPath)) return; if (!fs.existsSync(directoryPath)) return;
const files = await fs.promises.readdir(directoryPath); const files = await fs.promises.readdir(directoryPath);
@@ -80,66 +42,53 @@ export class GameFilesManager {
(file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file) (file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file)
); );
if (filesToExtract.length === 0) return; await Promise.all(
filesToExtract.map((file) => {
await this.updateExtractionProgress(0, true); return new Promise((resolve, reject) => {
SevenZip.extractFile(
const totalFiles = filesToExtract.length; {
let completedFiles = 0; filePath: path.join(directoryPath, file),
cwd: directoryPath,
for (const file of filesToExtract) { passwords: ["online-fix.me", "steamrip.com"],
try { },
const result = await SevenZip.extractFile( () => {
{ resolve(true);
filePath: path.join(directoryPath, file), },
cwd: directoryPath, () => {
passwords: ["online-fix.me", "steamrip.com"], reject(new Error(`Failed to extract file: ${file}`));
}, this.clearExtractionState();
(progress) => { }
const overallProgress =
(completedFiles + progress.percent / 100) / totalFiles;
this.updateExtractionProgress(overallProgress);
}
);
if (result.success) {
completedFiles++;
await this.updateExtractionProgress(
completedFiles / totalFiles,
true
); );
} });
} catch (err) { })
logger.error(`Failed to extract file: ${file}`, err); );
await this.clearExtractionState();
return; compressedFiles.forEach((file) => {
const extractionPath = path.join(directoryPath, file);
if (fs.existsSync(extractionPath)) {
fs.unlink(extractionPath, (err) => {
if (err) {
logger.error(`Failed to delete file: ${file}`, err);
this.clearExtractionState();
}
});
} }
} });
const archivePaths = compressedFiles
.map((file) => path.join(directoryPath, file))
.filter((archivePath) => fs.existsSync(archivePath));
if (archivePaths.length > 0) {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
archivePaths
);
}
} }
async setExtractionComplete(publishNotification = true) { async setExtractionComplete(publishNotification = true) {
const gameKey = levelKeys.game(this.shop, this.objectId);
const [download, game] = await Promise.all([ const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey), downloadsSublevel.get(gameKey),
gamesSublevel.get(this.gameKey), gamesSublevel.get(gameKey),
]); ]);
if (!download) return; await downloadsSublevel.put(gameKey, {
...download!,
await downloadsSublevel.put(this.gameKey, {
...download,
extracting: false, extracting: false,
extractionProgress: 0,
}); });
WindowManager.mainWindow?.webContents.send( WindowManager.mainWindow?.webContents.send(
@@ -148,15 +97,17 @@ export class GameFilesManager {
this.objectId this.objectId
); );
if (publishNotification && game) { if (publishNotification) {
publishExtractionCompleteNotification(game); publishExtractionCompleteNotification(game!);
} }
} }
async extractDownloadedFile() { async extractDownloadedFile() {
const gameKey = levelKeys.game(this.shop, this.objectId);
const [download, game] = await Promise.all([ const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey), downloadsSublevel.get(gameKey),
gamesSublevel.get(this.gameKey), gamesSublevel.get(gameKey),
]); ]);
if (!download || !game) return false; if (!download || !game) return false;
@@ -168,39 +119,39 @@ export class GameFilesManager {
path.parse(download.folderName!).name path.parse(download.folderName!).name
); );
await this.updateExtractionProgress(0, true); SevenZip.extractFile(
{
try { filePath,
const result = await SevenZip.extractFile( outputPath: extractionPath,
{ passwords: ["online-fix.me", "steamrip.com"],
filePath, },
outputPath: extractionPath, async () => {
passwords: ["online-fix.me", "steamrip.com"],
},
this.handleProgress
);
if (result.success) {
await this.extractFilesInDirectory(extractionPath); await this.extractFilesInDirectory(extractionPath);
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) { if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
WindowManager.mainWindow?.webContents.send( fs.unlink(filePath, (err) => {
"on-archive-deletion-prompt", if (err) {
[filePath] logger.error(
); `Failed to delete file: ${download.folderName}`,
err
);
this.clearExtractionState();
}
});
} }
await downloadsSublevel.put(this.gameKey, { await downloadsSublevel.put(gameKey, {
...download, ...download!,
folderName: path.parse(download.folderName!).name, folderName: path.parse(download.folderName!).name,
}); });
await this.setExtractionComplete(); this.setExtractionComplete();
},
() => {
this.clearExtractionState();
} }
} catch (err) { );
logger.error(`Failed to extract downloaded file: ${filePath}`, err);
await this.clearExtractionState();
}
return true; return true;
} }

View File

@@ -36,13 +36,16 @@ export class GofileApi {
} }
public static async getDownloadLink(id: string) { public static async getDownloadLink(id: string) {
const searchParams = new URLSearchParams({
wt: WT,
});
const response = await axios.get<{ const response = await axios.get<{
status: string; status: string;
data: GofileContentsResponse; data: GofileContentsResponse;
}>(`https://api.gofile.io/contents/${id}`, { }>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
headers: { headers: {
Authorization: `Bearer ${this.token}`, Authorization: `Bearer ${this.token}`,
"X-Website-Token": WT,
}, },
}); });

View File

@@ -58,13 +58,7 @@ export class HydraApi {
const decodedBase64 = atob(payload as string); const decodedBase64 = atob(payload as string);
const jsonData = JSON.parse(decodedBase64); const jsonData = JSON.parse(decodedBase64);
const { const { accessToken, expiresIn, refreshToken } = jsonData;
accessToken,
expiresIn,
refreshToken,
featurebaseJwt,
workwondersJwt,
} = jsonData;
const now = new Date(); const now = new Date();
@@ -91,8 +85,6 @@ export class HydraApi {
accessToken, accessToken,
refreshToken, refreshToken,
tokenExpirationTimestamp, tokenExpirationTimestamp,
featurebaseJwt,
workwondersJwt,
}, },
{ valueEncoding: "json" } { valueEncoding: "json" }
); );

View File

@@ -1,87 +0,0 @@
declare module "node-7z" {
import { ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
export interface CommandLineSwitches {
$bin?: string;
$progress?: boolean;
$spawnOptions?: {
cwd?: string;
};
outputDir?: string;
yes?: boolean;
password?: string;
[key: string]: unknown;
}
export interface ProgressInfo {
percent: number;
fileCount?: number;
}
export interface FileInfo {
file?: string;
[key: string]: unknown;
}
export interface ZipStream extends EventEmitter {
on(event: "progress", listener: (progress: ProgressInfo) => void): this;
on(event: "data", listener: (data: FileInfo) => void): this;
on(event: "end", listener: () => void): this;
on(event: "error", listener: (err: Error) => void): this;
info: Map<string, unknown>;
_childProcess?: ChildProcess;
}
export function extractFull(
archive: string,
output: string,
options?: CommandLineSwitches
): ZipStream;
export function extract(
archive: string,
output: string,
options?: CommandLineSwitches
): ZipStream;
export function list(
archive: string,
options?: CommandLineSwitches
): ZipStream;
export function add(
archive: string,
files: string | string[],
options?: CommandLineSwitches
): ZipStream;
export function update(
archive: string,
files: string | string[],
options?: CommandLineSwitches
): ZipStream;
export function deleteFiles(
archive: string,
files: string | string[],
options?: CommandLineSwitches
): ZipStream;
export function test(
archive: string,
options?: CommandLineSwitches
): ZipStream;
const Seven: {
extractFull: typeof extractFull;
extract: typeof extract;
list: typeof list;
add: typeof add;
update: typeof update;
delete: typeof deleteFiles;
test: typeof test;
};
export default Seven;
}

View File

@@ -138,8 +138,7 @@ export class WindowManager {
(details, callback) => { (details, callback) => {
if ( if (
details.webContentsId !== this.mainWindow?.webContents.id || details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot") || details.url.includes("chatwoot")
details.url.includes("workwonders")
) { ) {
return callback(details); return callback(details);
} }
@@ -160,8 +159,7 @@ export class WindowManager {
if ( if (
details.webContentsId !== this.mainWindow?.webContents.id || details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("featurebase") || details.url.includes("featurebase") ||
details.url.includes("chatwoot") || details.url.includes("chatwoot")
details.url.includes("workwonders")
) { ) {
return callback(details); return callback(details);
} }
@@ -356,6 +354,8 @@ export class WindowManager {
public static async createNotificationWindow() { public static async createNotificationWindow() {
if (this.notificationWindow) return; if (this.notificationWindow) return;
if (process.platform === "darwin") return;
const userPreferences = await db.get<string, UserPreferences | undefined>( const userPreferences = await db.get<string, UserPreferences | undefined>(
levelKeys.userPreferences, levelKeys.userPreferences,
{ {

View File

@@ -267,29 +267,6 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.on("on-extraction-complete", listener); ipcRenderer.on("on-extraction-complete", listener);
return () => ipcRenderer.removeListener("on-extraction-complete", listener); return () => ipcRenderer.removeListener("on-extraction-complete", listener);
}, },
onExtractionProgress: (
cb: (shop: GameShop, objectId: string, progress: number) => void
) => {
const listener = (
_event: Electron.IpcRendererEvent,
shop: GameShop,
objectId: string,
progress: number
) => cb(shop, objectId, progress);
ipcRenderer.on("on-extraction-progress", listener);
return () => ipcRenderer.removeListener("on-extraction-progress", listener);
},
onArchiveDeletionPrompt: (cb: (archivePaths: string[]) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
archivePaths: string[]
) => cb(archivePaths);
ipcRenderer.on("on-archive-deletion-prompt", listener);
return () =>
ipcRenderer.removeListener("on-archive-deletion-prompt", listener);
},
deleteArchive: (filePath: string) =>
ipcRenderer.invoke("deleteArchive", filePath),
/* Hardware */ /* Hardware */
getDiskFreeSpace: (path: string) => getDiskFreeSpace: (path: string) =>

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef } from "react";
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
import { import {
@@ -19,14 +19,11 @@ import {
setUserDetails, setUserDetails,
setProfileBackground, setProfileBackground,
setGameRunning, setGameRunning,
setExtractionProgress,
clearExtraction,
} from "@renderer/features"; } from "@renderer/features";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
import { useSubscription } from "./hooks/use-subscription"; import { useSubscription } from "./hooks/use-subscription";
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal"; import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
import { ArchiveDeletionModal } from "./pages/downloads/archive-deletion-error-modal";
import { import {
injectCustomCss, injectCustomCss,
@@ -81,10 +78,6 @@ export function App() {
const { showSuccessToast } = useToast(); const { showSuccessToast } = useToast();
const [showArchiveDeletionModal, setShowArchiveDeletionModal] =
useState(false);
const [archivePaths, setArchivePaths] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
levelDBService.get("userPreferences", null, "json"), levelDBService.get("userPreferences", null, "json"),
@@ -191,23 +184,12 @@ export function App() {
updateLibrary(); updateLibrary();
}), }),
window.electron.onSignOut(() => clearUserDetails()), window.electron.onSignOut(() => clearUserDetails()),
window.electron.onExtractionProgress((shop, objectId, progress) => {
dispatch(setExtractionProgress({ shop, objectId, progress }));
}),
window.electron.onExtractionComplete(() => {
dispatch(clearExtraction());
updateLibrary();
}),
window.electron.onArchiveDeletionPrompt((paths) => {
setArchivePaths(paths);
setShowArchiveDeletionModal(true);
}),
]; ];
return () => { return () => {
listeners.forEach((unsubscribe) => unsubscribe()); listeners.forEach((unsubscribe) => unsubscribe());
}; };
}, [onSignIn, updateLibrary, clearUserDetails, dispatch]); }, [onSignIn, updateLibrary, clearUserDetails]);
useEffect(() => { useEffect(() => {
if (contentRef.current) contentRef.current.scrollTop = 0; if (contentRef.current) contentRef.current.scrollTop = 0;
@@ -299,12 +281,6 @@ export function App() {
feature={hydraCloudFeature} feature={hydraCloudFeature}
/> />
<ArchiveDeletionModal
visible={showArchiveDeletionModal}
archivePaths={archivePaths}
onClose={() => setShowArchiveDeletionModal(false)}
/>
{userDetails && ( {userDetails && (
<UserFriendModal <UserFriendModal
visible={isFriendsModalVisible} visible={isFriendsModalVisible}

View File

@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
useAppSelector,
useDownload, useDownload,
useLibrary, useLibrary,
useToast, useToast,
@@ -27,8 +26,6 @@ export function BottomPanel() {
const { lastPacket, progress, downloadSpeed, eta } = useDownload(); const { lastPacket, progress, downloadSpeed, eta } = useDownload();
const extraction = useAppSelector((state) => state.download.extraction);
const [version, setVersion] = useState(""); const [version, setVersion] = useState("");
const [sessionHash, setSessionHash] = useState<null | string>(""); const [sessionHash, setSessionHash] = useState<null | string>("");
const [commonRedistStatus, setCommonRedistStatus] = useState<string | null>( const [commonRedistStatus, setCommonRedistStatus] = useState<string | null>(
@@ -71,20 +68,6 @@ export function BottomPanel() {
return t("installing_common_redist", { log: commonRedistStatus }); return t("installing_common_redist", { log: commonRedistStatus });
} }
if (extraction) {
const extractingGame = library.find(
(game) => game.id === extraction.visibleId
);
if (extractingGame) {
const extractionPercentage = Math.round(extraction.progress * 100);
return t("extracting", {
title: extractingGame.title,
percentage: `${extractionPercentage}%`,
});
}
}
const game = lastPacket const game = lastPacket
? library.find((game) => game.id === lastPacket?.gameId) ? library.find((game) => game.id === lastPacket?.gameId)
: undefined; : undefined;
@@ -126,7 +109,6 @@ export function BottomPanel() {
eta, eta,
downloadSpeed, downloadSpeed,
commonRedistStatus, commonRedistStatus,
extraction,
]); ]);
return ( return (
@@ -140,10 +122,10 @@ export function BottomPanel() {
</button> </button>
<button <button
data-open-workwonders-changelog-mini data-featurebase-changelog
className="bottom-panel__version-button" className="bottom-panel__version-button"
> >
<small> <small data-featurebase-changelog>
{sessionHash ? `${sessionHash} -` : ""} v{version} &quot; {sessionHash ? `${sessionHash} -` : ""} v{version} &quot;
{VERSION_CODENAME}&quot; {VERSION_CODENAME}&quot;
</small> </small>

View File

@@ -1,82 +0,0 @@
@use "../../scss/globals.scss";
.fullscreen-media-modal__overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(2px);
z-index: globals.$backdrop-z-index;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
margin: 0;
}
.fullscreen-media-modal {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
position: relative;
margin: 0;
padding: 0;
border: none;
background: transparent;
max-width: none;
max-height: none;
&__close-button {
position: absolute;
top: calc(globals.$spacing-unit * 5);
right: calc(globals.$spacing-unit * 4);
cursor: pointer;
color: globals.$body-color;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
border: 1px solid globals.$border-color;
padding: globals.$spacing-unit;
display: flex;
align-items: center;
justify-content: center;
transition: all ease 0.2s;
z-index: 10;
&:hover {
background-color: rgba(0, 0, 0, 0.8);
transform: scale(1.1);
}
}
&__image-container {
max-width: 90%;
max-height: 90%;
display: flex;
justify-content: center;
align-items: center;
}
&__image {
max-width: 100%;
max-height: 60vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: image-appear 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
}
@keyframes image-appear {
0% {
opacity: 0;
transform: scale(0.85);
}
100% {
opacity: 1;
transform: scale(1);
}
}

View File

@@ -1,87 +0,0 @@
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { XIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import "./fullscreen-media-modal.scss";
export interface FullscreenMediaModalProps {
visible: boolean;
onClose: () => void;
src: string | null | undefined;
alt?: string;
}
export function FullscreenMediaModal({
visible,
onClose,
src,
alt,
}: FullscreenMediaModalProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const { t } = useTranslation("modal");
useEffect(() => {
if (visible) {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}
return () => {};
}, [onClose, visible]);
useEffect(() => {
const onMouseDown = (e: MouseEvent) => {
if (containerRef.current) {
const clickedOnImage = containerRef.current.contains(e.target as Node);
if (!clickedOnImage) {
onClose();
}
}
};
if (visible) {
window.addEventListener("mousedown", onMouseDown);
}
return () => {
window.removeEventListener("mousedown", onMouseDown);
};
}, [onClose, visible]);
if (!visible || !src) return null;
return createPortal(
<div className="fullscreen-media-modal__overlay">
<dialog className="fullscreen-media-modal" open aria-label={alt}>
<button
type="button"
onClick={onClose}
className="fullscreen-media-modal__close-button"
aria-label={t("close")}
>
<XIcon size={24} />
</button>
<div
ref={containerRef}
className="fullscreen-media-modal__image-container"
>
<img src={src} alt={alt} className="fullscreen-media-modal__image" />
</div>
</dialog>
</div>,
document.body
);
}

View File

@@ -20,4 +20,3 @@ export * from "./game-context-menu/game-context-menu";
export * from "./game-context-menu/use-game-actions"; export * from "./game-context-menu/use-game-actions";
export * from "./star-rating/star-rating"; export * from "./star-rating/star-rating";
export * from "./search-dropdown/search-dropdown"; export * from "./search-dropdown/search-dropdown";
export * from "./fullscreen-media-modal/fullscreen-media-modal";

View File

@@ -32,4 +32,15 @@ export const routes = [
nameKey: "settings", nameKey: "settings",
render: () => <GearIcon />, render: () => <GearIcon />,
}, },
{
path: "https://hydrawrapped.com",
nameKey: "hydra_2025_wrapped",
render: () => (
<img
src="https://cdn.losbroxas.org/thumbnail_hydra_badge2_fb01af31e3.png"
alt="Hydra 2025 Wrapped"
style={{ width: 16, height: 16 }}
/>
),
},
]; ];

View File

@@ -88,6 +88,34 @@
); );
} }
} }
&--wrapped {
background: linear-gradient(
135deg,
rgba(74, 144, 226, 0.25) 0%,
rgba(123, 104, 238, 0.2) 25%,
rgba(59, 130, 246, 0.25) 50%,
rgba(96, 165, 250, 0.2) 75%,
rgba(74, 144, 226, 0.25) 100%
);
background-size: 200% 200%;
animation: wrapped-gradient-flow 8s ease infinite;
color: globals.$muted-color;
position: relative;
overflow: hidden;
&:hover {
background: linear-gradient(
135deg,
rgba(74, 144, 226, 0.35) 0%,
rgba(123, 104, 238, 0.3) 25%,
rgba(59, 130, 246, 0.35) 50%,
rgba(96, 165, 250, 0.3) 75%,
rgba(74, 144, 226, 0.35) 100%
);
background-size: 200% 200%;
}
}
} }
&__menu-item-button { &__menu-item-button {
@@ -106,6 +134,21 @@
overflow: hidden; overflow: hidden;
} }
&__menu-item-marquee {
overflow: hidden;
flex: 1;
min-width: 0;
}
&__menu-item-marquee-content {
display: inline-flex;
}
&__menu-item-marquee-content span {
display: inline-block;
flex-shrink: 0;
}
&__game-icon { &__game-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -228,3 +271,24 @@
} }
} }
} }
@keyframes wrapped-gradient-flow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes marquee-scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-50% - 1em));
}
}

View File

@@ -22,6 +22,8 @@ import { buildGameDetailsPath } from "@renderer/helpers";
import { SidebarProfile } from "./sidebar-profile"; import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es"; import { sortBy } from "lodash-es";
import cn from "classnames"; import cn from "classnames";
import { logger } from "@renderer/logger";
import { motion } from "framer-motion";
import { import {
CommentDiscussionIcon, CommentDiscussionIcon,
PlayIcon, PlayIcon,
@@ -238,8 +240,32 @@ export function Sidebar() {
return game.title; return game.title;
}; };
const handleSidebarItemClick = (path: string) => { const handleSidebarItemClick = async (path: string) => {
if (path !== location.pathname) { if (path.startsWith("http")) {
if (path === "https://hydrawrapped.com") {
try {
const auth = await window.electron.getAuth();
if (auth) {
const payload = {
accessToken: auth.accessToken,
refreshToken: auth.refreshToken,
expiresIn: 3600,
};
const base64Payload = btoa(JSON.stringify(payload));
window.electron.openExternal(
`${path}?payload=${encodeURIComponent(base64Payload)}`
);
} else {
window.electron.openExternal(path);
}
} catch (error) {
logger.error("Failed to get auth for wrapped:", error);
window.electron.openExternal(path);
}
} else {
window.electron.openExternal(path);
}
} else if (path !== location.pathname) {
navigate(path); navigate(path);
} }
}; };
@@ -297,7 +323,10 @@ export function Sidebar() {
<li <li
key={nameKey} key={nameKey}
className={cn("sidebar__menu-item", { className={cn("sidebar__menu-item", {
"sidebar__menu-item--active": location.pathname === path, "sidebar__menu-item--active":
!path.startsWith("http") && location.pathname === path,
"sidebar__menu-item--wrapped":
nameKey === "hydra_2025_wrapped",
})} })}
> >
<button <button
@@ -306,7 +335,33 @@ export function Sidebar() {
onClick={() => handleSidebarItemClick(path)} onClick={() => handleSidebarItemClick(path)}
> >
{render()} {render()}
<span>{t(nameKey)}</span> {nameKey === "hydra_2025_wrapped" ? (
<div className="sidebar__menu-item-marquee">
<motion.div
className="sidebar__menu-item-marquee-content"
animate={{
x: ["0%", "-50%"],
}}
transition={{
x: {
repeat: Infinity,
repeatType: "loop",
duration: 8,
ease: "linear",
},
}}
>
<span>
{t(nameKey)} &nbsp;&nbsp;&nbsp;&nbsp;
</span>
<span>
{t(nameKey)} &nbsp;&nbsp;&nbsp;&nbsp;
</span>
</motion.div>
</div>
) : (
<span>{t(nameKey)}</span>
)}
</button> </button>
</li> </li>
))} ))}

View File

@@ -208,13 +208,6 @@ declare global {
onExtractionComplete: ( onExtractionComplete: (
cb: (shop: GameShop, objectId: string) => void cb: (shop: GameShop, objectId: string) => void
) => () => Electron.IpcRenderer; ) => () => Electron.IpcRenderer;
onExtractionProgress: (
cb: (shop: GameShop, objectId: string, progress: number) => void
) => () => Electron.IpcRenderer;
onArchiveDeletionPrompt: (
cb: (archivePaths: string[]) => void
) => () => Electron.IpcRenderer;
deleteArchive: (filePath: string) => Promise<boolean>;
getDefaultWinePrefixSelectionPath: () => Promise<string | null>; getDefaultWinePrefixSelectionPath: () => Promise<string | null>;
createSteamShortcut: (shop: GameShop, objectId: string) => Promise<void>; createSteamShortcut: (shop: GameShop, objectId: string) => Promise<void>;

View File

@@ -1,28 +1,17 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit";
import type { DownloadProgress, GameShop } from "@types"; import type { DownloadProgress } from "@types";
export interface ExtractionInfo {
visibleId: string;
progress: number;
}
export interface DownloadState { export interface DownloadState {
lastPacket: DownloadProgress | null; lastPacket: DownloadProgress | null;
gameId: string | null; gameId: string | null;
gamesWithDeletionInProgress: string[]; gamesWithDeletionInProgress: string[];
extraction: ExtractionInfo | null;
peakSpeeds: Record<string, number>;
speedHistory: Record<string, number[]>;
} }
const initialState: DownloadState = { const initialState: DownloadState = {
lastPacket: null, lastPacket: null,
gameId: null, gameId: null,
gamesWithDeletionInProgress: [], gamesWithDeletionInProgress: [],
extraction: null,
peakSpeeds: {},
speedHistory: {},
}; };
export const downloadSlice = createSlice({ export const downloadSlice = createSlice({
@@ -32,27 +21,6 @@ export const downloadSlice = createSlice({
setLastPacket: (state, action: PayloadAction<DownloadProgress | null>) => { setLastPacket: (state, action: PayloadAction<DownloadProgress | null>) => {
state.lastPacket = action.payload; state.lastPacket = action.payload;
if (!state.gameId && action.payload) state.gameId = action.payload.gameId; if (!state.gameId && action.payload) state.gameId = action.payload.gameId;
// Track peak speed and speed history atomically when packet arrives
if (action.payload?.gameId && action.payload.downloadSpeed != null) {
const { gameId, downloadSpeed } = action.payload;
// Update peak speed if this is higher
const currentPeak = state.peakSpeeds[gameId] || 0;
if (downloadSpeed > currentPeak) {
state.peakSpeeds[gameId] = downloadSpeed;
}
// Update speed history for chart
if (!state.speedHistory[gameId]) {
state.speedHistory[gameId] = [];
}
state.speedHistory[gameId].push(downloadSpeed);
// Keep only last 120 entries
if (state.speedHistory[gameId].length > 120) {
state.speedHistory[gameId].shift();
}
}
}, },
clearDownload: (state) => { clearDownload: (state) => {
state.lastPacket = null; state.lastPacket = null;
@@ -70,37 +38,6 @@ export const downloadSlice = createSlice({
const index = state.gamesWithDeletionInProgress.indexOf(action.payload); const index = state.gamesWithDeletionInProgress.indexOf(action.payload);
if (index >= 0) state.gamesWithDeletionInProgress.splice(index, 1); if (index >= 0) state.gamesWithDeletionInProgress.splice(index, 1);
}, },
setExtractionProgress: (
state,
action: PayloadAction<{
shop: GameShop;
objectId: string;
progress: number;
}>
) => {
const { shop, objectId, progress } = action.payload;
state.extraction = {
visibleId: `${shop}:${objectId}`,
progress,
};
},
clearExtraction: (state) => {
state.extraction = null;
},
updatePeakSpeed: (
state,
action: PayloadAction<{ gameId: string; speed: number }>
) => {
const { gameId, speed } = action.payload;
const currentPeak = state.peakSpeeds[gameId] || 0;
if (speed > currentPeak) {
state.peakSpeeds[gameId] = speed;
}
},
clearPeakSpeed: (state, action: PayloadAction<string>) => {
state.peakSpeeds[action.payload] = 0;
state.speedHistory[action.payload] = [];
},
}, },
}); });
@@ -109,8 +46,4 @@ export const {
clearDownload, clearDownload,
setGameDeleting, setGameDeleting,
removeGameFromDeleting, removeGameFromDeleting,
setExtractionProgress,
clearExtraction,
updatePeakSpeed,
clearPeakSpeed,
} = downloadSlice.actions; } = downloadSlice.actions;

View File

@@ -1,44 +0,0 @@
import { useTranslation } from "react-i18next";
import { ConfirmationModal } from "@renderer/components";
interface ArchiveDeletionModalProps {
visible: boolean;
archivePaths: string[];
onClose: () => void;
}
export function ArchiveDeletionModal({
visible,
archivePaths,
onClose,
}: Readonly<ArchiveDeletionModalProps>) {
const { t } = useTranslation("downloads");
const fullFileName =
archivePaths.length > 0 ? (archivePaths[0].split(/[/\\]/).pop() ?? "") : "";
const maxLength = 40;
const fileName =
fullFileName.length > maxLength
? `${fullFileName.slice(0, maxLength)}`
: fullFileName;
const handleConfirm = async () => {
for (const archivePath of archivePaths) {
await window.electron.deleteArchive(archivePath);
}
onClose();
};
return (
<ConfirmationModal
visible={visible}
title={t("delete_archive_title", { fileName })}
descriptionText={t("delete_archive_description")}
confirmButtonLabel={t("yes")}
cancelButtonLabel={t("no")}
onConfirm={handleConfirm}
onClose={onClose}
/>
);
}

View File

@@ -108,11 +108,16 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
transition: scale 0.2s ease; transition: opacity 0.2s ease;
outline: none; outline: none;
&:hover { &:hover {
scale: 1.05; opacity: 0.8;
}
&:focus,
&:focus-visible {
outline: none;
} }
} }
@@ -390,21 +395,6 @@
flex-shrink: 0; flex-shrink: 0;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
border: 1px solid globals.$border-color; border: 1px solid globals.$border-color;
padding: 0;
cursor: pointer;
transition:
opacity 0.2s ease,
transform 0.2s ease;
&:hover {
opacity: 0.9;
}
&:focus,
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 2px;
}
img { img {
width: 100%; width: 100%;
@@ -421,21 +411,6 @@
gap: calc(globals.$spacing-unit / 1); gap: calc(globals.$spacing-unit / 1);
} }
&__simple-title-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
text-align: left;
width: 100%;
transition: opacity 0.2s ease;
&:focus,
&:focus-visible {
outline: none;
}
}
&__simple-title { &__simple-title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
@@ -536,9 +511,5 @@
background-color: #fff; background-color: #fff;
transition: width 0.3s ease; transition: width 0.3s ease;
border-radius: 4px; border-radius: 4px;
&--extraction {
background-color: #fff;
}
} }
} }

View File

@@ -128,20 +128,16 @@ function SpeedChart({
g = 255, g = 255,
b = 255; b = 255;
if (color.startsWith("#")) { if (color.startsWith("#")) {
let hex = color.replace("#", ""); const hex = color.replace("#", "");
// Handle shorthand hex colors (e.g., "#fff" -> "#ffffff") r = Number.parseInt(hex.substring(0, 2), 16);
if (hex.length === 3) { g = Number.parseInt(hex.substring(2, 4), 16);
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; b = Number.parseInt(hex.substring(4, 6), 16);
}
r = Number.parseInt(hex.substring(0, 2), 16) || 255;
g = Number.parseInt(hex.substring(2, 4), 16) || 255;
b = Number.parseInt(hex.substring(4, 6), 16) || 255;
} else if (color.startsWith("rgb")) { } else if (color.startsWith("rgb")) {
const matches = color.match(/\d+/g); const matches = color.match(/\d+/g);
if (matches && matches.length >= 3) { if (matches && matches.length >= 3) {
r = Number.parseInt(matches[0]) || 255; r = Number.parseInt(matches[0]);
g = Number.parseInt(matches[1]) || 255; g = Number.parseInt(matches[1]);
b = Number.parseInt(matches[2]) || 255; b = Number.parseInt(matches[2]);
} }
} }
const displaySpeeds = speeds.slice(-totalBars); const displaySpeeds = speeds.slice(-totalBars);
@@ -207,7 +203,6 @@ function SpeedChart({
interface HeroDownloadViewProps { interface HeroDownloadViewProps {
game: LibraryGame; game: LibraryGame;
isGameDownloading: boolean; isGameDownloading: boolean;
isGameExtracting?: boolean;
downloadSpeed: number; downloadSpeed: number;
finalDownloadSize: string; finalDownloadSize: string;
peakSpeed: number; peakSpeed: number;
@@ -226,7 +221,6 @@ interface HeroDownloadViewProps {
function HeroDownloadView({ function HeroDownloadView({
game, game,
isGameDownloading, isGameDownloading,
isGameExtracting = false,
downloadSpeed, downloadSpeed,
finalDownloadSize, finalDownloadSize,
peakSpeed, peakSpeed,
@@ -284,17 +278,11 @@ function HeroDownloadView({
<div className="download-group__progress-row download-group__progress-row--bar"> <div className="download-group__progress-row download-group__progress-row--bar">
<div className="download-group__progress-wrapper"> <div className="download-group__progress-wrapper">
<div className="download-group__progress-info-row"> <div className="download-group__progress-info-row">
{isGameExtracting && ( {lastPacket?.isCheckingFiles ? (
<span className="download-group__progress-status">
{t("extracting")}
</span>
)}
{!isGameExtracting && lastPacket?.isCheckingFiles && (
<span className="download-group__progress-status"> <span className="download-group__progress-status">
{t("checking_files")} {t("checking_files")}
</span> </span>
)} ) : (
{!isGameExtracting && !lastPacket?.isCheckingFiles && (
<span className="download-group__progress-size"> <span className="download-group__progress-size">
<DownloadIcon size={14} /> <DownloadIcon size={14} />
{isGameDownloading && lastPacket {isGameDownloading && lastPacket
@@ -305,7 +293,7 @@ function HeroDownloadView({
<span></span> <span></span>
</div> </div>
<div className="download-group__progress-info-row"> <div className="download-group__progress-info-row">
{!lastPacket?.isCheckingFiles && !isGameExtracting && ( {!lastPacket?.isCheckingFiles && (
<span className="download-group__progress-time"> <span className="download-group__progress-time">
{isGameDownloading && {isGameDownloading &&
lastPacket?.timeRemaining && lastPacket?.timeRemaining &&
@@ -317,50 +305,50 @@ function HeroDownloadView({
)} )}
</span> </span>
)} )}
<span className="download-group__progress-percentage"> {(!lastPacket?.isCheckingFiles || currentProgress > 0) && (
<AnimatedPercentage value={currentProgress} /> <span className="download-group__progress-percentage">
</span> <AnimatedPercentage value={currentProgress} />
</span>
)}
</div> </div>
<div className="download-group__progress-bar"> <div className="download-group__progress-bar">
<div <div
className={`download-group__progress-fill ${isGameExtracting ? "download-group__progress-fill--extraction" : ""}`} className="download-group__progress-fill"
style={{ style={{
width: `${currentProgress * 100}%`, width: `${currentProgress * 100}%`,
}} }}
/> />
</div> </div>
</div> </div>
{!isGameExtracting && ( <div className="download-group__hero-buttons">
<div className="download-group__hero-buttons"> {isGameDownloading ? (
{isGameDownloading ? (
<button
type="button"
onClick={() => pauseDownload(game.shop, game.objectId)}
className="download-group__glass-btn"
>
<ColumnsIcon size={14} />
{t("pause")}
</button>
) : (
<button
type="button"
onClick={() => resumeDownload(game.shop, game.objectId)}
className="download-group__glass-btn"
>
<PlayIcon size={14} />
{t("resume")}
</button>
)}
<button <button
type="button" type="button"
onClick={() => cancelDownload(game.shop, game.objectId)} onClick={() => pauseDownload(game.shop, game.objectId)}
className="download-group__glass-btn" className="download-group__glass-btn"
> >
<XCircleIcon size={14} /> <ColumnsIcon size={14} />
{t("cancel")} {t("pause")}
</button> </button>
</div> ) : (
)} <button
type="button"
onClick={() => resumeDownload(game.shop, game.objectId)}
className="download-group__glass-btn"
>
<PlayIcon size={14} />
{t("resume")}
</button>
)}
<button
type="button"
onClick={() => cancelDownload(game.shop, game.objectId)}
className="download-group__glass-btn"
>
<XCircleIcon size={14} />
{t("cancel")}
</button>
</div>
</div> </div>
</div> </div>
@@ -372,7 +360,7 @@ function HeroDownloadView({
</span> </span>
<div className="download-group__stat-content"> <div className="download-group__stat-content">
<span className="download-group__stat-label"> <span className="download-group__stat-label">
{t("network")}: {t("network")}
</span> </span>
<span className="download-group__stat-value"> <span className="download-group__stat-value">
{isGameDownloading ? formatSpeed(downloadSpeed) : "0 B/s"} {isGameDownloading ? formatSpeed(downloadSpeed) : "0 B/s"}
@@ -385,39 +373,38 @@ function HeroDownloadView({
<GraphIcon size={16} /> <GraphIcon size={16} />
</span> </span>
<div className="download-group__stat-content"> <div className="download-group__stat-content">
<span className="download-group__stat-label">{t("peak")}:</span> <span className="download-group__stat-label">{t("peak")}</span>
<span className="download-group__stat-value"> <span className="download-group__stat-value">
{peakSpeed > 0 ? formatSpeed(peakSpeed) : "0 B/s"} {peakSpeed > 0 ? formatSpeed(peakSpeed) : "0 B/s"}
</span> </span>
</div> </div>
</div> </div>
{game.download?.downloader === Downloader.Torrent && {game.download?.downloader && (
isGameDownloading &&
lastPacket &&
(lastPacket.numSeeds > 0 || lastPacket.numPeers > 0) && (
<div className="download-group__stat-item">
<div className="download-group__stat-content">
<span className="download-group__stat-label">
Seeds:{" "}
<span className="download-group__stat-value">
{lastPacket.numSeeds}
</span>
, Peers:{" "}
<span className="download-group__stat-value">
{lastPacket.numPeers}
</span>
</span>
</div>
</div>
)}
{game.download?.downloader !== undefined && (
<div className="download-group__stat-item"> <div className="download-group__stat-item">
<div className="download-group__stat-content"> <div
<Badge> className="download-group__stat-content"
{DOWNLOADER_NAME[Number(game.download.downloader)]} style={{
</Badge> justifyContent: "space-between",
alignItems: "center",
}}
>
<Badge>{DOWNLOADER_NAME[game.download.downloader]}</Badge>
{game.download?.downloader === Downloader.Torrent &&
isGameDownloading &&
lastPacket &&
(lastPacket.numSeeds > 0 || lastPacket.numPeers > 0) && (
<span className="download-group__stat-label">
{t("seeds")}{" "}
<span className="download-group__stat-value">
{lastPacket.numSeeds}
</span>
, {t("peers")}{" "}
<span className="download-group__stat-value">
{lastPacket.numPeers}
</span>
</span>
)}
</div> </div>
</div> </div>
)} )}
@@ -458,8 +445,6 @@ export function DownloadGroup({
(state) => state.userPreferences.value (state) => state.userPreferences.value
); );
const extraction = useAppSelector((state) => state.download.extraction);
const { updateLibrary } = useLibrary(); const { updateLibrary } = useLibrary();
const { const {
@@ -514,9 +499,8 @@ export function DownloadGroup({
const { formatDistance } = useDate(); const { formatDistance } = useDate();
// Get speed history and peak speeds from Redux (centralized state) const [peakSpeeds, setPeakSpeeds] = useState<Record<string, number>>({});
const speedHistory = useAppSelector((state) => state.download.speedHistory); const speedHistoryRef = useRef<Record<string, number[]>>({});
const peakSpeeds = useAppSelector((state) => state.download.peakSpeeds);
const [dominantColors, setDominantColors] = useState<Record<string, string>>( const [dominantColors, setDominantColors] = useState<Record<string, string>>(
{} {}
); );
@@ -579,8 +563,68 @@ export function DownloadGroup({
}); });
}, [library, lastPacket?.gameId]); }, [library, lastPacket?.gameId]);
// Speed history and peak speeds are now tracked in Redux (in setLastPacket reducer) useEffect(() => {
// No local effect needed - data is updated atomically when packets arrive if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) {
const gameId = lastPacket.gameId;
const currentPeak = peakSpeeds[gameId] || 0;
if (lastPacket.downloadSpeed > currentPeak) {
setPeakSpeeds((prev) => ({
...prev,
[gameId]: lastPacket.downloadSpeed,
}));
}
if (!speedHistoryRef.current[gameId]) {
speedHistoryRef.current[gameId] = [];
}
speedHistoryRef.current[gameId].push(lastPacket.downloadSpeed);
if (speedHistoryRef.current[gameId].length > 120) {
speedHistoryRef.current[gameId].shift();
}
}
}, [lastPacket?.gameId, lastPacket?.downloadSpeed, peakSpeeds]);
useEffect(() => {
for (const game of library) {
if (
game.download &&
game.download.progress < 0.01 &&
game.download.status !== "paused"
) {
// Fresh download - clear any old data
if (speedHistoryRef.current[game.id]?.length > 0) {
speedHistoryRef.current[game.id] = [];
setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 }));
}
}
}
}, [library]);
useEffect(() => {
const timeouts: NodeJS.Timeout[] = [];
for (const game of library) {
if (
game.download?.progress === 1 &&
speedHistoryRef.current[game.id]?.length > 0
) {
const timeout = setTimeout(() => {
speedHistoryRef.current[game.id] = [];
setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 }));
}, 10_000);
timeouts.push(timeout);
}
}
return () => {
for (const timeout of timeouts) {
clearTimeout(timeout);
}
};
}, [library]);
useEffect(() => { useEffect(() => {
if (library.length > 0 && title === t("download_in_progress")) { if (library.length > 0 && title === t("download_in_progress")) {
@@ -778,28 +822,16 @@ export function DownloadGroup({
if (isDownloadingGroup && library.length > 0) { if (isDownloadingGroup && library.length > 0) {
const game = library[0]; const game = library[0];
const isGameExtracting = extraction?.visibleId === game.id; const isGameDownloading = isGameDownloadingMap[game.id];
const isGameDownloading =
isGameDownloadingMap[game.id] && !isGameExtracting;
const downloadSpeed = isGameDownloading const downloadSpeed = isGameDownloading
? (lastPacket?.downloadSpeed ?? 0) ? (lastPacket?.downloadSpeed ?? 0)
: 0; : 0;
const finalDownloadSize = getFinalDownloadSize(game); const finalDownloadSize = getFinalDownloadSize(game);
// Use lastPacket.gameId for lookup since that's the key used to store the data const peakSpeed = peakSpeeds[game.id] || 0;
// Fall back to game.id if lastPacket is not available const currentProgress =
const dataKey = lastPacket?.gameId ?? game.id; isGameDownloading && lastPacket
const gameSpeedHistory = speedHistory[dataKey] ?? []; ? lastPacket.progress
const storedPeak = peakSpeeds[dataKey]; : game.download?.progress || 0;
// Use stored peak if available and > 0, otherwise use current speed as initial value
const peakSpeed =
storedPeak !== undefined && storedPeak > 0 ? storedPeak : downloadSpeed;
let currentProgress = game.download?.progress || 0;
if (isGameExtracting) {
currentProgress = extraction.progress;
} else if (isGameDownloading && lastPacket) {
currentProgress = lastPacket.progress;
}
const dominantColor = dominantColors[game.id] || "#fff"; const dominantColor = dominantColors[game.id] || "#fff";
@@ -807,14 +839,13 @@ export function DownloadGroup({
<HeroDownloadView <HeroDownloadView
game={game} game={game}
isGameDownloading={isGameDownloading} isGameDownloading={isGameDownloading}
isGameExtracting={isGameExtracting}
downloadSpeed={downloadSpeed} downloadSpeed={downloadSpeed}
finalDownloadSize={finalDownloadSize} finalDownloadSize={finalDownloadSize}
peakSpeed={peakSpeed} peakSpeed={peakSpeed}
currentProgress={currentProgress} currentProgress={currentProgress}
dominantColor={dominantColor} dominantColor={dominantColor}
lastPacket={lastPacket} lastPacket={lastPacket}
speedHistory={gameSpeedHistory} speedHistory={speedHistoryRef.current[game.id] || []}
formatSpeed={formatSpeed} formatSpeed={formatSpeed}
calculateETA={calculateETA} calculateETA={calculateETA}
pauseDownload={pauseDownload} pauseDownload={pauseDownload}
@@ -842,8 +873,14 @@ export function DownloadGroup({
<li key={game.id} className="download-group__simple-card"> <li key={game.id} className="download-group__simple-card">
<button <button
type="button" type="button"
onClick={() => navigate(buildGameDetailsPath(game))}
className="download-group__simple-thumbnail" className="download-group__simple-thumbnail"
onClick={() => navigate(buildGameDetailsPath(game))}
style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
}}
> >
<img src={game.libraryImageUrl || ""} alt={game.title} /> <img src={game.libraryImageUrl || ""} alt={game.title} />
</button> </button>
@@ -851,22 +888,27 @@ export function DownloadGroup({
<div className="download-group__simple-info"> <div className="download-group__simple-info">
<button <button
type="button" type="button"
className="download-group__simple-title"
onClick={() => navigate(buildGameDetailsPath(game))} onClick={() => navigate(buildGameDetailsPath(game))}
className="download-group__simple-title-button" style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
textAlign: "left",
width: "100%",
}}
> >
<h3 className="download-group__simple-title">{game.title}</h3> {game.title}
</button> </button>
<div className="download-group__simple-meta"> <div className="download-group__simple-meta">
<div className="download-group__simple-meta-row"> <div className="download-group__simple-meta-row">
<Badge> <Badge>{DOWNLOADER_NAME[game.download!.downloader]}</Badge>
{DOWNLOADER_NAME[Number(game.download!.downloader)]}
</Badge>
</div> </div>
<div className="download-group__simple-meta-row"> <div className="download-group__simple-meta-row">
{extraction?.visibleId === game.id ? ( {game.download?.extracting ? (
<span className="download-group__simple-extracting"> <span className="download-group__simple-extracting">
{t("extracting")} ( {t("extracting")}
{Math.round(extraction.progress * 100)}%)
</span> </span>
) : ( ) : (
<span className="download-group__simple-size"> <span className="download-group__simple-size">

View File

@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks"; import { useDownload, useLibrary } from "@renderer/hooks";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal"; import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
@@ -13,7 +13,6 @@ import { ArrowDownIcon } from "@primer/octicons-react";
export default function Downloads() { export default function Downloads() {
const { library, updateLibrary } = useLibrary(); const { library, updateLibrary } = useLibrary();
const extraction = useAppSelector((state) => state.download.extraction);
const { t } = useTranslation("downloads"); const { t } = useTranslation("downloads");
@@ -40,13 +39,11 @@ export default function Downloads() {
useEffect(() => { useEffect(() => {
window.electron.onSeedingStatus((value) => setSeedingStatus(value)); window.electron.onSeedingStatus((value) => setSeedingStatus(value));
const unsubscribeExtraction = window.electron.onExtractionComplete(() => { const unsubscribe = window.electron.onExtractionComplete(() => {
updateLibrary(); updateLibrary();
}); });
return () => { return () => unsubscribe();
unsubscribeExtraction();
};
}, [updateLibrary]); }, [updateLibrary]);
const handleOpenGameInstaller = (shop: GameShop, objectId: string) => const handleOpenGameInstaller = (shop: GameShop, objectId: string) =>
@@ -75,10 +72,8 @@ export default function Downloads() {
/* Game has been manually added to the library */ /* Game has been manually added to the library */
if (!next.download) return prev; if (!next.download) return prev;
/* Is downloading or extracting */ /* Is downloading */
const isExtracting = if (lastPacket?.gameId === next.id || next.download.extracting)
next.download.extracting || extraction?.visibleId === next.id;
if (lastPacket?.gameId === next.id || isExtracting)
return { ...prev, downloading: [...prev.downloading, next] }; return { ...prev, downloading: [...prev.downloading, next] };
/* Is either queued or paused */ /* Is either queued or paused */
@@ -101,7 +96,7 @@ export default function Downloads() {
queued, queued,
complete, complete,
}; };
}, [library, lastPacket?.gameId, extraction?.visibleId]); }, [library, lastPacket?.gameId]);
const downloadGroups = [ const downloadGroups = [
{ {

View File

@@ -1,12 +1,7 @@
import { useContext, useEffect, useMemo, useState } from "react"; import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { formatDownloadProgress } from "@renderer/helpers"; import { formatDownloadProgress } from "@renderer/helpers";
import { import { useDate, useDownload, useFormat } from "@renderer/hooks";
useAppSelector,
useDate,
useDownload,
useFormat,
} from "@renderer/hooks";
import { Link } from "@renderer/components"; import { Link } from "@renderer/components";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
@@ -22,9 +17,6 @@ export function HeroPanelPlaytime() {
const { numberFormatter } = useFormat(); const { numberFormatter } = useFormat();
const { progress, lastPacket } = useDownload(); const { progress, lastPacket } = useDownload();
const { formatDistance } = useDate(); const { formatDistance } = useDate();
const extraction = useAppSelector((state) => state.download.extraction);
const isExtracting = extraction?.visibleId === game?.id;
useEffect(() => { useEffect(() => {
if (game?.lastTimePlayed) { if (game?.lastTimePlayed) {
@@ -60,16 +52,6 @@ export function HeroPanelPlaytime() {
const isGameDownloading = const isGameDownloading =
game.download?.status === "active" && lastPacket?.gameId === game.id; game.download?.status === "active" && lastPacket?.gameId === game.id;
const extractionInProgressInfo = (
<div className="hero-panel-playtime__download-details">
<Link to="/downloads" className="hero-panel-playtime__downloads-link">
{t("extracting")}
</Link>
<small>{formatDownloadProgress(extraction?.progress ?? 0)}</small>
</div>
);
const downloadInProgressInfo = ( const downloadInProgressInfo = (
<div className="hero-panel-playtime__download-details"> <div className="hero-panel-playtime__download-details">
<Link to="/downloads" className="hero-panel-playtime__downloads-link"> <Link to="/downloads" className="hero-panel-playtime__downloads-link">
@@ -90,8 +72,7 @@ export function HeroPanelPlaytime() {
return ( return (
<> <>
<p>{t("not_played_yet", { title: game?.title })}</p> <p>{t("not_played_yet", { title: game?.title })}</p>
{isExtracting && extractionInProgressInfo} {hasDownload && downloadInProgressInfo}
{!isExtracting && hasDownload && downloadInProgressInfo}
</> </>
); );
} }
@@ -100,8 +81,7 @@ export function HeroPanelPlaytime() {
return ( return (
<> <>
<p>{t("playing_now")}</p> <p>{t("playing_now")}</p>
{isExtracting && extractionInProgressInfo} {hasDownload && downloadInProgressInfo}
{!isExtracting && hasDownload && downloadInProgressInfo}
</> </>
); );
} }
@@ -133,9 +113,9 @@ export function HeroPanelPlaytime() {
})} })}
</p> </p>
{isExtracting && extractionInProgressInfo} {hasDownload ? (
{!isExtracting && hasDownload && downloadInProgressInfo} downloadInProgressInfo
{!isExtracting && !hasDownload && ( ) : (
<p> <p>
{t("last_time_played", { {t("last_time_played", {
period: lastTimePlayed, period: lastTimePlayed,

View File

@@ -80,11 +80,5 @@
&--disabled { &--disabled {
opacity: globals.$disabled-opacity; opacity: globals.$disabled-opacity;
} }
&--extraction {
&::-webkit-progress-value {
background-color: #fff;
}
}
} }
} }

View File

@@ -1,7 +1,7 @@
import { useContext } from "react"; import { useContext } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppSelector, useDate, useDownload } from "@renderer/hooks"; import { useDate, useDownload } from "@renderer/hooks";
import { HeroPanelActions } from "./hero-panel-actions"; import { HeroPanelActions } from "./hero-panel-actions";
import { HeroPanelPlaytime } from "./hero-panel-playtime"; import { HeroPanelPlaytime } from "./hero-panel-playtime";
@@ -18,13 +18,9 @@ export function HeroPanel() {
const { lastPacket } = useDownload(); const { lastPacket } = useDownload();
const extraction = useAppSelector((state) => state.download.extraction);
const isGameDownloading = const isGameDownloading =
game?.download?.status === "active" && lastPacket?.gameId === game?.id; game?.download?.status === "active" && lastPacket?.gameId === game?.id;
const isExtracting = extraction?.visibleId === game?.id;
const getInfo = () => { const getInfo = () => {
if (!game) { if (!game) {
const [latestRepack] = repacks; const [latestRepack] = repacks;
@@ -53,8 +49,6 @@ export function HeroPanel() {
(game?.download?.status === "active" && game?.download?.progress < 1) || (game?.download?.status === "active" && game?.download?.progress < 1) ||
game?.download?.status === "paused"; game?.download?.status === "paused";
const showExtractionProgressBar = isExtracting;
return ( return (
<div className="hero-panel__container"> <div className="hero-panel__container">
<div className="hero-panel"> <div className="hero-panel">
@@ -78,14 +72,6 @@ export function HeroPanel() {
}`} }`}
/> />
)} )}
{showExtractionProgressBar && (
<progress
max={1}
value={extraction?.progress ?? 0}
className="hero-panel__progress-bar hero-panel__progress-bar--extraction"
/>
)}
</div> </div>
</div> </div>
); );

View File

@@ -87,12 +87,16 @@ export function LibraryTab({
<ul className="profile-content__games-grid"> <ul className="profile-content__games-grid">
{pinnedGames?.map((game) => ( {pinnedGames?.map((game) => (
<li key={game.objectId} style={{ listStyle: "none" }}> <li
key={game.objectId}
style={{ listStyle: "none" }}
className="user-library-game__wrapper"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<UserLibraryGameCard <UserLibraryGameCard
game={game} game={game}
statIndex={statsIndex} statIndex={statsIndex}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
sortBy={sortBy} sortBy={sortBy}
/> />
</li> </li>
@@ -134,6 +138,9 @@ export function LibraryTab({
<motion.li <motion.li
key={`${sortBy}-${game.objectId}`} key={`${sortBy}-${game.objectId}`}
style={{ listStyle: "none" }} style={{ listStyle: "none" }}
className="user-library-game__wrapper"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
initial={ initial={
isNewGame isNewGame
? { opacity: 0.5, y: 15, scale: 0.96 } ? { opacity: 0.5, y: 15, scale: 0.96 }
@@ -160,8 +167,6 @@ export function LibraryTab({
<UserLibraryGameCard <UserLibraryGameCard
game={game} game={game}
statIndex={statsIndex} statIndex={statsIndex}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
sortBy={sortBy} sortBy={sortBy}
/> />
</motion.li> </motion.li>

View File

@@ -25,16 +25,12 @@ import "./user-library-game-card.scss";
interface UserLibraryGameCardProps { interface UserLibraryGameCardProps {
game: UserGame; game: UserGame;
statIndex: number; statIndex: number;
onMouseEnter: () => void;
onMouseLeave: () => void;
sortBy?: string; sortBy?: string;
} }
export function UserLibraryGameCard({ export function UserLibraryGameCard({
game, game,
statIndex, statIndex,
onMouseEnter,
onMouseLeave,
sortBy, sortBy,
}: UserLibraryGameCardProps) { }: UserLibraryGameCardProps) {
const { userProfile, isMe, getUserLibraryGames } = const { userProfile, isMe, getUserLibraryGames } =
@@ -130,129 +126,119 @@ export function UserLibraryGameCard({
return ( return (
<> <>
<li <button
onMouseEnter={onMouseEnter} type="button"
onMouseLeave={onMouseLeave} className="user-library-game__cover"
className="user-library-game__wrapper" onClick={() => navigate(buildUserGameDetailsPath(game))}
title={isTooltipHovered ? undefined : game.title} title={isTooltipHovered ? undefined : game.title}
> >
<button <div className="user-library-game__overlay">
type="button" {isMe && (
className="user-library-game__cover" <div className="user-library-game__actions-container">
onClick={() => navigate(buildUserGameDetailsPath(game))} <button
> type="button"
<div className="user-library-game__overlay"> className="user-library-game__pin-button"
{isMe && ( onClick={(e) => {
<div className="user-library-game__actions-container"> e.stopPropagation();
<button toggleGamePinned();
type="button" }}
className="user-library-game__pin-button" disabled={isPinning}
onClick={(e) => { >
e.stopPropagation(); {game.isPinned ? (
toggleGamePinned(); <PinSlashIcon size={12} />
}} ) : (
disabled={isPinning} <PinIcon size={12} />
> )}
{game.isPinned ? ( </button>
<PinSlashIcon size={12} />
) : (
<PinIcon size={12} />
)}
</button>
</div>
)}
<div
className="user-library-game__playtime"
data-tooltip-place="top"
data-tooltip-content={
game.hasManuallyUpdatedPlaytime
? t("manual_playtime_tooltip")
: undefined
}
data-tooltip-id={game.objectId}
>
{game.hasManuallyUpdatedPlaytime ? (
<AlertFillIcon
size={11}
className="user-library-game__manual-playtime"
/>
) : (
<ClockIcon size={11} />
)}
<span className="user-library-game__playtime-long">
{formatPlayTime(game.playTimeInSeconds)}
</span>
<span className="user-library-game__playtime-short">
{formatPlayTime(game.playTimeInSeconds, true)}
</span>
</div> </div>
)}
<div
className="user-library-game__playtime"
data-tooltip-place="top"
data-tooltip-content={
game.hasManuallyUpdatedPlaytime
? t("manual_playtime_tooltip")
: undefined
}
data-tooltip-id={game.objectId}
>
{game.hasManuallyUpdatedPlaytime ? (
<AlertFillIcon
size={11}
className="user-library-game__manual-playtime"
/>
) : (
<ClockIcon size={11} />
)}
<span className="user-library-game__playtime-long">
{formatPlayTime(game.playTimeInSeconds)}
</span>
<span className="user-library-game__playtime-short">
{formatPlayTime(game.playTimeInSeconds, true)}
</span>
</div>
{userProfile?.hasActiveSubscription && {userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
game.achievementCount > 0 && ( <div className="user-library-game__stats">
<div className="user-library-game__stats"> <div className="user-library-game__stats-header">
<div className="user-library-game__stats-header"> <div className="user-library-game__stats-content">
<div className="user-library-game__stats-content"> <div
<div className="user-library-game__stats-item"
className="user-library-game__stats-item" style={{
style={{ transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`, }}
}} >
> <TrophyIcon size={13} />
<TrophyIcon size={13} />
<span>
{game.unlockedAchievementCount} /{" "}
{game.achievementCount}
</span>
</div>
{game.achievementsPointsEarnedSum > 0 && (
<div
className="user-library-game__stats-item"
style={{
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
}}
>
<HydraIcon width={16} height={16} />
{formatAchievementPoints(
game.achievementsPointsEarnedSum
)}
</div>
)}
</div>
<span> <span>
{formatDownloadProgress( {game.unlockedAchievementCount} / {game.achievementCount}
game.unlockedAchievementCount / game.achievementCount,
1
)}
</span> </span>
</div> </div>
<progress {game.achievementsPointsEarnedSum > 0 && (
max={1} <div
value={ className="user-library-game__stats-item"
game.unlockedAchievementCount / game.achievementCount style={{
} transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
className="user-library-game__achievements-progress" }}
/> >
<HydraIcon width={16} height={16} />
{formatAchievementPoints(
game.achievementsPointsEarnedSum
)}
</div>
)}
</div> </div>
)}
</div>
{imageError || !game.coverImageUrl ? ( <span>
<div className="user-library-game__cover-placeholder"> {formatDownloadProgress(
<ImageIcon size={48} /> game.unlockedAchievementCount / game.achievementCount,
1
)}
</span>
</div>
<progress
max={1}
value={game.unlockedAchievementCount / game.achievementCount}
className="user-library-game__achievements-progress"
/>
</div> </div>
) : (
<img
src={game.coverImageUrl}
alt={game.title}
className="user-library-game__game-image"
onError={() => setImageError(true)}
/>
)} )}
</button> </div>
</li>
{imageError || !game.coverImageUrl ? (
<div className="user-library-game__cover-placeholder">
<ImageIcon size={48} />
</div>
) : (
<img
src={game.coverImageUrl}
alt={game.title}
className="user-library-game__game-image"
onError={() => setImageError(true)}
/>
)}
</button>
<Tooltip <Tooltip
id={game.objectId} id={game.objectId}
style={{ style={{

View File

@@ -9,12 +9,7 @@ import {
XCircleFillIcon, XCircleFillIcon,
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import { import { Avatar, Button, Link } from "@renderer/components";
Avatar,
Button,
FullscreenMediaModal,
Link,
} from "@renderer/components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
useAppSelector, useAppSelector,
@@ -38,7 +33,6 @@ type FriendAction =
export function ProfileHero() { export function ProfileHero() {
const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false);
const [isPerformingAction, setIsPerformingAction] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false);
const { const {
@@ -252,12 +246,10 @@ export function ProfileHero() {
]); ]);
const handleAvatarClick = useCallback(() => { const handleAvatarClick = useCallback(() => {
if (userProfile?.profileImageUrl) { if (isMe) {
setShowFullscreenAvatar(true);
} else if (isMe) {
setShowEditProfileModal(true); setShowEditProfileModal(true);
} }
}, [isMe, userProfile?.profileImageUrl]); }, [isMe]);
const currentGame = useMemo(() => { const currentGame = useMemo(() => {
if (isMe) { if (isMe) {
@@ -280,13 +272,6 @@ export function ProfileHero() {
onClose={() => setShowEditProfileModal(false)} onClose={() => setShowEditProfileModal(false)}
/> />
<FullscreenMediaModal
visible={showFullscreenAvatar}
onClose={() => setShowFullscreenAvatar(false)}
src={userProfile?.profileImageUrl}
alt={userProfile?.displayName}
/>
<section <section
className="profile-hero__content-box" className="profile-hero__content-box"
style={{ background: !backgroundImage ? heroBackground : undefined }} style={{ background: !backgroundImage ? heroBackground : undefined }}

View File

@@ -20,8 +20,6 @@ export interface Auth {
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
tokenExpirationTimestamp: number; tokenExpirationTimestamp: number;
featurebaseJwt: string;
workwondersJwt: string;
} }
export interface User { export interface User {
@@ -82,7 +80,6 @@ export interface Download {
timestamp: number; timestamp: number;
extracting: boolean; extracting: boolean;
automaticallyExtract: boolean; automaticallyExtract: boolean;
extractionProgress: number;
} }
export interface GameAchievement { export interface GameAchievement {

View File

@@ -6330,7 +6330,7 @@ jsonwebtoken@^9.0.2:
object.assign "^4.1.4" object.assign "^4.1.4"
object.values "^1.1.6" object.values "^1.1.6"
jwa@^1.4.2: jwa@^1.4.1:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
@@ -6340,11 +6340,11 @@ jwa@^1.4.2:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
jws@^3.2.2: jws@^3.2.2:
version "3.2.3" version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g== integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies: dependencies:
jwa "^1.4.2" jwa "^1.4.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
keyv@^4.0.0, keyv@^4.5.3: keyv@^4.0.0, keyv@^4.5.3:
@@ -6438,26 +6438,11 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.defaultsdeep@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
lodash.defaultto@^4.14.0:
version "4.14.0"
resolved "https://registry.yarnpkg.com/lodash.defaultto/-/lodash.defaultto-4.14.0.tgz#38bd3d425acee733e0e2bbbd4e4b29711cc2ee11"
integrity sha512-G6tizqH6rg4P5j32Wy4Z3ZIip7OfG8YWWlPFzUFGcYStH1Ld0l1tWs6NevEQNEDnO1M3NZYjuHuraaFSN5WqeQ==
lodash.escaperegexp@^4.1.2: lodash.escaperegexp@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw== integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==
lodash.flattendeep@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==
lodash.includes@^4.3.0: lodash.includes@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
@@ -6468,11 +6453,6 @@ lodash.isboolean@^3.0.3:
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
lodash.isempty@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
integrity sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==
lodash.isequal@^4.5.0: lodash.isequal@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
@@ -6513,11 +6493,6 @@ lodash.mergewith@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.negate@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/lodash.negate/-/lodash.negate-3.0.2.tgz#9c897b0bf610019e0b43b8ff3f0afef3d7b66f34"
integrity sha512-JGJYYVslKYC0tRMm/7igfdHulCjoXjoganRNWM8AgS+RXfOvFnPkOveDhPI65F9aAypCX9QEEQoBqWf7Q6uAeA==
lodash.once@^4.0.0: lodash.once@^4.0.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
@@ -6897,19 +6872,6 @@ no-case@^3.0.4:
lower-case "^2.0.2" lower-case "^2.0.2"
tslib "^2.0.3" tslib "^2.0.3"
node-7z@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/node-7z/-/node-7z-3.0.0.tgz#42f71c5a43b00028749f7c88291a7abf2e2623e3"
integrity sha512-KIznWSxIkOYO/vOgKQfJEaXd7rgoFYKZbaurainCEdMhYc7V7mRHX+qdf2HgbpQFcdJL/Q6/XOPrDLoBeTfuZA==
dependencies:
debug "^4.3.2"
lodash.defaultsdeep "^4.6.1"
lodash.defaultto "^4.14.0"
lodash.flattendeep "^4.4.0"
lodash.isempty "^4.4.0"
lodash.negate "^3.0.2"
normalize-path "^3.0.0"
node-abi@^3.45.0: node-abi@^3.45.0:
version "3.78.0" version "3.78.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.78.0.tgz#fd0ecbd0aa89857b98da06bd3909194abb0821ba" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.78.0.tgz#fd0ecbd0aa89857b98da06bd3909194abb0821ba"
@@ -6965,11 +6927,6 @@ nopt@^6.0.0:
dependencies: dependencies:
abbrev "^1.0.0" abbrev "^1.0.0"
normalize-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-url@^6.0.1: normalize-url@^6.0.1:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"