refactor: streamline HTML structure and enhance UI components

- Cleaned up the HTML structure in index.html and pdf-multi-tool.html for better readability and maintainability.
- Improved the user interface of the PDF multi-tool with responsive button designs and better layout.
- Added new CSS styles for button states and cursor behavior.
- Updated README with corrected Docker deployment link.
- Refactored JavaScript logic to utilize new helper functions for formatting star counts.
- Commented out unused attachment functionalities in the logic files for future integration.
This commit is contained in:
abdullahalam123
2025-11-10 21:54:41 +05:30
parent 0634600073
commit 7e14c83ab8
11 changed files with 1161 additions and 1321 deletions

View File

@@ -105,7 +105,7 @@ You can run BentoPDF locally for development or personal use.
### 🚀 Quick Start with Docker ### 🚀 Quick Start with Docker
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/LWO8I0?referralCode=LokiSalmonNeko) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/K4AU2B)
You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository: You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository:

1582
index.html

File diff suppressed because it is too large Load Diff

View File

@@ -510,3 +510,14 @@ details > summary .icon {
details[open] > summary .icon { details[open] > summary .icon {
transform: rotate(45deg); transform: rotate(45deg);
} }
button,
.btn,
.btn-gradient {
cursor: pointer;
}
button:disabled,
.btn:disabled {
cursor: not-allowed;
}

View File

@@ -3,6 +3,12 @@ export const categories = [
{ {
name: 'Popular Tools', name: 'Popular Tools',
tools: [ tools: [
{
href: '/src/pages/pdf-multi-tool.html',
name: 'PDF Multi Tool',
icon: 'pencil-ruler',
subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.',
},
{ {
id: 'merge', id: 'merge',
name: 'Merge PDF', name: 'Merge PDF',
@@ -312,22 +318,23 @@ export const categories = [
icon: 'paperclip', icon: 'paperclip',
subtitle: 'Embed one or more files into your PDF.', subtitle: 'Embed one or more files into your PDF.',
}, },
{ // TODO@ALAM - MAKE THIS LATER, ONCE INTEGERATED WITH CPDF
id: 'extract-attachments', // {
name: 'Extract Attachments', // id: 'extract-attachments',
icon: 'download', // name: 'Extract Attachments',
subtitle: 'Extract all embedded files from PDF(s) as a ZIP.', // icon: 'download',
}, // subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
{ // },
id: 'edit-attachments', // {
name: 'Edit Attachments', // id: 'edit-attachments',
icon: 'file-edit', // name: 'Edit Attachments',
subtitle: 'View, remove, or replace attachments in your PDF.', // icon: 'file-edit',
}, // subtitle: 'View, remove, or replace attachments in your PDF.',
// },
{ {
href: '/src/pages/pdf-multi-tool.html', href: '/src/pages/pdf-multi-tool.html',
name: 'PDF Multi Tool', name: 'PDF Multi Tool',
icon: 'layers', icon: 'pencil-ruler',
subtitle: 'Full-featured PDF editor with page management.', subtitle: 'Full-featured PDF editor with page management.',
}, },
{ {

View File

@@ -1,205 +1,207 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; // TODO@ALAM - USE CPDF HERE
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
let currentAttachments: Array<{ name: string; index: number; size: number }> = []; // import { showLoader, hideLoader, showAlert } from '../ui.js';
let attachmentsToRemove: Set<number> = new Set(); // import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
let attachmentsToReplace: Map<number, File> = new Map(); // import { state } from '../state.js';
// import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function setupEditAttachmentsTool() { // let currentAttachments: Array<{ name: string; index: number; size: number }> = [];
const optionsDiv = document.getElementById('edit-attachments-options'); // let attachmentsToRemove: Set<number> = new Set();
if (!optionsDiv || !state.pdfDoc) return; // let attachmentsToReplace: Map<number, File> = new Map();
optionsDiv.classList.remove('hidden'); // export async function setupEditAttachmentsTool() {
await loadAttachmentsList(); // const optionsDiv = document.getElementById('edit-attachments-options');
} // if (!optionsDiv || !state.pdfDoc) return;
async function loadAttachmentsList() { // optionsDiv.classList.remove('hidden');
const attachmentsList = document.getElementById('attachments-list'); // await loadAttachmentsList();
if (!attachmentsList || !state.pdfDoc) return; // }
attachmentsList.innerHTML = ''; // async function loadAttachmentsList() {
currentAttachments = []; // const attachmentsList = document.getElementById('attachments-list');
attachmentsToRemove.clear(); // if (!attachmentsList || !state.pdfDoc) return;
attachmentsToReplace.clear();
try { // attachmentsList.innerHTML = '';
// Get embedded files from PDF // currentAttachments = [];
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() // attachmentsToRemove.clear();
.filter(([ref, obj]: any) => { // attachmentsToReplace.clear();
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
return dict && dict.get('Type')?.toString() === '/Filespec';
});
if (embeddedFiles.length === 0) { // try {
attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>'; // // Get embedded files from PDF
return; // const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
} // .filter(([ref, obj]: any) => {
// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
// return dict && dict.get('Type')?.toString() === '/Filespec';
// });
let index = 0; // if (embeddedFiles.length === 0) {
for (const [ref, fileSpec] of embeddedFiles) { // attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>';
try { // return;
const fileSpecDict = fileSpec as any; // }
const fileName = fileSpecDict.get('UF')?.decodeText() ||
fileSpecDict.get('F')?.decodeText() || // let index = 0;
`attachment-${index + 1}`; // for (const [ref, fileSpec] of embeddedFiles) {
// try {
// const fileSpecDict = fileSpec as any;
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
// fileSpecDict.get('F')?.decodeText() ||
// `attachment-${index + 1}`;
const ef = fileSpecDict.get('EF'); // const ef = fileSpecDict.get('EF');
let fileSize = 0; // let fileSize = 0;
if (ef) { // if (ef) {
const fRef = ef.get('F') || ef.get('UF'); // const fRef = ef.get('F') || ef.get('UF');
if (fRef) { // if (fRef) {
const fileStream = state.pdfDoc.context.lookup(fRef); // const fileStream = state.pdfDoc.context.lookup(fRef);
if (fileStream) { // if (fileStream) {
fileSize = (fileStream as any).getContents().length; // fileSize = (fileStream as any).getContents().length;
} // }
} // }
} // }
currentAttachments.push({ name: fileName, index, size: fileSize }); // currentAttachments.push({ name: fileName, index, size: fileSize });
const attachmentDiv = document.createElement('div'); // const attachmentDiv = document.createElement('div');
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700'; // attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
attachmentDiv.dataset.attachmentIndex = index.toString(); // attachmentDiv.dataset.attachmentIndex = index.toString();
const infoDiv = document.createElement('div'); // const infoDiv = document.createElement('div');
infoDiv.className = 'flex-1'; // infoDiv.className = 'flex-1';
const nameSpan = document.createElement('span'); // const nameSpan = document.createElement('span');
nameSpan.className = 'text-white font-medium block'; // nameSpan.className = 'text-white font-medium block';
nameSpan.textContent = fileName; // nameSpan.textContent = fileName;
const sizeSpan = document.createElement('span'); // const sizeSpan = document.createElement('span');
sizeSpan.className = 'text-gray-400 text-sm'; // sizeSpan.className = 'text-gray-400 text-sm';
sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`; // sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
infoDiv.append(nameSpan, sizeSpan); // infoDiv.append(nameSpan, sizeSpan);
const actionsDiv = document.createElement('div'); // const actionsDiv = document.createElement('div');
actionsDiv.className = 'flex items-center gap-2'; // actionsDiv.className = 'flex items-center gap-2';
// Remove button // // Remove button
const removeBtn = document.createElement('button'); // const removeBtn = document.createElement('button');
removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm'; // removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>'; // removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.title = 'Remove attachment'; // removeBtn.title = 'Remove attachment';
removeBtn.onclick = () => { // removeBtn.onclick = () => {
attachmentsToRemove.add(index); // attachmentsToRemove.add(index);
attachmentDiv.classList.add('opacity-50', 'line-through'); // attachmentDiv.classList.add('opacity-50', 'line-through');
removeBtn.disabled = true; // removeBtn.disabled = true;
}; // };
// Replace button // // Replace button
const replaceBtn = document.createElement('button'); // const replaceBtn = document.createElement('button');
replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm'; // replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm';
replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>'; // replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>';
replaceBtn.title = 'Replace attachment'; // replaceBtn.title = 'Replace attachment';
replaceBtn.onclick = () => { // replaceBtn.onclick = () => {
const input = document.createElement('input'); // const input = document.createElement('input');
input.type = 'file'; // input.type = 'file';
input.onchange = async (e) => { // input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0]; // const file = (e.target as HTMLInputElement).files?.[0];
if (file) { // if (file) {
attachmentsToReplace.set(index, file); // attachmentsToReplace.set(index, file);
nameSpan.textContent = `${fileName}${file.name}`; // nameSpan.textContent = `${fileName} → ${file.name}`;
nameSpan.classList.add('text-yellow-400'); // nameSpan.classList.add('text-yellow-400');
} // }
}; // };
input.click(); // input.click();
}; // };
actionsDiv.append(replaceBtn, removeBtn); // actionsDiv.append(replaceBtn, removeBtn);
attachmentDiv.append(infoDiv, actionsDiv); // attachmentDiv.append(infoDiv, actionsDiv);
attachmentsList.appendChild(attachmentDiv); // attachmentsList.appendChild(attachmentDiv);
index++; // index++;
} catch (e) { // } catch (e) {
console.warn(`Failed to process attachment ${index}:`, e); // console.warn(`Failed to process attachment ${index}:`, e);
index++; // index++;
} // }
} // }
} catch (e) { // } catch (e) {
console.error('Error loading attachments:', e); // console.error('Error loading attachments:', e);
showAlert('Error', 'Failed to load attachments from PDF.'); // showAlert('Error', 'Failed to load attachments from PDF.');
} // }
} // }
export async function editAttachments() { // export async function editAttachments() {
if (!state.pdfDoc) { // if (!state.pdfDoc) {
showAlert('Error', 'PDF is not loaded.'); // showAlert('Error', 'PDF is not loaded.');
return; // return;
} // }
showLoader('Updating attachments...'); // showLoader('Updating attachments...');
try { // try {
// Create a new PDF document // // Create a new PDF document
const newPdfDoc = await PDFLibDocument.create(); // const newPdfDoc = await PDFLibDocument.create();
// Copy all pages // // Copy all pages
const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices()); // const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices());
pages.forEach((page: any) => newPdfDoc.addPage(page)); // pages.forEach((page: any) => newPdfDoc.addPage(page));
// Handle attachments // // Handle attachments
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() // const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
.filter(([ref, obj]: any) => { // .filter(([ref, obj]: any) => {
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null; // const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
return dict && dict.get('Type')?.toString() === '/Filespec'; // return dict && dict.get('Type')?.toString() === '/Filespec';
}); // });
let attachmentIndex = 0; // let attachmentIndex = 0;
for (const [ref, fileSpec] of embeddedFiles) { // for (const [ref, fileSpec] of embeddedFiles) {
if (attachmentsToRemove.has(attachmentIndex)) { // if (attachmentsToRemove.has(attachmentIndex)) {
attachmentIndex++; // attachmentIndex++;
continue; // Skip removed attachments // continue; // Skip removed attachments
} // }
if (attachmentsToReplace.has(attachmentIndex)) { // if (attachmentsToReplace.has(attachmentIndex)) {
// Replace attachment // // Replace attachment
const replacementFile = attachmentsToReplace.get(attachmentIndex)!; // const replacementFile = attachmentsToReplace.get(attachmentIndex)!;
const fileBytes = await readFileAsArrayBuffer(replacementFile); // const fileBytes = await readFileAsArrayBuffer(replacementFile);
await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, { // await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, {
mimeType: replacementFile.type || 'application/octet-stream', // mimeType: replacementFile.type || 'application/octet-stream',
description: `Attached file: ${replacementFile.name}`, // description: `Attached file: ${replacementFile.name}`,
creationDate: new Date(), // creationDate: new Date(),
modificationDate: new Date(replacementFile.lastModified), // modificationDate: new Date(replacementFile.lastModified),
}); // });
} else { // } else {
// Keep existing attachment - copy it // // Keep existing attachment - copy it
try { // try {
const fileSpecDict = fileSpec as any; // const fileSpecDict = fileSpec as any;
const fileName = fileSpecDict.get('UF')?.decodeText() || // const fileName = fileSpecDict.get('UF')?.decodeText() ||
fileSpecDict.get('F')?.decodeText() || // fileSpecDict.get('F')?.decodeText() ||
`attachment-${attachmentIndex + 1}`; // `attachment-${attachmentIndex + 1}`;
const ef = fileSpecDict.get('EF'); // const ef = fileSpecDict.get('EF');
if (ef) { // if (ef) {
const fRef = ef.get('F') || ef.get('UF'); // const fRef = ef.get('F') || ef.get('UF');
if (fRef) { // if (fRef) {
const fileStream = state.pdfDoc.context.lookup(fRef); // const fileStream = state.pdfDoc.context.lookup(fRef);
if (fileStream) { // if (fileStream) {
const fileData = (fileStream as any).getContents(); // const fileData = (fileStream as any).getContents();
await newPdfDoc.attach(fileData, fileName, { // await newPdfDoc.attach(fileData, fileName, {
mimeType: 'application/octet-stream', // mimeType: 'application/octet-stream',
description: `Attached file: ${fileName}`, // description: `Attached file: ${fileName}`,
}); // });
} // }
} // }
} // }
} catch (e) { // } catch (e) {
console.warn(`Failed to copy attachment ${attachmentIndex}:`, e); // console.warn(`Failed to copy attachment ${attachmentIndex}:`, e);
} // }
} // }
attachmentIndex++; // attachmentIndex++;
} // }
const pdfBytes = await newPdfDoc.save(); // const pdfBytes = await newPdfDoc.save();
downloadFile( // downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), // new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
`edited-attachments-${state.files[0].name}` // `edited-attachments-${state.files[0].name}`
); // );
showAlert('Success', 'Attachments updated successfully!'); // showAlert('Success', 'Attachments updated successfully!');
} catch (e) { // } catch (e) {
console.error(e); // console.error(e);
showAlert('Error', 'Failed to edit attachments.'); // showAlert('Error', 'Failed to edit attachments.');
} finally { // } finally {
hideLoader(); // hideLoader();
} // }
} // }

View File

@@ -1,86 +1,88 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; // TODO@ALAM - USE CPDF HERE
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import JSZip from 'jszip';
export async function extractAttachments() { // import { showLoader, hideLoader, showAlert } from '../ui.js';
if (state.files.length === 0) { // import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
showAlert('No Files', 'Please select at least one PDF file.'); // import { state } from '../state.js';
return; // import { PDFDocument as PDFLibDocument } from 'pdf-lib';
} // import JSZip from 'jszip';
showLoader('Extracting attachments...'); // export async function extractAttachments() {
try { // if (state.files.length === 0) {
const zip = new JSZip(); // showAlert('No Files', 'Please select at least one PDF file.');
let totalAttachments = 0; // return;
// }
for (const file of state.files) { // showLoader('Extracting attachments...');
const pdfBytes = await readFileAsArrayBuffer(file); // try {
const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { // const zip = new JSZip();
ignoreEncryption: true, // let totalAttachments = 0;
});
const embeddedFiles = pdfDoc.context.enumerateIndirectObjects() // for (const file of state.files) {
.filter(([ref, obj]: any) => { // const pdfBytes = await readFileAsArrayBuffer(file);
// obj must be a PDFDict // const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
if (obj && typeof obj.get === 'function') { // ignoreEncryption: true,
const type = obj.get('Type'); // });
return type && type.toString() === '/Filespec';
}
return false;
});
if (embeddedFiles.length === 0) { // const embeddedFiles = pdfDoc.context.enumerateIndirectObjects()
console.warn(`No attachments found in ${file.name}`); // .filter(([ref, obj]: any) => {
continue; // // obj must be a PDFDict
} // if (obj && typeof obj.get === 'function') {
// const type = obj.get('Type');
// return type && type.toString() === '/Filespec';
// }
// return false;
// });
// Extract attachments // if (embeddedFiles.length === 0) {
const baseName = file.name.replace(/\.pdf$/i, ''); // console.warn(`No attachments found in ${file.name}`);
for (let i = 0; i < embeddedFiles.length; i++) { // continue;
try { // }
const [ref, fileSpec] = embeddedFiles[i];
const fileSpecDict = fileSpec as any; // // Extract attachments
// const baseName = file.name.replace(/\.pdf$/i, '');
// for (let i = 0; i < embeddedFiles.length; i++) {
// try {
// const [ref, fileSpec] = embeddedFiles[i];
// const fileSpecDict = fileSpec as any;
// Get attachment name // // Get attachment name
const fileName = fileSpecDict.get('UF')?.decodeText() || // const fileName = fileSpecDict.get('UF')?.decodeText() ||
fileSpecDict.get('F')?.decodeText() || // fileSpecDict.get('F')?.decodeText() ||
`attachment-${i + 1}`; // `attachment-${i + 1}`;
// Get embedded file stream // // Get embedded file stream
const ef = fileSpecDict.get('EF'); // const ef = fileSpecDict.get('EF');
if (ef) { // if (ef) {
const fRef = ef.get('F') || ef.get('UF'); // const fRef = ef.get('F') || ef.get('UF');
if (fRef) { // if (fRef) {
const fileStream = pdfDoc.context.lookup(fRef); // const fileStream = pdfDoc.context.lookup(fRef);
if (fileStream) { // if (fileStream) {
const fileData = (fileStream as any).getContents(); // const fileData = (fileStream as any).getContents();
zip.file(`${baseName}_${fileName}`, fileData); // zip.file(`${baseName}_${fileName}`, fileData);
totalAttachments++; // totalAttachments++;
} // }
} // }
} // }
} catch (e) { // } catch (e) {
console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e); // console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
} // }
} // }
} // }
if (totalAttachments === 0) { // if (totalAttachments === 0) {
showAlert('No Attachments', 'No attachments were found in the selected PDF(s).'); // showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
hideLoader(); // hideLoader();
return; // return;
} // }
const zipBlob = await zip.generateAsync({ type: 'blob' }); // const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'extracted-attachments.zip'); // downloadFile(zipBlob, 'extracted-attachments.zip');
showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`); // showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`);
} catch (e) { // } catch (e) {
console.error(e); // console.error(e);
showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.'); // showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
} finally { // } finally {
hideLoader(); // hideLoader();
} // }
} // }

View File

@@ -63,8 +63,8 @@ import {
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
import { linearizePdf } from './linearize.js'; import { linearizePdf } from './linearize.js';
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js'; import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
import { extractAttachments } from './extract-attachments.js'; // import { extractAttachments } from './extract-attachments.js';
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js'; // import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
import { sanitizePdf } from './sanitize-pdf.js'; import { sanitizePdf } from './sanitize-pdf.js';
import { removeRestrictions } from './remove-restrictions.js'; import { removeRestrictions } from './remove-restrictions.js';
@@ -140,10 +140,10 @@ export const toolLogic = {
process: addAttachments, process: addAttachments,
setup: setupAddAttachmentsTool, setup: setupAddAttachmentsTool,
}, },
'extract-attachments': extractAttachments, // 'extract-attachments': extractAttachments,
'edit-attachments': { // 'edit-attachments': {
process: editAttachments, // process: editAttachments,
setup: setupEditAttachmentsTool, // setup: setupEditAttachmentsTool,
}, // },
'sanitize-pdf': sanitizePdf, 'sanitize-pdf': sanitizePdf,
}; };

View File

@@ -1,8 +1,12 @@
// @TODO:@ALAM- sometimes I think... and then I forget...
//
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib'; import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import JSZip from 'jszip'; import JSZip from 'jszip';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { downloadFile } from '../utils/helpers';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -34,7 +38,7 @@ const redoStack: Snapshot[] = [];
function snapshot() { function snapshot() {
const snap: Snapshot = { const snap: Snapshot = {
allPages: allPages.map(p => ({ ...p })), allPages: allPages.map(p => ({ ...p, canvas: p.canvas })),
selectedPages: Array.from(selectedPages), selectedPages: Array.from(selectedPages),
splitMarkers: Array.from(splitMarkers), splitMarkers: Array.from(splitMarkers),
}; };
@@ -43,7 +47,10 @@ function snapshot() {
} }
function restore(snap: Snapshot) { function restore(snap: Snapshot) {
allPages = snap.allPages.map(p => ({ ...p })); allPages = snap.allPages.map(p => ({
...p,
canvas: p.canvas
}));
selectedPages = new Set(snap.selectedPages); selectedPages = new Set(snap.selectedPages);
splitMarkers = new Set(snap.splitMarkers); splitMarkers = new Set(snap.splitMarkers);
updatePageDisplay(); updatePageDisplay();
@@ -202,7 +209,6 @@ function initializeTool() {
} }
}); });
// Modal close button
document.getElementById('modal-close-btn')?.addEventListener('click', hideModal); document.getElementById('modal-close-btn')?.addEventListener('click', hideModal);
document.getElementById('modal')?.addEventListener('click', (e) => { document.getElementById('modal')?.addEventListener('click', (e) => {
if (e.target === document.getElementById('modal')) { if (e.target === document.getElementById('modal')) {
@@ -210,7 +216,6 @@ function initializeTool() {
} }
}); });
// Drag and drop
const uploadArea = document.getElementById('upload-area'); const uploadArea = document.getElementById('upload-area');
if (uploadArea) { if (uploadArea) {
uploadArea.addEventListener('dragover', (e) => { uploadArea.addEventListener('dragover', (e) => {
@@ -230,7 +235,6 @@ function initializeTool() {
}); });
} }
// Show upload area initially
document.getElementById('upload-area')?.classList.remove('hidden'); document.getElementById('upload-area')?.classList.remove('hidden');
} }
@@ -315,7 +319,6 @@ async function loadPdfs(files: File[]) {
} }
function getCacheKey(pdfIndex: number, pageIndex: number): string { function getCacheKey(pdfIndex: number, pageIndex: number): string {
// Removed rotation from cache key - canvas is always rendered at 0 degrees
return `${pdfIndex}-${pageIndex}`; return `${pdfIndex}-${pageIndex}`;
} }
@@ -330,12 +333,10 @@ async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: n
if (pageCanvasCache.has(cacheKey)) { if (pageCanvasCache.has(cacheKey)) {
canvas = pageCanvasCache.get(cacheKey)!; canvas = pageCanvasCache.get(cacheKey)!;
} else { } else {
// Render page preview at 0 degrees rotation using pdfjs
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise; const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const page = await pdf.getPage(pageIndex + 1); const page = await pdf.getPage(pageIndex + 1);
// Always render at 0 rotation - visual rotation is applied via CSS
const viewport = page.getViewport({ scale: 0.5, rotation: 0 }); const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
@@ -384,20 +385,14 @@ function createPageCard(pageData: PageData, index: number) {
const preview = document.createElement('div'); const preview = document.createElement('div');
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative'; preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
preview.style.minHeight = '160px'; preview.style.minHeight = '160px';
preview.style.maxHeight = '256px'; preview.style.height = '250px';
const previewCanvas = pageData.canvas; const previewCanvas = pageData.canvas;
previewCanvas.className = 'max-w-full max-h-full object-contain'; previewCanvas.className = 'max-w-full max-h-full object-contain';
// Apply visual rotation using CSS transform // Apply visual rotation using CSS transform
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`; previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
previewCanvas.style.transition = 'transform 0.2s ease';
// Adjust container dimensions based on rotation
if (pageData.visualRotation === 90 || pageData.visualRotation === 270) {
preview.style.aspectRatio = `${previewCanvas.height} / ${previewCanvas.width}`;
} else {
preview.style.aspectRatio = `${previewCanvas.width} / ${previewCanvas.height}`;
}
preview.appendChild(previewCanvas); preview.appendChild(previewCanvas);
@@ -408,7 +403,11 @@ function createPageCard(pageData: PageData, index: number) {
// Actions toolbar // Actions toolbar
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity'; actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity absolute bottom-2 left-0 right-0';
const actionsInner = document.createElement('div');
actionsInner.className = 'flex items-center gap-1 bg-gray-900/90 rounded px-2 py-1';
actions.appendChild(actionsInner);
// Select checkbox // Select checkbox
const selectBtn = document.createElement('button'); const selectBtn = document.createElement('button');
@@ -441,6 +440,7 @@ function createPageCard(pageData: PageData, index: number) {
const duplicateBtn = document.createElement('button'); const duplicateBtn = document.createElement('button');
duplicateBtn.className = 'p-1 rounded hover:bg-gray-700'; duplicateBtn.className = 'p-1 rounded hover:bg-gray-700';
duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>'; duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>';
duplicateBtn.title = 'Duplicate this page';
duplicateBtn.onclick = (e) => { duplicateBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
snapshot(); snapshot();
@@ -451,6 +451,7 @@ function createPageCard(pageData: PageData, index: number) {
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = 'p-1 rounded hover:bg-gray-700'; deleteBtn.className = 'p-1 rounded hover:bg-gray-700';
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>'; deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>';
deleteBtn.title = 'Delete this page';
deleteBtn.onclick = (e) => { deleteBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
snapshot(); snapshot();
@@ -480,7 +481,7 @@ function createPageCard(pageData: PageData, index: number) {
renderSplitMarkers(); renderSplitMarkers();
}; };
actions.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
card.append(preview, info, actions, selectBtn); card.append(preview, info, actions, selectBtn);
pagesContainer.appendChild(card); pagesContainer.appendChild(card);
@@ -506,7 +507,6 @@ function setupSortable() {
}); });
} }
// Optimized selection that only updates the specific card
function toggleSelectOptimized(index: number) { function toggleSelectOptimized(index: number) {
if (selectedPages.has(index)) { if (selectedPages.has(index)) {
selectedPages.delete(index); selectedPages.delete(index);
@@ -546,7 +546,6 @@ function deselectAll() {
updatePageDisplay(); updatePageDisplay();
} }
// Instant rotation - just update visual rotation, no re-rendering
function rotatePage(index: number, delta: number) { function rotatePage(index: number, delta: number) {
snapshot(); snapshot();
@@ -554,7 +553,6 @@ function rotatePage(index: number, delta: number) {
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360; pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
pageData.rotation = (pageData.rotation + delta + 360) % 360; pageData.rotation = (pageData.rotation + delta + 360) % 360;
// Just update the specific card's transform
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
@@ -566,13 +564,7 @@ function rotatePage(index: number, delta: number) {
if (canvas && preview) { if (canvas && preview) {
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`; canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
canvas.style.transition = 'transform 0.2s ease';
// Adjust container aspect ratio
if (pageData.visualRotation === 90 || pageData.visualRotation === 270) {
(preview as HTMLElement).style.aspectRatio = `${canvas.height} / ${canvas.width}`;
} else {
(preview as HTMLElement).style.aspectRatio = `${canvas.width} / ${canvas.height}`;
}
} }
} }
@@ -580,7 +572,6 @@ function duplicatePage(index: number) {
const originalPageData = allPages[index]; const originalPageData = allPages[index];
const originalCanvas = originalPageData.canvas; const originalCanvas = originalPageData.canvas;
// Create a new canvas and copy content
const newCanvas = document.createElement('canvas'); const newCanvas = document.createElement('canvas');
newCanvas.width = originalCanvas.width; newCanvas.width = originalCanvas.width;
newCanvas.height = originalCanvas.height; newCanvas.height = originalCanvas.height;
@@ -603,13 +594,18 @@ function duplicatePage(index: number) {
function deletePage(index: number) { function deletePage(index: number) {
allPages.splice(index, 1); allPages.splice(index, 1);
selectedPages.delete(index); selectedPages.delete(index);
// Update selected indices
const newSelected = new Set<number>(); const newSelected = new Set<number>();
selectedPages.forEach(i => { selectedPages.forEach(i => {
if (i > index) newSelected.add(i - 1); if (i > index) newSelected.add(i - 1);
else if (i < index) newSelected.add(i); else if (i < index) newSelected.add(i);
}); });
selectedPages = newSelected; selectedPages = newSelected;
if (allPages.length === 0) {
resetAll();
return;
}
updatePageDisplay(); updatePageDisplay();
} }
@@ -640,7 +636,6 @@ async function handleInsertPdf(e: Event) {
newPages.push(allPages.pop()!); newPages.push(allPages.pop()!);
} }
// Insert pages after the specified index
allPages.splice(insertAfterIndex + 1, 0, ...newPages); allPages.splice(insertAfterIndex + 1, 0, ...newPages);
updatePageDisplay(); updatePageDisplay();
} catch (e) { } catch (e) {
@@ -660,10 +655,8 @@ function renderSplitMarkers() {
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
// Remove all existing split markers
pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove()); pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove());
// Add split markers between cards
Array.from(pagesContainer.children).forEach((cardEl, i) => { Array.from(pagesContainer.children).forEach((cardEl, i) => {
if (splitMarkers.has(i)) { if (splitMarkers.has(i)) {
const marker = document.createElement('div'); const marker = document.createElement('div');
@@ -675,7 +668,6 @@ function renderSplitMarkers() {
} }
function addBlankPage() { function addBlankPage() {
// Create a blank page
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = 595; canvas.width = 595;
canvas.height = 842; canvas.height = 842;
@@ -699,7 +691,6 @@ function addBlankPage() {
updatePageDisplay(); updatePageDisplay();
} }
// Instant bulk rotation - just update visual rotation
function bulkRotate(delta: number) { function bulkRotate(delta: number) {
if (selectedPages.size === 0) { if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to rotate.', 'info'); showModal('No Selection', 'Please select pages to rotate.', 'info');
@@ -712,7 +703,6 @@ function bulkRotate(delta: number) {
pageData.rotation = (pageData.rotation + delta + 360) % 360; pageData.rotation = (pageData.rotation + delta + 360) % 360;
}); });
// Update display for all rotated pages
updatePageDisplay(); updatePageDisplay();
} }
@@ -724,6 +714,12 @@ function bulkDelete() {
const indices = Array.from(selectedPages).sort((a, b) => b - a); const indices = Array.from(selectedPages).sort((a, b) => b - a);
indices.forEach(index => allPages.splice(index, 1)); indices.forEach(index => allPages.splice(index, 1));
selectedPages.clear(); selectedPages.clear();
if (allPages.length === 0) {
resetAll();
return;
}
updatePageDisplay(); updatePageDisplay();
} }
@@ -742,11 +738,18 @@ function bulkDuplicate() {
function bulkSplit() { function bulkSplit() {
if (selectedPages.size === 0) { if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to split.', 'info'); showModal('No Selection', 'Please select pages to mark for splitting.', 'info');
return; return;
} }
const indices = Array.from(selectedPages); const indices = Array.from(selectedPages);
downloadPagesAsPdf(indices, 'selected-pages.pdf'); indices.forEach(index => {
if (!splitMarkers.has(index)) {
splitMarkers.add(index);
}
});
renderSplitMarkers();
selectedPages.clear();
updatePageDisplay();
} }
async function bulkDownload() { async function bulkDownload() {
@@ -759,8 +762,79 @@ async function bulkDownload() {
} }
async function downloadAll() { async function downloadAll() {
const indices = Array.from({ length: allPages.length }, (_, i) => i); if (allPages.length === 0) {
await downloadPagesAsPdf(indices, 'all-pages.pdf'); showModal('No Pages', 'Please upload PDFs first.', 'info');
return;
}
// Check if there are split markers
if (splitMarkers.size > 0) {
// Split into multiple PDFs and download as ZIP
await downloadSplitPdfs();
} else {
// Download as single PDF
const indices = Array.from({ length: allPages.length }, (_, i) => i);
await downloadPagesAsPdf(indices, 'all-pages.pdf');
}
}
async function downloadSplitPdfs() {
try {
const zip = new JSZip();
const sortedMarkers = Array.from(splitMarkers).sort((a, b) => a - b);
// Create segments based on split markers
const segments: number[][] = [];
let currentSegment: number[] = [];
for (let i = 0; i < allPages.length; i++) {
currentSegment.push(i);
// If this page has a split marker after it, start a new segment
if (splitMarkers.has(i)) {
segments.push(currentSegment);
currentSegment = [];
}
}
// Add the last segment if it has pages
if (currentSegment.length > 0) {
segments.push(currentSegment);
}
// Create PDFs for each segment
for (let segIndex = 0; segIndex < segments.length; segIndex++) {
const segment = segments[segIndex];
const newPdf = await PDFLibDocument.create();
for (const index of segment) {
const pageData = allPages[index];
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
const page = newPdf.addPage(copiedPage);
if (pageData.rotation !== 0) {
const currentRotation = page.getRotation().angle;
page.setRotation(degrees(currentRotation + pageData.rotation));
}
} else {
newPdf.addPage([595, 842]);
}
}
const pdfBytes = await newPdf.save();
zip.file(`document-${segIndex + 1}.pdf`, pdfBytes);
}
// Generate and download ZIP
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'split-documents.zip');
showModal('Success', `Downloaded ${segments.length} PDF files in a ZIP archive.`, 'success');
} catch (e) {
console.error('Failed to create split PDFs:', e);
showModal('Error', 'Failed to create split PDFs.', 'error');
}
} }
async function downloadPagesAsPdf(indices: number[], filename: string) { async function downloadPagesAsPdf(indices: number[], filename: string) {
@@ -785,12 +859,8 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
const pdfBytes = await newPdf.save(); const pdfBytes = await newPdf.save();
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }); const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); downloadFile(blob, filename);
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
} catch (e) { } catch (e) {
console.error('Failed to create PDF:', e); console.error('Failed to create PDF:', e);
showModal('Error', 'Failed to create PDF.', 'error'); showModal('Error', 'Failed to create PDF.', 'error');

View File

@@ -4,6 +4,7 @@ import { setupToolInterface } from './handlers/toolSelectionHandler.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import '../css/styles.css'; import '../css/styles.css';
import { formatStars } from './utils/helpers.js';
const init = () => { const init = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@@ -274,7 +275,7 @@ const init = () => {
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (data.stargazers_count !== undefined) { if (data.stargazers_count !== undefined) {
githubStarsElement.textContent = data.stargazers_count.toLocaleString(); githubStarsElement.textContent = formatStars(data.stargazers_count);
} }
}) })
.catch(() => { .catch(() => {

View File

@@ -49,10 +49,10 @@ export const hexToRgb = (hex: any) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result return result
? { ? {
r: parseInt(result[1], 16) / 255, r: parseInt(result[1], 16) / 255,
g: parseInt(result[2], 16) / 255, g: parseInt(result[2], 16) / 255,
b: parseInt(result[3], 16) / 255, b: parseInt(result[3], 16) / 255,
} }
: { r: 0, g: 0, b: 0 }; // Default to black : { r: 0, g: 0, b: 0 }; // Default to black
}; };
@@ -187,3 +187,10 @@ export function initializeIcons(): void {
}, },
}); });
} }
export function formatStars(num: number) {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toLocaleString();
};

View File

@@ -32,70 +32,88 @@
<!-- Main Container --> <!-- Main Container -->
<div class="flex flex-col h-[calc(100vh-4rem)]"> <div class="flex flex-col h-[calc(100vh-4rem)]">
<!-- Toolbar --> <!-- Toolbar -->
<div class="bg-gray-800 border-b border-gray-700 p-4 flex flex-wrap items-center gap-2 overflow-x-auto"> <div class="bg-gray-800 border-b border-gray-700 p-2 sm:p-4 overflow-x-auto">
<button id="upload-pdfs-btn" <div class="flex flex-wrap items-center gap-1 sm:gap-2 min-w-max">
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2"> <button id="upload-pdfs-btn"
<i data-lucide="upload" class="w-4 h-4"></i> Upload PDFs class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-4 py-1.5 sm:py-2 rounded flex items-center gap-1 sm:gap-2 text-sm">
</button> <i data-lucide="upload" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<div class="border-l border-gray-600 h-6 mx-2"></div> <span class="hidden sm:inline">Upload PDFs</span>
<button id="add-blank-page-btn" <span class="sm:hidden">Upload</span>
class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm"> </button>
<i data-lucide="file-plus" class="w-4 h-4"></i> Add Blank <div class="border-l border-gray-600 h-6 mx-1"></div>
</button> <button id="add-blank-page-btn"
<div class="border-l border-gray-600 h-6 mx-2"></div> class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm">
<button id="undo-btn" <i data-lucide="file-plus" class="w-3 h-3 sm:w-4 sm:h-4"></i>
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> <span class="hidden sm:inline">Add Blank</span>
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Undo </button>
</button> <div class="border-l border-gray-600 h-6 mx-1"></div>
<button id="redo-btn" <button id="undo-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="rotate-cw" class="w-4 h-4"></i> Redo <i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
</button> <span class="hidden lg:inline">Undo</span>
<button id="reset-btn" </button>
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> <button id="redo-btn"
<i data-lucide="refresh-ccw" class="w-4 h-4"></i> Reset class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
</button> <i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<div class="border-l border-gray-600 h-6 mx-2"></div> <span class="hidden lg:inline">Redo</span>
<span class="text-gray-400 text-sm">Selection:</span> </button>
<button id="select-all-btn" <button id="reset-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="check-square" class="w-4 h-4"></i> Select All <i data-lucide="refresh-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
</button> <span class="hidden lg:inline">Reset</span>
<button id="deselect-all-btn" </button>
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> <div class="border-l border-gray-600 h-6 mx-1"></div>
<i data-lucide="square" class="w-4 h-4"></i> Deselect All <span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Selection:</span>
</button> <button id="select-all-btn"
<div class="border-l border-gray-600 h-6 mx-2"></div> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<span class="text-gray-400 text-sm">Bulk Actions:</span> <i data-lucide="check-square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<button id="bulk-rotate-left-btn" <span class="hidden lg:inline">Select All</span>
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> </button>
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Rotate Left <button id="deselect-all-btn"
</button> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<button id="bulk-rotate-btn" <i data-lucide="square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> <span class="hidden lg:inline">Deselect All</span>
<i data-lucide="rotate-cw" class="w-4 h-4"></i> Rotate Right </button>
</button> <div class="border-l border-gray-600 h-6 mx-1"></div>
<button id="bulk-delete-btn" <span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Bulk:</span>
class="btn bg-red-600 hover:bg-red-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> <button id="bulk-rotate-left-btn"
<i data-lucide="trash-2" class="w-4 h-4"></i> Delete class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
</button> <i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<button id="bulk-duplicate-btn" <span class="hidden xl:inline">Rotate Left</span>
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> </button>
<i data-lucide="copy" class="w-4 h-4"></i> Duplicate <button id="bulk-rotate-btn"
</button> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<button id="bulk-split-btn" <i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> <span class="hidden xl:inline">Rotate Right</span>
<i data-lucide="scissors" class="w-4 h-4"></i> Split </button>
</button> <button id="bulk-delete-btn"
<button id="bulk-download-btn" class="btn bg-red-600 hover:bg-red-700 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
class="btn bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> <i data-lucide="trash-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<i data-lucide="download" class="w-4 h-4"></i> Download Selected <span class="hidden xl:inline">Delete</span>
</button> </button>
<div class="flex-1"></div> <button id="bulk-duplicate-btn"
<button id="export-pdf-btn" class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2"> <i data-lucide="copy" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<i data-lucide="download" class="w-4 h-4"></i> Export PDF <span class="hidden xl:inline">Duplicate</span>
</button> </button>
<button id="bulk-split-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="scissors" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Split</span>
</button>
<button id="bulk-download-btn"
class="btn bg-green-600 hover:bg-green-700 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Download Selected</span>
</button>
<div class="flex-1 min-w-[20px]"></div>
<button id="export-pdf-btn"
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-4 py-1.5 sm:py-2 rounded flex items-center gap-1 sm:gap-2 text-sm">
<i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden sm:inline">Export PDF</span>
<span class="sm:hidden">Export</span>
</button>
</div>
</div> </div>
<!-- Content Area --> <!-- Content Area -->