diff --git a/package-lock.json b/package-lock.json
index e476fed..5e12aef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "pdfseal",
- "version": "1.1.0",
+ "version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pdfseal",
- "version": "1.1.0",
+ "version": "1.2.0",
"license": "AGPL-3.0-only",
"dependencies": {
"@pdfsmaller/pdf-encrypt": "^1.2.0",
diff --git a/package.json b/package.json
index 083b065..c36cd2e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pdfseal",
- "version": "1.1.0",
+ "version": "1.2.0",
"private": true,
"type": "module",
"license": "AGPL-3.0-only",
diff --git a/src/App.vue b/src/App.vue
index 006cc61..eeb8531 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -29,14 +29,16 @@
]"
>
-
+
+
+
diff --git a/src/config/siteConfig.js b/src/config/siteConfig.js
index 42deb7a..04ad822 100644
--- a/src/config/siteConfig.js
+++ b/src/config/siteConfig.js
@@ -59,7 +59,7 @@ export const isOfficialMode = ref(checkInitialOfficialMode());
export const siteConfig = reactive({
// Version Identity
- version: import.meta.env.VITE_APP_VERSION || '1.1.0',
+ version: import.meta.env.VITE_APP_VERSION || '1.2.0',
// Brand Identity
brandName: import.meta.env.VITE_BRAND_NAME || 'PDFSeal',
diff --git a/src/tools/CompressTool.vue b/src/tools/CompressTool.vue
index 3e38018..6ec981f 100644
--- a/src/tools/CompressTool.vue
+++ b/src/tools/CompressTool.vue
@@ -499,9 +499,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
const filename = ref('');
const totalPages = ref(0);
const isDragOver = ref(false);
@@ -779,5 +776,11 @@ function checkIncomingFile() {
}
onMounted(checkIncomingFile);
-onActivated(checkIncomingFile);
+onActivated(() => {
+ if (lastExportedFile.value) {
+ reset();
+ }
+ workspaceState?.setActiveFile(Boolean(docBytes.value));
+ checkIncomingFile();
+});
diff --git a/src/tools/ImageToPdfTool.vue b/src/tools/ImageToPdfTool.vue
index 3c31f81..7df09c5 100644
--- a/src/tools/ImageToPdfTool.vue
+++ b/src/tools/ImageToPdfTool.vue
@@ -671,6 +671,9 @@ watch(() => imageList.value.length > 0, (active) => {
}, { immediate: true });
onActivated(() => {
+ if (lastExportedFile.value) {
+ clearAll();
+ }
workspaceState?.setActiveFile(imageList.value.length > 0);
});
diff --git a/src/tools/MergeTool.vue b/src/tools/MergeTool.vue
index 77dbb36..e9abe27 100644
--- a/src/tools/MergeTool.vue
+++ b/src/tools/MergeTool.vue
@@ -343,10 +343,6 @@ watch(() => files.value.length > 0, (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(files.value.length > 0);
-});
-
const customOutputBaseName = ref(`${userSettings.defaultExportPrefix || 'PDFSeal'}_Merged_${new Date().toISOString().slice(0, 10)}`);
const autoSaveToVault = ref(userSettings.autoSaveToVault !== false);
@@ -605,7 +601,13 @@ function checkIncomingFile() {
}
onMounted(checkIncomingFile);
-onActivated(checkIncomingFile);
+onActivated(() => {
+ if (lastExportedFile.value) {
+ clearAll();
+ }
+ workspaceState?.setActiveFile(files.value.length > 0);
+ checkIncomingFile();
+});
async function handlePasswordSubmit(pwd) {
if (!pendingFileObj) return;
diff --git a/src/tools/OrganizeTool.vue b/src/tools/OrganizeTool.vue
index 99eec50..a6470bd 100644
--- a/src/tools/OrganizeTool.vue
+++ b/src/tools/OrganizeTool.vue
@@ -679,9 +679,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
const filename = ref('');
const pages = ref([]);
const isDragOver = ref(false);
@@ -1223,6 +1220,17 @@ async function downloadPageAsImage(idx) {
needDestroy = true;
}
+ if (!targetPdf && docBytes.value) {
+ const loadingTask = pdfjsLib.getDocument({
+ data: new Uint8Array(docBytes.value.slice(0)),
+ cMapUrl: typeof window !== 'undefined' ? (window.location.origin + '/cmaps/') : '/cmaps/',
+ cMapPacked: true,
+ standardFontDataUrl: typeof window !== 'undefined' ? (window.location.origin + '/standard_fonts/') : '/standard_fonts/'
+ });
+ pdfDoc = await loadingTask.promise;
+ targetPdf = pdfDoc;
+ }
+
if (!targetPdf) return;
const page = await targetPdf.getPage(item.pageIndex + 1);
@@ -1448,8 +1456,17 @@ onMounted(() => {
});
onActivated(() => {
+ if (lastExportedFile.value) {
+ reset();
+ }
+ workspaceState?.setActiveFile(Boolean(docBytes.value));
checkIncomingFile();
window.addEventListener('keydown', handleKeydown);
+ if (gridRef.value && !sortableInstance && pages.value.length > 0) {
+ nextTick(() => {
+ initSortable();
+ });
+ }
});
onDeactivated(() => {
diff --git a/src/tools/PageNumberTool.vue b/src/tools/PageNumberTool.vue
index b3240ad..f642e08 100644
--- a/src/tools/PageNumberTool.vue
+++ b/src/tools/PageNumberTool.vue
@@ -625,9 +625,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
const filename = ref('');
const totalPages = ref(0);
const isDragOver = ref(false);
@@ -1156,6 +1153,9 @@ function checkIncomingFile() {
onMounted(checkIncomingFile);
onActivated(() => {
+ if (lastExportedFile.value) {
+ reset();
+ }
workspaceState?.setActiveFile(Boolean(docBytes.value));
checkIncomingFile();
if (docBytes.value && !lastExportedFile.value) {
diff --git a/src/tools/PdfToImageTool.vue b/src/tools/PdfToImageTool.vue
index cf41437..1fdd30f 100644
--- a/src/tools/PdfToImageTool.vue
+++ b/src/tools/PdfToImageTool.vue
@@ -375,9 +375,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
const filename = ref('');
const pages = ref([]);
const isDragOver = ref(false);
@@ -464,6 +461,19 @@ async function destroyPdfDoc() {
}
}
+async function ensurePdfDoc() {
+ if (!pdfDoc && docBytes.value) {
+ const loadingTask = pdfjsLib.getDocument({
+ data: new Uint8Array(docBytes.value.slice(0)),
+ cMapUrl: typeof window !== 'undefined' ? (window.location.origin + '/cmaps/') : '/cmaps/',
+ cMapPacked: true,
+ standardFontDataUrl: typeof window !== 'undefined' ? (window.location.origin + '/standard_fonts/') : '/standard_fonts/'
+ });
+ pdfDoc = await loadingTask.promise;
+ }
+ return pdfDoc;
+}
+
function onFileSelected(e) {
const file = e.target.files[0];
if (file) loadFile(file);
@@ -598,6 +608,7 @@ function getBaseName() {
* (JPEG has no alpha channel; PNG gets consistent output too).
*/
async function renderPageBlob(pageIndex) {
+ await ensurePdfDoc();
if (!pdfDoc) throw new Error('No PDF document loaded');
const page = await pdfDoc.getPage(pageIndex + 1);
const viewport = page.getViewport({ scale: outputDpi.value / 72 });
@@ -643,6 +654,7 @@ async function downloadPage(idx) {
}
async function downloadAllAsZip() {
+ await ensurePdfDoc();
if (!pdfDoc || pages.value.length === 0 || isZipping.value || isRenderingPage.value !== null) return;
isZipping.value = true;
zipProgress.value = 0;
@@ -750,7 +762,13 @@ function checkIncomingFile() {
}
onMounted(checkIncomingFile);
-onActivated(checkIncomingFile);
+onActivated(async () => {
+ if (lastExportedFile.value) {
+ await reset();
+ }
+ workspaceState?.setActiveFile(Boolean(docBytes.value));
+ checkIncomingFile();
+});
onDeactivated(() => {
// Free the pdf.js worker resources when navigating away (KeepAlive safe)
destroyPdfDoc();
diff --git a/src/tools/PipelineTool.vue b/src/tools/PipelineTool.vue
index d148bf3..bbbdab0 100644
--- a/src/tools/PipelineTool.vue
+++ b/src/tools/PipelineTool.vue
@@ -2834,6 +2834,9 @@ onMounted(() => {
});
onActivated(() => {
+ if (outputResults.value.length > 0) {
+ resetBatchAndResults();
+ }
workspaceState?.setActiveFile(inputFiles.value.length > 0);
checkIncomingFile();
});
diff --git a/src/tools/ProtectTool.vue b/src/tools/ProtectTool.vue
index 9c57530..aad8406 100644
--- a/src/tools/ProtectTool.vue
+++ b/src/tools/ProtectTool.vue
@@ -717,9 +717,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
const filename = ref('');
const totalPages = ref(0);
const originalSizeFormatted = ref('0 B');
@@ -1142,5 +1139,11 @@ function checkIncomingFile() {
}
onMounted(checkIncomingFile);
-onActivated(checkIncomingFile);
+onActivated(() => {
+ if (lastExportedFile.value) {
+ reset();
+ }
+ workspaceState?.setActiveFile(Boolean(docBytes.value));
+ checkIncomingFile();
+});
diff --git a/src/tools/RedactTool.vue b/src/tools/RedactTool.vue
index 6d53ca0..56cc43c 100644
--- a/src/tools/RedactTool.vue
+++ b/src/tools/RedactTool.vue
@@ -920,7 +920,7 @@
diff --git a/src/tools/SignTool.vue b/src/tools/SignTool.vue
index 650dba5..dd35ce3 100644
--- a/src/tools/SignTool.vue
+++ b/src/tools/SignTool.vue
@@ -830,9 +830,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
const filename = ref('');
const totalPages = ref(0);
const currentPage = ref(1);
@@ -1769,5 +1766,16 @@ onUnmounted(() => {
window.removeEventListener('resize', handleWindowResize);
reset();
});
-onActivated(checkIncomingFile);
+onActivated(() => {
+ if (lastExportedFile.value) {
+ reset();
+ }
+ workspaceState?.setActiveFile(Boolean(docBytes.value));
+ checkIncomingFile();
+ if (docBytes.value && !lastExportedFile.value) {
+ nextTick(() => {
+ renderCurrentPage();
+ });
+ }
+});
diff --git a/src/tools/SplitTool.vue b/src/tools/SplitTool.vue
index 11607d6..d69b781 100644
--- a/src/tools/SplitTool.vue
+++ b/src/tools/SplitTool.vue
@@ -695,10 +695,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
-
const filename = ref('');
const pages = ref([]);
const totalPages = ref(0);
@@ -1399,6 +1395,12 @@ function checkIncomingFile() {
}
onMounted(checkIncomingFile);
-onActivated(checkIncomingFile);
+onActivated(() => {
+ if (lastExportedFile.value) {
+ reset();
+ }
+ workspaceState?.setActiveFile(Boolean(docBytes.value));
+ checkIncomingFile();
+});
onUnmounted(destroySplitPdfDoc);
diff --git a/src/tools/UnlockTool.vue b/src/tools/UnlockTool.vue
index 49cf104..1e2a8fc 100644
--- a/src/tools/UnlockTool.vue
+++ b/src/tools/UnlockTool.vue
@@ -305,9 +305,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
const filename = ref('');
const totalPages = ref(0);
const isDragOver = ref(false);
@@ -516,5 +513,11 @@ function checkIncomingFile() {
}
onMounted(checkIncomingFile);
-onActivated(checkIncomingFile);
+onActivated(() => {
+ if (lastExportedFile.value) {
+ reset();
+ }
+ workspaceState?.setActiveFile(Boolean(docBytes.value));
+ checkIncomingFile();
+});
diff --git a/src/tools/WatermarkTool.vue b/src/tools/WatermarkTool.vue
index f0ca8c4..280cbc1 100644
--- a/src/tools/WatermarkTool.vue
+++ b/src/tools/WatermarkTool.vue
@@ -454,9 +454,6 @@ watch(() => Boolean(docBytes.value), (active) => {
workspaceState?.setActiveFile(active);
}, { immediate: true });
-onActivated(() => {
- workspaceState?.setActiveFile(Boolean(docBytes.value));
-});
const filename = ref('');
const totalPages = ref(0);
const isDragOver = ref(false);
@@ -807,6 +804,10 @@ function checkIncomingFile() {
onMounted(checkIncomingFile);
onActivated(() => {
+ if (lastExportedFile.value) {
+ reset();
+ }
+ workspaceState?.setActiveFile(Boolean(docBytes.value));
checkIncomingFile();
if (docBytes.value && !lastExportedFile.value) {
nextTick(() => {
diff --git a/tests/keepAliveLifecycle.test.js b/tests/keepAliveLifecycle.test.js
new file mode 100644
index 0000000..1210f7b
--- /dev/null
+++ b/tests/keepAliveLifecycle.test.js
@@ -0,0 +1,62 @@
+import { describe, it, expect } from 'vitest';
+import fs from 'fs';
+import path from 'path';
+
+describe('Smart State KeepAlive & Lifecycle Management', () => {
+ const rootDir = path.resolve(__dirname, '..');
+ const appVuePath = path.join(rootDir, 'src/App.vue');
+ const toolsDir = path.join(rootDir, 'src/tools');
+
+ it('preserves component instances in App.vue using KeepAlive with bounded capacity', () => {
+ const appContent = fs.readFileSync(appVuePath, 'utf-8');
+ expect(appContent).toMatch(//);
+ expect(appContent).toContain('');
+ });
+
+ it('resets delivered results on activation across all 13 standard processing tools', () => {
+ const standardTools = [
+ 'CompressTool.vue',
+ 'SplitTool.vue',
+ 'OrganizeTool.vue',
+ 'PdfToImageTool.vue',
+ 'ImageToPdfTool.vue',
+ 'MergeTool.vue',
+ 'WatermarkTool.vue',
+ 'PageNumberTool.vue',
+ 'ProtectTool.vue',
+ 'UnlockTool.vue',
+ 'SignTool.vue',
+ 'RedactTool.vue',
+ 'SanitizeTool.vue'
+ ];
+
+ for (const toolFile of standardTools) {
+ const filePath = path.join(toolsDir, toolFile);
+ const content = fs.readFileSync(filePath, 'utf-8');
+
+ // 1. Must use onActivated lifecycle hook
+ expect(content, `${toolFile} must import and use onActivated`).toMatch(/\bonActivated\s*\(/);
+
+ // 2. Must check lastExportedFile on activation before resetting
+ expect(
+ content,
+ `${toolFile} must check lastExportedFile in onActivated to reset completed delivery while preserving draft`
+ ).toMatch(/if\s*\(\s*lastExportedFile\.value\s*\)\s*\{\s*(await\s+)?(reset|clearAll)\s*\(\s*\);?\s*\}/);
+
+ // 3. Must notify workspaceState of active file status
+ expect(content, `${toolFile} must update workspaceState on activation`).toContain('workspaceState?.setActiveFile');
+
+ // 4. Must not contain duplicate onActivated hooks (clean single activation handler)
+ const onActivatedMatches = content.match(/onActivated\s*\(/g) || [];
+ expect(onActivatedMatches.length, `${toolFile} should have exactly one onActivated hook`).toBe(1);
+ }
+ });
+
+ it('resets completed batch results on activation in PipelineTool', () => {
+ const filePath = path.join(toolsDir, 'PipelineTool.vue');
+ const content = fs.readFileSync(filePath, 'utf-8');
+
+ expect(content).toMatch(/if\s*\(\s*outputResults\.value\.length\s*>\s*0\s*\)\s*\{\s*resetBatchAndResults\s*\(\s*\);?\s*\}/);
+ expect(content).toContain('workspaceState?.setActiveFile');
+ });
+});