diff --git a/proto b/proto index 7a23620f..6f11c99c 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 7a23620f930f6fbb84c0abcaab5149a34ab4b4eb +Subproject commit 6f11c99c572420a282ba5149b6866e39b8a4569c diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index cf88cd42..d0db9110 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -420,7 +420,9 @@ "delete_archive_title": "Would you like to delete {{fileName}}?", "delete_archive_description": "The file has been successfully extracted and it's no longer needed.", "yes": "Yes", - "no": "No" + "no": "No", + "network": "NETWORK", + "peak": "PEAK" }, "settings": { "downloads_path": "Downloads path", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 8fc07722..48bf6086 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -27,7 +27,69 @@ "friends": "Amis", "need_help": "Besoin d'aide ?", "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": { "search": "Rechercher", @@ -37,7 +99,15 @@ "search_results": "Résultats de la recherche", "settings": "Paramètres", "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": { "no_downloads_in_progress": "Aucun téléchargement en cours", @@ -47,7 +117,8 @@ "checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)", "installing_common_redist": "{{log}}…", "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": { "search": "Filtrer…", @@ -198,7 +269,113 @@ "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_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": { "title": "Activer Hydra", @@ -237,7 +414,11 @@ "resume_seeding": "Reprendre le partage", "options": "Gérer", "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": { "downloads_path": "Chemin des téléchargements", @@ -366,7 +547,40 @@ "bottom-left": "En bas à gauche", "bottom-center": "En bas au centre", "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": { "download_complete": "Téléchargement terminé", diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index c2a59873..b83fec51 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -22,7 +22,7 @@ "downloading": "{{title}} ({{percentage}} - Letöltés…)", "filter": "Könyvtár szűrése", "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", "sign_in": "Bejelentkezés", "friends": "Barátok", @@ -94,6 +94,12 @@ "header": { "search": "Keresés", "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", "catalogue": "Katalógus", "library": "Könyvtár", @@ -109,6 +115,7 @@ "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ő…", "checking_files": "A(z) {{title}} fájljaiból… ({{percentage}} kész)", + "extracting": "{{title}} kicsomagolása… ({{percentage}} kicsomagolva)", "installing_common_redist": "{{log}}…", "installation_complete": "Telepítés befejezve", "installation_complete_message": "A(z) Alapvető segédprogramok sikeresen telepítve" @@ -165,7 +172,7 @@ "playing_now": "Játékban: ", "change": "Változtatás", "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 menüjében változtathatod meg", + "select_folder_hint": "A letöltési mappát a <0>Beállításokban változtathatod meg", "download_now": "Letöltés", "no_shop_details": "A bolt adatai nem érhetőek el.", "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", "download_in_progress": "Letöltés folyamatban", "download_paused": "Letöltés szüneteltetve", + "extracting": "Kicsomagolás", "last_downloaded_option": "Utoljára letöltött", "new_download_option": "Új", "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", "install": "Telepít", "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", "queued": "Várakozásban", "no_downloads_title": "Oly üres..", @@ -408,7 +416,11 @@ "resume_seeding": "Seedelés folytatása", "options": "Kezelés", "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": { "downloads_path": "Letöltési útvonalak", @@ -669,7 +681,7 @@ "no_blocked_users": "Nincs letiltott felhasználó", "friend_code_copied": "Barát kód kimásolva", "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 menüjébe", + "privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállításokba", "locked_profile": "Ez a profil privát", "image_process_failure": "Hiba a kép feldolgozása közben", "required_field": "Ez a mező kötelező", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index ee0da176..719f72f7 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -408,7 +408,9 @@ "delete_archive_title": "Deseja deletar {{fileName}}?", "delete_archive_description": "O arquivo foi extraído com sucesso e não é mais necessário.", "yes": "Sim", - "no": "Não" + "no": "Não", + "network": "REDE", + "peak": "PICO" }, "settings": { "downloads_path": "Diretório dos downloads", diff --git a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss new file mode 100644 index 00000000..454653b8 --- /dev/null +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss @@ -0,0 +1,61 @@ +@use "../../scss/globals.scss"; + +.fullscreen-media-modal { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + position: relative; + background-color: rgba(0, 0, 0, 0.5); + + &__close-button { + position: absolute; + top: calc(globals.$spacing-unit * 4); + right: calc(globals.$spacing-unit * 3); + 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); + } +} diff --git a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx new file mode 100644 index 00000000..700bddb1 --- /dev/null +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -0,0 +1,88 @@ +import { useEffect, useRef } from "react"; +import { createPortal } from "react-dom"; +import { XIcon } from "@primer/octicons-react"; +import { useTranslation } from "react-i18next"; + +import { Backdrop } from "../backdrop/backdrop"; +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(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( + + + + +
+ {alt} +
+
+
, + document.body + ); +} diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index e8876fcb..8bb028bd 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -20,3 +20,4 @@ export * from "./game-context-menu/game-context-menu"; export * from "./game-context-menu/use-game-actions"; export * from "./star-rating/star-rating"; export * from "./search-dropdown/search-dropdown"; +export * from "./fullscreen-media-modal/fullscreen-media-modal"; diff --git a/src/renderer/src/features/download-slice.ts b/src/renderer/src/features/download-slice.ts index 0330cca3..f70421c0 100644 --- a/src/renderer/src/features/download-slice.ts +++ b/src/renderer/src/features/download-slice.ts @@ -12,6 +12,8 @@ export interface DownloadState { gameId: string | null; gamesWithDeletionInProgress: string[]; extraction: ExtractionInfo | null; + peakSpeeds: Record; + speedHistory: Record; } const initialState: DownloadState = { @@ -19,6 +21,8 @@ const initialState: DownloadState = { gameId: null, gamesWithDeletionInProgress: [], extraction: null, + peakSpeeds: {}, + speedHistory: {}, }; export const downloadSlice = createSlice({ @@ -28,6 +32,27 @@ export const downloadSlice = createSlice({ setLastPacket: (state, action: PayloadAction) => { state.lastPacket = action.payload; 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) => { state.lastPacket = null; @@ -62,6 +87,20 @@ export const downloadSlice = createSlice({ 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) => { + state.peakSpeeds[action.payload] = 0; + state.speedHistory[action.payload] = []; + }, }, }); @@ -72,4 +111,6 @@ export const { removeGameFromDeleting, setExtractionProgress, clearExtraction, + updatePeakSpeed, + clearPeakSpeed, } = downloadSlice.actions; diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 52fbcdfd..6a22148a 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -412,10 +412,12 @@ function HeroDownloadView({ )} - {game.download?.downloader && ( + {game.download?.downloader !== undefined && (
- {DOWNLOADER_NAME[game.download.downloader]} + + {DOWNLOADER_NAME[Number(game.download.downloader)]} +
)} @@ -512,8 +514,9 @@ export function DownloadGroup({ const { formatDistance } = useDate(); - const [peakSpeeds, setPeakSpeeds] = useState>({}); - const speedHistoryRef = useRef>({}); + // Get speed history and peak speeds from Redux (centralized state) + const speedHistory = useAppSelector((state) => state.download.speedHistory); + const peakSpeeds = useAppSelector((state) => state.download.peakSpeeds); const [dominantColors, setDominantColors] = useState>( {} ); @@ -576,68 +579,8 @@ export function DownloadGroup({ }); }, [library, lastPacket?.gameId]); - useEffect(() => { - if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) { - const gameId = lastPacket.gameId; - - const currentPeak = peakSpeeds[gameId] || 0; - if (lastPacket.downloadSpeed > currentPeak) { - setPeakSpeeds((prev) => ({ - ...prev, - [gameId]: lastPacket.downloadSpeed, - })); - } - - if (!speedHistoryRef.current[gameId]) { - speedHistoryRef.current[gameId] = []; - } - - speedHistoryRef.current[gameId].push(lastPacket.downloadSpeed); - - if (speedHistoryRef.current[gameId].length > 120) { - speedHistoryRef.current[gameId].shift(); - } - } - }, [lastPacket?.gameId, lastPacket?.downloadSpeed, peakSpeeds]); - - useEffect(() => { - for (const game of library) { - if ( - game.download && - game.download.progress < 0.01 && - game.download.status !== "paused" - ) { - // Fresh download - clear any old data - if (speedHistoryRef.current[game.id]?.length > 0) { - speedHistoryRef.current[game.id] = []; - setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 })); - } - } - } - }, [library]); - - useEffect(() => { - const timeouts: NodeJS.Timeout[] = []; - - for (const game of library) { - if ( - game.download?.progress === 1 && - speedHistoryRef.current[game.id]?.length > 0 - ) { - const timeout = setTimeout(() => { - speedHistoryRef.current[game.id] = []; - setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 })); - }, 10_000); - timeouts.push(timeout); - } - } - - return () => { - for (const timeout of timeouts) { - clearTimeout(timeout); - } - }; - }, [library]); + // Speed history and peak speeds are now tracked in Redux (in setLastPacket reducer) + // No local effect needed - data is updated atomically when packets arrive useEffect(() => { if (library.length > 0 && title === t("download_in_progress")) { @@ -842,7 +785,14 @@ export function DownloadGroup({ ? (lastPacket?.downloadSpeed ?? 0) : 0; 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 + // Fall back to game.id if lastPacket is not available + const dataKey = lastPacket?.gameId ?? game.id; + const gameSpeedHistory = speedHistory[dataKey] ?? []; + 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) { @@ -864,7 +814,7 @@ export function DownloadGroup({ currentProgress={currentProgress} dominantColor={dominantColor} lastPacket={lastPacket} - speedHistory={speedHistoryRef.current[game.id] || []} + speedHistory={gameSpeedHistory} formatSpeed={formatSpeed} calculateETA={calculateETA} pauseDownload={pauseDownload} @@ -908,7 +858,9 @@ export function DownloadGroup({
- {DOWNLOADER_NAME[game.download!.downloader]} + + {DOWNLOADER_NAME[Number(game.download!.downloader)]} +
{extraction?.visibleId === game.id ? ( diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index 8293f331..c833caf5 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -10,7 +10,12 @@ import { XCircleFillIcon, } from "@primer/octicons-react"; 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 { useAppSelector, @@ -36,6 +41,7 @@ type FriendAction = export function ProfileHero() { const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [showWrappedModal, setShowWrappedModal] = useState(false); + const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false); const { @@ -249,10 +255,12 @@ export function ProfileHero() { ]); const handleAvatarClick = useCallback(() => { - if (isMe) { + if (userProfile?.profileImageUrl) { + setShowFullscreenAvatar(true); + } else if (isMe) { setShowEditProfileModal(true); } - }, [isMe]); + }, [isMe, userProfile?.profileImageUrl]); const currentGame = useMemo(() => { if (isMe) { @@ -282,6 +290,12 @@ export function ProfileHero() { onClose={() => setShowWrappedModal(false)} /> )} + setShowFullscreenAvatar(false)} + src={userProfile?.profileImageUrl} + alt={userProfile?.displayName} + />