mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-21 10:03:56 +00:00
Compare commits
37 Commits
fix/fixing
...
fix/fullsc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24d65b50b4 | ||
|
|
34681b3bc2 | ||
|
|
4b71a3f5bb | ||
|
|
a0f669c97b | ||
|
|
96fca88601 | ||
|
|
3f65bb86a8 | ||
|
|
b494c7c8ec | ||
|
|
e10007b1c3 | ||
|
|
dae825a75a | ||
|
|
677f34fe3d | ||
|
|
1524e73ee6 | ||
|
|
40d428c19e | ||
|
|
affa7a2b2e | ||
|
|
d5e6bed3b7 | ||
|
|
214267df7e | ||
|
|
21f46c9af3 | ||
|
|
cc5e0014f7 | ||
|
|
142bd3156c | ||
|
|
95a7bc2236 | ||
|
|
78d2be85f2 | ||
|
|
67ea9e78a2 | ||
|
|
67f863e0f3 | ||
|
|
77b6f1b2ad | ||
|
|
1552a5f359 | ||
|
|
63f8289d0a | ||
|
|
0470958629 | ||
|
|
3b574e6578 | ||
|
|
7f28fc8ca1 | ||
|
|
d1eb174429 | ||
|
|
82a125237b | ||
|
|
19e312d31e | ||
|
|
79b1f05cde | ||
|
|
cc9ac9dc0f | ||
|
|
19406dd051 | ||
|
|
8aa6e113e7 | ||
|
|
91ad4a68f7 | ||
|
|
817870cdbb |
@@ -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": "yarn run typecheck:node && yarn run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"build": "yarn run typecheck && electron-vite build",
|
"build": "npm 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": "yarn run build && electron-builder --dir",
|
"build:unpack": "npm 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,6 +70,7 @@
|
|||||||
"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
2
proto
Submodule proto updated: 7a23620f93...6f11c99c57
@@ -115,6 +115,7 @@
|
|||||||
"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"
|
||||||
@@ -202,6 +203,7 @@
|
|||||||
"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",
|
||||||
@@ -415,10 +417,12 @@
|
|||||||
"options": "Manage",
|
"options": "Manage",
|
||||||
"extract": "Extract files",
|
"extract": "Extract files",
|
||||||
"extracting": "Extracting files…",
|
"extracting": "Extracting files…",
|
||||||
"network": "Network",
|
"delete_archive_title": "Would you like to delete {{fileName}}?",
|
||||||
"peak": "Peak",
|
"delete_archive_description": "The file has been successfully extracted and it's no longer needed.",
|
||||||
"seeds": "Seeds",
|
"yes": "Yes",
|
||||||
"peers": "Peers"
|
"no": "No",
|
||||||
|
"network": "NETWORK",
|
||||||
|
"peak": "PEAK"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Downloads path",
|
"downloads_path": "Downloads path",
|
||||||
|
|||||||
@@ -414,11 +414,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -27,7 +27,69 @@
|
|||||||
"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 l’ajout 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 l’image",
|
||||||
|
"edit_game_modal_icon": "Icône",
|
||||||
|
"edit_game_modal_select_icon": "Sélectionner une icône",
|
||||||
|
"edit_game_modal_icon_preview": "Aperçu de l’icô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 l’image 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 l’image de l’icône ici",
|
||||||
|
"edit_game_modal_drop_logo_image_here": "Déposez l’image du logo ici",
|
||||||
|
"edit_game_modal_drop_hero_image_here": "Déposez l’image de la bannière ici",
|
||||||
|
"edit_game_modal_drop_to_replace_icon": "Déposez pour remplacer l’icô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 l’installation du plugin Decky : {{error}}",
|
||||||
|
"decky_plugin_installation_error": "Erreur lors de l’installation du plugin Decky : {{error}}",
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"cancel": "Annuler"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Rechercher",
|
"search": "Rechercher",
|
||||||
@@ -37,7 +99,15 @@
|
|||||||
"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",
|
||||||
@@ -47,7 +117,8 @@
|
|||||||
"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…",
|
||||||
@@ -198,7 +269,113 @@
|
|||||||
"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 l’envoi de l’avis. Veuillez réessayer.",
|
||||||
|
"review_cannot_be_empty": "Le champ de l’avis ne peut pas être vide.",
|
||||||
|
"review_deleted_successfully": "Avis supprimé avec succès.",
|
||||||
|
"review_deletion_failed": "Échec de la suppression de l’avis.",
|
||||||
|
"loading_reviews": "Chargement des avis…",
|
||||||
|
"loading_more_reviews": "Chargement de plus d’avis…",
|
||||||
|
"load_more_reviews": "Charger plus d’avis",
|
||||||
|
"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 l’avis",
|
||||||
|
"remove_review": "Retirer l’avis",
|
||||||
|
"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 l’enregistrement de votre vote. Veuillez réessayer.",
|
||||||
|
"show_original": "Afficher l’original",
|
||||||
|
"show_translation": "Afficher la traduction",
|
||||||
|
"show_original_translated_from": "Afficher l’original (traduit depuis {{language}})",
|
||||||
|
"hide_original": "Masquer l’original",
|
||||||
|
"review_from_blocked_user": "Avis d’un utilisateur bloqué",
|
||||||
|
"show": "Afficher",
|
||||||
|
"hide": "Masquer"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activer Hydra",
|
"title": "Activer Hydra",
|
||||||
@@ -237,7 +414,11 @@
|
|||||||
"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 n’est plus nécessaire.",
|
||||||
|
"yes": "Oui",
|
||||||
|
"no": "Non"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Chemin des téléchargements",
|
"downloads_path": "Chemin des téléchargements",
|
||||||
@@ -366,7 +547,40 @@
|
|||||||
"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 l’ajout 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 d’un jeu"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Téléchargement terminé",
|
"download_complete": "Téléchargement terminé",
|
||||||
|
|||||||
@@ -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": "A(z) {{title}} (Várakozósorban van)",
|
"queued": "{{title}} (Várakozásban)",
|
||||||
"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,6 +94,12 @@
|
|||||||
"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",
|
||||||
@@ -109,6 +115,7 @@
|
|||||||
"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"
|
||||||
@@ -165,7 +172,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ások</0> menüjében változtathatod meg",
|
"select_folder_hint": "A letöltési mappát a <0>Beállításokban</0> 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",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"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",
|
||||||
@@ -397,7 +405,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ósoron lévő letöltések",
|
"queued_downloads": "Várakozásban 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..",
|
||||||
@@ -408,7 +416,11 @@
|
|||||||
"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",
|
||||||
@@ -669,7 +681,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ások</0> menüjébe",
|
"privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállításokba</0>",
|
||||||
"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ő",
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
"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"
|
||||||
@@ -190,6 +191,7 @@
|
|||||||
"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",
|
||||||
@@ -403,10 +405,12 @@
|
|||||||
"options": "Gerenciar",
|
"options": "Gerenciar",
|
||||||
"extract": "Extrair arquivos",
|
"extract": "Extrair arquivos",
|
||||||
"extracting": "Extraindo arquivos…",
|
"extracting": "Extraindo arquivos…",
|
||||||
"network": "Rede",
|
"delete_archive_title": "Deseja deletar {{fileName}}?",
|
||||||
"peak": "Pico",
|
"delete_archive_description": "O arquivo foi extraído com sucesso e não é mais necessário.",
|
||||||
"seeds": "Seeds",
|
"yes": "Sim",
|
||||||
"peers": "Peers"
|
"no": "Não",
|
||||||
|
"network": "REDE",
|
||||||
|
"peak": "PICO"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Diretório dos downloads",
|
"downloads_path": "Diretório dos downloads",
|
||||||
|
|||||||
@@ -229,13 +229,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -414,11 +414,7 @@
|
|||||||
"resume_seeding": "Продолжить раздачу",
|
"resume_seeding": "Продолжить раздачу",
|
||||||
"options": "Управлять",
|
"options": "Управлять",
|
||||||
"extract": "Распаковать файлы",
|
"extract": "Распаковать файлы",
|
||||||
"extracting": "Распаковка файлов…",
|
"extracting": "Распаковка файлов…"
|
||||||
"network": "Сеть",
|
|
||||||
"peak": "Пик",
|
|
||||||
"seeds": "Seeds",
|
|
||||||
"peers": "Peers"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Путь загрузок",
|
"downloads_path": "Путь загрузок",
|
||||||
|
|||||||
20
src/main/events/auth/get-session-hash.ts
Normal file
20
src/main/events/auth/get-session-hash.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { Auth } from "@types";
|
||||||
|
|
||||||
|
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!auth) return null;
|
||||||
|
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||||
|
|
||||||
|
if (!payload) return null;
|
||||||
|
|
||||||
|
return payload.sessionId;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getSessionHash", getSessionHash);
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
import "./get-session-hash";
|
||||||
import "./open-auth-window";
|
import "./open-auth-window";
|
||||||
import "./sign-out";
|
import "./sign-out";
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { getDownloadSourcesCheckBaseline } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getDownloadSourcesCheckBaselineHandler = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent
|
||||||
|
) => {
|
||||||
|
return await getDownloadSourcesCheckBaseline();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent(
|
||||||
|
"getDownloadSourcesCheckBaseline",
|
||||||
|
getDownloadSourcesCheckBaselineHandler
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { getDownloadSourcesSinceValue } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getDownloadSourcesSinceValueHandler = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent
|
||||||
|
) => {
|
||||||
|
return await getDownloadSourcesSinceValue();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent(
|
||||||
|
"getDownloadSourcesSinceValue",
|
||||||
|
getDownloadSourcesSinceValueHandler
|
||||||
|
);
|
||||||
10
src/main/events/download-sources/get-download-sources.ts
Normal file
10
src/main/events/download-sources/get-download-sources.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { downloadSourcesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { orderBy } from "lodash-es";
|
||||||
|
|
||||||
|
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
const allSources = await downloadSourcesSublevel.values().all();
|
||||||
|
return orderBy(allSources, "createdAt", "desc");
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getDownloadSources", getDownloadSources);
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
import "./add-download-source";
|
import "./add-download-source";
|
||||||
|
import "./get-download-sources-check-baseline";
|
||||||
|
import "./get-download-sources-since-value";
|
||||||
|
import "./get-download-sources";
|
||||||
import "./remove-download-source";
|
import "./remove-download-source";
|
||||||
import "./sync-download-sources";
|
import "./sync-download-sources";
|
||||||
|
|||||||
27
src/main/events/library/clear-new-download-options.ts
Normal file
27
src/main/events/library/clear-new-download-options.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { logger } from "@main/services";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
const clearNewDownloadOptions = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
newDownloadOptionsCount: undefined,
|
||||||
|
});
|
||||||
|
logger.info(`Cleared newDownloadOptionsCount for game ${gameKey}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to clear newDownloadOptionsCount: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("clearNewDownloadOptions", clearNewDownloadOptions);
|
||||||
23
src/main/events/library/delete-archive.ts
Normal file
23
src/main/events/library/delete-archive.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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);
|
||||||
@@ -22,6 +22,7 @@ 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);
|
||||||
|
|||||||
21
src/main/events/library/get-game-by-object-id.ts
Normal file
21
src/main/events/library/get-game-by-object-id.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
const getGameByObjectId = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
const [game, download] = await Promise.all([
|
||||||
|
gamesSublevel.get(gameKey),
|
||||||
|
downloadsSublevel.get(gameKey),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!game || game.isDeleted) return null;
|
||||||
|
|
||||||
|
return { id: gameKey, ...game, download };
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getGameByObjectId", getGameByObjectId);
|
||||||
49
src/main/events/library/get-library.ts
Normal file
49
src/main/events/library/get-library.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { LibraryGame } from "@types";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import {
|
||||||
|
downloadsSublevel,
|
||||||
|
gameAchievementsSublevel,
|
||||||
|
gamesShopAssetsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
} from "@main/level";
|
||||||
|
|
||||||
|
const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||||
|
return gamesSublevel
|
||||||
|
.iterator()
|
||||||
|
.all()
|
||||||
|
.then((results) => {
|
||||||
|
return Promise.all(
|
||||||
|
results
|
||||||
|
.filter(([_key, game]) => game.isDeleted === false)
|
||||||
|
.map(async ([key, game]) => {
|
||||||
|
const download = await downloadsSublevel.get(key);
|
||||||
|
const gameAssets = await gamesShopAssetsSublevel.get(key);
|
||||||
|
|
||||||
|
let unlockedAchievementCount = game.unlockedAchievementCount ?? 0;
|
||||||
|
|
||||||
|
if (!game.unlockedAchievementCount) {
|
||||||
|
const achievements = await gameAchievementsSublevel.get(key);
|
||||||
|
|
||||||
|
unlockedAchievementCount =
|
||||||
|
achievements?.unlockedAchievements.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
...game,
|
||||||
|
download: download ?? null,
|
||||||
|
unlockedAchievementCount,
|
||||||
|
achievementCount: game.achievementCount ?? 0,
|
||||||
|
// Spread gameAssets last to ensure all image URLs are properly set
|
||||||
|
...gameAssets,
|
||||||
|
// Preserve custom image URLs from game if they exist
|
||||||
|
customIconUrl: game.customIconUrl,
|
||||||
|
customLogoImageUrl: game.customLogoImageUrl,
|
||||||
|
customHeroImageUrl: game.customHeroImageUrl,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getLibrary", getLibrary);
|
||||||
@@ -3,13 +3,17 @@ import "./add-game-to-favorites";
|
|||||||
import "./add-game-to-library";
|
import "./add-game-to-library";
|
||||||
import "./change-game-playtime";
|
import "./change-game-playtime";
|
||||||
import "./cleanup-unused-assets";
|
import "./cleanup-unused-assets";
|
||||||
|
import "./clear-new-download-options";
|
||||||
import "./close-game";
|
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";
|
||||||
|
import "./get-game-by-object-id";
|
||||||
|
import "./get-library";
|
||||||
import "./open-game-executable-path";
|
import "./open-game-executable-path";
|
||||||
import "./open-game-installer-path";
|
import "./open-game-installer-path";
|
||||||
import "./open-game-installer";
|
import "./open-game-installer";
|
||||||
|
|||||||
12
src/main/events/themes/add-custom-theme.ts
Normal file
12
src/main/events/themes/add-custom-theme.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Theme } from "@types";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
|
||||||
|
const addCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
|
await themesSublevel.put(theme.id, theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("addCustomTheme", addCustomTheme);
|
||||||
8
src/main/events/themes/delete-all-custom-themes.ts
Normal file
8
src/main/events/themes/delete-all-custom-themes.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const deleteAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
await themesSublevel.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("deleteAllCustomThemes", deleteAllCustomThemes);
|
||||||
11
src/main/events/themes/delete-custom-theme.ts
Normal file
11
src/main/events/themes/delete-custom-theme.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const deleteCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string
|
||||||
|
) => {
|
||||||
|
await themesSublevel.del(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("deleteCustomTheme", deleteCustomTheme);
|
||||||
9
src/main/events/themes/get-active-custom-theme.ts
Normal file
9
src/main/events/themes/get-active-custom-theme.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getActiveCustomTheme = async () => {
|
||||||
|
const allThemes = await themesSublevel.values().all();
|
||||||
|
return allThemes.find((theme) => theme.isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getActiveCustomTheme", getActiveCustomTheme);
|
||||||
8
src/main/events/themes/get-all-custom-themes.ts
Normal file
8
src/main/events/themes/get-all-custom-themes.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
return themesSublevel.values().all();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getAllCustomThemes", getAllCustomThemes);
|
||||||
11
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
11
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getCustomThemeById = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string
|
||||||
|
) => {
|
||||||
|
return themesSublevel.get(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getCustomThemeById", getCustomThemeById);
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import "./add-custom-theme";
|
||||||
import "./close-editor-window";
|
import "./close-editor-window";
|
||||||
import "./copy-theme-achievement-sound";
|
import "./copy-theme-achievement-sound";
|
||||||
|
import "./delete-all-custom-themes";
|
||||||
|
import "./delete-custom-theme";
|
||||||
|
import "./get-active-custom-theme";
|
||||||
|
import "./get-all-custom-themes";
|
||||||
|
import "./get-custom-theme-by-id";
|
||||||
import "./get-theme-sound-data-url";
|
import "./get-theme-sound-data-url";
|
||||||
import "./get-theme-sound-path";
|
import "./get-theme-sound-path";
|
||||||
import "./import-theme-sound-from-store";
|
import "./import-theme-sound-from-store";
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const startGameDownload = async (
|
|||||||
queued: true,
|
queued: true,
|
||||||
extracting: false,
|
extracting: false,
|
||||||
automaticallyExtract,
|
automaticallyExtract,
|
||||||
|
extractionProgress: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
10
src/main/events/user-preferences/get-user-preferences.ts
Normal file
10
src/main/events/user-preferences/get-user-preferences.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
|
const getUserPreferences = async () =>
|
||||||
|
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
|
registerEvent("getUserPreferences", getUserPreferences);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import "./authenticate-real-debrid";
|
import "./authenticate-real-debrid";
|
||||||
import "./authenticate-torbox";
|
import "./authenticate-torbox";
|
||||||
import "./auto-launch";
|
import "./auto-launch";
|
||||||
|
import "./get-user-preferences";
|
||||||
import "./update-user-preferences";
|
import "./update-user-preferences";
|
||||||
|
|||||||
11
src/main/events/user/get-auth.ts
Normal file
11
src/main/events/user/get-auth.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { Auth } from "@types";
|
||||||
|
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getAuth = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||||
|
db.get<string, Auth>(levelKeys.auth, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
|
registerEvent("getAuth", getAuth);
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
import "./get-auth";
|
||||||
import "./get-compared-unlocked-achievements";
|
import "./get-compared-unlocked-achievements";
|
||||||
import "./get-unlocked-achievements";
|
import "./get-unlocked-achievements";
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ export const loadState = async () => {
|
|||||||
|
|
||||||
await import("./events");
|
await import("./events");
|
||||||
|
|
||||||
if (process.platform !== "darwin") {
|
Aria2.spawn();
|
||||||
Aria2.spawn();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import cp from "node:child_process";
|
import Seven, { CommandLineSwitches } from "node-7z";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
|
|
||||||
@@ -9,6 +9,17 @@ 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])
|
||||||
@@ -32,43 +43,109 @@ export class SevenZip {
|
|||||||
cwd?: string;
|
cwd?: string;
|
||||||
passwords?: string[];
|
passwords?: string[];
|
||||||
},
|
},
|
||||||
successCb: () => void,
|
onProgress?: (progress: ExtractionProgress) => void
|
||||||
errorCb: () => void
|
): Promise<ExtractionResult> {
|
||||||
) {
|
return new Promise((resolve, reject) => {
|
||||||
const tryPassword = (index = -1) => {
|
const tryPassword = (index = -1) => {
|
||||||
const password = passwords[index] ?? "";
|
const password = passwords[index] ?? "";
|
||||||
logger.info(`Trying password ${password} on ${filePath}`);
|
logger.info(
|
||||||
|
`Trying password "${password || "(empty)"}" on ${filePath}`
|
||||||
|
);
|
||||||
|
|
||||||
const args = ["x", filePath, "-y", "-p" + password];
|
const extractedFiles: string[] = [];
|
||||||
|
let fileCount = 0;
|
||||||
|
|
||||||
if (outputPath) {
|
const options: CommandLineSwitches = {
|
||||||
args.push("-o" + outputPath);
|
$bin: this.binaryPath,
|
||||||
}
|
$progress: true,
|
||||||
|
yes: true,
|
||||||
|
password: password || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const child = cp.execFile(this.binaryPath, args, {
|
if (outputPath) {
|
||||||
cwd,
|
options.outputDir = outputPath;
|
||||||
});
|
|
||||||
|
|
||||||
child.once("exit", (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
successCb();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < passwords.length - 1) {
|
const stream = Seven.extractFull(filePath, outputPath || cwd || ".", {
|
||||||
|
...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(
|
||||||
`Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
|
`Successfully extracted ${filePath} (${extractedFiles.length} files)`
|
||||||
);
|
);
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
extractedFiles,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
tryPassword(index + 1);
|
stream.on("error", (err) => {
|
||||||
} else {
|
logger.error(`Extraction error for ${filePath}:`, err);
|
||||||
logger.info(`Failed to extract file: ${filePath}`);
|
|
||||||
|
|
||||||
errorCb();
|
if (index < passwords.length - 1) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
tryPassword();
|
stream.on("end", () => {
|
||||||
|
resolve(files);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("error", (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ 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 = app.isPackaged
|
const binaryPath =
|
||||||
? path.join(process.resourcesPath, "aria2c")
|
process.platform === "darwin"
|
||||||
: path.join(__dirname, "..", "..", "binaries", "aria2c");
|
? "aria2c"
|
||||||
|
: app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, "aria2c")
|
||||||
|
: path.join(__dirname, "..", "..", "binaries", "aria2c");
|
||||||
|
|
||||||
this.process = cp.spawn(
|
this.process = cp.spawn(
|
||||||
binaryPath,
|
binaryPath,
|
||||||
|
|||||||
@@ -74,21 +74,16 @@ export class DeckyPlugin {
|
|||||||
|
|
||||||
await fs.promises.mkdir(extractPath, { recursive: true });
|
await fs.promises.mkdir(extractPath, { recursive: true });
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
SevenZip.extractFile(
|
await SevenZip.extractFile({
|
||||||
{
|
filePath: zipPath,
|
||||||
filePath: zipPath,
|
outputPath: extractPath,
|
||||||
outputPath: extractPath,
|
});
|
||||||
},
|
logger.log(`Plugin extracted to: ${extractPath}`);
|
||||||
() => {
|
return extractPath;
|
||||||
logger.log(`Plugin extracted to: ${extractPath}`);
|
} catch {
|
||||||
resolve(extractPath);
|
throw new Error("Failed to extract plugin");
|
||||||
},
|
}
|
||||||
() => {
|
|
||||||
reject(new Error("Failed to extract plugin"));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static needsSudo(): boolean {
|
private static needsSudo(): boolean {
|
||||||
|
|||||||
@@ -126,21 +126,10 @@ 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);
|
||||||
|
|
||||||
@@ -154,6 +143,7 @@ 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, {
|
||||||
@@ -162,12 +152,22 @@ 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,6 +209,18 @@ 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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,58 @@ 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 } from "./7zip";
|
import { SevenZip, ExtractionProgress } 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 async clearExtractionState() {
|
private get gameKey() {
|
||||||
const gameKey = levelKeys.game(this.shop, this.objectId);
|
return levelKeys.game(this.shop, this.objectId);
|
||||||
const download = await downloadsSublevel.get(gameKey);
|
}
|
||||||
|
|
||||||
await downloadsSublevel.put(gameKey, {
|
private async updateExtractionProgress(progress: number, force = false) {
|
||||||
...download!,
|
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() {
|
||||||
|
const download = await downloadsSublevel.get(this.gameKey);
|
||||||
|
if (!download) return;
|
||||||
|
|
||||||
|
await downloadsSublevel.put(this.gameKey, {
|
||||||
|
...download,
|
||||||
extracting: false,
|
extracting: false,
|
||||||
|
extractionProgress: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
@@ -30,6 +64,10 @@ 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);
|
||||||
@@ -42,53 +80,66 @@ 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)
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(
|
if (filesToExtract.length === 0) return;
|
||||||
filesToExtract.map((file) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
SevenZip.extractFile(
|
|
||||||
{
|
|
||||||
filePath: path.join(directoryPath, file),
|
|
||||||
cwd: directoryPath,
|
|
||||||
passwords: ["online-fix.me", "steamrip.com"],
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
resolve(true);
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
reject(new Error(`Failed to extract file: ${file}`));
|
|
||||||
this.clearExtractionState();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
compressedFiles.forEach((file) => {
|
await this.updateExtractionProgress(0, true);
|
||||||
const extractionPath = path.join(directoryPath, file);
|
|
||||||
|
|
||||||
if (fs.existsSync(extractionPath)) {
|
const totalFiles = filesToExtract.length;
|
||||||
fs.unlink(extractionPath, (err) => {
|
let completedFiles = 0;
|
||||||
if (err) {
|
|
||||||
logger.error(`Failed to delete file: ${file}`, err);
|
|
||||||
|
|
||||||
this.clearExtractionState();
|
for (const file of filesToExtract) {
|
||||||
|
try {
|
||||||
|
const result = await SevenZip.extractFile(
|
||||||
|
{
|
||||||
|
filePath: path.join(directoryPath, file),
|
||||||
|
cwd: directoryPath,
|
||||||
|
passwords: ["online-fix.me", "steamrip.com"],
|
||||||
|
},
|
||||||
|
(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;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
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(gameKey),
|
downloadsSublevel.get(this.gameKey),
|
||||||
gamesSublevel.get(gameKey),
|
gamesSublevel.get(this.gameKey),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await downloadsSublevel.put(gameKey, {
|
if (!download) return;
|
||||||
...download!,
|
|
||||||
|
await downloadsSublevel.put(this.gameKey, {
|
||||||
|
...download,
|
||||||
extracting: false,
|
extracting: false,
|
||||||
|
extractionProgress: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
@@ -97,17 +148,15 @@ export class GameFilesManager {
|
|||||||
this.objectId
|
this.objectId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (publishNotification) {
|
if (publishNotification && game) {
|
||||||
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(gameKey),
|
downloadsSublevel.get(this.gameKey),
|
||||||
gamesSublevel.get(gameKey),
|
gamesSublevel.get(this.gameKey),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!download || !game) return false;
|
if (!download || !game) return false;
|
||||||
@@ -119,39 +168,39 @@ export class GameFilesManager {
|
|||||||
path.parse(download.folderName!).name
|
path.parse(download.folderName!).name
|
||||||
);
|
);
|
||||||
|
|
||||||
SevenZip.extractFile(
|
await this.updateExtractionProgress(0, true);
|
||||||
{
|
|
||||||
filePath,
|
try {
|
||||||
outputPath: extractionPath,
|
const result = await SevenZip.extractFile(
|
||||||
passwords: ["online-fix.me", "steamrip.com"],
|
{
|
||||||
},
|
filePath,
|
||||||
async () => {
|
outputPath: extractionPath,
|
||||||
|
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)) {
|
||||||
fs.unlink(filePath, (err) => {
|
WindowManager.mainWindow?.webContents.send(
|
||||||
if (err) {
|
"on-archive-deletion-prompt",
|
||||||
logger.error(
|
[filePath]
|
||||||
`Failed to delete file: ${download.folderName}`,
|
);
|
||||||
err
|
|
||||||
);
|
|
||||||
|
|
||||||
this.clearExtractionState();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await downloadsSublevel.put(gameKey, {
|
await downloadsSublevel.put(this.gameKey, {
|
||||||
...download!,
|
...download,
|
||||||
folderName: path.parse(download.folderName!).name,
|
folderName: path.parse(download.folderName!).name,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setExtractionComplete();
|
await this.setExtractionComplete();
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.clearExtractionState();
|
|
||||||
}
|
}
|
||||||
);
|
} catch (err) {
|
||||||
|
logger.error(`Failed to extract downloaded file: ${filePath}`, err);
|
||||||
|
await this.clearExtractionState();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,16 +36,13 @@ 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}?${searchParams.toString()}`, {
|
}>(`https://api.gofile.io/contents/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.token}`,
|
Authorization: `Bearer ${this.token}`,
|
||||||
|
"X-Website-Token": WT,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
87
src/main/services/node-7z.d.ts
vendored
Normal file
87
src/main/services/node-7z.d.ts
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
SeedingStatus,
|
SeedingStatus,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
|
Theme,
|
||||||
FriendRequestSync,
|
FriendRequestSync,
|
||||||
ShortcutLocation,
|
ShortcutLocation,
|
||||||
AchievementCustomNotificationPosition,
|
AchievementCustomNotificationPosition,
|
||||||
@@ -85,8 +86,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/* User preferences */
|
/* User preferences */
|
||||||
getUserPreferences: () =>
|
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||||
ipcRenderer.invoke("leveldbGet", "userPreferences", null, "json"),
|
|
||||||
updateUserPreferences: (preferences: UserPreferences) =>
|
updateUserPreferences: (preferences: UserPreferences) =>
|
||||||
ipcRenderer.invoke("updateUserPreferences", preferences),
|
ipcRenderer.invoke("updateUserPreferences", preferences),
|
||||||
autoLaunch: (autoLaunchProps: { enabled: boolean; minimized: boolean }) =>
|
autoLaunch: (autoLaunchProps: { enabled: boolean; minimized: boolean }) =>
|
||||||
@@ -101,7 +101,12 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("addDownloadSource", url),
|
ipcRenderer.invoke("addDownloadSource", url),
|
||||||
removeDownloadSource: (url: string, removeAll?: boolean) =>
|
removeDownloadSource: (url: string, removeAll?: boolean) =>
|
||||||
ipcRenderer.invoke("removeDownloadSource", url, removeAll),
|
ipcRenderer.invoke("removeDownloadSource", url, removeAll),
|
||||||
|
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
||||||
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
|
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
|
||||||
|
getDownloadSourcesCheckBaseline: () =>
|
||||||
|
ipcRenderer.invoke("getDownloadSourcesCheckBaseline"),
|
||||||
|
getDownloadSourcesSinceValue: () =>
|
||||||
|
ipcRenderer.invoke("getDownloadSourcesSinceValue"),
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
toggleAutomaticCloudSync: (
|
toggleAutomaticCloudSync: (
|
||||||
@@ -178,6 +183,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||||
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||||
|
clearNewDownloadOptions: (shop: GameShop, objectId: string) =>
|
||||||
|
ipcRenderer.invoke("clearNewDownloadOptions", shop, objectId),
|
||||||
toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) =>
|
toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) =>
|
||||||
ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned),
|
ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned),
|
||||||
updateLaunchOptions: (
|
updateLaunchOptions: (
|
||||||
@@ -194,6 +201,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("selectGameWinePrefix", shop, objectId, winePrefixPath),
|
ipcRenderer.invoke("selectGameWinePrefix", shop, objectId, winePrefixPath),
|
||||||
verifyExecutablePathInUse: (executablePath: string) =>
|
verifyExecutablePathInUse: (executablePath: string) =>
|
||||||
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
|
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
|
||||||
|
getLibrary: () => ipcRenderer.invoke("getLibrary"),
|
||||||
refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"),
|
refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"),
|
||||||
openGameInstaller: (shop: GameShop, objectId: string) =>
|
openGameInstaller: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("openGameInstaller", shop, objectId),
|
ipcRenderer.invoke("openGameInstaller", shop, objectId),
|
||||||
@@ -222,6 +230,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("removeGame", shop, objectId),
|
ipcRenderer.invoke("removeGame", shop, objectId),
|
||||||
deleteGameFolder: (shop: GameShop, objectId: string) =>
|
deleteGameFolder: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("deleteGameFolder", shop, objectId),
|
ipcRenderer.invoke("deleteGameFolder", shop, objectId),
|
||||||
|
getGameByObjectId: (shop: GameShop, objectId: string) =>
|
||||||
|
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
||||||
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||||
changeGamePlayTime: (shop: GameShop, objectId: string, playtime: number) =>
|
changeGamePlayTime: (shop: GameShop, objectId: string, playtime: number) =>
|
||||||
@@ -257,6 +267,29 @@ 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) =>
|
||||||
@@ -277,6 +310,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
gameArtifactId: string
|
gameArtifactId: string
|
||||||
) =>
|
) =>
|
||||||
ipcRenderer.invoke("downloadGameArtifact", objectId, shop, gameArtifactId),
|
ipcRenderer.invoke("downloadGameArtifact", objectId, shop, gameArtifactId),
|
||||||
|
getGameArtifacts: (objectId: string, shop: GameShop) =>
|
||||||
|
ipcRenderer.invoke("getGameArtifacts", objectId, shop),
|
||||||
getGameBackupPreview: (objectId: string, shop: GameShop) =>
|
getGameBackupPreview: (objectId: string, shop: GameShop) =>
|
||||||
ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
|
ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
|
||||||
selectGameBackupPath: (
|
selectGameBackupPath: (
|
||||||
@@ -491,10 +526,11 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
getAuth: () => ipcRenderer.invoke("leveldbGet", "auth", null, "json"),
|
getAuth: () => ipcRenderer.invoke("getAuth"),
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
openAuthWindow: (page: AuthPage) =>
|
openAuthWindow: (page: AuthPage) =>
|
||||||
ipcRenderer.invoke("openAuthWindow", page),
|
ipcRenderer.invoke("openAuthWindow", page),
|
||||||
|
getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
|
||||||
onSignIn: (cb: () => void) => {
|
onSignIn: (cb: () => void) => {
|
||||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||||
ipcRenderer.on("on-signin", listener);
|
ipcRenderer.on("on-signin", listener);
|
||||||
@@ -552,8 +588,16 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("showAchievementTestNotification"),
|
ipcRenderer.invoke("showAchievementTestNotification"),
|
||||||
|
|
||||||
/* Themes */
|
/* Themes */
|
||||||
|
addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme),
|
||||||
|
getAllCustomThemes: () => ipcRenderer.invoke("getAllCustomThemes"),
|
||||||
|
deleteAllCustomThemes: () => ipcRenderer.invoke("deleteAllCustomThemes"),
|
||||||
|
deleteCustomTheme: (themeId: string) =>
|
||||||
|
ipcRenderer.invoke("deleteCustomTheme", themeId),
|
||||||
updateCustomTheme: (themeId: string, code: string) =>
|
updateCustomTheme: (themeId: string, code: string) =>
|
||||||
ipcRenderer.invoke("updateCustomTheme", themeId, code),
|
ipcRenderer.invoke("updateCustomTheme", themeId, code),
|
||||||
|
getCustomThemeById: (themeId: string) =>
|
||||||
|
ipcRenderer.invoke("getCustomThemeById", themeId),
|
||||||
|
getActiveCustomTheme: () => ipcRenderer.invoke("getActiveCustomTheme"),
|
||||||
toggleCustomTheme: (themeId: string, isActive: boolean) =>
|
toggleCustomTheme: (themeId: string, isActive: boolean) =>
|
||||||
ipcRenderer.invoke("toggleCustomTheme", themeId, isActive),
|
ipcRenderer.invoke("toggleCustomTheme", themeId, isActive),
|
||||||
copyThemeAchievementSound: (themeId: string, sourcePath: string) =>
|
copyThemeAchievementSound: (themeId: string, sourcePath: string) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -19,11 +19,14 @@ 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,
|
||||||
@@ -78,6 +81,10 @@ 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"),
|
||||||
@@ -184,12 +191,23 @@ 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]);
|
}, [onSignIn, updateLibrary, clearUserDetails, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contentRef.current) contentRef.current.scrollTop = 0;
|
if (contentRef.current) contentRef.current.scrollTop = 0;
|
||||||
@@ -281,6 +299,12 @@ export function App() {
|
|||||||
feature={hydraCloudFeature}
|
feature={hydraCloudFeature}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ArchiveDeletionModal
|
||||||
|
visible={showArchiveDeletionModal}
|
||||||
|
archivePaths={archivePaths}
|
||||||
|
onClose={() => setShowArchiveDeletionModal(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
{userDetails && (
|
{userDetails && (
|
||||||
<UserFriendModal
|
<UserFriendModal
|
||||||
visible={isFriendsModalVisible}
|
visible={isFriendsModalVisible}
|
||||||
|
|||||||
@@ -2,18 +2,17 @@ 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,
|
||||||
useUserDetails,
|
useUserDetails,
|
||||||
} from "@renderer/hooks";
|
} from "@renderer/hooks";
|
||||||
import { levelDBService } from "@renderer/services/leveldb.service";
|
|
||||||
|
|
||||||
import "./bottom-panel.scss";
|
import "./bottom-panel.scss";
|
||||||
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { VERSION_CODENAME } from "@renderer/constants";
|
import { VERSION_CODENAME } from "@renderer/constants";
|
||||||
import type jwt from "jsonwebtoken";
|
|
||||||
|
|
||||||
export function BottomPanel() {
|
export function BottomPanel() {
|
||||||
const { t } = useTranslation("bottom_panel");
|
const { t } = useTranslation("bottom_panel");
|
||||||
@@ -28,6 +27,8 @@ 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>(
|
||||||
@@ -62,28 +63,7 @@ export function BottomPanel() {
|
|||||||
}, [t, showSuccessToast]);
|
}, [t, showSuccessToast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getSessionHash = async () => {
|
window.electron.getSessionHash().then((result) => setSessionHash(result));
|
||||||
const auth = (await levelDBService.get("auth", null, "json")) as {
|
|
||||||
accessToken?: string;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
if (!auth?.accessToken) {
|
|
||||||
setSessionHash(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const jwtModule = await import("jsonwebtoken");
|
|
||||||
const payload = jwtModule.decode(
|
|
||||||
auth.accessToken
|
|
||||||
) as jwt.JwtPayload | null;
|
|
||||||
setSessionHash(payload?.sessionId ?? null);
|
|
||||||
} catch {
|
|
||||||
setSessionHash(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getSessionHash();
|
|
||||||
}, [userDetails?.id]);
|
}, [userDetails?.id]);
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
@@ -91,6 +71,20 @@ 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;
|
||||||
@@ -132,6 +126,7 @@ export function BottomPanel() {
|
|||||||
eta,
|
eta,
|
||||||
downloadSpeed,
|
downloadSpeed,
|
||||||
commonRedistStatus,
|
commonRedistStatus,
|
||||||
|
extraction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,3 +20,4 @@ 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";
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from "@renderer/hooks";
|
} from "@renderer/hooks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Download,
|
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
GameRepack,
|
GameRepack,
|
||||||
GameShop,
|
GameShop,
|
||||||
@@ -96,19 +95,9 @@ export function GameDetailsContextProvider({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const updateGame = useCallback(async () => {
|
const updateGame = useCallback(async () => {
|
||||||
const gameKey = `${shop}:${objectId}`;
|
return window.electron
|
||||||
const [game, download] = await Promise.all([
|
.getGameByObjectId(shop, objectId)
|
||||||
levelDBService.get(gameKey, "games") as Promise<LibraryGame | null>,
|
.then((result) => setGame(result));
|
||||||
levelDBService.get(gameKey, "downloads") as Promise<Download | null>,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!game || game.isDeleted) {
|
|
||||||
setGame(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id: _id, ...gameWithoutId } = game;
|
|
||||||
setGame({ id: gameKey, ...gameWithoutId, download: download ?? null });
|
|
||||||
}, [shop, objectId]);
|
}, [shop, objectId]);
|
||||||
|
|
||||||
const isGameDownloading =
|
const isGameDownloading =
|
||||||
|
|||||||
35
src/renderer/src/declaration.d.ts
vendored
35
src/renderer/src/declaration.d.ts
vendored
@@ -14,11 +14,14 @@ import type {
|
|||||||
GameStats,
|
GameStats,
|
||||||
UserDetails,
|
UserDetails,
|
||||||
FriendRequestSync,
|
FriendRequestSync,
|
||||||
|
GameArtifact,
|
||||||
LudusaviBackup,
|
LudusaviBackup,
|
||||||
UserAchievement,
|
UserAchievement,
|
||||||
ComparedAchievements,
|
ComparedAchievements,
|
||||||
|
LibraryGame,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
TorBoxUser,
|
TorBoxUser,
|
||||||
|
Theme,
|
||||||
Auth,
|
Auth,
|
||||||
ShortcutLocation,
|
ShortcutLocation,
|
||||||
ShopAssets,
|
ShopAssets,
|
||||||
@@ -139,6 +142,10 @@ declare global {
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string
|
objectId: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
clearNewDownloadOptions: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => Promise<void>;
|
||||||
toggleGamePin: (
|
toggleGamePin: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -155,6 +162,7 @@ declare global {
|
|||||||
winePrefixPath: string | null
|
winePrefixPath: string | null
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
||||||
|
getLibrary: () => Promise<LibraryGame[]>;
|
||||||
refreshLibraryAssets: () => Promise<void>;
|
refreshLibraryAssets: () => Promise<void>;
|
||||||
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
|
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||||
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>;
|
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
@@ -169,6 +177,10 @@ declare global {
|
|||||||
removeGameFromLibrary: (shop: GameShop, objectId: string) => Promise<void>;
|
removeGameFromLibrary: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
removeGame: (shop: GameShop, objectId: string) => Promise<void>;
|
removeGame: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
deleteGameFolder: (shop: GameShop, objectId: string) => Promise<unknown>;
|
deleteGameFolder: (shop: GameShop, objectId: string) => Promise<unknown>;
|
||||||
|
getGameByObjectId: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => Promise<LibraryGame | null>;
|
||||||
onGamesRunning: (
|
onGamesRunning: (
|
||||||
cb: (
|
cb: (
|
||||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||||
@@ -182,9 +194,9 @@ declare global {
|
|||||||
playtimeInSeconds: number
|
playtimeInSeconds: number
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
/* User preferences */
|
/* User preferences */
|
||||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
|
||||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||||
|
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||||
updateUserPreferences: (
|
updateUserPreferences: (
|
||||||
preferences: Partial<UserPreferences>
|
preferences: Partial<UserPreferences>
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
@@ -196,6 +208,13 @@ 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>;
|
||||||
|
|
||||||
@@ -205,7 +224,10 @@ declare global {
|
|||||||
removeAll = false,
|
removeAll = false,
|
||||||
downloadSourceId?: string
|
downloadSourceId?: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||||
syncDownloadSources: () => Promise<void>;
|
syncDownloadSources: () => Promise<void>;
|
||||||
|
getDownloadSourcesCheckBaseline: () => Promise<string | null>;
|
||||||
|
getDownloadSourcesSinceValue: () => Promise<string | null>;
|
||||||
|
|
||||||
/* Hardware */
|
/* Hardware */
|
||||||
getDiskFreeSpace: (path: string) => Promise<DiskUsage>;
|
getDiskFreeSpace: (path: string) => Promise<DiskUsage>;
|
||||||
@@ -222,6 +244,10 @@ declare global {
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
gameArtifactId: string
|
gameArtifactId: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
getGameArtifacts: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop
|
||||||
|
) => Promise<GameArtifact[]>;
|
||||||
getGameBackupPreview: (
|
getGameBackupPreview: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
@@ -336,6 +362,7 @@ declare global {
|
|||||||
getAuth: () => Promise<Auth | null>;
|
getAuth: () => Promise<Auth | null>;
|
||||||
signOut: () => Promise<void>;
|
signOut: () => Promise<void>;
|
||||||
openAuthWindow: (page: AuthPage) => Promise<void>;
|
openAuthWindow: (page: AuthPage) => Promise<void>;
|
||||||
|
getSessionHash: () => Promise<string | null>;
|
||||||
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
|
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
|
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
|
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
@@ -388,7 +415,13 @@ declare global {
|
|||||||
showAchievementTestNotification: () => Promise<void>;
|
showAchievementTestNotification: () => Promise<void>;
|
||||||
|
|
||||||
/* Themes */
|
/* Themes */
|
||||||
|
addCustomTheme: (theme: Theme) => Promise<void>;
|
||||||
|
getAllCustomThemes: () => Promise<Theme[]>;
|
||||||
|
deleteAllCustomThemes: () => Promise<void>;
|
||||||
|
deleteCustomTheme: (themeId: string) => Promise<void>;
|
||||||
updateCustomTheme: (themeId: string, code: string) => Promise<void>;
|
updateCustomTheme: (themeId: string, code: string) => Promise<void>;
|
||||||
|
getCustomThemeById: (themeId: string) => Promise<Theme | null>;
|
||||||
|
getActiveCustomTheme: () => Promise<Theme | null>;
|
||||||
toggleCustomTheme: (themeId: string, isActive: boolean) => Promise<void>;
|
toggleCustomTheme: (themeId: string, isActive: boolean) => Promise<void>;
|
||||||
copyThemeAchievementSound: (
|
copyThemeAchievementSound: (
|
||||||
themeId: string,
|
themeId: string,
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
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 } from "@types";
|
import type { DownloadProgress, GameShop } 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({
|
||||||
@@ -21,6 +32,27 @@ 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;
|
||||||
@@ -38,6 +70,37 @@ 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] = [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,4 +109,8 @@ export const {
|
|||||||
clearDownload,
|
clearDownload,
|
||||||
setGameDeleting,
|
setGameDeleting,
|
||||||
removeGameFromDeleting,
|
removeGameFromDeleting,
|
||||||
|
setExtractionProgress,
|
||||||
|
clearExtraction,
|
||||||
|
updatePeakSpeed,
|
||||||
|
clearPeakSpeed,
|
||||||
} = downloadSlice.actions;
|
} = downloadSlice.actions;
|
||||||
|
|||||||
@@ -1,65 +1,15 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "./redux";
|
import { useAppDispatch, useAppSelector } from "./redux";
|
||||||
import { setLibrary } from "@renderer/features";
|
import { setLibrary } from "@renderer/features";
|
||||||
import { levelDBService } from "@renderer/services/leveldb.service";
|
|
||||||
import type {
|
|
||||||
LibraryGame,
|
|
||||||
Game,
|
|
||||||
Download,
|
|
||||||
ShopAssets,
|
|
||||||
GameAchievement,
|
|
||||||
} from "@types";
|
|
||||||
|
|
||||||
export function useLibrary() {
|
export function useLibrary() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const library = useAppSelector((state) => state.library.value);
|
const library = useAppSelector((state) => state.library.value);
|
||||||
|
|
||||||
const updateLibrary = useCallback(async () => {
|
const updateLibrary = useCallback(async () => {
|
||||||
const results = await levelDBService.iterator("games");
|
return window.electron
|
||||||
|
.getLibrary()
|
||||||
const libraryGames = await Promise.all(
|
.then((updatedLibrary) => dispatch(setLibrary(updatedLibrary)));
|
||||||
results
|
|
||||||
.filter(([_key, game]) => (game as Game).isDeleted === false)
|
|
||||||
.map(async ([key, game]) => {
|
|
||||||
const gameData = game as Game;
|
|
||||||
const download = (await levelDBService.get(
|
|
||||||
key,
|
|
||||||
"downloads"
|
|
||||||
)) as Download | null;
|
|
||||||
const gameAssets = (await levelDBService.get(
|
|
||||||
key,
|
|
||||||
"gameShopAssets"
|
|
||||||
)) as (ShopAssets & { updatedAt: number }) | null;
|
|
||||||
|
|
||||||
let unlockedAchievementCount = gameData.unlockedAchievementCount ?? 0;
|
|
||||||
|
|
||||||
if (!gameData.unlockedAchievementCount) {
|
|
||||||
const achievements = (await levelDBService.get(
|
|
||||||
key,
|
|
||||||
"gameAchievements"
|
|
||||||
)) as GameAchievement | null;
|
|
||||||
|
|
||||||
unlockedAchievementCount =
|
|
||||||
achievements?.unlockedAchievements.length ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: key,
|
|
||||||
...gameData,
|
|
||||||
download: download ?? null,
|
|
||||||
unlockedAchievementCount,
|
|
||||||
achievementCount: gameData.achievementCount ?? 0,
|
|
||||||
// Spread gameAssets last to ensure all image URLs are properly set
|
|
||||||
...gameAssets,
|
|
||||||
// Preserve custom image URLs from game if they exist
|
|
||||||
customIconUrl: gameData.customIconUrl,
|
|
||||||
customLogoImageUrl: gameData.customLogoImageUrl,
|
|
||||||
customHeroImageUrl: gameData.customHeroImageUrl,
|
|
||||||
} as LibraryGame;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch(setLibrary(libraryGames));
|
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return { library, updateLibrary };
|
return { library, updateLibrary };
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -108,16 +108,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: opacity 0.2s ease;
|
transition: scale 0.2s ease;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
scale: 1.05;
|
||||||
}
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,6 +390,21 @@
|
|||||||
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%;
|
||||||
@@ -411,6 +421,21 @@
|
|||||||
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;
|
||||||
@@ -511,5 +536,9 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,16 +128,20 @@ function SpeedChart({
|
|||||||
g = 255,
|
g = 255,
|
||||||
b = 255;
|
b = 255;
|
||||||
if (color.startsWith("#")) {
|
if (color.startsWith("#")) {
|
||||||
const hex = color.replace("#", "");
|
let hex = color.replace("#", "");
|
||||||
r = Number.parseInt(hex.substring(0, 2), 16);
|
// Handle shorthand hex colors (e.g., "#fff" -> "#ffffff")
|
||||||
g = Number.parseInt(hex.substring(2, 4), 16);
|
if (hex.length === 3) {
|
||||||
b = Number.parseInt(hex.substring(4, 6), 16);
|
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||||||
|
}
|
||||||
|
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]);
|
r = Number.parseInt(matches[0]) || 255;
|
||||||
g = Number.parseInt(matches[1]);
|
g = Number.parseInt(matches[1]) || 255;
|
||||||
b = Number.parseInt(matches[2]);
|
b = Number.parseInt(matches[2]) || 255;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const displaySpeeds = speeds.slice(-totalBars);
|
const displaySpeeds = speeds.slice(-totalBars);
|
||||||
@@ -203,6 +207,7 @@ 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;
|
||||||
@@ -221,6 +226,7 @@ interface HeroDownloadViewProps {
|
|||||||
function HeroDownloadView({
|
function HeroDownloadView({
|
||||||
game,
|
game,
|
||||||
isGameDownloading,
|
isGameDownloading,
|
||||||
|
isGameExtracting = false,
|
||||||
downloadSpeed,
|
downloadSpeed,
|
||||||
finalDownloadSize,
|
finalDownloadSize,
|
||||||
peakSpeed,
|
peakSpeed,
|
||||||
@@ -278,11 +284,17 @@ 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">
|
||||||
{lastPacket?.isCheckingFiles ? (
|
{isGameExtracting && (
|
||||||
|
<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
|
||||||
@@ -293,7 +305,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 && (
|
{!lastPacket?.isCheckingFiles && !isGameExtracting && (
|
||||||
<span className="download-group__progress-time">
|
<span className="download-group__progress-time">
|
||||||
{isGameDownloading &&
|
{isGameDownloading &&
|
||||||
lastPacket?.timeRemaining &&
|
lastPacket?.timeRemaining &&
|
||||||
@@ -305,50 +317,50 @@ function HeroDownloadView({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{(!lastPacket?.isCheckingFiles || currentProgress > 0) && (
|
<span className="download-group__progress-percentage">
|
||||||
<span className="download-group__progress-percentage">
|
<AnimatedPercentage value={currentProgress} />
|
||||||
<AnimatedPercentage value={currentProgress} />
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="download-group__progress-bar">
|
<div className="download-group__progress-bar">
|
||||||
<div
|
<div
|
||||||
className="download-group__progress-fill"
|
className={`download-group__progress-fill ${isGameExtracting ? "download-group__progress-fill--extraction" : ""}`}
|
||||||
style={{
|
style={{
|
||||||
width: `${currentProgress * 100}%`,
|
width: `${currentProgress * 100}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="download-group__hero-buttons">
|
{!isGameExtracting && (
|
||||||
{isGameDownloading ? (
|
<div className="download-group__hero-buttons">
|
||||||
|
{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={() => pauseDownload(game.shop, game.objectId)}
|
onClick={() => cancelDownload(game.shop, game.objectId)}
|
||||||
className="download-group__glass-btn"
|
className="download-group__glass-btn"
|
||||||
>
|
>
|
||||||
<ColumnsIcon size={14} />
|
<XCircleIcon size={14} />
|
||||||
{t("pause")}
|
{t("cancel")}
|
||||||
</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>
|
||||||
|
|
||||||
@@ -360,7 +372,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"}
|
||||||
@@ -373,38 +385,39 @@ 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 && (
|
{game.download?.downloader === Downloader.Torrent &&
|
||||||
<div className="download-group__stat-item">
|
isGameDownloading &&
|
||||||
<div
|
lastPacket &&
|
||||||
className="download-group__stat-content"
|
(lastPacket.numSeeds > 0 || lastPacket.numPeers > 0) && (
|
||||||
style={{
|
<div className="download-group__stat-item">
|
||||||
justifyContent: "space-between",
|
<div className="download-group__stat-content">
|
||||||
alignItems: "center",
|
<span className="download-group__stat-label">
|
||||||
}}
|
Seeds:{" "}
|
||||||
>
|
<span className="download-group__stat-value">
|
||||||
<Badge>{DOWNLOADER_NAME[game.download.downloader]}</Badge>
|
{lastPacket.numSeeds}
|
||||||
{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>
|
</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-content">
|
||||||
|
<Badge>
|
||||||
|
{DOWNLOADER_NAME[Number(game.download.downloader)]}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -445,6 +458,8 @@ 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 {
|
||||||
@@ -499,8 +514,9 @@ export function DownloadGroup({
|
|||||||
|
|
||||||
const { formatDistance } = useDate();
|
const { formatDistance } = useDate();
|
||||||
|
|
||||||
const [peakSpeeds, setPeakSpeeds] = useState<Record<string, number>>({});
|
// Get speed history and peak speeds from Redux (centralized state)
|
||||||
const speedHistoryRef = useRef<Record<string, number[]>>({});
|
const speedHistory = useAppSelector((state) => state.download.speedHistory);
|
||||||
|
const peakSpeeds = useAppSelector((state) => state.download.peakSpeeds);
|
||||||
const [dominantColors, setDominantColors] = useState<Record<string, string>>(
|
const [dominantColors, setDominantColors] = useState<Record<string, string>>(
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@@ -563,68 +579,8 @@ export function DownloadGroup({
|
|||||||
});
|
});
|
||||||
}, [library, lastPacket?.gameId]);
|
}, [library, lastPacket?.gameId]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Speed history and peak speeds are now tracked in Redux (in setLastPacket reducer)
|
||||||
if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) {
|
// No local effect needed - data is updated atomically when packets arrive
|
||||||
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")) {
|
||||||
@@ -822,16 +778,28 @@ export function DownloadGroup({
|
|||||||
|
|
||||||
if (isDownloadingGroup && library.length > 0) {
|
if (isDownloadingGroup && library.length > 0) {
|
||||||
const game = library[0];
|
const game = library[0];
|
||||||
const isGameDownloading = isGameDownloadingMap[game.id];
|
const isGameExtracting = extraction?.visibleId === 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);
|
||||||
const peakSpeed = peakSpeeds[game.id] || 0;
|
// Use lastPacket.gameId for lookup since that's the key used to store the data
|
||||||
const currentProgress =
|
// Fall back to game.id if lastPacket is not available
|
||||||
isGameDownloading && lastPacket
|
const dataKey = lastPacket?.gameId ?? game.id;
|
||||||
? lastPacket.progress
|
const gameSpeedHistory = speedHistory[dataKey] ?? [];
|
||||||
: game.download?.progress || 0;
|
const storedPeak = peakSpeeds[dataKey];
|
||||||
|
// 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";
|
||||||
|
|
||||||
@@ -839,13 +807,14 @@ 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={speedHistoryRef.current[game.id] || []}
|
speedHistory={gameSpeedHistory}
|
||||||
formatSpeed={formatSpeed}
|
formatSpeed={formatSpeed}
|
||||||
calculateETA={calculateETA}
|
calculateETA={calculateETA}
|
||||||
pauseDownload={pauseDownload}
|
pauseDownload={pauseDownload}
|
||||||
@@ -873,14 +842,8 @@ 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"
|
||||||
className="download-group__simple-thumbnail"
|
|
||||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
style={{
|
className="download-group__simple-thumbnail"
|
||||||
background: "none",
|
|
||||||
border: "none",
|
|
||||||
padding: 0,
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<img src={game.libraryImageUrl || ""} alt={game.title} />
|
<img src={game.libraryImageUrl || ""} alt={game.title} />
|
||||||
</button>
|
</button>
|
||||||
@@ -888,27 +851,22 @@ 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))}
|
||||||
style={{
|
className="download-group__simple-title-button"
|
||||||
background: "none",
|
|
||||||
border: "none",
|
|
||||||
padding: 0,
|
|
||||||
cursor: "pointer",
|
|
||||||
textAlign: "left",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{game.title}
|
<h3 className="download-group__simple-title">{game.title}</h3>
|
||||||
</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>{DOWNLOADER_NAME[game.download!.downloader]}</Badge>
|
<Badge>
|
||||||
|
{DOWNLOADER_NAME[Number(game.download!.downloader)]}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="download-group__simple-meta-row">
|
<div className="download-group__simple-meta-row">
|
||||||
{game.download?.extracting ? (
|
{extraction?.visibleId === game.id ? (
|
||||||
<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">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useAppSelector, 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,6 +13,7 @@ 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");
|
||||||
|
|
||||||
@@ -39,11 +40,13 @@ export default function Downloads() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electron.onSeedingStatus((value) => setSeedingStatus(value));
|
window.electron.onSeedingStatus((value) => setSeedingStatus(value));
|
||||||
|
|
||||||
const unsubscribe = window.electron.onExtractionComplete(() => {
|
const unsubscribeExtraction = window.electron.onExtractionComplete(() => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => unsubscribe();
|
return () => {
|
||||||
|
unsubscribeExtraction();
|
||||||
|
};
|
||||||
}, [updateLibrary]);
|
}, [updateLibrary]);
|
||||||
|
|
||||||
const handleOpenGameInstaller = (shop: GameShop, objectId: string) =>
|
const handleOpenGameInstaller = (shop: GameShop, objectId: string) =>
|
||||||
@@ -72,8 +75,10 @@ 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 */
|
/* Is downloading or extracting */
|
||||||
if (lastPacket?.gameId === next.id || next.download.extracting)
|
const isExtracting =
|
||||||
|
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 */
|
||||||
@@ -96,7 +101,7 @@ export default function Downloads() {
|
|||||||
queued,
|
queued,
|
||||||
complete,
|
complete,
|
||||||
};
|
};
|
||||||
}, [library, lastPacket?.gameId]);
|
}, [library, lastPacket?.gameId, extraction?.visibleId]);
|
||||||
|
|
||||||
const downloadGroups = [
|
const downloadGroups = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
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 { useDate, useDownload, useFormat } from "@renderer/hooks";
|
import {
|
||||||
|
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";
|
||||||
@@ -17,6 +22,9 @@ 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) {
|
||||||
@@ -52,6 +60,16 @@ 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">
|
||||||
@@ -72,7 +90,8 @@ export function HeroPanelPlaytime() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>{t("not_played_yet", { title: game?.title })}</p>
|
<p>{t("not_played_yet", { title: game?.title })}</p>
|
||||||
{hasDownload && downloadInProgressInfo}
|
{isExtracting && extractionInProgressInfo}
|
||||||
|
{!isExtracting && hasDownload && downloadInProgressInfo}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,7 +100,8 @@ export function HeroPanelPlaytime() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>{t("playing_now")}</p>
|
<p>{t("playing_now")}</p>
|
||||||
{hasDownload && downloadInProgressInfo}
|
{isExtracting && extractionInProgressInfo}
|
||||||
|
{!isExtracting && hasDownload && downloadInProgressInfo}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,9 +133,9 @@ export function HeroPanelPlaytime() {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{hasDownload ? (
|
{isExtracting && extractionInProgressInfo}
|
||||||
downloadInProgressInfo
|
{!isExtracting && hasDownload && downloadInProgressInfo}
|
||||||
) : (
|
{!isExtracting && !hasDownload && (
|
||||||
<p>
|
<p>
|
||||||
{t("last_time_played", {
|
{t("last_time_played", {
|
||||||
period: lastTimePlayed,
|
period: lastTimePlayed,
|
||||||
|
|||||||
@@ -80,5 +80,11 @@
|
|||||||
&--disabled {
|
&--disabled {
|
||||||
opacity: globals.$disabled-opacity;
|
opacity: globals.$disabled-opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--extraction {
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useDate, useDownload } from "@renderer/hooks";
|
import { useAppSelector, 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,9 +18,13 @@ 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;
|
||||||
@@ -49,6 +53,8 @@ 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">
|
||||||
@@ -72,6 +78,14 @@ 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -87,16 +87,12 @@ export function LibraryTab({
|
|||||||
|
|
||||||
<ul className="profile-content__games-grid">
|
<ul className="profile-content__games-grid">
|
||||||
{pinnedGames?.map((game) => (
|
{pinnedGames?.map((game) => (
|
||||||
<li
|
<li key={game.objectId} style={{ listStyle: "none" }}>
|
||||||
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>
|
||||||
@@ -138,9 +134,6 @@ 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 }
|
||||||
@@ -167,6 +160,8 @@ 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>
|
||||||
|
|||||||
@@ -25,12 +25,16 @@ 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 } =
|
||||||
@@ -126,126 +130,129 @@ export function UserLibraryGameCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<li
|
||||||
className="user-library-game__cover"
|
onMouseEnter={onMouseEnter}
|
||||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
onMouseLeave={onMouseLeave}
|
||||||
onKeyDown={(e) => {
|
className="user-library-game__wrapper"
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
navigate(buildUserGameDetailsPath(game));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
title={isTooltipHovered ? undefined : game.title}
|
title={isTooltipHovered ? undefined : game.title}
|
||||||
>
|
>
|
||||||
<div className="user-library-game__overlay">
|
<button
|
||||||
{isMe && (
|
type="button"
|
||||||
<div className="user-library-game__actions-container">
|
className="user-library-game__cover"
|
||||||
<button
|
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||||
type="button"
|
>
|
||||||
className="user-library-game__pin-button"
|
<div className="user-library-game__overlay">
|
||||||
onClick={(e) => {
|
{isMe && (
|
||||||
e.stopPropagation();
|
<div className="user-library-game__actions-container">
|
||||||
toggleGamePinned();
|
<button
|
||||||
}}
|
type="button"
|
||||||
disabled={isPinning}
|
className="user-library-game__pin-button"
|
||||||
>
|
onClick={(e) => {
|
||||||
{game.isPinned ? (
|
e.stopPropagation();
|
||||||
<PinSlashIcon size={12} />
|
toggleGamePinned();
|
||||||
) : (
|
}}
|
||||||
<PinIcon size={12} />
|
disabled={isPinning}
|
||||||
)}
|
>
|
||||||
</button>
|
{game.isPinned ? (
|
||||||
</div>
|
<PinSlashIcon size={12} />
|
||||||
)}
|
) : (
|
||||||
<div
|
<PinIcon size={12} />
|
||||||
className="user-library-game__playtime"
|
)}
|
||||||
data-tooltip-place="top"
|
</button>
|
||||||
data-tooltip-content={
|
</div>
|
||||||
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">
|
<div
|
||||||
{formatPlayTime(game.playTimeInSeconds)}
|
className="user-library-game__playtime"
|
||||||
</span>
|
data-tooltip-place="top"
|
||||||
<span className="user-library-game__playtime-short">
|
data-tooltip-content={
|
||||||
{formatPlayTime(game.playTimeInSeconds, true)}
|
game.hasManuallyUpdatedPlaytime
|
||||||
</span>
|
? t("manual_playtime_tooltip")
|
||||||
</div>
|
: 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 &&
|
||||||
|
game.achievementCount > 0 && (
|
||||||
|
<div className="user-library-game__stats">
|
||||||
|
<div className="user-library-game__stats-header">
|
||||||
|
<div className="user-library-game__stats-content">
|
||||||
|
<div
|
||||||
|
className="user-library-game__stats-item"
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
|
||||||
<div className="user-library-game__stats">
|
|
||||||
<div className="user-library-game__stats-header">
|
|
||||||
<div className="user-library-game__stats-content">
|
|
||||||
<div
|
|
||||||
className="user-library-game__stats-item"
|
|
||||||
style={{
|
|
||||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrophyIcon size={13} />
|
|
||||||
<span>
|
<span>
|
||||||
{game.unlockedAchievementCount} / {game.achievementCount}
|
{formatDownloadProgress(
|
||||||
|
game.unlockedAchievementCount / game.achievementCount,
|
||||||
|
1
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{game.achievementsPointsEarnedSum > 0 && (
|
<progress
|
||||||
<div
|
max={1}
|
||||||
className="user-library-game__stats-item"
|
value={
|
||||||
style={{
|
game.unlockedAchievementCount / game.achievementCount
|
||||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
}
|
||||||
}}
|
className="user-library-game__achievements-progress"
|
||||||
>
|
/>
|
||||||
<HydraIcon width={16} height={16} />
|
|
||||||
{formatAchievementPoints(
|
|
||||||
game.achievementsPointsEarnedSum
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<span>
|
|
||||||
{formatDownloadProgress(
|
|
||||||
game.unlockedAchievementCount / game.achievementCount,
|
|
||||||
1
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<progress
|
|
||||||
max={1}
|
|
||||||
value={game.unlockedAchievementCount / game.achievementCount}
|
|
||||||
className="user-library-game__achievements-progress"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{imageError || !game.coverImageUrl ? (
|
|
||||||
<div className="user-library-game__cover-placeholder">
|
|
||||||
<ImageIcon size={48} />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<img
|
{imageError || !game.coverImageUrl ? (
|
||||||
src={game.coverImageUrl}
|
<div className="user-library-game__cover-placeholder">
|
||||||
alt={game.title}
|
<ImageIcon size={48} />
|
||||||
className="user-library-game__game-image"
|
</div>
|
||||||
onError={() => setImageError(true)}
|
) : (
|
||||||
/>
|
<img
|
||||||
)}
|
src={game.coverImageUrl}
|
||||||
</div>
|
alt={game.title}
|
||||||
|
className="user-library-game__game-image"
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
id={game.objectId}
|
id={game.objectId}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import {
|
|||||||
XCircleFillIcon,
|
XCircleFillIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { Avatar, Button, Link } from "@renderer/components";
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
FullscreenMediaModal,
|
||||||
|
Link,
|
||||||
|
} from "@renderer/components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
@@ -33,6 +38,7 @@ 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 {
|
||||||
@@ -246,10 +252,12 @@ export function ProfileHero() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handleAvatarClick = useCallback(() => {
|
const handleAvatarClick = useCallback(() => {
|
||||||
if (isMe) {
|
if (userProfile?.profileImageUrl) {
|
||||||
|
setShowFullscreenAvatar(true);
|
||||||
|
} else if (isMe) {
|
||||||
setShowEditProfileModal(true);
|
setShowEditProfileModal(true);
|
||||||
}
|
}
|
||||||
}, [isMe]);
|
}, [isMe, userProfile?.profileImageUrl]);
|
||||||
|
|
||||||
const currentGame = useMemo(() => {
|
const currentGame = useMemo(() => {
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
@@ -272,6 +280,13 @@ 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 }}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export interface Download {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
extracting: boolean;
|
extracting: boolean;
|
||||||
automaticallyExtract: boolean;
|
automaticallyExtract: boolean;
|
||||||
|
extractionProgress: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameAchievement {
|
export interface GameAchievement {
|
||||||
|
|||||||
43
yarn.lock
43
yarn.lock
@@ -6438,11 +6438,26 @@ 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"
|
||||||
@@ -6453,6 +6468,11 @@ 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"
|
||||||
@@ -6493,6 +6513,11 @@ 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"
|
||||||
@@ -6872,6 +6897,19 @@ 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"
|
||||||
@@ -6927,6 +6965,11 @@ 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user