From 14679fc31e557b26398bc21b14034fc0cbb5cf7c Mon Sep 17 00:00:00 2001 From: Kyatto <140931995+Lianela@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:05:59 -0600 Subject: [PATCH 01/26] Add new translation strings in Spanish --- src/locales/es/translation.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 4a582ef8..2a2dda98 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -458,6 +458,7 @@ "description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas", "button_delete_all_sources": "Eliminar todo", "added_download_source": "Añadir fuente de descarga", + "adding": "Añadiendo…" "download_sources_synced": "Todas las fuentes de descarga están sincronizadas", "insert_valid_json_url": "Introducí una URL de json válida", "found_download_option_zero": "Sin opciones de descargas encontrada", @@ -563,6 +564,19 @@ "debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.", "enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego", "autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego", + "change_achievement_sound": "Cambiar sonido de logro", + "download_source_already_exists": "Esta fuente de descarga URL ya existe.", + "download_source_failed": "Error", + "download_source_matched": "Actualizado", + "download_source_matching": "Actualizando", + "download_source_no_information": "Sin información disponible", + "download_source_pending_matching": "Actualizando pronto", + "download_sources_synced_successfully": "Todas las fuentes de descarga están sincronizadas", + "failed_add_download_source": "Error al añadir la fuente de descarga. Por favor intentá de nuevo.", + "hydra_cloud": "Hydra Cloud", + "preview_sound": "Vista previa de sonido", + "remove_achievement_sound": "Eliminar sonido de logros", + "removed_all_download_sources": "Todas las fuentes de descarga eliminadas", "hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego" }, "notifications": { From 214a7af4082917a0773cc301c74a7611a48e40fb Mon Sep 17 00:00:00 2001 From: Kyatto <140931995+Lianela@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:14:50 -0600 Subject: [PATCH 02/26] Fix JSON formatting in translation file --- src/locales/es/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 2a2dda98..12dae377 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -458,7 +458,7 @@ "description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas", "button_delete_all_sources": "Eliminar todo", "added_download_source": "Añadir fuente de descarga", - "adding": "Añadiendo…" + "adding": "Añadiendo…", "download_sources_synced": "Todas las fuentes de descarga están sincronizadas", "insert_valid_json_url": "Introducí una URL de json válida", "found_download_option_zero": "Sin opciones de descargas encontrada", From b59fb7dc36d23ac05f6f91f65ee9016f83ee6ca4 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:38:53 -0300 Subject: [PATCH 03/26] feat: support workwonders --- src/main/services/hydra-api.ts | 10 +++++++++- src/main/services/window-manager.ts | 6 ++++-- .../src/components/bottom-panel/bottom-panel.tsx | 4 ++-- src/types/level.types.ts | 2 ++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 7846571e..fa712105 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -58,7 +58,13 @@ export class HydraApi { const decodedBase64 = atob(payload as string); const jsonData = JSON.parse(decodedBase64); - const { accessToken, expiresIn, refreshToken } = jsonData; + const { + accessToken, + expiresIn, + refreshToken, + featurebaseJwt, + workwondersJwt, + } = jsonData; const now = new Date(); @@ -85,6 +91,8 @@ export class HydraApi { accessToken, refreshToken, tokenExpirationTimestamp, + featurebaseJwt, + workwondersJwt, }, { valueEncoding: "json" } ); diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index b11b4a9b..04c77619 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -138,7 +138,8 @@ export class WindowManager { (details, callback) => { if ( details.webContentsId !== this.mainWindow?.webContents.id || - details.url.includes("chatwoot") + details.url.includes("chatwoot") || + details.url.includes("workwonders") ) { return callback(details); } @@ -159,7 +160,8 @@ export class WindowManager { if ( details.webContentsId !== this.mainWindow?.webContents.id || details.url.includes("featurebase") || - details.url.includes("chatwoot") + details.url.includes("chatwoot") || + details.url.includes("workwonders") ) { return callback(details); } diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index 2c32c5da..186fcb4f 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -122,10 +122,10 @@ export function BottomPanel() {
{DOWNLOADER_NAME[game.download!.downloader]} From 19406dd05143e99fce621c84d184d3013a25ded9 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Wed, 10 Dec 2025 19:54:22 +0200 Subject: [PATCH 08/26] style(download-group): remove unnecessary blank line for cleaner SCSS --- src/renderer/src/pages/downloads/download-group.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/src/pages/downloads/download-group.scss b/src/renderer/src/pages/downloads/download-group.scss index 364a0391..3b098175 100644 --- a/src/renderer/src/pages/downloads/download-group.scss +++ b/src/renderer/src/pages/downloads/download-group.scss @@ -415,7 +415,6 @@ width: 100%; transition: opacity 0.2s ease; - &:focus, &:focus-visible { outline: none; From 82a125237b396316c090c34173fa3c2a847f2197 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Wed, 10 Dec 2025 20:36:24 +0200 Subject: [PATCH 09/26] fix: navigation on game image click not working --- .../src/pages/downloads/download-group.scss | 15 +++++++++++++++ .../src/pages/downloads/download-group.tsx | 8 ++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/downloads/download-group.scss b/src/renderer/src/pages/downloads/download-group.scss index 3b098175..e8921155 100644 --- a/src/renderer/src/pages/downloads/download-group.scss +++ b/src/renderer/src/pages/downloads/download-group.scss @@ -390,6 +390,21 @@ flex-shrink: 0; background-color: rgba(0, 0, 0, 0.3); 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 { width: 100%; diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 1efd2311..f956b113 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -868,9 +868,13 @@ export function DownloadGroup({ {downloadInfo.map(({ game, size, progress, isSeeding: seeding }) => { return (
  • -
    +
    +
    + ) : ( + + )} - ) : ( - - )} - -
    +
  • + )}
    @@ -442,6 +456,8 @@ export function DownloadGroup({ (state) => state.userPreferences.value ); + const extraction = useAppSelector((state) => state.download.extraction); + const { updateLibrary } = useLibrary(); const { @@ -819,16 +835,21 @@ export function DownloadGroup({ if (isDownloadingGroup && library.length > 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 ? (lastPacket?.downloadSpeed ?? 0) : 0; const finalDownloadSize = getFinalDownloadSize(game); const peakSpeed = peakSpeeds[game.id] || 0; - const currentProgress = - isGameDownloading && lastPacket - ? lastPacket.progress - : game.download?.progress || 0; + + let currentProgress = game.download?.progress || 0; + if (isGameExtracting) { + currentProgress = extraction.progress; + } else if (isGameDownloading && lastPacket) { + currentProgress = lastPacket.progress; + } const dominantColor = dominantColors[game.id] || "#fff"; @@ -836,6 +857,7 @@ export function DownloadGroup({ {DOWNLOADER_NAME[game.download!.downloader]}
    - {game.download?.extracting ? ( + {extraction?.visibleId === game.id ? ( - {t("extracting")} + {t("extracting")} ( + {Math.round(extraction.progress * 100)}%) ) : ( diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index c222ab65..35403ba1 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -1,6 +1,6 @@ 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 { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal"; @@ -13,6 +13,7 @@ import { ArrowDownIcon } from "@primer/octicons-react"; export default function Downloads() { const { library, updateLibrary } = useLibrary(); + const extraction = useAppSelector((state) => state.download.extraction); const { t } = useTranslation("downloads"); @@ -72,8 +73,10 @@ export default function Downloads() { /* Game has been manually added to the library */ if (!next.download) return prev; - /* Is downloading */ - if (lastPacket?.gameId === next.id || next.download.extracting) + /* Is downloading or extracting */ + const isExtracting = + next.download.extracting || extraction?.visibleId === next.id; + if (lastPacket?.gameId === next.id || isExtracting) return { ...prev, downloading: [...prev.downloading, next] }; /* Is either queued or paused */ @@ -96,7 +99,7 @@ export default function Downloads() { queued, complete, }; - }, [library, lastPacket?.gameId]); + }, [library, lastPacket?.gameId, extraction?.visibleId]); const downloadGroups = [ { diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx index 270ed030..24c37b18 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx @@ -1,7 +1,12 @@ import { useContext, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; 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 { gameDetailsContext } from "@renderer/context"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; @@ -17,6 +22,9 @@ export function HeroPanelPlaytime() { const { numberFormatter } = useFormat(); const { progress, lastPacket } = useDownload(); const { formatDistance } = useDate(); + const extraction = useAppSelector((state) => state.download.extraction); + + const isExtracting = extraction?.visibleId === game?.id; useEffect(() => { if (game?.lastTimePlayed) { @@ -52,6 +60,16 @@ export function HeroPanelPlaytime() { const isGameDownloading = game.download?.status === "active" && lastPacket?.gameId === game.id; + const extractionInProgressInfo = ( +
    + + {t("extracting")} + + + {formatDownloadProgress(extraction?.progress ?? 0)} +
    + ); + const downloadInProgressInfo = (
    @@ -72,7 +90,8 @@ export function HeroPanelPlaytime() { return ( <>

    {t("not_played_yet", { title: game?.title })}

    - {hasDownload && downloadInProgressInfo} + {isExtracting && extractionInProgressInfo} + {!isExtracting && hasDownload && downloadInProgressInfo} ); } @@ -81,7 +100,8 @@ export function HeroPanelPlaytime() { return ( <>

    {t("playing_now")}

    - {hasDownload && downloadInProgressInfo} + {isExtracting && extractionInProgressInfo} + {!isExtracting && hasDownload && downloadInProgressInfo} ); } @@ -113,9 +133,9 @@ export function HeroPanelPlaytime() { })}

    - {hasDownload ? ( - downloadInProgressInfo - ) : ( + {isExtracting && extractionInProgressInfo} + {!isExtracting && hasDownload && downloadInProgressInfo} + {!isExtracting && !hasDownload && (

    {t("last_time_played", { period: lastTimePlayed, diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.scss b/src/renderer/src/pages/game-details/hero/hero-panel.scss index c91e685c..10265b9e 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.scss +++ b/src/renderer/src/pages/game-details/hero/hero-panel.scss @@ -80,5 +80,11 @@ &--disabled { opacity: globals.$disabled-opacity; } + + &--extraction { + &::-webkit-progress-value { + background-color: #4caf50; + } + } } } diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.tsx b/src/renderer/src/pages/game-details/hero/hero-panel.tsx index 799f2c36..48cda106 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel.tsx @@ -1,7 +1,7 @@ import { useContext } from "react"; 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 { HeroPanelPlaytime } from "./hero-panel-playtime"; @@ -18,9 +18,13 @@ export function HeroPanel() { const { lastPacket } = useDownload(); + const extraction = useAppSelector((state) => state.download.extraction); + const isGameDownloading = game?.download?.status === "active" && lastPacket?.gameId === game?.id; + const isExtracting = extraction?.visibleId === game?.id; + const getInfo = () => { if (!game) { const [latestRepack] = repacks; @@ -49,6 +53,8 @@ export function HeroPanel() { (game?.download?.status === "active" && game?.download?.progress < 1) || game?.download?.status === "paused"; + const showExtractionProgressBar = isExtracting; + return (

    @@ -72,6 +78,14 @@ export function HeroPanel() { }`} /> )} + + {showExtractionProgressBar && ( + + )}
    ); diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 8059e000..c7abaacb 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -82,6 +82,7 @@ export interface Download { timestamp: number; extracting: boolean; automaticallyExtract: boolean; + extractionProgress: number; } export interface GameAchievement { diff --git a/yarn.lock b/yarn.lock index 416f4a21..9d354966 100644 --- a/yarn.lock +++ b/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" 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: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" 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: version "4.3.0" 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" 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: version "4.5.0" 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" 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: version "4.1.1" 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" 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: version "3.78.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.78.0.tgz#fd0ecbd0aa89857b98da06bd3909194abb0821ba" @@ -6927,6 +6965,11 @@ nopt@^6.0.0: dependencies: 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: version "6.1.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" From 0470958629e91c740becd3031cf73955dda69564 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 11 Dec 2025 15:35:40 +0200 Subject: [PATCH 11/26] refactor(decky-plugin): simplify plugin extraction logic using async/await --- src/main/services/decky-plugin.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/main/services/decky-plugin.ts b/src/main/services/decky-plugin.ts index 4dc1fdad..cb8999c3 100644 --- a/src/main/services/decky-plugin.ts +++ b/src/main/services/decky-plugin.ts @@ -74,21 +74,16 @@ export class DeckyPlugin { await fs.promises.mkdir(extractPath, { recursive: true }); - return new Promise((resolve, reject) => { - SevenZip.extractFile( - { - filePath: zipPath, - outputPath: extractPath, - }, - () => { - logger.log(`Plugin extracted to: ${extractPath}`); - resolve(extractPath); - }, - () => { - reject(new Error("Failed to extract plugin")); - } - ); - }); + try { + await SevenZip.extractFile({ + filePath: zipPath, + outputPath: extractPath, + }); + logger.log(`Plugin extracted to: ${extractPath}`); + return extractPath; + } catch { + throw new Error("Failed to extract plugin"); + } } private static needsSudo(): boolean { From 63f8289d0a7807374fc91be2a39b3892c67c1aa8 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 12 Dec 2025 12:44:02 +0200 Subject: [PATCH 12/26] feat: implement archive deletion prompt and translations for confirmation messages --- src/locales/en/translation.json | 6 ++- src/locales/pt-BR/translation.json | 6 ++- src/main/events/library/delete-archive.ts | 23 ++++++++++ src/main/events/library/index.ts | 1 + src/main/services/game-files-manager.ts | 28 +++++------- src/preload/index.ts | 11 +++++ src/renderer/src/app.tsx | 17 ++++++- src/renderer/src/declaration.d.ts | 4 ++ .../archive-deletion-error-modal.tsx | 44 +++++++++++++++++++ .../src/pages/downloads/download-group.scss | 2 +- .../src/pages/downloads/downloads.tsx | 6 ++- .../pages/game-details/hero/hero-panel.scss | 2 +- 12 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 src/main/events/library/delete-archive.ts create mode 100644 src/renderer/src/pages/downloads/archive-deletion-error-modal.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 3709a546..9be4ff26 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -416,7 +416,11 @@ "resume_seeding": "Resume seeding", "options": "Manage", "extract": "Extract files", - "extracting": "Extracting files…" + "extracting": "Extracting files…", + "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" }, "settings": { "downloads_path": "Downloads path", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 30a46278..ee0da176 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -404,7 +404,11 @@ "resume_seeding": "Semear", "options": "Gerenciar", "extract": "Extrair arquivos", - "extracting": "Extraindo arquivos…" + "extracting": "Extraindo arquivos…", + "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" }, "settings": { "downloads_path": "Diretório dos downloads", diff --git a/src/main/events/library/delete-archive.ts b/src/main/events/library/delete-archive.ts new file mode 100644 index 00000000..9cf64a63 --- /dev/null +++ b/src/main/events/library/delete-archive.ts @@ -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); diff --git a/src/main/events/library/index.ts b/src/main/events/library/index.ts index d9d628d0..75fc5cd9 100644 --- a/src/main/events/library/index.ts +++ b/src/main/events/library/index.ts @@ -8,6 +8,7 @@ import "./close-game"; import "./copy-custom-game-asset"; import "./create-game-shortcut"; import "./create-steam-shortcut"; +import "./delete-archive"; import "./delete-game-folder"; import "./extract-game-download"; import "./get-default-wine-prefix-selection-path"; diff --git a/src/main/services/game-files-manager.ts b/src/main/services/game-files-manager.ts index 3e0f1b47..f3684a0a 100644 --- a/src/main/services/game-files-manager.ts +++ b/src/main/services/game-files-manager.ts @@ -116,17 +116,15 @@ export class GameFilesManager { } } - for (const file of compressedFiles) { - const extractionPath = path.join(directoryPath, file); + const archivePaths = compressedFiles + .map((file) => path.join(directoryPath, file)) + .filter((archivePath) => fs.existsSync(archivePath)); - try { - if (fs.existsSync(extractionPath)) { - await fs.promises.unlink(extractionPath); - logger.info(`Deleted archive: ${file}`); - } - } catch (err) { - logger.error(`Failed to delete file: ${file}`, err); - } + if (archivePaths.length > 0) { + WindowManager.mainWindow?.webContents.send( + "on-archive-deletion-prompt", + archivePaths + ); } } @@ -186,12 +184,10 @@ export class GameFilesManager { await this.extractFilesInDirectory(extractionPath); if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) { - try { - await fs.promises.unlink(filePath); - logger.info(`Deleted archive: ${download.folderName}`); - } catch (err) { - logger.error(`Failed to delete file: ${download.folderName}`, err); - } + WindowManager.mainWindow?.webContents.send( + "on-archive-deletion-prompt", + [filePath] + ); } await downloadsSublevel.put(this.gameKey, { diff --git a/src/preload/index.ts b/src/preload/index.ts index 7be92065..5579b6fb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -279,6 +279,17 @@ contextBridge.exposeInMainWorld("electron", { 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 */ getDiskFreeSpace: (path: string) => diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 9c65d959..6619c890 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -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 { @@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next"; import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { useSubscription } from "./hooks/use-subscription"; import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal"; +import { ArchiveDeletionModal } from "./pages/downloads/archive-deletion-error-modal"; import { injectCustomCss, @@ -80,6 +81,10 @@ export function App() { const { showSuccessToast } = useToast(); + const [showArchiveDeletionModal, setShowArchiveDeletionModal] = + useState(false); + const [archivePaths, setArchivePaths] = useState([]); + useEffect(() => { Promise.all([ levelDBService.get("userPreferences", null, "json"), @@ -193,6 +198,10 @@ export function App() { dispatch(clearExtraction()); updateLibrary(); }), + window.electron.onArchiveDeletionPrompt((paths) => { + setArchivePaths(paths); + setShowArchiveDeletionModal(true); + }), ]; return () => { @@ -290,6 +299,12 @@ export function App() { feature={hydraCloudFeature} /> + setShowArchiveDeletionModal(false)} + /> + {userDetails && ( void ) => () => Electron.IpcRenderer; + onArchiveDeletionPrompt: ( + cb: (archivePaths: string[]) => void + ) => () => Electron.IpcRenderer; + deleteArchive: (filePath: string) => Promise; getDefaultWinePrefixSelectionPath: () => Promise; createSteamShortcut: (shop: GameShop, objectId: string) => Promise; diff --git a/src/renderer/src/pages/downloads/archive-deletion-error-modal.tsx b/src/renderer/src/pages/downloads/archive-deletion-error-modal.tsx new file mode 100644 index 00000000..ff931a61 --- /dev/null +++ b/src/renderer/src/pages/downloads/archive-deletion-error-modal.tsx @@ -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) { + 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 ( + + ); +} diff --git a/src/renderer/src/pages/downloads/download-group.scss b/src/renderer/src/pages/downloads/download-group.scss index 9d6cb111..bfd8fbda 100644 --- a/src/renderer/src/pages/downloads/download-group.scss +++ b/src/renderer/src/pages/downloads/download-group.scss @@ -538,7 +538,7 @@ border-radius: 4px; &--extraction { - background-color: #4caf50; + background-color: #fff; } } } diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index 35403ba1..10d817f1 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -40,11 +40,13 @@ export default function Downloads() { useEffect(() => { window.electron.onSeedingStatus((value) => setSeedingStatus(value)); - const unsubscribe = window.electron.onExtractionComplete(() => { + const unsubscribeExtraction = window.electron.onExtractionComplete(() => { updateLibrary(); }); - return () => unsubscribe(); + return () => { + unsubscribeExtraction(); + }; }, [updateLibrary]); const handleOpenGameInstaller = (shop: GameShop, objectId: string) => diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.scss b/src/renderer/src/pages/game-details/hero/hero-panel.scss index 10265b9e..6aa4d311 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.scss +++ b/src/renderer/src/pages/game-details/hero/hero-panel.scss @@ -83,7 +83,7 @@ &--extraction { &::-webkit-progress-value { - background-color: #4caf50; + background-color: #fff; } } } From 77b6f1b2ad6f6d98153d2c10fe8693c770180b54 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 16:10:57 +0200 Subject: [PATCH 13/26] feat: add peak speed tracking and management in download slice --- src/renderer/src/features/download-slice.ts | 17 +++++++ .../src/pages/downloads/download-group.tsx | 47 +++++++++---------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/renderer/src/features/download-slice.ts b/src/renderer/src/features/download-slice.ts index 0330cca3..ae0c9c9d 100644 --- a/src/renderer/src/features/download-slice.ts +++ b/src/renderer/src/features/download-slice.ts @@ -12,6 +12,7 @@ export interface DownloadState { gameId: string | null; gamesWithDeletionInProgress: string[]; extraction: ExtractionInfo | null; + peakSpeeds: Record; } const initialState: DownloadState = { @@ -19,6 +20,7 @@ const initialState: DownloadState = { gameId: null, gamesWithDeletionInProgress: [], extraction: null, + peakSpeeds: {}, }; export const downloadSlice = createSlice({ @@ -62,6 +64,19 @@ 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; + }, }, }); @@ -72,4 +87,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..56ab398b 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -11,10 +11,12 @@ import { addMilliseconds } from "date-fns"; import { DOWNLOADER_NAME } from "@renderer/constants"; import { useAppSelector, + useAppDispatch, useDownload, useLibrary, useDate, } from "@renderer/hooks"; +import { updatePeakSpeed, clearPeakSpeed } from "@renderer/features"; import "./download-group.scss"; import { useTranslation } from "react-i18next"; @@ -512,7 +514,8 @@ export function DownloadGroup({ const { formatDistance } = useDate(); - const [peakSpeeds, setPeakSpeeds] = useState>({}); + const dispatch = useAppDispatch(); + const peakSpeeds = useAppSelector((state) => state.download.peakSpeeds); const speedHistoryRef = useRef>({}); const [dominantColors, setDominantColors] = useState>( {} @@ -577,28 +580,23 @@ export function DownloadGroup({ }, [library, lastPacket?.gameId]); useEffect(() => { - if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) { - const gameId = lastPacket.gameId; + if (!lastPacket?.gameId || lastPacket.downloadSpeed === undefined) return; - const currentPeak = peakSpeeds[gameId] || 0; - if (lastPacket.downloadSpeed > currentPeak) { - setPeakSpeeds((prev) => ({ - ...prev, - [gameId]: lastPacket.downloadSpeed, - })); - } + const gameId = lastPacket.gameId; + const downloadSpeed = lastPacket.downloadSpeed; - if (!speedHistoryRef.current[gameId]) { - speedHistoryRef.current[gameId] = []; - } + dispatch(updatePeakSpeed({ gameId, speed: downloadSpeed })); - speedHistoryRef.current[gameId].push(lastPacket.downloadSpeed); - - if (speedHistoryRef.current[gameId].length > 120) { - speedHistoryRef.current[gameId].shift(); - } + if (!speedHistoryRef.current[gameId]) { + speedHistoryRef.current[gameId] = []; } - }, [lastPacket?.gameId, lastPacket?.downloadSpeed, peakSpeeds]); + + speedHistoryRef.current[gameId].push(downloadSpeed); + + if (speedHistoryRef.current[gameId].length > 120) { + speedHistoryRef.current[gameId].shift(); + } + }, [lastPacket, dispatch]); useEffect(() => { for (const game of library) { @@ -610,11 +608,11 @@ export function DownloadGroup({ // Fresh download - clear any old data if (speedHistoryRef.current[game.id]?.length > 0) { speedHistoryRef.current[game.id] = []; - setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 })); + dispatch(clearPeakSpeed(game.id)); } } } - }, [library]); + }, [library, dispatch]); useEffect(() => { const timeouts: NodeJS.Timeout[] = []; @@ -624,9 +622,10 @@ export function DownloadGroup({ game.download?.progress === 1 && speedHistoryRef.current[game.id]?.length > 0 ) { + const gameId = game.id; const timeout = setTimeout(() => { - speedHistoryRef.current[game.id] = []; - setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 })); + speedHistoryRef.current[gameId] = []; + dispatch(clearPeakSpeed(gameId)); }, 10_000); timeouts.push(timeout); } @@ -637,7 +636,7 @@ export function DownloadGroup({ clearTimeout(timeout); } }; - }, [library]); + }, [library, dispatch]); useEffect(() => { if (library.length > 0 && title === t("download_in_progress")) { From 67f863e0f32a245346bfbb00d1f8964adb9856fc Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 17:41:15 +0200 Subject: [PATCH 14/26] fix: remove unused peak speed update dispatch in DownloadGroup component --- src/renderer/src/features/download-slice.ts | 9 +++++++++ src/renderer/src/pages/downloads/download-group.tsx | 6 ++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/features/download-slice.ts b/src/renderer/src/features/download-slice.ts index ae0c9c9d..80d78b0d 100644 --- a/src/renderer/src/features/download-slice.ts +++ b/src/renderer/src/features/download-slice.ts @@ -30,6 +30,15 @@ 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 atomically when packet arrives + if (action.payload?.gameId && action.payload.downloadSpeed != null) { + const { gameId, downloadSpeed } = action.payload; + const currentPeak = state.peakSpeeds[gameId] || 0; + if (downloadSpeed > currentPeak) { + state.peakSpeeds[gameId] = downloadSpeed; + } + } }, clearDownload: (state) => { state.lastPacket = null; diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 56ab398b..7c3f0f77 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -16,7 +16,7 @@ import { useLibrary, useDate, } from "@renderer/hooks"; -import { updatePeakSpeed, clearPeakSpeed } from "@renderer/features"; +import { clearPeakSpeed } from "@renderer/features"; import "./download-group.scss"; import { useTranslation } from "react-i18next"; @@ -585,8 +585,6 @@ export function DownloadGroup({ const gameId = lastPacket.gameId; const downloadSpeed = lastPacket.downloadSpeed; - dispatch(updatePeakSpeed({ gameId, speed: downloadSpeed })); - if (!speedHistoryRef.current[gameId]) { speedHistoryRef.current[gameId] = []; } @@ -596,7 +594,7 @@ export function DownloadGroup({ if (speedHistoryRef.current[gameId].length > 120) { speedHistoryRef.current[gameId].shift(); } - }, [lastPacket, dispatch]); + }, [lastPacket]); useEffect(() => { for (const game of library) { From 67ea9e78a2d5ba50bd588312be9af2d340971b41 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 18:14:52 +0200 Subject: [PATCH 15/26] feat: enhance download tracking by adding speed history management in download slice --- src/renderer/src/features/download-slice.ts | 17 ++++- .../src/pages/downloads/download-group.tsx | 74 +++---------------- 2 files changed, 28 insertions(+), 63 deletions(-) diff --git a/src/renderer/src/features/download-slice.ts b/src/renderer/src/features/download-slice.ts index 80d78b0d..f70421c0 100644 --- a/src/renderer/src/features/download-slice.ts +++ b/src/renderer/src/features/download-slice.ts @@ -13,6 +13,7 @@ export interface DownloadState { gamesWithDeletionInProgress: string[]; extraction: ExtractionInfo | null; peakSpeeds: Record; + speedHistory: Record; } const initialState: DownloadState = { @@ -21,6 +22,7 @@ const initialState: DownloadState = { gamesWithDeletionInProgress: [], extraction: null, peakSpeeds: {}, + speedHistory: {}, }; export const downloadSlice = createSlice({ @@ -31,13 +33,25 @@ export const downloadSlice = createSlice({ state.lastPacket = action.payload; if (!state.gameId && action.payload) state.gameId = action.payload.gameId; - // Track peak speed atomically when packet arrives + // 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) => { @@ -85,6 +99,7 @@ export const downloadSlice = createSlice({ }, clearPeakSpeed: (state, action: PayloadAction) => { state.peakSpeeds[action.payload] = 0; + state.speedHistory[action.payload] = []; }, }, }); diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 7c3f0f77..bf6584f2 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -11,12 +11,10 @@ import { addMilliseconds } from "date-fns"; import { DOWNLOADER_NAME } from "@renderer/constants"; import { useAppSelector, - useAppDispatch, useDownload, useLibrary, useDate, } from "@renderer/hooks"; -import { clearPeakSpeed } from "@renderer/features"; import "./download-group.scss"; import { useTranslation } from "react-i18next"; @@ -514,9 +512,9 @@ export function DownloadGroup({ const { formatDistance } = useDate(); - const dispatch = useAppDispatch(); + // 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 speedHistoryRef = useRef>({}); const [dominantColors, setDominantColors] = useState>( {} ); @@ -579,62 +577,8 @@ export function DownloadGroup({ }); }, [library, lastPacket?.gameId]); - useEffect(() => { - if (!lastPacket?.gameId || lastPacket.downloadSpeed === undefined) return; - - const gameId = lastPacket.gameId; - const downloadSpeed = lastPacket.downloadSpeed; - - if (!speedHistoryRef.current[gameId]) { - speedHistoryRef.current[gameId] = []; - } - - speedHistoryRef.current[gameId].push(downloadSpeed); - - if (speedHistoryRef.current[gameId].length > 120) { - speedHistoryRef.current[gameId].shift(); - } - }, [lastPacket]); - - 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] = []; - dispatch(clearPeakSpeed(game.id)); - } - } - } - }, [library, dispatch]); - - useEffect(() => { - const timeouts: NodeJS.Timeout[] = []; - - for (const game of library) { - if ( - game.download?.progress === 1 && - speedHistoryRef.current[game.id]?.length > 0 - ) { - const gameId = game.id; - const timeout = setTimeout(() => { - speedHistoryRef.current[gameId] = []; - dispatch(clearPeakSpeed(gameId)); - }, 10_000); - timeouts.push(timeout); - } - } - - return () => { - for (const timeout of timeouts) { - clearTimeout(timeout); - } - }; - }, [library, dispatch]); + // 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")) { @@ -839,7 +783,13 @@ 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) { @@ -861,7 +811,7 @@ export function DownloadGroup({ currentProgress={currentProgress} dominantColor={dominantColor} lastPacket={lastPacket} - speedHistory={speedHistoryRef.current[game.id] || []} + speedHistory={gameSpeedHistory} formatSpeed={formatSpeed} calculateETA={calculateETA} pauseDownload={pauseDownload} From 78d2be85f2fc51b20267d8c39b1a1c1dc561e8ba Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 18:15:21 +0200 Subject: [PATCH 16/26] style: format peak speed calculation for improved readability in DownloadGroup component --- src/renderer/src/pages/downloads/download-group.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index bf6584f2..31d74d2e 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -789,7 +789,8 @@ export function DownloadGroup({ 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; + const peakSpeed = + storedPeak !== undefined && storedPeak > 0 ? storedPeak : downloadSpeed; let currentProgress = game.download?.progress || 0; if (isGameExtracting) { From 95a7bc2236feeb83fb320189a0f411e1a7002f89 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 18:32:16 +0200 Subject: [PATCH 17/26] feat: add new translation keys for network and peak in English and Portuguese locales --- src/locales/en/translation.json | 4 +++- src/locales/pt-BR/translation.json | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9be4ff26..85a44236 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/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", From 142bd3156c3a045a547e9fa9045e1d36c5400d6d Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 14 Dec 2025 11:27:29 +0200 Subject: [PATCH 18/26] fix: ensure downloader value is properly checked and converted to number in DownloadGroup component --- src/renderer/src/pages/downloads/download-group.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 31d74d2e..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)]} +
    )} @@ -856,7 +858,9 @@ export function DownloadGroup({
    - {DOWNLOADER_NAME[game.download!.downloader]} + + {DOWNLOADER_NAME[Number(game.download!.downloader)]} +
    {extraction?.visibleId === game.id ? ( From cc5e0014f76a249ed32097f8b94528d3a5add20d Mon Sep 17 00:00:00 2001 From: "Kiwo.2" Date: Sun, 14 Dec 2025 15:04:51 +0100 Subject: [PATCH 19/26] Re-written some parts, Adjusted to new version --- src/locales/hu/translation.json | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index c2a59873..7decc890 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..", @@ -409,6 +417,10 @@ "options": "Kezelés", "extract": "Fájlok kibontása", "extracting": "Fájlok kibontása…" + "delete_archive_title": "Szeretnéd törölni ezt a fájlt? {{fileName}}", + "delete_archive_description": "A tömörített fájl ki lett csomagolva, s többé nincs rá szükség. ", + "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ő", From 214267df7e206cd584d6489da28d706608357709 Mon Sep 17 00:00:00 2001 From: "Kiwo.2" Date: Sun, 14 Dec 2025 15:09:40 +0100 Subject: [PATCH 20/26] Re-written some parts, Adjusted to new version --- src/locales/hu/translation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 7decc890..61d4cdc9 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -94,7 +94,7 @@ "header": { "search": "Keresés", "search_library": "Könyvtár böngészése", - "recent_searches": "Korábbi Keresések", + "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", @@ -420,7 +420,7 @@ "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" + "no": "Nem" }, "settings": { "downloads_path": "Letöltési útvonalak", From d5e6bed3b7bb682c351505cea99eaa641cbfdb2d Mon Sep 17 00:00:00 2001 From: "Kiwo.2" Date: Sun, 14 Dec 2025 15:58:27 +0100 Subject: [PATCH 21/26] Re-written some parts, Adjusted to new version --- src/locales/hu/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 61d4cdc9..b83fec51 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -416,7 +416,7 @@ "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", From affa7a2b2e3dd4708b0f9b0905af7178a2bf3205 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 16 Dec 2025 15:42:34 +0200 Subject: [PATCH 22/26] feat: add fullscreen media modal to profile hero for avatar display --- proto | 2 +- .../fullscreen-media-modal.scss | 61 +++++++++++++ .../fullscreen-media-modal.tsx | 88 +++++++++++++++++++ src/renderer/src/components/index.ts | 1 + .../profile/profile-hero/profile-hero.tsx | 21 ++++- 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss create mode 100644 src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx 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/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..12052834 --- /dev/null +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -0,0 +1,88 @@ +import { useCallback, 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/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index fc354d01..f9109067 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -9,7 +9,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, @@ -33,6 +38,7 @@ type FriendAction = export function ProfileHero() { const [showEditProfileModal, setShowEditProfileModal] = useState(false); + const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false); const { @@ -246,10 +252,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) { @@ -272,6 +280,13 @@ export function ProfileHero() { onClose={() => setShowEditProfileModal(false)} /> + setShowFullscreenAvatar(false)} + src={userProfile?.profileImageUrl} + alt={userProfile?.displayName} + /> +
    Date: Tue, 16 Dec 2025 15:43:43 +0200 Subject: [PATCH 23/26] refactor(fullscreen-media-modal): remove unused useCallback import --- .../fullscreen-media-modal/fullscreen-media-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 12052834..2d2abb6d 100644 --- a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import { XIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; From 1524e73ee6d901858b27ab938c7c75c6f64aaa01 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 16 Dec 2025 15:49:10 +0200 Subject: [PATCH 24/26] refactor(fullscreen-media-modal): replace div with dialog element for improved semantics --- .../fullscreen-media-modal/fullscreen-media-modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 2d2abb6d..700bddb1 100644 --- a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -65,7 +65,7 @@ export function FullscreenMediaModal({ return createPortal( -
    +
    -
    + , document.body ); From 677f34fe3d632cdd2aaef0253c35023468e4254d Mon Sep 17 00:00:00 2001 From: moi952 <52632400+moi952@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:41:53 +0100 Subject: [PATCH 25/26] feat: add missing French translations --- src/locales/fr/translation.json | 226 +++++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 6 deletions(-) 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é", From 24d65b50b432405358d9659d03a304ba58f7ad3b Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 18 Dec 2025 22:56:50 +0200 Subject: [PATCH 26/26] style: enhance overlay styling and adjust close button positioning --- .../fullscreen-media-modal.scss | 27 ++++++++++++++++--- .../fullscreen-media-modal.tsx | 5 ++-- 2 files changed, 26 insertions(+), 6 deletions(-) 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 index 454653b8..d46958ff 100644 --- a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss @@ -1,5 +1,21 @@ @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; @@ -7,12 +23,17 @@ width: 100%; height: 100%; position: relative; - background-color: rgba(0, 0, 0, 0.5); + margin: 0; + padding: 0; + border: none; + background: transparent; + max-width: none; + max-height: none; &__close-button { position: absolute; - top: calc(globals.$spacing-unit * 4); - right: calc(globals.$spacing-unit * 3); + 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); 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 index 700bddb1..f3722154 100644 --- a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -3,7 +3,6 @@ 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 { @@ -64,7 +63,7 @@ export function FullscreenMediaModal({ if (!visible || !src) return null; return createPortal( - +
    -
    , +
    , document.body ); }