Compare commits

..

16 Commits

Author SHA1 Message Date
Moyase
98852ce31e Merge branch 'main' into feat/LBX-398 2026-01-15 23:18:34 +02:00
Zamitto
7293afb618 Merge branch 'release/v3.8.0' 2026-01-15 08:43:06 -03:00
Zamitto
194e7918ca feat: dont setup ww feedback widget if user has no token 2026-01-15 08:42:33 -03:00
Moyasee
f9678ece1b refactor: remove game disk usage functionality and update related components 2026-01-15 01:51:36 +02:00
Zamitto
979958aca6 feat: update ww webRequest interceptor 2026-01-14 19:37:17 -03:00
Zamitto
6e92e0f79f fix: getLibrary throwing error 2026-01-14 00:37:22 -03:00
Zamitto
aef069d4c7 Merge branch 'release/v3.8.1' 2026-01-14 00:07:53 -03:00
Zamitto
1f447cc478 chore: add sentry var to build-renderer action 2026-01-14 00:05:55 -03:00
Zamitto
5d2dc3616c Merge pull request #1938 from hydralauncher/release/v3.8.1
sync main
2026-01-13 23:43:48 -03:00
Zamitto
1f9972f74e Merge pull request #1937 from hydralauncher/chore/add-sentry
chore: add sentry
2026-01-13 23:43:16 -03:00
Zamitto
3344f68408 feat: add semver for sentry 2026-01-13 23:42:22 -03:00
Zamitto
65be11cc07 chore: add sentry 2026-01-13 23:34:09 -03:00
Moyasee
af6d027b06 feat: implement game disk usage tracking and enhance UI 2026-01-14 03:48:40 +02:00
Chubby Granny Chaser
7e78a0f9f1 chore: update version to 3.8.1 and enhance translations
- Bumped version number in package.json to 3.8.1.
- Added new translation keys for notifications and loading states in Spanish, Portuguese, and Russian.
- Improved UI elements in download group with updated styles for buttons and layout adjustments.
2026-01-11 19:25:11 +00:00
Chubby Granny Chaser
d56cc8695b Merge pull request #1928 from hydralauncher/feat/LBX-367
feat: implement native HTTP downloader option
2026-01-11 18:43:47 +00:00
Zamitto
96140e614c Merge pull request #1917 from hydralauncher/fix/friends-box-display
hotfix: add empty state for friends box and new translation key
2026-01-04 02:59:53 -03:00
25 changed files with 455 additions and 44 deletions

View File

@@ -42,6 +42,7 @@ jobs:
run: yarn build
env:
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- name: Deploy to Cloudflare Pages
env:

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.8.0",
"version": "3.8.1",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -40,6 +40,7 @@
"@primer/octicons-react": "^19.9.0",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@reduxjs/toolkit": "^2.2.3",
"@sentry/react": "^10.33.0",
"@tiptap/extension-bold": "^3.6.2",
"@tiptap/extension-italic": "^3.6.2",
"@tiptap/extension-link": "^3.6.2",

View File

@@ -189,7 +189,6 @@
"downloader_not_configured": "Available but not configured",
"downloader_offline": "Link is offline",
"downloader_not_available": "Not available",
"recommended": "Recommended",
"go_to_settings": "Go to Settings",
"select_executable": "Select",
"no_executable_selected": "No executable selected",
@@ -774,7 +773,10 @@
"manual_playtime_tooltip": "This playtime has been manually updated",
"all_games": "All Games",
"recently_played": "Recently Played",
"favorites": "Favorites"
"favorites": "Favorites",
"disk_usage": "Disk usage",
"disk_usage_tooltip": "Installed size on disk",
"installer_size_tooltip": "Installer size"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",

View File

@@ -26,6 +26,7 @@
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
"sign_in": "Iniciar Sesión",
"friends": "Amigos",
"notifications": "Notificaciones",
"need_help": "¿Necesitás ayuda?",
"favorites": "Favoritos",
"playable_button_title": "Solo mostrar juegos que podés jugar en este momento",
@@ -115,6 +116,7 @@
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Restante {{eta}} - {{speed}}",
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Comprobando tiempo restante…",
"checking_files": "Revisando archivos de {{title}}… ({{percentage}} completado)",
"extracting": "Extrayendo {{title}}… ({{percentage}} completado)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Instalación completada",
"installation_complete_message": "Common redistributables instalados correctamente"
@@ -173,6 +175,7 @@
"repacks_modal_description": "Elegí el repack que querés descargar",
"select_folder_hint": "Si querés cambiar la carpeta por defecto, andá a <0>Ajustes</0>",
"download_now": "Descargar ahora",
"loading": "Cargando...",
"no_shop_details": "No se pudieron obtener detalles de la tienda.",
"download_options": "Opciones de descarga",
"download_path": "Ruta de descarga",
@@ -206,6 +209,7 @@
"danger_zone_section_description": "Remover este juego de tu librería o los archivos descargados por Hydra",
"download_in_progress": "Descarga en progreso",
"download_paused": "Descarga pausada",
"extracting": "Extrayendo",
"last_downloaded_option": "Última opción de descarga",
"new_download_option": "Nuevo",
"create_steam_shortcut": "Crear atajo de Steam",
@@ -400,6 +404,10 @@
"completed": "Completado",
"removed": "No descargado",
"cancel": "Cancelar",
"cancel_download": "¿Cancelar descarga?",
"cancel_download_description": "¿Estás seguro de que querés cancelar esta descarga? Todos los archivos descargados serán eliminados.",
"keep_downloading": "No, seguir descargando",
"yes_cancel": "Sí, cancelar",
"filter": "Filtrar juegos descargados",
"remove": "Remover",
"downloading_metadata": "Descargando metadatos…",
@@ -420,7 +428,13 @@
"resume_seeding": "Continuar sembrando",
"options": "Administrar",
"extract": "Extraer archivos",
"extracting": "Extrayendo archivos…"
"extracting": "Extrayendo archivos…",
"delete_archive_title": "¿Querés eliminar {{fileName}}?",
"delete_archive_description": "El archivo se extrajo exitosamente y ya no es necesario.",
"yes": "Sí",
"no": "No",
"network": "RED",
"peak": "PICO"
},
"settings": {
"downloads_path": "Ruta de descarga",
@@ -544,6 +558,7 @@
"show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo",
"extract_files_by_default": "Extraer archivos por defecto después de descargar",
"enable_steam_achievements": "Habilitar búsqueda de logros de Steam",
"enable_new_download_options_badges": "Mostrar badges de nuevas opciones de descarga",
"achievement_custom_notification_position": "Posición de notificación de logros",
"top-left": "Superior Izquierda",
"top-center": "Superior Centro",
@@ -570,20 +585,10 @@
"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"
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego",
"downloads": "Descargas",
"use_native_http_downloader": "Usar descargador HTTP nativo (experimental)",
"cannot_change_downloader_while_downloading": "No se puede cambiar esta configuración mientras una descarga está en progreso"
},
"notifications": {
"download_complete": "Descarga completada",
@@ -675,6 +680,7 @@
"blocked_users": "Usuarios bloqueados",
"unblock": "Desbloquear",
"no_friends_added": "No tenés amistades añadidas",
"no_friends_yet": "Aún no has agregado ningún amigo",
"view_all": "Ver todo",
"load_more": "Cargar más",
"loading": "Cargando",

View File

@@ -26,6 +26,7 @@
"game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login",
"friends": "Amigos",
"notifications": "Notificações",
"need_help": "Precisa de ajuda?",
"favorites": "Favoritos",
"playable_button_title": "Mostrar apenas jogos que você pode jogar agora",
@@ -163,6 +164,7 @@
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
"select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes</0>",
"download_now": "Iniciar download",
"loading": "Carregando...",
"no_shop_details": "Não foi possível obter os detalhes da loja.",
"download_options": "Opções de download",
"download_path": "Diretório de download",
@@ -368,6 +370,7 @@
"show_translation": "Mostrar tradução",
"show_original_translated_from": "Mostrar original (traduzido do {{language}})",
"hide_original": "Ocultar original",
"vote_failed": "Falha ao registrar seu voto. Por favor, tente novamente.",
"rating_count": "Avaliação",
"review_from_blocked_user": "Avaliação de usuário bloqueado",
"show": "Mostrar",
@@ -390,6 +393,10 @@
"completed": "Concluído",
"removed": "Cancelado",
"cancel": "Cancelar",
"cancel_download": "Cancelar download?",
"cancel_download_description": "Tem certeza de que deseja cancelar este download? Todos os arquivos baixados serão excluídos.",
"keep_downloading": "Não, continuar baixando",
"yes_cancel": "Sim, cancelar",
"filter": "Filtrar jogos baixados",
"remove": "Remover",
"downloading_metadata": "Baixando metadados…",
@@ -463,6 +470,7 @@
"download_sources_synced_successfully": "Fontes de download sincronizadas",
"removed_download_source": "Fonte removida",
"removed_download_sources": "Fontes removidas",
"removed_all_download_sources": "Todas as fontes de download removidas",
"cancel_button_confirmation_delete_all_sources": "Não",
"confirm_button_confirmation_delete_all_sources": "Sim, excluir tudo",
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
@@ -488,6 +496,7 @@
"blocked_users": "Usuários bloqueados",
"user_unblocked": "Usuário desbloqueado",
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
"hydra_cloud": "Hydra Cloud",
"launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
"seed_after_download_complete": "Semear após a conclusão do download",
@@ -550,6 +559,7 @@
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
"enable_steam_achievements": "Habilitar busca por conquistas da Steam",
"enable_new_download_options_badges": "Mostrar badges de novas opções de download",
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
"top-left": "Superior esquerdo",
"top-center": "Superior central",
@@ -567,6 +577,9 @@
"test_notification": "Testar notificação",
"achievement_sound_volume": "Volume do som de conquista",
"select_achievement_sound": "Selecionar som de conquista",
"change_achievement_sound": "Alterar som de conquista",
"remove_achievement_sound": "Remover som de conquista",
"preview_sound": "Reproduzir som",
"select": "Selecionar",
"preview": "Reproduzir",
"remove": "Remover",
@@ -574,7 +587,10 @@
"notification_preview": "Prévia da Notificação de Conquistas",
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
"autoplay_trailers_on_game_page": "Reproduzir trailers automaticamente na página do jogo",
"hide_to_tray_on_game_start": "Ocultar o Hydra na bandeja ao iniciar um jogo"
"hide_to_tray_on_game_start": "Ocultar o Hydra na bandeja ao iniciar um jogo",
"downloads": "Downloads",
"use_native_http_downloader": "Usar downloader HTTP nativo (experimental)",
"cannot_change_downloader_while_downloading": "Não é possível alterar esta configuração enquanto um download estiver em andamento"
},
"notifications": {
"download_complete": "Download concluído",
@@ -680,6 +696,7 @@
"blocked_users": "Usuários bloqueados",
"unblock": "Desbloquear",
"no_friends_added": "Você ainda não possui amigos adicionados",
"no_friends_yet": "Você ainda não adicionou nenhum amigo",
"view_all": "Ver todos",
"load_more": "Carregar mais",
"loading": "Carregando",

View File

@@ -26,6 +26,7 @@
"game_has_no_executable": "Файл запуска игры не выбран",
"sign_in": "Войти",
"friends": "Друзья",
"notifications": "Уведомления",
"need_help": "Нужна помощь?",
"favorites": "Избранное",
"playable_button_title": "Показать только установленные игры.",
@@ -115,6 +116,7 @@
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}",
"calculating_eta": "Загрузка {{title}}… ({{percentage}} завершено) - Подсчёт оставшегося времени…",
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)",
"extracting": "Распаковка {{title}}… ({{percentage}} завершено)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Установка завершена",
"installation_complete_message": "Библиотеки успешно установлены"
@@ -173,6 +175,7 @@
"repacks_modal_description": "Выберите репак для загрузки",
"select_folder_hint": "Чтобы изменить папку загрузок по умолчанию, откройте <0>Настройки</0>",
"download_now": "Загрузить сейчас",
"loading": "Загрузка...",
"no_shop_details": "Не удалось получить описание",
"download_options": "Источники",
"download_path": "Путь для загрузок",
@@ -208,6 +211,7 @@
"danger_zone_section_description": "Вы можете удалить эту игру из вашей библиотеки или файлы скачанные из Hydra",
"download_in_progress": "Идёт загрузка",
"download_paused": "Загрузка приостановлена",
"extracting": "Распаковка",
"last_downloaded_option": "Последний вариант загрузки",
"new_download_option": "Новый",
"create_steam_shortcut": "Создать ярлык Steam",
@@ -400,6 +404,10 @@
"completed": "Завершено",
"removed": "Не скачано",
"cancel": "Отмена",
"cancel_download": "Отменить загрузку?",
"cancel_download_description": "Вы уверены, что хотите отменить эту загрузку? Все загруженные файлы будут удалены.",
"keep_downloading": "Нет, продолжить загрузку",
"yes_cancel": "Да, отменить",
"filter": "Поиск загруженных игр",
"remove": "Удалить",
"downloading_metadata": "Загрузка метаданных…",
@@ -420,7 +428,13 @@
"resume_seeding": "Продолжить раздачу",
"options": "Управлять",
"extract": "Распаковать файлы",
"extracting": "Распаковка файлов…"
"extracting": "Распаковка файлов…",
"delete_archive_title": "Хотите удалить {{fileName}}?",
"delete_archive_description": "Файл был успешно распакован и больше не нужен.",
"yes": "Да",
"no": "Нет",
"network": "СЕТЬ",
"peak": "ПИК"
},
"settings": {
"downloads_path": "Путь загрузок",
@@ -556,6 +570,7 @@
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
"enable_steam_achievements": "Включить поиск достижений Steam",
"enable_new_download_options_badges": "Показывать значки новых вариантов загрузки",
"achievement_custom_notification_position": "Позиция уведомлений достижений",
"top-left": "Верхний левый угол",
"top-center": "Верхний центр",
@@ -573,6 +588,9 @@
"test_notification": "Тестовое уведомление",
"achievement_sound_volume": "Громкость звука достижения",
"select_achievement_sound": "Выбрать звук достижения",
"change_achievement_sound": "Изменить звук достижения",
"remove_achievement_sound": "Удалить звук достижения",
"preview_sound": "Предпросмотр звука",
"select": "Выбрать",
"preview": "Предпросмотр",
"remove": "Удалить",
@@ -580,7 +598,10 @@
"notification_preview": "Предварительный просмотр уведомления о достижении",
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру",
"autoplay_trailers_on_game_page": "Автоматически начинать воспроизведение трейлеров на странице игры",
"hide_to_tray_on_game_start": "Скрывать Hydra в трей при запуске игры"
"hide_to_tray_on_game_start": "Скрывать Hydra в трей при запуске игры",
"downloads": "Загрузки",
"use_native_http_downloader": "Использовать встроенный HTTP-загрузчик (экспериментально)",
"cannot_change_downloader_while_downloading": "Нельзя изменить эту настройку во время загрузки"
},
"notifications": {
"download_complete": "Загрузка завершена",
@@ -675,6 +696,7 @@
"blocked_users": "Заблокированные пользователи",
"unblock": "Разблокировать",
"no_friends_added": "Вы ещё не добавили ни одного друга",
"no_friends_yet": "Вы ещё не добавили ни одного друга",
"view_all": "Показать все",
"load_more": "Загрузить еще",
"loading": "Загрузка",

View File

@@ -0,0 +1,39 @@
import path from "node:path";
import fs from "node:fs";
export const getDirectorySize = async (dirPath: string): Promise<number> => {
let totalSize = 0;
try {
const stat = await fs.promises.stat(dirPath);
if (stat.isFile()) {
return stat.size;
}
if (!stat.isDirectory()) {
return 0;
}
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
try {
if (entry.isDirectory()) {
totalSize += await getDirectorySize(fullPath);
} else if (entry.isFile()) {
const fileStat = await fs.promises.stat(fullPath);
totalSize += fileStat.size;
}
} catch {
// Skip files that can't be accessed
}
}
} catch {
// Path doesn't exist or can't be read
}
return totalSize;
};

View File

@@ -1,7 +1,9 @@
import path from "node:path";
import fs from "node:fs";
import { registerEvent } from "../register-event";
import { logger } from "@main/services";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
const deleteArchive = async (
_event: Electron.IpcMainInvokeEvent,
@@ -11,8 +13,33 @@ const deleteArchive = async (
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
logger.info(`Deleted archive: ${filePath}`);
return true;
}
// Find the game that has this archive and clear installer size
const normalizedPath = path.normalize(filePath);
const downloads = await downloadsSublevel.values().all();
for (const download of downloads) {
if (!download.folderName) continue;
const downloadPath = path.normalize(
path.join(download.downloadPath, download.folderName)
);
if (downloadPath === normalizedPath) {
const gameKey = levelKeys.game(download.shop, download.objectId);
const game = await gamesSublevel.get(gameKey);
if (game) {
await gamesSublevel.put(gameKey, {
...game,
installerSizeInBytes: null,
});
}
break;
}
}
return true;
} catch (err) {
logger.error(`Failed to delete archive: ${filePath}`, err);

View File

@@ -5,15 +5,15 @@ import { getDownloadsPath } from "../helpers/get-downloads-path";
import { logger } from "@main/services";
import { registerEvent } from "../register-event";
import { GameShop } from "@types";
import { downloadsSublevel, levelKeys } from "@main/level";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
const deleteGameFolder = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
): Promise<void> => {
const downloadKey = levelKeys.game(shop, objectId);
const download = await downloadsSublevel.get(downloadKey);
const gameKey = levelKeys.game(shop, objectId);
const download = await downloadsSublevel.get(gameKey);
if (!download?.folderName) return;
@@ -49,7 +49,16 @@ const deleteGameFolder = async (
await deleteFile(folderPath, true);
await deleteFile(metaPath);
await downloadsSublevel.del(downloadKey);
await downloadsSublevel.del(gameKey);
// Clear installer size from game record
const game = await gamesSublevel.get(gameKey);
if (game) {
await gamesSublevel.put(gameKey, {
...game,
installerSizeInBytes: null,
});
}
};
registerEvent("deleteGameFolder", deleteGameFolder);

View File

@@ -1,3 +1,6 @@
import path from "node:path";
import fs from "node:fs";
import type { LibraryGame } from "@types";
import { registerEvent } from "../register-event";
import {
@@ -25,12 +28,43 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
const achievements = await gameAchievementsSublevel.get(key);
unlockedAchievementCount =
achievements?.unlockedAchievements.length ?? 0;
achievements?.unlockedAchievements?.length ?? 0;
}
// Verify installer still exists, clear if deleted externally
let installerSizeInBytes = game.installerSizeInBytes;
if (installerSizeInBytes && download?.folderName) {
const installerPath = path.join(
download.downloadPath,
download.folderName
);
if (!fs.existsSync(installerPath)) {
installerSizeInBytes = null;
gamesSublevel.put(key, { ...game, installerSizeInBytes: null });
}
}
// Verify installed folder still exists, clear if deleted externally
let installedSizeInBytes = game.installedSizeInBytes;
if (installedSizeInBytes && game.executablePath) {
const executableDir = path.dirname(game.executablePath);
if (!fs.existsSync(executableDir)) {
installedSizeInBytes = null;
gamesSublevel.put(key, {
...game,
installerSizeInBytes,
installedSizeInBytes: null,
});
}
}
return {
id: key,
...game,
installerSizeInBytes,
installedSizeInBytes,
download: download ?? null,
unlockedAchievementCount,
achievementCount: game.achievementCount ?? 0,

View File

@@ -1,5 +1,8 @@
import path from "node:path";
import { registerEvent } from "../register-event";
import { parseExecutablePath } from "../helpers/parse-executable-path";
import { getDirectorySize } from "../helpers/get-directory-size";
import { gamesSublevel, levelKeys } from "@main/level";
import type { GameShop } from "@types";
@@ -18,12 +21,29 @@ const updateExecutablePath = async (
const game = await gamesSublevel.get(gameKey);
if (!game) return;
// Update immediately without size so UI responds fast
await gamesSublevel.put(gameKey, {
...game,
executablePath: parsedPath,
installedSizeInBytes: parsedPath ? game.installedSizeInBytes : null,
automaticCloudSync:
executablePath === null ? false : game.automaticCloudSync,
});
// Calculate size in background and update later
if (parsedPath) {
const executableDir = path.dirname(parsedPath);
getDirectorySize(executableDir).then(async (installedSizeInBytes) => {
const currentGame = await gamesSublevel.get(gameKey);
if (!currentGame) return;
await gamesSublevel.put(gameKey, {
...currentGame,
installedSizeInBytes,
});
});
}
};
registerEvent("updateExecutablePath", updateExecutablePath);

View File

@@ -27,6 +27,7 @@ import { GameFilesManager } from "../game-files-manager";
import { HydraDebridClient } from "./hydra-debrid";
import { BuzzheavierApi, FuckingFastApi } from "@main/services/hosters";
import { JsHttpDownloader } from "./js-http-downloader";
import { getDirectorySize } from "@main/events/helpers/get-directory-size";
export class DownloadManager {
private static downloadingGameId: string | null = null;
@@ -360,6 +361,24 @@ export class DownloadManager {
userPreferences?.seedAfterDownloadComplete
);
// Calculate installer size in background
if (download.folderName) {
const installerPath = path.join(
download.downloadPath,
download.folderName
);
getDirectorySize(installerPath).then(async (installerSizeInBytes) => {
const currentGame = await gamesSublevel.get(gameId);
if (!currentGame) return;
await gamesSublevel.put(gameId, {
...currentGame,
installerSizeInBytes,
});
});
}
if (download.automaticallyExtract) {
this.handleExtraction(download, game);
}

View File

@@ -7,6 +7,7 @@ import { SevenZip, ExtractionProgress } from "./7zip";
import { WindowManager } from "./window-manager";
import { publishExtractionCompleteNotification } from "./notifications";
import { logger } from "./logger";
import { getDirectorySize } from "@main/events/helpers/get-directory-size";
const PROGRESS_THROTTLE_MS = 1000;
@@ -142,6 +143,17 @@ export class GameFilesManager {
extractionProgress: 0,
});
// Calculate and store the installed size
if (game && download.folderName) {
const gamePath = path.join(download.downloadPath, download.folderName);
const installedSizeInBytes = await getDirectorySize(gamePath);
await gamesSublevel.put(this.gameKey, {
...game,
installedSizeInBytes,
});
}
WindowManager.mainWindow?.webContents.send(
"on-extraction-complete",
this.shop,

View File

@@ -138,12 +138,21 @@ export class WindowManager {
(details, callback) => {
if (
details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot") ||
details.url.includes("workwonders")
details.url.includes("chatwoot")
) {
return callback(details);
}
if (details.url.includes("workwonders")) {
return callback({
...details,
requestHeaders: {
Origin: "https://workwonders.app",
...details.requestHeaders,
},
});
}
const userAgent = new UserAgent();
callback({

View File

@@ -134,7 +134,10 @@ export function App() {
await workwondersRef.current.initChangelogWidget();
workwondersRef.current.initChangelogWidgetMini();
workwondersRef.current.initFeedbackWidget();
if (token) {
workwondersRef.current.initFeedbackWidget();
}
},
[workwondersRef]
);

View File

@@ -21,6 +21,7 @@ import resources from "@locales";
import { logger } from "./logger";
import { addCookieInterceptor } from "./cookies";
import * as Sentry from "@sentry/react";
import { levelDBService } from "./services/leveldb.service";
import Catalogue from "./pages/catalogue/catalogue";
import Home from "./pages/home/home";
@@ -36,6 +37,18 @@ import { AchievementNotification } from "./pages/achievements/notification/achie
console.log = logger.log;
Sentry.init({
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
tracesSampleRate: 0.5,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
release: "hydra-launcher@" + (await window.electron.getVersion()),
});
const isStaging = await window.electron.isStaging();
addCookieInterceptor(isStaging);

View File

@@ -427,7 +427,7 @@
padding: 0;
cursor: pointer;
text-align: left;
width: 100%;
width: fit-content;
transition: opacity 0.2s ease;
&:focus,
@@ -509,6 +509,7 @@
&__simple-menu-btn {
padding: calc(globals.$spacing-unit);
min-height: unset;
border-radius: 8px;
}
&__simple-action-btn {
@@ -516,6 +517,7 @@
min-height: unset;
gap: calc(globals.$spacing-unit);
min-width: 120px;
border-radius: 8px;
}
&__progress-wrapper {

View File

@@ -698,14 +698,6 @@ export function DownloadGroup({
if (game.download?.progress === 1) {
const actions = [
{
label: t("install"),
disabled: deleting,
onClick: () => {
openGameInstaller(game.shop, game.objectId);
},
icon: <DownloadIcon />,
},
{
label: t("extract"),
disabled: game.download.extracting,

View File

@@ -84,6 +84,45 @@
gap: calc(globals.$spacing-unit);
}
&__size-badges {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: solid 1px rgba(255, 255, 255, 0.15);
border-radius: 4px;
padding: 6px 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
min-height: 28px;
box-sizing: border-box;
}
&__size-bar {
display: flex;
align-items: center;
gap: 6px;
color: rgba(255, 255, 255, 0.95);
}
&__size-bar-line {
height: 4px;
border-radius: 2px;
transition: width 0.3s ease;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.5),
rgba(255, 255, 255, 0.8)
);
}
&__size-bar-text {
font-size: 12px;
font-weight: 500;
}
&__logo-container {
flex: 1;
display: flex;

View File

@@ -1,7 +1,15 @@
import { LibraryGame } from "@types";
import { useGameCard } from "@renderer/hooks";
import { ClockIcon, AlertFillIcon, TrophyIcon } from "@primer/octicons-react";
import { formatBytes } from "@renderer/utils";
import {
ClockIcon,
AlertFillIcon,
TrophyIcon,
DatabaseIcon,
FileZipIcon,
} from "@primer/octicons-react";
import { memo, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import "./library-game-card-large.scss";
interface LibraryGameCardLargeProps {
@@ -30,9 +38,53 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
game,
onContextMenu,
}: Readonly<LibraryGameCardLargeProps>) {
const { t } = useTranslation("library");
const { formatPlayTime, handleCardClick, handleContextMenuClick } =
useGameCard(game, onContextMenu);
const sizeBars = useMemo(() => {
const items: {
type: "installer" | "installed";
bytes: number;
formatted: string;
icon: typeof FileZipIcon;
tooltipKey: string;
}[] = [];
if (game.installerSizeInBytes) {
items.push({
type: "installer",
bytes: game.installerSizeInBytes,
formatted: formatBytes(game.installerSizeInBytes),
icon: FileZipIcon,
tooltipKey: "installer_size_tooltip",
});
}
if (game.installedSizeInBytes) {
items.push({
type: "installed",
bytes: game.installedSizeInBytes,
formatted: formatBytes(game.installedSizeInBytes),
icon: DatabaseIcon,
tooltipKey: "disk_usage_tooltip",
});
}
if (items.length === 0) return [];
// Sort by size descending (larger first)
items.sort((a, b) => b.bytes - a.bytes);
// Calculate proportional widths in pixels (max bar is 80px)
const maxBytes = items[0].bytes;
const maxWidth = 80;
return items.map((item) => ({
...item,
widthPx: Math.round((item.bytes / maxBytes) * maxWidth),
}));
}, [game.installerSizeInBytes, game.installedSizeInBytes]);
const backgroundImage = useMemo(
() =>
getImageWithCustomPriority(
@@ -94,6 +146,27 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
<div className="library-game-card-large__overlay">
<div className="library-game-card-large__top-section">
{sizeBars.length > 0 && (
<div className="library-game-card-large__size-badges">
{sizeBars.map((bar) => (
<div
key={bar.type}
className="library-game-card-large__size-bar"
title={t(bar.tooltipKey)}
>
<bar.icon size={11} />
<div
className={`library-game-card-large__size-bar-line library-game-card-large__size-bar-line--${bar.type}`}
style={{ width: `${bar.widthPx}px` }}
/>
<span className="library-game-card-large__size-bar-text">
{bar.formatted}
</span>
</div>
))}
</div>
)}
<div className="library-game-card-large__playtime">
{game.hasManuallyUpdatedPlaytime ? (
<AlertFillIcon

View File

@@ -14,10 +14,6 @@
&__section {
display: flex;
flex-direction: column;
&:not(:last-child) {
margin-bottom: calc(globals.$spacing-unit * 2);
}
}
&__section-header {

View File

@@ -0,0 +1,18 @@
/**
* Converts a number of bytes into a human-readable string with appropriate units
* @param bytes - The number of bytes to format
* @param decimals - Number of decimal places (default: 2)
* @returns Formatted string like "1.5 GB", "256 MB", etc.
*/
export const formatBytes = (bytes: number, decimals = 2): string => {
if (bytes === 0) return "0 B";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const index = Math.min(i, sizes.length - 1);
return `${parseFloat((bytes / Math.pow(k, index)).toFixed(dm))} ${sizes[index]}`;
};

View File

@@ -0,0 +1 @@
export * from "./format-bytes";

View File

@@ -64,6 +64,8 @@ export interface Game {
automaticCloudSync?: boolean;
hasManuallyUpdatedPlaytime?: boolean;
newDownloadOptionsCount?: number;
installedSizeInBytes?: number | null;
installerSizeInBytes?: number | null;
}
export interface Download {

View File

@@ -2174,6 +2174,60 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz#5b2dd648a960b8fa00d76f2cc4eea2f03daa80f4"
integrity sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==
"@sentry-internal/browser-utils@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-10.33.0.tgz#4a5d98352267b63fcc449efe14627c0fc082089e"
integrity sha512-nDJFHAfiFifBfJB0OF6DV6BIsIV5uah4lDsV4UBAgPBf+YAHclO10y1gi2U/JMh58c+s4lXi9p+PI1TFXZ0c6w==
dependencies:
"@sentry/core" "10.33.0"
"@sentry-internal/feedback@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-10.33.0.tgz#5865b4a68d607bb48d8159a100464ae640a638e7"
integrity sha512-sN/VLWtEf0BeV6w6wldIpTxUQxNVc9o9tjLRQa8je1ZV2FCgXA124Iff/zsowsz82dLqtg7qp6GA5zYXVq+JMA==
dependencies:
"@sentry/core" "10.33.0"
"@sentry-internal/replay-canvas@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-10.33.0.tgz#9ea15b320618ad220e5d8f7c804a0d9ca55b04af"
integrity sha512-MTmP6uoAVzw4CCPeqCgCLsRSiOfGLxgyMFjGTCW3E7t62MJ9S0H5sLsQ34sHxXUa1gFU9UNAjEvRRpZ0JvWrPw==
dependencies:
"@sentry-internal/replay" "10.33.0"
"@sentry/core" "10.33.0"
"@sentry-internal/replay@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-10.33.0.tgz#8cfe3a353731fcd81e7afb646b6befeb0f9feb0f"
integrity sha512-UOU9PYxuXnPop3HoQ3l4Q7SZUXJC3Vmfm0Adgad8U03UcrThWIHYc5CxECSrVzfDFNOT7w9o7HQgRAgWxBPMXg==
dependencies:
"@sentry-internal/browser-utils" "10.33.0"
"@sentry/core" "10.33.0"
"@sentry/browser@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.33.0.tgz#33284952a1cdf43cdac15ac144c85e81e7cbaa93"
integrity sha512-iWiPjik9zetM84jKfk01UveW1J0+X7w8XmJ8+IrhTyNDBVUWCRJWD8FrksiN1dRSg5mFWgfMRzKMz27hAScRwg==
dependencies:
"@sentry-internal/browser-utils" "10.33.0"
"@sentry-internal/feedback" "10.33.0"
"@sentry-internal/replay" "10.33.0"
"@sentry-internal/replay-canvas" "10.33.0"
"@sentry/core" "10.33.0"
"@sentry/core@10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.33.0.tgz#ea4964fbec290503b419ccaf1a313924d30ad1c8"
integrity sha512-ehH1VSUclIHZKEZVdv+klofsFIh8FFzqA6AAV23RtLepptzA8wqQzUGraEuSN25sYcNmYJ0jti5U0Ys+WZv5Dw==
"@sentry/react@^10.33.0":
version "10.33.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-10.33.0.tgz#89a3be88d43e49de90943ad2ac86ee1664048097"
integrity sha512-iMdC2Iw54ibAccatJ5TjoLlIy3VotFteied7JFvOudgj1/2eBBeWthRobZ5p6/nAOpj4p9vJk0DeLrc012sd2g==
dependencies:
"@sentry/browser" "10.33.0"
"@sentry/core" "10.33.0"
"@sindresorhus/is@^4.0.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"