[V2] feat(unzip/front-end): Implement ZIP extraction confirmation for archives over 20 files (#4834)

# Description of Changes

TLDR:

- Introduced a user confirmation dialog for extracting ZIP files with
more than **20 files**.
- Created `useZipConfirmation` hook to handle confirmation dialog logic
and state.
- Implemented `ZipWarningModal` component to display the confirmation
dialog.
- Updated `zipFileService` to count files in ZIP and trigger
confirmation callback for large files.
- Integrated confirmation flow into `FileContext` and
`useToolResources`.
- Added translations for new ZIP warning dialog messages.

This pull request introduces a user confirmation dialog when attempting
to extract large ZIP files (**over 20 files**), improving safety and
user experience by preventing accidental extraction of very large
archives. The implementation includes a reusable confirmation modal, a
custom hook to handle dialog state and resolution, and updates to the
ZIP extraction logic to support this workflow.

**User Experience Improvements**
* Added a new localized warning dialog (`ZipWarningModal`) that prompts
users for confirmation when extracting ZIP files containing more than 20
files. This dialog displays the ZIP file name, file count, and offers
"Cancel" and "Extract" actions, with responsive layouts for desktop and
mobile

**ZIP Extraction Workflow Enhancements**
* Updated the ZIP extraction logic in `ZipFileService` to count the
number of files in a ZIP and invoke a confirmation callback if the file
count exceeds the threshold. Extraction proceeds only if the user
confirms; otherwise, the ZIP remains unextracted.
* Added a new hook (`useZipConfirmation`) to manage the confirmation
dialog’s state and provide a promise-based API for requesting user
confirmation.

**Integration with Application State**
* Integrated the confirmation workflow into `FileContext`, passing the
confirmation function into ZIP extraction calls and rendering the modal
dialog at the appropriate time.
* Updated relevant interfaces and method signatures to support the
optional confirmation callback for large ZIP extractions throughout the
codebase.


<img width="515" height="321" alt="image"
src="https://github.com/user-attachments/assets/f35a7588-4635-4ccd-9ee6-95edb17fee99"
/>
<img width="515" height="321" alt="image"
src="https://github.com/user-attachments/assets/0525acf3-4174-42cd-8912-910e754c467c"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <[email protected]>
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
Balázs Szücs
2025-11-12 15:18:15 +00:00
committed by GitHub
co-authored by Anthony Stirling
parent c8615518a6
commit 74a1438c21
9 changed files with 255 additions and 32 deletions
+16 -2
View File
@@ -31,6 +31,8 @@ import { addFiles, addStirlingFileStubs, consumeFiles, undoConsumeFiles, createF
import { FileLifecycleManager } from '@app/contexts/file/lifecycle';
import { FileStateContext, FileActionsContext } from '@app/contexts/file/contexts';
import { IndexedDBProvider, useIndexedDB } from '@app/contexts/IndexedDBContext';
import { useZipConfirmation } from '@app/hooks/useZipConfirmation';
import ZipWarningModal from '@app/components/shared/ZipWarningModal';
const DEBUG = process.env.NODE_ENV === 'development';
@@ -52,6 +54,9 @@ function FileContextInner({
const stateRef = useRef(state);
stateRef.current = state;
// ZIP confirmation dialog
const { confirmationState, requestConfirmation, handleConfirm, handleCancel } = useZipConfirmation();
// Create lifecycle manager
const lifecycleManagerRef = useRef<FileLifecycleManager | null>(null);
if (!lifecycleManagerRef.current) {
@@ -86,7 +91,9 @@ function FileContextInner({
...options,
// For direct file uploads: ALWAYS unzip (except HTML ZIPs)
// skipAutoUnzip bypasses preference checks - HTML detection still applies
skipAutoUnzip: true
skipAutoUnzip: true,
// Provide confirmation callback for large ZIP files
confirmLargeExtraction: requestConfirmation
},
stateRef,
filesRef,
@@ -101,7 +108,7 @@ function FileContextInner({
}
return stirlingFiles;
}, [enablePersistence]);
}, [enablePersistence, requestConfirmation]);
const addStirlingFileStubsAction = useCallback(async (stirlingFileStubs: StirlingFileStub[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
// StirlingFileStubs preserve all metadata - perfect for FileManager use case!
@@ -237,6 +244,13 @@ function FileContextInner({
<FileStateContext.Provider value={stateValue}>
<FileActionsContext.Provider value={actionsValue}>
{children}
<ZipWarningModal
opened={confirmationState.opened}
onConfirm={handleConfirm}
onCancel={handleCancel}
fileCount={confirmationState.fileCount}
zipFileName={confirmationState.fileName}
/>
</FileActionsContext.Provider>
</FileStateContext.Provider>
);
@@ -186,6 +186,7 @@ interface AddFileOptions {
autoUnzip?: boolean;
autoUnzipFileLimit?: number;
skipAutoUnzip?: boolean; // When true: always unzip (except HTML). Used for file uploads. When false: respect autoUnzip/autoUnzipFileLimit preferences. Used for tool outputs.
confirmLargeExtraction?: (fileCount: number, fileName: string) => Promise<boolean>; // Optional callback to confirm extraction of large ZIP files
}
/**
@@ -219,6 +220,7 @@ export async function addFiles(
const autoUnzip = options.autoUnzip ?? true; // Default to true
const autoUnzipFileLimit = options.autoUnzipFileLimit ?? 4; // Default limit
const skipAutoUnzip = options.skipAutoUnzip ?? false;
const confirmLargeExtraction = options.confirmLargeExtraction;
for (const file of files) {
// Check if file is a ZIP
@@ -238,7 +240,8 @@ export async function addFiles(
const extractedFiles = await zipFileService.extractWithPreferences(file, {
autoUnzip,
autoUnzipFileLimit,
skipAutoUnzip
skipAutoUnzip,
confirmLargeExtraction
});
if (extractedFiles.length === 1 && extractedFiles[0] === file) {