Feat/lbx 650 (#2117)

* feat: Add new user preference for deleting archive files after extraction

* feat: Implement user preference for automatic archive deletion after extraction

* feat: Add checkbox for user preference to delete archive files after extraction

* feat: Add delete archive files after extraction preference to settings

* i18n: Add translations for "delete archive files after extraction" in English, Portuguese, and Russian

* fix: Change default setting for extracting files after download to false

* refactor: Rename user preference for deleting archive files after extraction to clarify default behavior

* feat: Add automaticallyDeleteArchiveFiles property to download and game download payload types

* refactor: Enhance archive deletion logic based on user preferences in GameFilesManager

* refactor: Update addGameToQueue and startGameDownload to include automaticallyDeleteArchiveFiles parameter

* refactor: Integrate automaticallyDeleteArchiveFiles parameter into DownloadSettingsModal and RepacksModal, and update related user preferences in SettingsContextDownloads

* refactor: Include automaticallyDeleteArchiveFiles parameter in GameDetails component for enhanced download handling

* refactor: streamline archive deletion prompt handling in App component

* fix: enable extractFilesByDefault in SettingsContextDownloads for improved user experience

* refactor: remove automatic extraction setting from DownloadSettingsModal

* refactor: update download flow to ensure download starts before saving to sublevel

* refactor: update GameFilesManager to send delete notifications instead of unlinking files directly

* refactor: separate deleteArchive logic into a dedicated function and update GameFilesManager to use it

---------

Co-authored-by: Chubby Granny Chaser <weak.fern2638@heliokroger.com>
Co-authored-by: Helio Kroger <me@heliokroger.com>
This commit is contained in:
japa2k
2026-04-28 09:51:24 -03:00
committed by GitHub
parent 0438fc7dea
commit e1de9e9086
14 changed files with 169 additions and 73 deletions
+2
View File
@@ -486,6 +486,7 @@
"game_removed_from_pinned": "Game removed from pinned",
"game_added_to_pinned": "Game added to pinned",
"automatically_extract_downloaded_files": "Automatically extract downloaded files",
"delete_archive_files_after_extraction": "Always delete archive files after extraction",
"create_start_menu_shortcut": "Create Start Menu shortcut",
"invalid_wine_prefix_path": "Invalid Wine prefix path",
"invalid_wine_prefix_path_description": "The path to the Wine prefix is invalid. Please check the path and try again.",
@@ -803,6 +804,7 @@
"max_download_speed_unit_megabits": "Mbps",
"max_download_speed_unlimited": "Unlimited",
"extract_files_by_default": "Extract files by default after download",
"delete_archive_files_after_extraction": "Always delete archive files after extraction",
"enable_steam_achievements": "Enable search for Steam achievements",
"enable_new_download_options_badges": "Show new download options badges",
"achievement_custom_notification_position": "Achievement custom notification position",
+2
View File
@@ -349,6 +349,7 @@
"files_removed_success": "Arquivos removidos com sucesso",
"failed_remove_files": "Falha ao remover arquivos",
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados",
"delete_archive_files_after_extraction": "Sempre deletar arquivos de arquivo após a extração",
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
"invalid_wine_prefix_path": "Caminho do prefixo Wine inválido",
"invalid_wine_prefix_path_description": "O caminho para o prefixo Wine é inválido. Por favor, verifique o caminho e tente novamente.",
@@ -754,6 +755,7 @@
"max_download_speed_unit_megabits": "Mbps",
"max_download_speed_unlimited": "Ilimitada",
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
"delete_archive_files_after_extraction": "Sempre deletar arquivos de arquivo após a extração",
"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",
+2
View File
@@ -486,6 +486,7 @@
"game_removed_from_pinned": "Игра удалена из закрепленных",
"game_added_to_pinned": "Игра добавлена в закрепленные",
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов",
"delete_archive_files_after_extraction": "Всегда удалять архивные файлы после извлечения",
"create_start_menu_shortcut": "Создать ярлык в меню «Пуск»",
"invalid_wine_prefix_path": "Недопустимый путь префикса Wine",
"invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.",
@@ -798,6 +799,7 @@
"max_download_speed_unit_megabits": "Mbps",
"max_download_speed_unlimited": "Без ограничений",
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
"delete_archive_files_after_extraction": "Всегда удалять архивные файлы после извлечения",
"enable_steam_achievements": "Включить поиск достижений Steam",
"enable_new_download_options_badges": "Показывать значки новых вариантов загрузки",
"achievement_custom_notification_position": "Позиция уведомлений достижений",
+7 -5
View File
@@ -2,13 +2,10 @@ import path from "node:path";
import fs from "node:fs";
import { registerEvent } from "../register-event";
import { logger } from "@main/services";
import { logger } from "@main/services/logger";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
const deleteArchive = async (
_event: Electron.IpcMainInvokeEvent,
filePath: string
) => {
export const deleteArchiveFile = async (filePath: string) => {
try {
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
@@ -47,4 +44,9 @@ const deleteArchive = async (
}
};
const deleteArchive = async (
_event: Electron.IpcMainInvokeEvent,
filePath: string
) => deleteArchiveFile(filePath);
registerEvent("deleteArchive", deleteArchive);
@@ -22,6 +22,7 @@ const addGameToQueue = async (
downloader,
uri,
automaticallyExtract,
automaticallyDeleteArchiveFiles,
fileSize,
fileIndices,
selectedFilesSize,
@@ -47,6 +48,7 @@ const addGameToQueue = async (
queued: true,
extracting: false,
automaticallyExtract,
automaticallyDeleteArchiveFiles,
extractionProgress: 0,
fileIndices,
selectedFilesSize,
@@ -21,6 +21,7 @@ const startGameDownload = async (
downloader,
uri,
automaticallyExtract,
automaticallyDeleteArchiveFiles,
fileIndices,
selectedFilesSize,
} = payload;
@@ -61,6 +62,7 @@ const startGameDownload = async (
queued: true,
extracting: false,
automaticallyExtract,
automaticallyDeleteArchiveFiles,
extractionProgress: 0,
fileIndices,
selectedFilesSize,
+55 -22
View File
@@ -1,27 +1,28 @@
import path from "node:path";
import fs from "node:fs";
import axios from "axios";
import sharp from "sharp";
import pngToIco from "png-to-ico";
import type { GameShop, UserPreferences } from "@types";
import { ASSETS_PATH } from "@main/constants";
import { getGameAssets } from "@main/events/catalogue/get-game-assets";
import { getDirectorySize } from "@main/events/helpers/get-directory-size";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import {
Downloader,
FILE_EXTENSIONS_TO_EXTRACT,
removeSymbolsFromName,
} from "@shared";
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";
import { GameExecutables } from "./game-executables";
import type { GameShop, UserPreferences } from "@types";
import axios from "axios";
import createDesktopShortcut from "create-desktop-shortcuts";
import { app } from "electron";
import { SystemPath } from "./system-path";
import { ASSETS_PATH } from "@main/constants";
import { getGameAssets } from "@main/events/catalogue/get-game-assets";
import fs from "node:fs";
import path from "node:path";
import pngToIco from "png-to-ico";
import sharp from "sharp";
import { ExtractionProgress, SevenZip } from "./7zip";
import { getPathType } from "./extraction-path";
import { GameExecutables } from "./game-executables";
import { logger } from "./logger";
import { deleteArchiveFile } from "@main/events/library/delete-archive";
import { publishExtractionCompleteNotification } from "./notifications";
import { SystemPath } from "./system-path";
import { WindowManager } from "./window-manager";
const PROGRESS_THROTTLE_MS = 1000;
@@ -186,10 +187,28 @@ export class GameFilesManager {
.filter((archivePath) => fs.existsSync(archivePath));
if (archivePaths.length > 0) {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
archivePaths
);
const [download, userPreferences] = await Promise.all([
downloadsSublevel.get(this.gameKey),
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
valueEncoding: "json",
}),
]);
const shouldDelete =
download?.automaticallyDeleteArchiveFiles ??
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ??
false;
if (shouldDelete) {
for (const archivePath of archivePaths) {
await deleteArchiveFile(archivePath);
}
} else {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
archivePaths
);
}
}
return true;
@@ -656,10 +675,24 @@ export class GameFilesManager {
}
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
[filePath]
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{ valueEncoding: "json" }
);
const shouldDelete =
download.automaticallyDeleteArchiveFiles ??
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ??
false;
if (shouldDelete) {
await deleteArchiveFile(filePath);
} else {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
[filePath]
);
}
}
await downloadsSublevel.put(this.gameKey, {
+18 -18
View File
@@ -1,6 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
import { WorkWonders } from "workwonders-sdk";
import { BottomPanel, Header, Sidebar, Toast } from "@renderer/components";
import {
useAppDispatch,
useAppSelector,
@@ -10,32 +8,34 @@ import {
useUserDetails,
} from "@renderer/hooks";
import { useDownloadOptionsListener } from "@renderer/hooks/use-download-options-listener";
import { useCallback, useEffect, useRef, useState } from "react";
import { WorkWonders } from "workwonders-sdk";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import {
clearExtraction,
closeToast,
setExtractionProgress,
setGameRunning,
setProfileBackground,
setUserDetails,
setUserPreferences,
toggleDraggingDisabled,
closeToast,
setUserDetails,
setProfileBackground,
setGameRunning,
setExtractionProgress,
clearExtraction,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
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 { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
import {
injectCustomCss,
removeCustomCss,
getAchievementSoundUrl,
getAchievementSoundVolume,
} from "./helpers";
import { levelDBService } from "./services/leveldb.service";
import type { UserPreferences } from "@types";
import "./app.scss";
import {
getAchievementSoundUrl,
getAchievementSoundVolume,
injectCustomCss,
removeCustomCss,
} from "./helpers";
import { levelDBService } from "./services/leveldb.service";
export interface AppProps {
children: React.ReactNode;
@@ -105,6 +105,7 @@ export default function GameDetails() {
addToQueueOnly = false,
fileIndices?: number[],
selectedFilesSize?: number | null,
automaticallyDeleteArchiveFiles = false,
signal?: AbortSignal
) => {
const response = addToQueueOnly
@@ -116,7 +117,8 @@ export default function GameDetails() {
shop,
downloadPath,
uri: selectRepackUri(repack, downloader),
automaticallyExtract: automaticallyExtract,
automaticallyExtract,
automaticallyDeleteArchiveFiles,
fileSize: repack.fileSize,
fileIndices,
selectedFilesSize,
@@ -131,7 +133,8 @@ export default function GameDetails() {
shop,
downloadPath,
uri: selectRepackUri(repack, downloader),
automaticallyExtract: automaticallyExtract,
automaticallyExtract,
automaticallyDeleteArchiveFiles,
fileSize: repack.fileSize,
fileIndices,
selectedFilesSize,
@@ -1,12 +1,13 @@
import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Trans, useTranslation } from "react-i18next";
CheckCircleFillIcon,
CheckIcon,
ChevronDownIcon,
DownloadIcon,
FileDirectoryIcon,
FileIcon,
PlusIcon,
SyncIcon,
} from "@primer/octicons-react";
import {
Badge,
Button,
@@ -16,23 +17,6 @@ import {
SelectField,
TextField,
} from "@renderer/components";
import {
DownloadIcon,
SyncIcon,
CheckCircleFillIcon,
CheckIcon,
PlusIcon,
ChevronDownIcon,
FileDirectoryIcon,
FileIcon,
} from "@primer/octicons-react";
import {
DownloadError,
Downloader,
formatBytes,
getDownloadersForUri,
} from "@shared";
import type { GameRepack, TorrentFile, TorrentFilesResponse } from "@types";
import { DOWNLOADER_NAME } from "@renderer/constants";
import {
useAppSelector,
@@ -40,10 +24,26 @@ import {
useFeature,
useToast,
} from "@renderer/hooks";
import {
DownloadError,
Downloader,
formatBytes,
getDownloadersForUri,
} from "@shared";
import type { GameRepack, TorrentFile, TorrentFilesResponse } from "@types";
import { motion } from "framer-motion";
import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Trans, useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
import { RealDebridInfoModal } from "./real-debrid-info-modal";
import "./download-settings-modal.scss";
import { RealDebridInfoModal } from "./real-debrid-info-modal";
export interface DownloadSettingsModalProps {
visible: boolean;
@@ -56,6 +56,7 @@ export interface DownloadSettingsModalProps {
addToQueueOnly?: boolean,
fileIndices?: number[],
selectedFilesSize?: number | null,
automaticallyDeleteArchiveFiles?: boolean,
signal?: AbortSignal
) => Promise<{ ok: boolean; error?: string }>;
repack: GameRepack | null;
@@ -251,6 +252,12 @@ export function DownloadSettingsModal({
const [automaticExtractionEnabled, setAutomaticExtractionEnabled] = useState(
userPreferences?.extractFilesByDefault ?? true
);
const [
deleteArchiveFilesAfterExtraction,
setDeleteArchiveFilesAfterExtraction,
] = useState(
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ?? false
);
const [selectedDownloader, setSelectedDownloader] =
useState<Downloader | null>(null);
const [hasWritePermission, setHasWritePermission] = useState<boolean | null>(
@@ -469,6 +476,18 @@ export function DownloadSettingsModal({
setSelectedDownloader(getDefaultDownloader(availableDownloaders));
}, [getDefaultDownloader, userPreferences?.downloadsPath, downloadOptions]);
useEffect(() => {
if (visible) {
setDeleteArchiveFilesAfterExtraction(
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ?? false
);
setAutomaticExtractionEnabled(
userPreferences?.extractFilesByDefault ?? true
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
const torrentFilesByIndex = useMemo(() => {
const fileMap = new Map<number, TorrentFile>();
torrentFiles.forEach((file) => fileMap.set(file.index, file));
@@ -931,6 +950,7 @@ export function DownloadSettingsModal({
hasActiveDownload,
selectedFileIndices,
totalSelectedSize,
deleteArchiveFilesAfterExtraction,
abortController.signal
);
@@ -1382,6 +1402,16 @@ export function DownloadSettingsModal({
}
/>
<CheckboxField
label={t("delete_archive_files_after_extraction")}
checked={deleteArchiveFilesAfterExtraction}
onChange={() =>
setDeleteArchiveFilesAfterExtraction(
!deleteArchiveFilesAfterExtraction
)
}
/>
<Button
onClick={handlePrimaryButtonClick}
disabled={
@@ -43,6 +43,7 @@ export interface RepacksModalProps {
addToQueueOnly?: boolean,
fileIndices?: number[],
selectedFilesSize?: number | null,
automaticallyDeleteArchiveFiles?: boolean,
signal?: AbortSignal
) => Promise<{ ok: boolean; error?: string }>;
onClose: () => void;
@@ -49,6 +49,7 @@ export function SettingsContextDownloads() {
extractFilesByDefault: true,
createStartMenuShortcut: true,
maxDownloadSpeedMegabytes: "",
deleteArchiveFilesAfterExtractionByDefault: false,
});
useEffect(() => {
@@ -69,6 +70,8 @@ export function SettingsContextDownloads() {
userPreferences.showDownloadSpeedInMegabytes ?? false
)
: "",
deleteArchiveFilesAfterExtractionByDefault:
userPreferences.deleteArchiveFilesAfterExtractionByDefault ?? false,
});
}, [userPreferences]);
@@ -182,6 +185,17 @@ export function SettingsContextDownloads() {
onChange={handleSpeedUnitChange}
/>
<CheckboxField
label={t("delete_archive_files_after_extraction")}
checked={form.deleteArchiveFilesAfterExtractionByDefault}
onChange={() =>
handleChange({
deleteArchiveFilesAfterExtractionByDefault:
!form.deleteArchiveFilesAfterExtractionByDefault,
})
}
/>
{window.electron.platform === "win32" && (
<CheckboxField
label={t("create_shortcuts_on_download")}
+1
View File
@@ -138,6 +138,7 @@ export interface StartGameDownloadPayload {
downloadPath: string;
downloader: Downloader;
automaticallyExtract: boolean;
automaticallyDeleteArchiveFiles: boolean;
fileSize?: string | null;
fileIndices?: number[];
selectedFilesSize?: number | null;
+2
View File
@@ -88,6 +88,7 @@ export interface Download {
timestamp: number;
extracting: boolean;
automaticallyExtract: boolean;
automaticallyDeleteArchiveFiles: boolean;
extractionProgress: number;
fileIndices?: number[];
selectedFilesSize?: number | null;
@@ -136,6 +137,7 @@ export interface UserPreferences {
friendStartGameNotificationsEnabled?: boolean;
showDownloadSpeedInMegabytes?: boolean;
extractFilesByDefault?: boolean;
deleteArchiveFilesAfterExtractionByDefault?: boolean;
enableSteamAchievements?: boolean;
autoplayGameTrailers?: boolean;
hideToTrayOnGameStart?: boolean;