From 817870cdbb44853c94c858a3001518dc23c0f949 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 10 Dec 2025 17:11:10 +0000 Subject: [PATCH 01/34] refactor: simplify Aria2 spawn logic and update GofileApi download link request --- src/main/main.ts | 4 +--- src/main/services/aria2.ts | 9 ++++++--- src/main/services/hosters/gofile.ts | 7 ++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index c176efa7..86bfb458 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -33,9 +33,7 @@ export const loadState = async () => { await import("./events"); - if (process.platform !== "darwin") { - Aria2.spawn(); - } + Aria2.spawn(); if (userPreferences?.realDebridApiToken) { RealDebridClient.authorize(userPreferences.realDebridApiToken); diff --git a/src/main/services/aria2.ts b/src/main/services/aria2.ts index f6835558..f3f49018 100644 --- a/src/main/services/aria2.ts +++ b/src/main/services/aria2.ts @@ -7,9 +7,12 @@ export class Aria2 { private static process: cp.ChildProcess | null = null; public static spawn() { - const binaryPath = app.isPackaged - ? path.join(process.resourcesPath, "aria2c") - : path.join(__dirname, "..", "..", "binaries", "aria2c"); + const binaryPath = + process.platform === "darwin" + ? "aria2c" + : app.isPackaged + ? path.join(process.resourcesPath, "aria2c") + : path.join(__dirname, "..", "..", "binaries", "aria2c"); this.process = cp.spawn( binaryPath, diff --git a/src/main/services/hosters/gofile.ts b/src/main/services/hosters/gofile.ts index 5560ad31..fb9b97e3 100644 --- a/src/main/services/hosters/gofile.ts +++ b/src/main/services/hosters/gofile.ts @@ -36,16 +36,13 @@ export class GofileApi { } public static async getDownloadLink(id: string) { - const searchParams = new URLSearchParams({ - wt: WT, - }); - const response = await axios.get<{ status: string; data: GofileContentsResponse; - }>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, { + }>(`https://api.gofile.io/contents/${id}`, { headers: { Authorization: `Bearer ${this.token}`, + "X-Website-Token": WT, }, }); From 8aa6e113e74f8fd1053b867570b862343931769e Mon Sep 17 00:00:00 2001 From: Moyasee Date: Wed, 10 Dec 2025 19:53:53 +0200 Subject: [PATCH 02/34] refactor(download-group): update button interaction and styles --- .../src/pages/downloads/download-group.scss | 25 +++++++++++++------ .../src/pages/downloads/download-group.tsx | 9 ++++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/pages/downloads/download-group.scss b/src/renderer/src/pages/downloads/download-group.scss index 0b9deea3..364a0391 100644 --- a/src/renderer/src/pages/downloads/download-group.scss +++ b/src/renderer/src/pages/downloads/download-group.scss @@ -108,16 +108,11 @@ cursor: pointer; display: flex; align-items: center; - transition: opacity 0.2s ease; + transition: scale 0.2s ease; outline: none; &:hover { - opacity: 0.8; - } - - &:focus, - &:focus-visible { - outline: none; + scale: 1.05; } } @@ -411,6 +406,22 @@ gap: calc(globals.$spacing-unit / 1); } + &__simple-title-button { + background: none; + border: none; + padding: 0; + cursor: pointer; + text-align: left; + width: 100%; + transition: opacity 0.2s ease; + + + &:focus, + &:focus-visible { + outline: none; + } + } + &__simple-title { font-size: 16px; font-weight: 600; diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index bcecbc7c..1efd2311 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -436,6 +436,7 @@ export function DownloadGroup({ seedingStatus, }: Readonly) { const { t } = useTranslation("downloads"); + const navigate = useNavigate(); const userPreferences = useAppSelector( (state) => state.userPreferences.value @@ -872,7 +873,13 @@ export function DownloadGroup({
-

{game.title}

+
{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 03/34] 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 04/34] 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 06/34] 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 07/34] 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 0268829946468ad04bd027d93d6209f219cf677f Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 12 Dec 2025 13:53:12 +0200 Subject: [PATCH 08/34] feat: add Wrapped 2025 view in profile --- src/locales/en/translation.json | 6 +- src/main/services/window-manager.ts | 8 +- .../profile-content/profile-content.tsx | 15 ++- .../profile/profile-content/profile-tabs.tsx | 17 ++- .../profile/profile-content/wrapped-tab.scss | 73 ++++++++++++ .../profile/profile-content/wrapped-tab.tsx | 104 ++++++++++++++++++ 6 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 src/renderer/src/pages/profile/profile-content/wrapped-tab.scss create mode 100644 src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ed8c7d4e..bc6f45ee 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -715,7 +715,11 @@ "karma_description": "Earned from positive likes on reviews", "user_reviews": "Reviews", "delete_review": "Delete Review", - "loading_reviews": "Loading reviews..." + "loading_reviews": "Loading reviews...", + "wrapped_2025": "Wrapped 2025", + "view_wrapped_title": "View {{displayName}}'s Wrapped 2025?", + "view_wrapped_yes": "Yes", + "view_wrapped_no": "No" }, "library": { "library": "Library", diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 04c77619..26d13228 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -36,9 +36,9 @@ export class WindowManager { private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions = { width: 1200, - height: 720, + height: 860, minWidth: 1024, - minHeight: 540, + minHeight: 860, backgroundColor: "#1c1c1c", titleBarStyle: process.platform === "linux" ? "default" : "hidden", icon, @@ -106,7 +106,7 @@ export class WindowManager { valueEncoding: "json", } ); - return data ?? { isMaximized: false, height: 720, width: 1200 }; + return data ?? { isMaximized: false, height: 860, width: 1200 }; } private static updateInitialConfig( @@ -224,7 +224,7 @@ export class WindowManager { ? { x: undefined, y: undefined, - height: this.initialConfigInitializationMainWindow.height ?? 720, + height: this.initialConfigInitializationMainWindow.height ?? 860, width: this.initialConfigInitializationMainWindow.width ?? 1200, isMaximized: true, } diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 8176bace..a117c12a 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -21,9 +21,10 @@ import { UserKarmaBox } from "./user-karma-box"; import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal"; import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; -import { ProfileTabs } from "./profile-tabs"; +import { ProfileTabs, type ProfileTabType } from "./profile-tabs"; import { LibraryTab } from "./library-tab"; import { ReviewsTab } from "./reviews-tab"; +import { WrappedConfirmModal } from "./wrapped-tab"; import { AnimatePresence } from "framer-motion"; import "./profile-content.scss"; @@ -95,7 +96,7 @@ export function ProfileContent() { const [sortBy, setSortBy] = useState("playedRecently"); const statsAnimation = useRef(-1); - const [activeTab, setActiveTab] = useState<"library" | "reviews">("library"); + const [activeTab, setActiveTab] = useState("library"); // User reviews state const [reviews, setReviews] = useState([]); @@ -104,6 +105,7 @@ export function ProfileContent() { const [votingReviews, setVotingReviews] = useState>(new Set()); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [reviewToDelete, setReviewToDelete] = useState(null); + const [wrappedModalVisible, setWrappedModalVisible] = useState(false); const dispatch = useAppDispatch(); @@ -386,6 +388,7 @@ export function ProfileContent() { activeTab={activeTab} reviewsTotalCount={reviewsTotalCount} onTabChange={setActiveTab} + onWrappedClick={() => setWrappedModalVisible(true)} />
    @@ -439,6 +442,13 @@ export function ProfileContent() { onClose={handleDeleteCancel} onConfirm={handleDeleteConfirm} /> + + setWrappedModalVisible(false)} + /> ); }, [ @@ -460,6 +470,7 @@ export function ProfileContent() { isLoadingReviews, votingReviews, deleteModalVisible, + wrappedModalVisible, ]); return ( diff --git a/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx b/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx index bc76f40c..9eac8843 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx @@ -2,16 +2,20 @@ import { motion } from "framer-motion"; import { useTranslation } from "react-i18next"; import "./profile-content.scss"; +export type ProfileTabType = "library" | "reviews"; + interface ProfileTabsProps { - activeTab: "library" | "reviews"; + activeTab: ProfileTabType; reviewsTotalCount: number; - onTabChange: (tab: "library" | "reviews") => void; + onTabChange: (tab: ProfileTabType) => void; + onWrappedClick: () => void; } export function ProfileTabs({ activeTab, reviewsTotalCount, onTabChange, + onWrappedClick, }: Readonly) { const { t } = useTranslation("user_profile"); @@ -62,6 +66,15 @@ export function ProfileTabs({ /> )}
    +
    + +
    ); } diff --git a/src/renderer/src/pages/profile/profile-content/wrapped-tab.scss b/src/renderer/src/pages/profile/profile-content/wrapped-tab.scss new file mode 100644 index 00000000..6669586d --- /dev/null +++ b/src/renderer/src/pages/profile/profile-content/wrapped-tab.scss @@ -0,0 +1,73 @@ +@use "../../../scss/globals.scss"; + +.wrapped-fullscreen-modal { + position: fixed; + inset: 0; + z-index: 999; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + margin: 0; + border: none; + background: transparent; + width: 100%; + height: 100%; + + &__backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.9); + border: none; + z-index: 1; + } + + &__container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: calc(globals.$spacing-unit * 2); + pointer-events: none; + z-index: 2; + } + + &__close-button { + position: absolute; + top: calc(globals.$spacing-unit * 5); + right: calc(globals.$spacing-unit * 5); + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: white; + transition: background 0.2s ease; + z-index: 10; + pointer-events: auto; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } + } + + &__content { + border-radius: 8px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5); + pointer-events: auto; + } + + &__iframe { + width: 100%; + height: 100%; + border: none; + } +} diff --git a/src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx b/src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx new file mode 100644 index 00000000..1716fcc0 --- /dev/null +++ b/src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx @@ -0,0 +1,104 @@ +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { XIcon } from "@primer/octicons-react"; +import { ConfirmationModal } from "@renderer/components"; +import "./wrapped-tab.scss"; + +interface WrappedModalProps { + userId: string; + displayName: string; + isOpen: boolean; + onClose: () => void; +} + +interface ScaleConfig { + scale: number; + width: number; + height: number; +} + +const SCALE_CONFIGS: Record = { + 0.25: { scale: 0.25, width: 270, height: 480 }, + 0.3: { scale: 0.3, width: 324, height: 576 }, + 0.5: { scale: 0.5, width: 540, height: 960 }, +}; + +const getScaleConfigForHeight = (height: number): ScaleConfig => { + if (height >= 1000) return SCALE_CONFIGS[0.5]; + if (height >= 650) return SCALE_CONFIGS[0.3]; + return SCALE_CONFIGS[0.25]; +}; + +export function WrappedConfirmModal({ + userId, + displayName, + isOpen, + onClose, +}: Readonly) { + const { t } = useTranslation("user_profile"); + const [showFullscreen, setShowFullscreen] = useState(false); + const [config, setConfig] = useState(SCALE_CONFIGS[0.5]); + + useEffect(() => { + if (!showFullscreen) return; + + const updateConfig = () => { + setConfig(getScaleConfigForHeight(window.innerHeight)); + }; + + updateConfig(); + window.addEventListener("resize", updateConfig); + return () => window.removeEventListener("resize", updateConfig); + }, [showFullscreen]); + + const handleConfirm = () => { + onClose(); + setShowFullscreen(true); + }; + + return ( + <> + + + {showFullscreen && ( + + + +
    +