From 2ccc93ea61d93d2067741b8f970eaa2dd17be728 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 4 Jan 2026 04:23:59 +0200 Subject: [PATCH 1/6] feat: add empty state for friends box and new translation key --- src/locales/en/translation.json | 1 + .../pages/profile/profile-content/friends-box.scss | 14 ++++++++++++++ .../pages/profile/profile-content/friends-box.tsx | 11 ++++++++++- .../profile/profile-content/profile-content.tsx | 4 ++-- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 87ad52b3..0aaf2c09 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -689,6 +689,7 @@ "blocked_users": "Blocked users", "unblock": "Unblock", "no_friends_added": "You have no added friends", + "no_friends_yet": "You haven't added any friends yet", "view_all": "View all", "load_more": "Load more", "loading": "Loading", diff --git a/src/renderer/src/pages/profile/profile-content/friends-box.scss b/src/renderer/src/pages/profile/profile-content/friends-box.scss index 088b204f..c1f4826d 100644 --- a/src/renderer/src/pages/profile/profile-content/friends-box.scss +++ b/src/renderer/src/pages/profile/profile-content/friends-box.scss @@ -4,6 +4,20 @@ &__box { padding: calc(globals.$spacing-unit * 2); position: relative; + + &--empty { + display: flex; + flex-direction: column; + align-items: center; + gap: calc(globals.$spacing-unit * 2); + } + } + + &__empty-text { + color: globals.$muted-color; + font-size: globals.$small-font-size; + margin: 0; + text-align: center; } &__add-friend-button { diff --git a/src/renderer/src/pages/profile/profile-content/friends-box.tsx b/src/renderer/src/pages/profile/profile-content/friends-box.tsx index cd0fed24..81e2b708 100644 --- a/src/renderer/src/pages/profile/profile-content/friends-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/friends-box.tsx @@ -19,6 +19,7 @@ export function FriendsBox() { const [showAddFriendModal, setShowAddFriendModal] = useState(false); const isMe = userDetails?.id === userProfile?.id; + const hasFriends = userProfile?.friends && userProfile.friends.length > 0; const getGameImage = (game: { iconUrl: string | null; title: string }) => { if (game.iconUrl) { @@ -35,7 +36,15 @@ export function FriendsBox() { return ; }; - if (!userProfile?.friends.length) return null; + if (!hasFriends) { + if (!isMe) return null; + + return ( +
+

{t("no_friends_yet")}

+
+ ); + } const visibleFriends = userProfile.friends.slice(0, MAX_VISIBLE_FRIENDS); const totalFriends = userProfile.friends.length; 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 dfd489ec..81246124 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -376,7 +376,7 @@ export function ProfileContent() { const hasAnyGames = hasGames || hasPinnedGames; const shouldShowRightContent = - hasAnyGames || userProfile.friends.length > 0; + hasAnyGames || userProfile.friends.length > 0 || isMe; return (
@@ -444,7 +444,7 @@ export function ProfileContent() { )} - {userProfile?.friends.length > 0 && ( + {(userProfile?.friends.length > 0 || isMe) && ( Date: Sun, 4 Jan 2026 21:10:37 +0200 Subject: [PATCH 2/6] fix: update password index initialization in tryPassword function for correct behavior --- src/main/services/7zip.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/services/7zip.ts b/src/main/services/7zip.ts index 0fa333dc..72c952c7 100644 --- a/src/main/services/7zip.ts +++ b/src/main/services/7zip.ts @@ -46,7 +46,7 @@ export class SevenZip { onProgress?: (progress: ExtractionProgress) => void ): Promise { return new Promise((resolve, reject) => { - const tryPassword = (index = -1) => { + const tryPassword = (index = 0) => { const password = passwords[index] ?? ""; logger.info( `Trying password "${password || "(empty)"}" on ${filePath}` @@ -115,7 +115,7 @@ export class SevenZip { }); }; - tryPassword(); + tryPassword(0); }); } From f37ccbb4c0b8c7baa699471a36cc4f7502ad765e Mon Sep 17 00:00:00 2001 From: Sneezedip <97994282+Sneezedip@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:33:14 -0100 Subject: [PATCH 3/6] Fix translation for hydra_cloud_feature_found --- src/locales/pt-PT/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index e48e1458..313a4fd9 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -508,7 +508,7 @@ "show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores", "animated_profile_banner": "Banner animado no perfil", "cloud_saving": "Progresso dos jogos na nuvem", - "hydra_cloud_feature_found": "Descubriste uma funcionalidade Hydra Cloud!", + "hydra_cloud_feature_found": "Descobriste uma funcionalidade Hydra Cloud!", "learn_more": "Saber mais" } } From e7a62c16fa3f0a5e30af17e7c3f925bd398d6e19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:29:01 +0000 Subject: [PATCH 4/6] chore(deps): bump @smithy/config-resolver Bumps the npm_and_yarn group with 1 update in the / directory: [@smithy/config-resolver](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/config-resolver). Updates `@smithy/config-resolver` from 4.3.1 to 4.4.5 - [Release notes](https://github.com/smithy-lang/smithy-typescript/releases) - [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/config-resolver/CHANGELOG.md) - [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/config-resolver@4.4.5/packages/config-resolver) --- updated-dependencies: - dependency-name: "@smithy/config-resolver" dependency-version: 4.4.5 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- yarn.lock | 63 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index d98c554d..27d1e902 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2203,14 +2203,15 @@ tslib "^2.6.2" "@smithy/config-resolver@^4.3.0", "@smithy/config-resolver@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.3.1.tgz#f1a0ed6faa52377909440002e1632be9fc901840" - integrity sha512-tWDwrWy37CDVGeaP8AIGZPFL2RoFtmd5Y+nTzLw5qroXNedT2S66EY2d+XzB1zxulCd6nfDXnAQu4auq90aj5Q== + version "4.4.5" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.4.5.tgz#35e792b6db00887bdd029df9b41780ca005d064b" + integrity sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg== dependencies: - "@smithy/node-config-provider" "^4.3.1" - "@smithy/types" "^4.7.0" + "@smithy/node-config-provider" "^4.3.7" + "@smithy/types" "^4.11.0" "@smithy/util-config-provider" "^4.2.0" - "@smithy/util-middleware" "^4.2.1" + "@smithy/util-endpoints" "^3.2.7" + "@smithy/util-middleware" "^4.2.7" tslib "^2.6.2" "@smithy/core@^3.15.0", "@smithy/core@^3.16.0": @@ -2421,6 +2422,16 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/node-config-provider@^4.3.7": + version "4.3.7" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz#c023fa857b008c314f621fb5b124724c157b2fd3" + integrity sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw== + dependencies: + "@smithy/property-provider" "^4.2.7" + "@smithy/shared-ini-file-loader" "^4.4.2" + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/node-http-handler@^4.3.0", "@smithy/node-http-handler@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.0.tgz#e1f6ae4a90cd7257699263bf8e06e653ff0e5f83" @@ -2440,6 +2451,14 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/property-provider@^4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.2.7.tgz#cd0044e13495cf4064b3a6ed3299e5f549ba7513" + integrity sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA== + dependencies: + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/protocol-http@^5.3.0", "@smithy/protocol-http@^5.3.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.3.1.tgz#add01f73290f1e8fd49d7102b63e3fe53a5e6e18" @@ -2480,6 +2499,14 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/shared-ini-file-loader@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz#8fa1b459de485b11185fe8c64182e3205a280ba9" + integrity sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg== + dependencies: + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/signature-v4@^5.3.0": version "5.3.1" resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.3.1.tgz#c3d711c29d37f3db4daf51750eea75204c4f51d4" @@ -2507,6 +2534,13 @@ "@smithy/util-stream" "^4.5.1" tslib "^2.6.2" +"@smithy/types@^4.11.0": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.11.0.tgz#c02f6184dcb47c4f0b387a32a7eca47956cc09f1" + integrity sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA== + dependencies: + tslib "^2.6.2" + "@smithy/types@^4.6.0", "@smithy/types@^4.7.0": version "4.7.0" resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.7.0.tgz#42d707276d9184aef705f04e04615cd1979d044f" @@ -2601,6 +2635,15 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/util-endpoints@^3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz#78cd5dd4aac8d9977f49d256d1e3418a09cade72" + integrity sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg== + dependencies: + "@smithy/node-config-provider" "^4.3.7" + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/util-hex-encoding@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz#1c22ea3d1e2c3a81ff81c0a4f9c056a175068a7b" @@ -2616,6 +2659,14 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/util-middleware@^4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-4.2.7.tgz#1cae2c4fd0389ac858d29f7170c33b4443e83524" + integrity sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w== + dependencies: + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/util-retry@^4.2.0", "@smithy/util-retry@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.2.1.tgz#8336368586a458cdce86fc92d6fb11fd1db41521" From 46e248c62a311b158c1849069a3e8115ab732311 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 11 Jan 2026 17:13:54 +0000 Subject: [PATCH 5/6] feat: add banner management features and translations - Introduced new translations for banner actions including "Change banner", "Replace banner", "Remove banner", and confirmation prompts in English, Spanish, Portuguese, and Russian. - Updated the UploadBackgroundImageButton component to support banner management with options to change, replace, or remove the banner. - Implemented a confirmation modal for removing the banner. - Enhanced user experience with animations for dropdown menus and button interactions. - Removed deprecated Qiwi downloader support and added Rootz downloader integration. --- src/locales/en/translation.json | 11 +- src/locales/es/translation.json | 9 +- src/locales/pt-BR/translation.json | 9 +- src/locales/ru/translation.json | 9 +- .../library/get-game-installer-action-type.ts | 59 ++++++ src/main/events/library/index.ts | 1 + src/main/events/profile/update-profile.ts | 32 +-- .../services/download/download-manager.ts | 20 +- src/main/services/hosters/index.ts | 2 +- src/main/services/hosters/qiwi.ts | 15 -- src/main/services/hosters/rootz.ts | 58 ++++++ src/preload/index.ts | 2 + .../dropdown-menu/dropdown-menu.scss | 12 ++ src/renderer/src/components/header/header.tsx | 15 -- src/renderer/src/constants.ts | 2 +- src/renderer/src/declaration.d.ts | 4 + .../src/pages/downloads/download-group.scss | 7 + .../src/pages/downloads/download-group.tsx | 75 +++++-- .../profile-content/user-stats-box.scss | 17 ++ .../profile-content/user-stats-box.tsx | 24 ++- .../profile/profile-hero/profile-hero.scss | 35 ---- .../profile/profile-hero/profile-hero.tsx | 53 ++--- .../upload-background-image-button.scss | 81 +++++++- .../upload-background-image-button.tsx | 183 ++++++++++++++++-- src/shared/constants.ts | 2 +- src/shared/index.ts | 4 +- 26 files changed, 579 insertions(+), 162 deletions(-) create mode 100644 src/main/events/library/get-game-installer-action-type.ts delete mode 100644 src/main/services/hosters/qiwi.ts create mode 100644 src/main/services/hosters/rootz.ts diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 87ad52b3..752cbc25 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -716,8 +716,15 @@ "profile_reported": "Profile reported", "your_friend_code": "Your friend code:", "copy_friend_code": "Copy friend code", + "copied": "Copied!", "upload_banner": "Upload banner", "uploading_banner": "Uploading banner…", + "change_banner": "Change banner", + "replace_banner": "Replace banner", + "remove_banner": "Remove banner", + "remove_banner_modal_title": "Remove banner?", + "remove_banner_confirmation": "Are you sure you want to remove your banner? You can always pick a new one when you want.", + "remove": "Remove", "background_image_updated": "Background image updated", "stats": "Stats", "achievements": "achievements", @@ -738,9 +745,7 @@ "user_reviews": "Reviews", "delete_review": "Delete Review", "loading_reviews": "Loading reviews...", - "wrapped_2025": "Wrapped 2025", - "view_my_wrapped_button": "View My Wrapped 2025", - "view_wrapped_button": "View {{displayName}}'s Wrapped 2025" + "wrapped_2025": "Wrapped 2025" }, "library": { "library": "Library", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 666dd065..cce710ca 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -702,8 +702,15 @@ "profile_reported": "Perfil reportado", "your_friend_code": "Tu código de amistad:", "copy_friend_code": "Copiar código de amistad", + "copied": "¡Copiado!", "upload_banner": "Subir banner", "uploading_banner": "Subiendo banner…", + "change_banner": "Cambiar banner", + "replace_banner": "Reemplazar banner", + "remove_banner": "Eliminar banner", + "remove_banner_modal_title": "¿Eliminar banner?", + "remove_banner_confirmation": "¿Estás seguro de que querés eliminar tu banner? Siempre podés elegir uno nuevo cuando quieras.", + "remove": "Eliminar", "background_image_updated": "Imagen de fondo actualizada", "stats": "Estadísticas", "achievements": "logros", @@ -727,8 +734,6 @@ "user_reviews": "Reseñas", "loading_reviews": "Cargando reseñas...", "wrapped_2025": "Wrapped 2025", - "view_my_wrapped_button": "Ver Mi Wrapped 2025", - "view_wrapped_button": "Ver Wrapped 2025 de {{displayName}}", "no_reviews": "Sin reseñas aún", "delete_review": "Eliminar reseña" }, diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index ee5ef5dd..d3eaa995 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -707,8 +707,15 @@ "profile_reported": "Perfil reportado", "your_friend_code": "Seu código de amigo:", "copy_friend_code": "Copiar código de amigo", + "copied": "Copiado!", "upload_banner": "Carregar banner", "uploading_banner": "Carregando banner…", + "change_banner": "Alterar banner", + "replace_banner": "Substituir banner", + "remove_banner": "Remover banner", + "remove_banner_modal_title": "Remover banner?", + "remove_banner_confirmation": "Tem certeza de que deseja remover seu banner? Você sempre pode escolher um novo quando quiser.", + "remove": "Remover", "background_image_updated": "Imagem de fundo salva", "stats": "Estatísticas", "achievements": "conquistas", @@ -736,8 +743,6 @@ "user_reviews": "Avaliações", "loading_reviews": "Carregando avaliações...", "wrapped_2025": "Wrapped 2025", - "view_my_wrapped_button": "Ver Meu Wrapped 2025", - "view_wrapped_button": "Ver Wrapped 2025 de {{displayName}}", "no_reviews": "Ainda não há avaliações", "delete_review": "Excluir avaliação" }, diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index ff863617..bb0bc6ce 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -702,8 +702,15 @@ "profile_reported": "Жалоба на профиль отправлена", "your_friend_code": "Код вашего друга:", "copy_friend_code": "Копировать код друга", + "copied": "Скопировано!", "upload_banner": "Загрузить баннер", "uploading_banner": "Загрузка баннера...", + "change_banner": "Изменить баннер", + "replace_banner": "Заменить баннер", + "remove_banner": "Удалить баннер", + "remove_banner_modal_title": "Удалить баннер?", + "remove_banner_confirmation": "Вы уверены, что хотите удалить свой баннер? Вы всегда можете выбрать новый, когда захотите.", + "remove": "Удалить", "background_image_updated": "Фоновое изображение обновлено", "stats": "Статистика", "achievements": "Достижения", @@ -724,8 +731,6 @@ "user_reviews": "Отзывы", "loading_reviews": "Загрузка отзывов...", "wrapped_2025": "Wrapped 2025", - "view_my_wrapped_button": "Просмотреть мой Wrapped 2025", - "view_wrapped_button": "Просмотреть Wrapped 2025 {{displayName}}", "no_reviews": "Пока нет отзывов", "delete_review": "Удалить отзыв" }, diff --git a/src/main/events/library/get-game-installer-action-type.ts b/src/main/events/library/get-game-installer-action-type.ts new file mode 100644 index 00000000..2e58968a --- /dev/null +++ b/src/main/events/library/get-game-installer-action-type.ts @@ -0,0 +1,59 @@ +import path from "node:path"; +import fs from "node:fs"; + +import { getDownloadsPath } from "../helpers/get-downloads-path"; +import { registerEvent } from "../register-event"; +import { downloadsSublevel, levelKeys } from "@main/level"; +import { GameShop } from "@types"; + +const getGameInstallerActionType = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string +): Promise<"install" | "open-folder"> => { + const downloadKey = levelKeys.game(shop, objectId); + const download = await downloadsSublevel.get(downloadKey); + + if (!download?.folderName) return "open-folder"; + + const gamePath = path.join( + download.downloadPath ?? (await getDownloadsPath()), + download.folderName + ); + + if (!fs.existsSync(gamePath)) { + await downloadsSublevel.del(downloadKey); + return "open-folder"; + } + + // macOS always opens folder + if (process.platform === "darwin") { + return "open-folder"; + } + + // If path is a file, it will show in folder (open-folder behavior) + if (fs.lstatSync(gamePath).isFile()) { + return "open-folder"; + } + + // Check for setup.exe + const setupPath = path.join(gamePath, "setup.exe"); + if (fs.existsSync(setupPath)) { + return "install"; + } + + // Check if there's exactly one .exe file + const gamePathFileNames = fs.readdirSync(gamePath); + const gamePathExecutableFiles = gamePathFileNames.filter( + (fileName: string) => path.extname(fileName).toLowerCase() === ".exe" + ); + + if (gamePathExecutableFiles.length === 1) { + return "install"; + } + + // Otherwise, opens folder + return "open-folder"; +}; + +registerEvent("getGameInstallerActionType", getGameInstallerActionType); diff --git a/src/main/events/library/index.ts b/src/main/events/library/index.ts index 75fc5cd9..1e4db10d 100644 --- a/src/main/events/library/index.ts +++ b/src/main/events/library/index.ts @@ -13,6 +13,7 @@ import "./delete-game-folder"; import "./extract-game-download"; import "./get-default-wine-prefix-selection-path"; import "./get-game-by-object-id"; +import "./get-game-installer-action-type"; import "./get-library"; import "./open-game-executable-path"; import "./open-game-installer-path"; diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index f5a04f0d..1f0a0f95 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -51,22 +51,30 @@ const updateProfile = async ( "backgroundImageUrl", ]); - if (updateProfile.profileImageUrl) { - const profileImageUrl = await uploadImage( - "profile-image", - updateProfile.profileImageUrl - ).catch(() => undefined); + if (updateProfile.profileImageUrl !== undefined) { + if (updateProfile.profileImageUrl === null) { + payload["profileImageUrl"] = null; + } else { + const profileImageUrl = await uploadImage( + "profile-image", + updateProfile.profileImageUrl + ).catch(() => undefined); - payload["profileImageUrl"] = profileImageUrl; + payload["profileImageUrl"] = profileImageUrl; + } } - if (updateProfile.backgroundImageUrl) { - const backgroundImageUrl = await uploadImage( - "background-image", - updateProfile.backgroundImageUrl - ).catch(() => undefined); + if (updateProfile.backgroundImageUrl !== undefined) { + if (updateProfile.backgroundImageUrl === null) { + payload["backgroundImageUrl"] = null; + } else { + const backgroundImageUrl = await uploadImage( + "background-image", + updateProfile.backgroundImageUrl + ).catch(() => undefined); - payload["backgroundImageUrl"] = backgroundImageUrl; + payload["backgroundImageUrl"] = backgroundImageUrl; + } } return patchUserProfile(payload); diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index bc6746e2..86bb15cf 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -4,11 +4,11 @@ import { publishDownloadCompleteNotification } from "../notifications"; import type { Download, DownloadProgress, UserPreferences } from "@types"; import { GofileApi, - QiwiApi, DatanodesApi, MediafireApi, PixelDrainApi, VikingFileApi, + RootzApi, } from "../hosters"; import { PythonRPC } from "../python-rpc"; import { @@ -400,15 +400,6 @@ export class DownloadManager { save_path: download.downloadPath, }; } - case Downloader.Qiwi: { - const downloadUrl = await QiwiApi.getDownloadUrl(download.uri); - return { - action: "start", - game_id: downloadId, - url: downloadUrl, - save_path: download.downloadPath, - }; - } case Downloader.Datanodes: { const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri); return { @@ -537,6 +528,15 @@ export class DownloadManager { throw error; } } + case Downloader.Rootz: { + const downloadUrl = await RootzApi.getDownloadUrl(download.uri); + return { + action: "start", + game_id: downloadId, + url: downloadUrl, + save_path: download.downloadPath, + }; + } } } diff --git a/src/main/services/hosters/index.ts b/src/main/services/hosters/index.ts index e22fb680..d4e1b09c 100644 --- a/src/main/services/hosters/index.ts +++ b/src/main/services/hosters/index.ts @@ -1,8 +1,8 @@ export * from "./gofile"; -export * from "./qiwi"; export * from "./datanodes"; export * from "./mediafire"; export * from "./pixeldrain"; export * from "./buzzheavier"; export * from "./fuckingfast"; export * from "./vikingfile"; +export * from "./rootz"; diff --git a/src/main/services/hosters/qiwi.ts b/src/main/services/hosters/qiwi.ts deleted file mode 100644 index e18b011c..00000000 --- a/src/main/services/hosters/qiwi.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { requestWebPage } from "@main/helpers"; - -export class QiwiApi { - public static async getDownloadUrl(url: string) { - const document = await requestWebPage(url); - const fileName = document.querySelector("h1")?.textContent; - - const slug = url.split("/").pop(); - const extension = fileName?.split(".").pop(); - - const downloadUrl = `https://spyderrock.com/${slug}.${extension}`; - - return downloadUrl; - } -} diff --git a/src/main/services/hosters/rootz.ts b/src/main/services/hosters/rootz.ts new file mode 100644 index 00000000..cb4658fc --- /dev/null +++ b/src/main/services/hosters/rootz.ts @@ -0,0 +1,58 @@ +import axios, { AxiosError } from "axios"; +import { logger } from "../logger"; + +interface RootzApiResponse { + success: boolean; + data?: { + url: string; + fileName: string; + size: number; + mimeType: string; + expiresIn: number; + expiresAt: string | null; + downloads: number; + canDelete: boolean; + fileId: string; + isMirrored: boolean; + sourceService: string | null; + adsEnabled: boolean; + }; + error?: string; +} + +export class RootzApi { + public static async getDownloadUrl(uri: string): Promise { + try { + const url = new URL(uri); + const pathSegments = url.pathname.split("/").filter(Boolean); + + if (pathSegments.length < 2 || pathSegments[0] !== "d") { + throw new Error("Invalid rootz URL format"); + } + + const id = pathSegments[1]; + const apiUrl = `https://www.rootz.so/api/files/download-by-short/${id}`; + + const response = await axios.get(apiUrl); + + if (response.data.success && response.data.data?.url) { + return response.data.data.url; + } + + throw new Error("Failed to get download URL from rootz API"); + } catch (error) { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (axiosError.response?.status === 404) { + const errorMessage = + axiosError.response.data?.error || "File not found"; + logger.error(`[Rootz] ${errorMessage}`); + throw new Error(errorMessage); + } + } + + logger.error("[Rootz] Error fetching download URL:", error); + throw error; + } + } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 32bc0f88..dd7497bf 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -206,6 +206,8 @@ contextBridge.exposeInMainWorld("electron", { refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"), openGameInstaller: (shop: GameShop, objectId: string) => ipcRenderer.invoke("openGameInstaller", shop, objectId), + getGameInstallerActionType: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("getGameInstallerActionType", shop, objectId), openGameInstallerPath: (shop: GameShop, objectId: string) => ipcRenderer.invoke("openGameInstallerPath", shop, objectId), openGameExecutablePath: (shop: GameShop, objectId: string) => diff --git a/src/renderer/src/components/dropdown-menu/dropdown-menu.scss b/src/renderer/src/components/dropdown-menu/dropdown-menu.scss index 0f73c608..7406a483 100644 --- a/src/renderer/src/components/dropdown-menu/dropdown-menu.scss +++ b/src/renderer/src/components/dropdown-menu/dropdown-menu.scss @@ -8,6 +8,7 @@ min-width: 200px; flex-direction: column; align-items: center; + animation: dropdown-menu-fade-in 0.2s ease-out; } &__group { @@ -66,3 +67,14 @@ justify-content: center; } } + +@keyframes dropdown-menu-fade-in { + 0% { + opacity: 0; + transform: translateY(-8px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 5c058252..acaec9f1 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -224,21 +224,6 @@ export function Header() { setActiveIndex(-1); }; - useEffect(() => { - const prevPath = sessionStorage.getItem("prevPath"); - const currentPath = location.pathname; - - if ( - prevPath?.startsWith("/catalogue") && - !currentPath.startsWith("/catalogue") && - catalogueSearchValue - ) { - dispatch(setFilters({ title: "" })); - } - - sessionStorage.setItem("prevPath", currentPath); - }, [location.pathname, catalogueSearchValue, dispatch]); - useEffect(() => { if (!isDropdownVisible) return; diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index d227969b..b41938b0 100644 --- a/src/renderer/src/constants.ts +++ b/src/renderer/src/constants.ts @@ -7,7 +7,6 @@ export const DOWNLOADER_NAME = { [Downloader.Torrent]: "Torrent", [Downloader.Gofile]: "Gofile", [Downloader.PixelDrain]: "PixelDrain", - [Downloader.Qiwi]: "Qiwi", [Downloader.Datanodes]: "Datanodes", [Downloader.Mediafire]: "Mediafire", [Downloader.Buzzheavier]: "Buzzheavier", @@ -15,6 +14,7 @@ export const DOWNLOADER_NAME = { [Downloader.TorBox]: "TorBox", [Downloader.Hydra]: "Nimbus", [Downloader.VikingFile]: "VikingFile", + [Downloader.Rootz]: "Rootz", }; export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 4e7fd245..a2cc6ccf 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -167,6 +167,10 @@ declare global { getLibrary: () => Promise; refreshLibraryAssets: () => Promise; openGameInstaller: (shop: GameShop, objectId: string) => Promise; + getGameInstallerActionType: ( + shop: GameShop, + objectId: string + ) => Promise<"install" | "open-folder">; openGameInstallerPath: (shop: GameShop, objectId: string) => Promise; openGameExecutablePath: (shop: GameShop, objectId: string) => Promise; openGame: ( diff --git a/src/renderer/src/pages/downloads/download-group.scss b/src/renderer/src/pages/downloads/download-group.scss index bfd8fbda..76ec8958 100644 --- a/src/renderer/src/pages/downloads/download-group.scss +++ b/src/renderer/src/pages/downloads/download-group.scss @@ -511,6 +511,13 @@ min-height: unset; } + &__simple-action-btn { + padding: calc(globals.$spacing-unit); + min-height: unset; + gap: calc(globals.$spacing-unit); + min-width: 120px; + } + &__progress-wrapper { flex: 1; display: flex; diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 6a22148a..152f0f36 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -32,12 +32,12 @@ import { FileDirectoryIcon, LinkIcon, PlayIcon, - ThreeBarsIcon, TrashIcon, UnlinkIcon, XCircleIcon, GraphIcon, } from "@primer/octicons-react"; +import { MoreVertical, Folder } from "lucide-react"; import { average } from "color.js"; interface AnimatedPercentageProps { @@ -452,6 +452,7 @@ export function DownloadGroup({ seedingStatus, }: Readonly) { const { t } = useTranslation("downloads"); + const { t: tGameDetails } = useTranslation("game_details"); const navigate = useNavigate(); const userPreferences = useAppSelector( @@ -523,6 +524,9 @@ export function DownloadGroup({ const [optimisticallyResumed, setOptimisticallyResumed] = useState< Record >({}); + const [gameActionTypes, setGameActionTypes] = useState< + Record + >({}); const extractDominantColor = useCallback( async (imageUrl: string, gameId: string) => { @@ -770,6 +774,38 @@ export function DownloadGroup({ ] ); + // Fetch action types for completed games + useEffect(() => { + const fetchActionTypes = async () => { + const completedGames = library.filter( + (game) => game.download?.progress === 1 + ); + + const actionTypesPromises = completedGames.map(async (game) => { + try { + const actionType = + await window.electron.getGameInstallerActionType( + game.shop, + game.objectId + ); + return { gameId: game.id, actionType }; + } catch { + return { gameId: game.id, actionType: "open-folder" as const }; + } + }); + + const results = await Promise.all(actionTypesPromises); + const newActionTypes: Record = {}; + results.forEach(({ gameId, actionType }) => { + newActionTypes[gameId] = actionType; + }); + + setGameActionTypes((prev) => ({ ...prev, ...newActionTypes })); + }; + + fetchActionTypes(); + }, [library]); + if (!library.length) return null; const isDownloadingGroup = title === t("download_in_progress"); @@ -901,16 +937,31 @@ export function DownloadGroup({ )}
- {game.download?.progress === 1 && ( - - )} + {game.download?.progress === 1 && (() => { + const actionType = gameActionTypes[game.id] || "open-folder"; + const isInstall = actionType === "install"; + + return ( + + ); + })()} {isQueuedGroup && game.download?.progress !== 1 && (
diff --git a/src/renderer/src/pages/profile/profile-content/user-stats-box.scss b/src/renderer/src/pages/profile/profile-content/user-stats-box.scss index 72a4d580..a93153ac 100644 --- a/src/renderer/src/pages/profile/profile-content/user-stats-box.scss +++ b/src/renderer/src/pages/profile/profile-content/user-stats-box.scss @@ -29,6 +29,12 @@ background-color: rgba(255, 255, 255, 0.15); text-decoration: none; } + + &--wrapped { + &:hover { + background-color: transparent; + } + } } &__list-title { @@ -70,4 +76,15 @@ font-size: 0.75rem; line-height: 1.4; } + + &__wrapped-link { + background: none; + border: none; + padding: 0; + text-align: start; + color: globals.$body-color; + font-size: 0.875rem; + cursor: pointer; + text-decoration: underline; + } } diff --git a/src/renderer/src/pages/profile/profile-content/user-stats-box.tsx b/src/renderer/src/pages/profile/profile-content/user-stats-box.tsx index 8b61cdd6..9bdbd648 100644 --- a/src/renderer/src/pages/profile/profile-content/user-stats-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-stats-box.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext } from "react"; +import { useCallback, useContext, useState } from "react"; import { userProfileContext } from "@renderer/context"; import { useTranslation } from "react-i18next"; import { useFormat, useUserDetails } from "@renderer/hooks"; @@ -7,9 +7,11 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import { useSubscription } from "@renderer/hooks/use-subscription"; import { ClockIcon, TrophyIcon } from "@primer/octicons-react"; import { Award } from "lucide-react"; +import { WrappedFullscreenModal } from "./wrapped-tab"; import "./user-stats-box.scss"; export function UserStatsBox() { + const [showWrappedModal, setShowWrappedModal] = useState(false); const { showHydraCloudModal } = useSubscription(); const { userStats, isMe, userProfile } = useContext(userProfileContext); const { userDetails } = useUserDetails(); @@ -41,6 +43,18 @@ export function UserStatsBox() { return (
    + {userProfile?.hasCompletedWrapped2025 && ( +
  • + +
  • + )} + {(isMe || userStats.unlockedAchievementSum !== undefined) && (
  • @@ -126,6 +140,14 @@ export function UserStatsBox() {

  • )}
+ + {userProfile && ( + setShowWrappedModal(false)} + /> + )}
); } diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.scss b/src/renderer/src/pages/profile/profile-hero/profile-hero.scss index e3459823..7061eceb 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.scss +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.scss @@ -144,11 +144,6 @@ } } - &__left-actions { - display: flex; - gap: globals.$spacing-unit; - } - &__actions { display: flex; gap: globals.$spacing-unit; @@ -160,35 +155,5 @@ &--outline { border-color: globals.$body-color; } - - &--wrapped { - background: linear-gradient( - 120deg, - #2a57ff 0%, - #2951e6 11%, - #2f5bff 16%, - #2c56e8 29%, - #244acc 34%, - #2245c2 40%, - #3a6bff 45%, - #3766f2 50%, - #2444b8 56%, - #122a73 82%, - #2348b3 86%, - #1f429e 87%, - #10286a 93%, - #0e2a63 100% - ); - background-color: #2a57ff; - background-size: 105% 100%; - background-position: 100% 50%; - border: none; - color: white; - transition: background-position 0.4s ease; - - &:hover { - background-position: 0% 50%; - } - } } } 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 6fafc95e..624f177f 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -7,7 +7,6 @@ import { PencilIcon, PersonAddIcon, SignOutIcon, - TrophyIcon, XCircleFillIcon, } from "@primer/octicons-react"; import { buildGameDetailsPath } from "@renderer/helpers"; @@ -30,7 +29,6 @@ import { motion } from "framer-motion"; import type { FriendRequestAction } from "@types"; import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal"; -import { WrappedFullscreenModal } from "../profile-content/wrapped-tab"; import Skeleton from "react-loading-skeleton"; import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button"; import "./profile-hero.scss"; @@ -41,10 +39,10 @@ type FriendAction = export function ProfileHero() { const [showEditProfileModal, setShowEditProfileModal] = useState(false); - const [showWrappedModal, setShowWrappedModal] = useState(false); const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false); const [isCopyButtonHovered, setIsCopyButtonHovered] = useState(false); + const [isCopied, setIsCopied] = useState(false); const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } = useContext(userProfileContext); @@ -261,9 +259,23 @@ export function ProfileHero() { const copyFriendCode = useCallback(() => { if (userProfile?.id) { navigator.clipboard.writeText(userProfile.id); - showSuccessToast(t("friend_code_copied")); + setIsCopied(true); + + const startTime = performance.now(); + const duration = 1200; // 1.2 seconds + + const animate = (currentTime: number) => { + const elapsed = currentTime - startTime; + if (elapsed < duration) { + requestAnimationFrame(animate); + } else { + setIsCopied(false); + } + }; + + requestAnimationFrame(animate); } - }, [userProfile, showSuccessToast, t]); + }, [userProfile]); const currentGame = useMemo(() => { if (isMe) { @@ -286,13 +298,6 @@ export function ProfileHero() { onClose={() => setShowEditProfileModal(false)} /> - {userProfile && ( - setShowWrappedModal(false)} - /> - )} setShowFullscreenAvatar(false)} @@ -348,7 +353,7 @@ export function ProfileHero() { onMouseLeave={() => setIsCopyButtonHovered(false)} initial={{ width: 28 }} animate={{ - width: isCopyButtonHovered ? 105 : 28, + width: isCopyButtonHovered || isCopied ? 105 : 28, }} transition={{ duration: 0.2, ease: "easeInOut" }} > @@ -356,12 +361,12 @@ export function ProfileHero() { className="profile-hero__friend-code" initial={{ opacity: 0, marginRight: 0 }} animate={{ - opacity: isCopyButtonHovered ? 1 : 0, - marginRight: isCopyButtonHovered ? 8 : 0, + opacity: isCopyButtonHovered || isCopied ? 1 : 0, + marginRight: isCopyButtonHovered || isCopied ? 8 : 0, }} transition={{ duration: 0.2, ease: "easeInOut" }} > - {userProfile?.id} + {isCopied ? t("copied") : userProfile?.id} @@ -410,22 +415,6 @@ export function ProfileHero() { background: !backgroundImage ? heroBackground : undefined, }} > - {userProfile?.hasCompletedWrapped2025 && ( -
- -
- )}
{profileActions}
diff --git a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.scss b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.scss index e43cad6c..d7cd5ace 100644 --- a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.scss +++ b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.scss @@ -1,11 +1,86 @@ @use "../../../scss/globals.scss"; .upload-background-image-button { - position: absolute; - top: 16px; - right: 16px; + &__wrapper { + position: absolute; + top: 16px; + right: 16px; + } + border-color: globals.$body-color; box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.1); backdrop-filter: blur(20px); + + &__menu { + background-color: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + min-width: 180px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + z-index: 1000; + padding: 4px; + display: flex; + flex-direction: column; + animation: menu-fade-in 0.2s ease-out; + + &--closing { + animation: menu-fade-out 0.15s ease-in; + } + } + + &__menu-item { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 8px; + border-radius: 4px; + padding: 5px 12px; + cursor: pointer; + transition: background-color 0.1s ease-in-out; + font-size: 14px; + background: none; + border: none; + color: globals.$body-color; + text-align: left; + + &:hover:not(:disabled) { + background-color: rgba(255, 255, 255, 0.1); + } + + &:disabled { + cursor: default; + opacity: 0.6; + } + + &:focus { + background-color: rgba(255, 255, 255, 0.1); + outline: none; + } + } +} + +@keyframes menu-fade-in { + 0% { + opacity: 0; + transform: translateY(-8px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes menu-fade-out { + 0% { + opacity: 1; + transform: translateY(0); + } + 100% { + opacity: 0; + transform: translateY(-8px); + } } diff --git a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx index 33f90692..c0347712 100644 --- a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx +++ b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx @@ -1,6 +1,8 @@ -import { UploadIcon } from "@primer/octicons-react"; -import { Button } from "@renderer/components"; -import { useContext, useState } from "react"; +import { TrashIcon, UploadIcon } from "@primer/octicons-react"; +import { MoreVertical } from "lucide-react"; +import { Button, ConfirmationModal } from "@renderer/components"; +import { createPortal } from "react-dom"; +import { useContext, useEffect, useRef, useState } from "react"; import { userProfileContext } from "@renderer/context"; import { useToast, useUserDetails } from "@renderer/hooks"; import { useTranslation } from "react-i18next"; @@ -9,16 +11,33 @@ import "./upload-background-image-button.scss"; export function UploadBackgroundImageButton() { const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isMenuClosing, setIsMenuClosing] = useState(false); + const [showRemoveBannerModal, setShowRemoveBannerModal] = useState(false); + const buttonRef = useRef(null); + const menuRef = useRef(null); const { hasActiveSubscription } = useUserDetails(); const { t } = useTranslation("user_profile"); - const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext); + const { isMe, setSelectedBackgroundImage, userProfile, getUserProfile } = + useContext(userProfileContext); const { patchUser, fetchUserDetails } = useUserDetails(); const { showSuccessToast } = useToast(); - const handleChangeCoverClick = async () => { + const hasBanner = !!userProfile?.backgroundImageUrl; + + const closeMenu = () => { + setIsMenuClosing(true); + setTimeout(() => { + setIsMenuOpen(false); + setIsMenuClosing(false); + }, 150); + }; + + const handleReplaceBanner = async () => { + closeMenu(); try { const { filePaths } = await window.electron.showOpenDialog({ properties: ["openFile"], @@ -40,23 +59,159 @@ export function UploadBackgroundImageButton() { showSuccessToast(t("background_image_updated")); await fetchUserDetails(); + await getUserProfile(); } } finally { setIsUploadingBackgorundImage(false); } }; + const handleRemoveBannerClick = () => { + closeMenu(); + setShowRemoveBannerModal(true); + }; + + const handleRemoveBannerConfirm = async () => { + setShowRemoveBannerModal(false); + try { + setIsUploadingBackgorundImage(true); + setSelectedBackgroundImage(""); + await patchUser({ backgroundImageUrl: null }); + showSuccessToast(t("background_image_updated")); + await fetchUserDetails(); + await getUserProfile(); + } finally { + setIsUploadingBackgorundImage(false); + } + }; + + // Handle click outside, scroll, and escape key to close menu + useEffect(() => { + if (!isMenuOpen) return; + + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if ( + menuRef.current && + !menuRef.current.contains(target) && + buttonRef.current && + !buttonRef.current.contains(target) + ) { + closeMenu(); + } + }; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + closeMenu(); + } + }; + + const handleScroll = () => { + closeMenu(); + }; + + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleEscape); + window.addEventListener("scroll", handleScroll, true); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleEscape); + window.removeEventListener("scroll", handleScroll, true); + }; + }, [isMenuOpen]); + if (!isMe || !hasActiveSubscription) return null; - return ( - + + ); + } + + // Calculate menu position + const getMenuPosition = () => { + if (!buttonRef.current) return { top: 0, right: 0 }; + const rect = buttonRef.current.getBoundingClientRect(); + return { + top: rect.bottom + 5, + right: window.innerWidth - rect.right, + }; + }; + + const menuPosition = isMenuOpen ? getMenuPosition() : { top: 0, right: 0 }; + + const menuContent = isMenuOpen && ( +
- - {isUploadingBackgroundImage ? t("uploading_banner") : t("upload_banner")} - + + +
+ ); + + return ( + <> +
+ +
+ {createPortal(menuContent, document.body)} + setShowRemoveBannerModal(false)} + onConfirm={handleRemoveBannerConfirm} + cancelButtonLabel={t("cancel")} + confirmButtonLabel={t("remove")} + buttonsIsDisabled={isUploadingBackgroundImage} + /> + ); } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 5c28a27e..26be0632 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -3,7 +3,6 @@ export enum Downloader { Torrent, Gofile, PixelDrain, - Qiwi, Datanodes, Mediafire, TorBox, @@ -11,6 +10,7 @@ export enum Downloader { Buzzheavier, FuckingFast, VikingFile, + Rootz, } export enum DownloadSourceStatus { diff --git a/src/shared/index.ts b/src/shared/index.ts index 36996e1d..5da36bd9 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -110,7 +110,6 @@ export const getDownloadersForUri = (uri: string) => { if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile]; if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain]; - if (uri.startsWith("https://qiwi.gg")) return [Downloader.Qiwi]; if (uri.startsWith("https://datanodes.to")) return [Downloader.Datanodes]; if (uri.startsWith("https://www.mediafire.com")) return [Downloader.Mediafire]; @@ -127,6 +126,9 @@ export const getDownloadersForUri = (uri: string) => { if (uri.startsWith("https://vikingfile.com")) { return [Downloader.VikingFile]; } + if (uri.startsWith("https://www.rootz.so")) { + return [Downloader.Rootz]; + } if (realDebridHosts.some((host) => uri.startsWith(host))) return [Downloader.RealDebrid]; From f8ba72a0e2f3e1dcc03fe74afe3df0c18a66fab5 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 11 Jan 2026 17:14:24 +0000 Subject: [PATCH 6/6] refactor: clean up code formatting and improve readability in DownloadGroup and ProfileHero components - Adjusted indentation and spacing for better code clarity in DownloadGroup. - Removed unnecessary blank lines in ProfileHero to streamline the code structure. - Ensured consistent formatting across both components. --- .../src/pages/downloads/download-group.tsx | 63 ++++++++++--------- .../profile/profile-hero/profile-hero.tsx | 6 +- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 152f0f36..ce26a4b9 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -783,11 +783,10 @@ export function DownloadGroup({ const actionTypesPromises = completedGames.map(async (game) => { try { - const actionType = - await window.electron.getGameInstallerActionType( - game.shop, - game.objectId - ); + const actionType = await window.electron.getGameInstallerActionType( + game.shop, + game.objectId + ); return { gameId: game.id, actionType }; } catch { return { gameId: game.id, actionType: "open-folder" as const }; @@ -937,31 +936,35 @@ export function DownloadGroup({ )}
- {game.download?.progress === 1 && (() => { - const actionType = gameActionTypes[game.id] || "open-folder"; - const isInstall = actionType === "install"; - - return ( - - ); - })()} + {game.download?.progress === 1 && + (() => { + const actionType = + gameActionTypes[game.id] || "open-folder"; + const isInstall = actionType === "install"; + + return ( + + ); + })()} {isQueuedGroup && game.download?.progress !== 1 && (