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:
@@ -105,7 +105,7 @@ You can run BentoPDF locally for development or personal use.
|
|||||||
|
|
||||||
### 🚀 Quick Start with Docker
|
### 🚀 Quick Start with Docker
|
||||||
|
|
||||||
[](https://zeabur.com/templates/LWO8I0?referralCode=LokiSalmonNeko)
|
[](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:
|
||||||
|
|
||||||
|
|||||||
560
index.html
560
index.html
@@ -1,26 +1,20 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>BentoPDF - The Privacy First PDF Toolkit</title>
|
<title>BentoPDF - The Privacy First PDF Toolkit</title>
|
||||||
<link rel="icon" type="image/png" href="./images/favicon.svg" />
|
<link rel="icon" type="image/png" href="./images/favicon.svg" />
|
||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="antialiased">
|
<body class="antialiased">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div
|
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||||
id="home-logo"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="images/favicon.svg"
|
|
||||||
alt="Bento PDF Logo"
|
|
||||||
class="h-8 w-8"
|
|
||||||
/>
|
|
||||||
<span class="text-white font-bold text-xl ml-2">
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
<a href="index.html">BentoPDF</a>
|
<a href="index.html">BentoPDF</a>
|
||||||
</span>
|
</span>
|
||||||
@@ -36,47 +30,19 @@
|
|||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Mobile Hamburger Button -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center">
|
||||||
<button
|
<button id="mobile-menu-button" type="button"
|
||||||
id="mobile-menu-button"
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
aria-controls="mobile-menu"
|
aria-controls="mobile-menu" aria-expanded="false">
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Open main menu</span>
|
<span class="sr-only">Open main menu</span>
|
||||||
<!-- Hamburger Icon -->
|
<!-- Hamburger Icon -->
|
||||||
<svg
|
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
id="menu-icon"
|
stroke="currentColor" aria-hidden="true">
|
||||||
class="block h-6 w-6"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Close Icon -->
|
<!-- Close Icon -->
|
||||||
<svg
|
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
id="close-icon"
|
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
class="hidden h-6 w-6"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,17 +50,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu Dropdown -->
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div
|
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||||
id="mobile-menu"
|
|
||||||
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
|
||||||
>
|
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||||
>All Tools</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -103,63 +64,47 @@
|
|||||||
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
|
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
|
||||||
The <span class="marker-slanted"> PDF Toolkit </span> built for
|
The <span class="marker-slanted"> PDF Toolkit </span> built for
|
||||||
privacy<span
|
privacy<span
|
||||||
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500"
|
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
|
||||||
>.</span
|
|
||||||
>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p>
|
<p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p>
|
||||||
<div
|
<div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
|
||||||
class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8"
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm"
|
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
|
||||||
>
|
|
||||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
||||||
No Signups
|
No Signups
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm"
|
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
|
||||||
>
|
|
||||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
||||||
Unlimited Use
|
Unlimited Use
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm"
|
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
|
||||||
>
|
|
||||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
||||||
Works Offline
|
Works Offline
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<a
|
<a href="#tools-header"
|
||||||
href="#tools-header"
|
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">Start
|
||||||
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1"
|
Using - Forever Free</a>
|
||||||
>Start Using - Forever Free</a
|
<a href="https://github.com/alam00000/bentopdf/stargazers" target="_blank" rel="noopener noreferrer" class="
|
||||||
>
|
inline-flex items-center gap-1.5 text-sm font-medium
|
||||||
<a
|
bg-white text-gray-800 border border-gray-300
|
||||||
href="https://github.com/alam00000/bentopdf"
|
dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600
|
||||||
target="_blank"
|
pl-2.5 pr-3 py-1
|
||||||
rel="noopener noreferrer"
|
rounded-full transition-colors duration-200
|
||||||
class="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-indigo-400 underline transition-colors"
|
shadow-sm hover:shadow-md hover:bg-gray-50 dark:hover:bg-gray-700
|
||||||
>
|
">
|
||||||
<svg
|
<svg class="w-4 h-4 flex-shrink-0 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 24 24"
|
||||||
class="w-4 h-4"
|
aria-hidden="true">
|
||||||
fill="currentColor"
|
<path fill-rule="evenodd"
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd" />
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<span>View on GitHub</span>
|
|
||||||
<span class="inline-flex items-center gap-1">
|
|
||||||
<i data-lucide="star" class="w-3 h-3"></i>
|
|
||||||
<span id="github-stars">-</span>
|
<span id="github-stars">-</span>
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -173,10 +118,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<div class="bg-gray-800 p-6 rounded-lg">
|
<div class="bg-gray-800 p-6 rounded-lg">
|
||||||
<div class="flex items-center gap-4 mb-3">
|
<div class="flex items-center gap-4 mb-3">
|
||||||
<i
|
<i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||||
data-lucide="user-plus"
|
|
||||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
<h3 class="text-xl font-bold text-white">No Signup</h3>
|
<h3 class="text-xl font-bold text-white">No Signup</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 pl-14">
|
<p class="text-gray-400 pl-14">
|
||||||
@@ -185,10 +127,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 p-6 rounded-lg">
|
<div class="bg-gray-800 p-6 rounded-lg">
|
||||||
<div class="flex items-center gap-4 mb-3">
|
<div class="flex items-center gap-4 mb-3">
|
||||||
<i
|
<i data-lucide="shield" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||||
data-lucide="shield"
|
|
||||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
<h3 class="text-xl font-bold text-white">No Uploads</h3>
|
<h3 class="text-xl font-bold text-white">No Uploads</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 pl-14">
|
<p class="text-gray-400 pl-14">
|
||||||
@@ -197,10 +136,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 p-6 rounded-lg">
|
<div class="bg-gray-800 p-6 rounded-lg">
|
||||||
<div class="flex items-center gap-4 mb-3">
|
<div class="flex items-center gap-4 mb-3">
|
||||||
<i
|
<i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||||
data-lucide="badge-dollar-sign"
|
|
||||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
<h3 class="text-xl font-bold text-white">Forever Free</h3>
|
<h3 class="text-xl font-bold text-white">Forever Free</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 pl-14">
|
<p class="text-gray-400 pl-14">
|
||||||
@@ -209,10 +145,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 p-6 rounded-lg">
|
<div class="bg-gray-800 p-6 rounded-lg">
|
||||||
<div class="flex items-center gap-4 mb-3">
|
<div class="flex items-center gap-4 mb-3">
|
||||||
<i
|
<i data-lucide="infinity" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||||
data-lucide="infinity"
|
|
||||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
<h3 class="text-xl font-bold text-white">No Limits</h3>
|
<h3 class="text-xl font-bold text-white">No Limits</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 pl-14">
|
<p class="text-gray-400 pl-14">
|
||||||
@@ -221,20 +154,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 p-6 rounded-lg">
|
<div class="bg-gray-800 p-6 rounded-lg">
|
||||||
<div class="flex items-center gap-4 mb-3">
|
<div class="flex items-center gap-4 mb-3">
|
||||||
<i
|
<i data-lucide="layers" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||||
data-lucide="layers"
|
|
||||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
<h3 class="text-xl font-bold text-white">Batch Processing</h3>
|
<h3 class="text-xl font-bold text-white">Batch Processing</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p>
|
<p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 p-6 rounded-lg">
|
<div class="bg-gray-800 p-6 rounded-lg">
|
||||||
<div class="flex items-center gap-4 mb-3">
|
<div class="flex items-center gap-4 mb-3">
|
||||||
<i
|
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||||
data-lucide="zap"
|
|
||||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
<h3 class="text-xl font-bold text-white">Lightning Fast</h3>
|
<h3 class="text-xl font-bold text-white">Lightning Fast</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 pl-14">
|
<p class="text-gray-400 pl-14">
|
||||||
@@ -259,47 +186,30 @@
|
|||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<i data-lucide="search" class="w-5 h-5 text-gray-400"></i>
|
<i data-lucide="search" class="w-5 h-5 text-gray-400"></i>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input type="text" id="search-bar"
|
||||||
type="text"
|
|
||||||
id="search-bar"
|
|
||||||
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
|
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
placeholder="Search for a tool (e.g., 'split', 'organize'...)"
|
placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
|
||||||
/>
|
<span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2">
|
||||||
<span
|
|
||||||
class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2"
|
|
||||||
>
|
|
||||||
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
|
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div id="tool-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6"></div>
|
||||||
id="tool-grid"
|
|
||||||
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div id="tool-interface"
|
||||||
id="tool-interface"
|
class="hidden w-full max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8 border border-gray-700">
|
||||||
class="hidden w-full max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8 border border-gray-700"
|
<button id="back-to-grid"
|
||||||
>
|
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||||
<button
|
|
||||||
id="back-to-grid"
|
|
||||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
|
|
||||||
>
|
|
||||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||||
<span class="cursor-pointer"> Back to Tools </span>
|
<span class="cursor-pointer"> Back to Tools </span>
|
||||||
</button>
|
</button>
|
||||||
<div id="tool-content"></div>
|
<div id="tool-content"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||||
id="loader-modal"
|
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||||
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl"
|
|
||||||
>
|
|
||||||
<div class="solid-spinner"></div>
|
<div class="solid-spinner"></div>
|
||||||
<p id="loader-text" class="text-white text-lg font-medium">
|
<p id="loader-text" class="text-white text-lg font-medium">
|
||||||
Processing...
|
Processing...
|
||||||
@@ -307,48 +217,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div id="alert-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4">
|
||||||
id="alert-modal"
|
<div class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center">
|
||||||
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center"
|
|
||||||
>
|
|
||||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">
|
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">
|
||||||
Alert
|
Alert
|
||||||
</h3>
|
</h3>
|
||||||
<p id="alert-message" class="text-gray-300 mb-6 whitespace-pre-line">
|
<p id="alert-message" class="text-gray-300 mb-6 whitespace-pre-line">
|
||||||
This is an alert message.
|
This is an alert message.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button id="alert-ok"
|
||||||
id="alert-ok"
|
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">
|
||||||
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700"
|
|
||||||
>
|
|
||||||
OK
|
OK
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div id="preview-modal"
|
||||||
id="preview-modal"
|
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40 p-4">
|
||||||
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40 p-4"
|
<div class="bg-white text-black rounded-lg shadow-xl w-full max-w-4xl h-[90vh] flex flex-col">
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-white text-black rounded-lg shadow-xl w-full max-w-4xl h-[90vh] flex flex-col"
|
|
||||||
>
|
|
||||||
<div class="p-4 border-b flex justify-between items-center">
|
<div class="p-4 border-b flex justify-between items-center">
|
||||||
<h3 class="text-xl font-bold">Document Preview</h3>
|
<h3 class="text-xl font-bold">Document Preview</h3>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<button
|
<button id="preview-download-btn"
|
||||||
id="preview-download-btn"
|
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">
|
||||||
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700"
|
|
||||||
>
|
|
||||||
Download as PDF
|
Download as PDF
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button id="preview-close-btn"
|
||||||
id="preview-close-btn"
|
class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700">
|
||||||
class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,96 +257,62 @@
|
|||||||
<!-- COMPLIANCE SECTION START -->
|
<!-- COMPLIANCE SECTION START -->
|
||||||
<section id="security-compliance-section" class="py-20 hide-section">
|
<section id="security-compliance-section" class="py-20 hide-section">
|
||||||
<div class="mb-8 text-center">
|
<div class="mb-8 text-center">
|
||||||
<h2
|
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance">
|
||||||
class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance"
|
|
||||||
>
|
|
||||||
Your data never leaves your device
|
Your data never leaves your device
|
||||||
<span class="inline-flex items-center gap-2">
|
<span class="inline-flex items-center gap-2">
|
||||||
<i
|
<i data-lucide="shield" class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"></i>
|
||||||
data-lucide="shield"
|
|
||||||
class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"
|
|
||||||
></i>
|
|
||||||
We keep
|
We keep
|
||||||
</span>
|
</span>
|
||||||
<br class="hidden sm:block" />
|
<br class="hidden sm:block" />
|
||||||
<span
|
<span
|
||||||
class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold"
|
class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold">
|
||||||
>
|
|
||||||
your information safe
|
your information safe
|
||||||
</span>
|
</span>
|
||||||
by following global security standards.
|
by following global security standards.
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-16 text-center">
|
<div class="mb-16 text-center">
|
||||||
<span
|
<span class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors">
|
||||||
class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
All the processing happens locally on your device.
|
All the processing happens locally on your device.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4">
|
||||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4"
|
|
||||||
>
|
|
||||||
<!-- GDPR Compliance -->
|
<!-- GDPR Compliance -->
|
||||||
<div class="flex flex-col items-center text-center">
|
<div class="flex flex-col items-center text-center">
|
||||||
<div
|
<div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
|
||||||
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4"
|
<img src="/images/gdpr.svg" alt="GDPR compliance" class="w-full h-full" />
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/images/gdpr.svg"
|
|
||||||
alt="GDPR compliance"
|
|
||||||
class="w-full h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
||||||
GDPR compliance
|
GDPR compliance
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
|
||||||
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
|
|
||||||
>
|
|
||||||
Protects the personal data and privacy of individuals within the
|
Protects the personal data and privacy of individuals within the
|
||||||
European Union.
|
European Union.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center text-center">
|
<div class="flex flex-col items-center text-center">
|
||||||
<div
|
<div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
|
||||||
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4"
|
<img src="/images/ccpa.svg" alt="CCPA compliance" class="w-full h-full" />
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/images/ccpa.svg"
|
|
||||||
alt="CCPA compliance"
|
|
||||||
class="w-full h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
||||||
CCPA compliance
|
CCPA compliance
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
|
||||||
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
|
|
||||||
>
|
|
||||||
Gives California residents rights over how their personal
|
Gives California residents rights over how their personal
|
||||||
information is collected, used, and shared.
|
information is collected, used, and shared.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center text-center">
|
<div class="flex flex-col items-center text-center">
|
||||||
<div
|
<div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
|
||||||
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4"
|
<img src="/images/hipaa.svg" alt="HIPAA compliance" class="w-full h-full" />
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/images/hipaa.svg"
|
|
||||||
alt="HIPAA compliance"
|
|
||||||
class="w-full h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
||||||
HIPAA compliance
|
HIPAA compliance
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
|
||||||
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
|
|
||||||
>
|
|
||||||
Sets safeguards for handling sensitive health information in the
|
Sets safeguards for handling sensitive health information in the
|
||||||
United States healthcare system.
|
United States healthcare system.
|
||||||
</p>
|
</p>
|
||||||
@@ -464,27 +325,16 @@
|
|||||||
<div class="section-divider hide-section"></div>
|
<div class="section-divider hide-section"></div>
|
||||||
|
|
||||||
<section id="faq-accordion" class="space-y-4 hide-section">
|
<section id="faq-accordion" class="space-y-4 hide-section">
|
||||||
<h2
|
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8">
|
||||||
class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8"
|
|
||||||
>
|
|
||||||
Frequently Asked <span class="marker-slanted">Questions</span>
|
Frequently Asked <span class="marker-slanted">Questions</span>
|
||||||
</h2>
|
</h2>
|
||||||
<!-- Existing FAQs here... -->
|
<!-- Existing FAQs here... -->
|
||||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||||
<button
|
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||||
class="faq-question w-full flex justify-between items-center text-left p-6"
|
<span class="text-lg font-semibold text-white">Is BentoPDF really free?</span>
|
||||||
>
|
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||||
<span class="text-lg font-semibold text-white"
|
|
||||||
>Is BentoPDF really free?</span
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="chevron-down"
|
|
||||||
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<p class="p-6 pt-0 text-gray-400">
|
<p class="p-6 pt-0 text-gray-400">
|
||||||
Yes, absolutely. All tools on BentoPDF are 100% free to use, with
|
Yes, absolutely. All tools on BentoPDF are 100% free to use, with
|
||||||
no file limits, no sign-ups, and no watermarks. We believe
|
no file limits, no sign-ups, and no watermarks. We believe
|
||||||
@@ -495,20 +345,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||||
<button
|
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||||
class="faq-question w-full flex justify-between items-center text-left p-6"
|
<span class="text-lg font-semibold text-white">Are my files secure? Where are they processed?</span>
|
||||||
>
|
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||||
<span class="text-lg font-semibold text-white"
|
|
||||||
>Are my files secure? Where are they processed?</span
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="chevron-down"
|
|
||||||
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<p class="p-6 pt-0 text-gray-400">
|
<p class="p-6 pt-0 text-gray-400">
|
||||||
Your files are as secure as possible because they **never leave
|
Your files are as secure as possible because they **never leave
|
||||||
your computer**. All processing happens directly in your web
|
your computer**. All processing happens directly in your web
|
||||||
@@ -519,20 +360,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||||
<button
|
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||||
class="faq-question w-full flex justify-between items-center text-left p-6"
|
<span class="text-lg font-semibold text-white">Does it work on Mac, Windows, and Mobile?</span>
|
||||||
>
|
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||||
<span class="text-lg font-semibold text-white"
|
|
||||||
>Does it work on Mac, Windows, and Mobile?</span
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="chevron-down"
|
|
||||||
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<p class="p-6 pt-0 text-gray-400">
|
<p class="p-6 pt-0 text-gray-400">
|
||||||
Yes! Since BentoPDF runs entirely in your browser, it works on any
|
Yes! Since BentoPDF runs entirely in your browser, it works on any
|
||||||
operating system with a modern web browser, including Windows,
|
operating system with a modern web browser, including Windows,
|
||||||
@@ -543,20 +375,11 @@
|
|||||||
|
|
||||||
<!-- New FAQ: GDPR Compliance -->
|
<!-- New FAQ: GDPR Compliance -->
|
||||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||||
<button
|
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||||
class="faq-question w-full flex justify-between items-center text-left p-6"
|
<span class="text-lg font-semibold text-white">Is BentoPDF GDPR compliant?</span>
|
||||||
>
|
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||||
<span class="text-lg font-semibold text-white"
|
|
||||||
>Is BentoPDF GDPR compliant?</span
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="chevron-down"
|
|
||||||
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<p class="p-6 pt-0 text-gray-400">
|
<p class="p-6 pt-0 text-gray-400">
|
||||||
Yes. BentoPDF is fully GDPR compliant. Since all file processing
|
Yes. BentoPDF is fully GDPR compliant. Since all file processing
|
||||||
happens locally in your browser and we never collect or transmit
|
happens locally in your browser and we never collect or transmit
|
||||||
@@ -568,20 +391,11 @@
|
|||||||
|
|
||||||
<!-- New FAQ: Data Storage -->
|
<!-- New FAQ: Data Storage -->
|
||||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||||
<button
|
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||||
class="faq-question w-full flex justify-between items-center text-left p-6"
|
<span class="text-lg font-semibold text-white">Do you store or track any of my files?</span>
|
||||||
>
|
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||||
<span class="text-lg font-semibold text-white"
|
|
||||||
>Do you store or track any of my files?</span
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="chevron-down"
|
|
||||||
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<p class="p-6 pt-0 text-gray-400">
|
<p class="p-6 pt-0 text-gray-400">
|
||||||
No. We never store, track, or log your files. Everything you do on
|
No. We never store, track, or log your files. Everything you do on
|
||||||
BentoPDF happens in your browser memory and disappears once you
|
BentoPDF happens in your browser memory and disappears once you
|
||||||
@@ -593,20 +407,11 @@
|
|||||||
|
|
||||||
<!-- New FAQ: Privacy -->
|
<!-- New FAQ: Privacy -->
|
||||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||||
<button
|
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||||
class="faq-question w-full flex justify-between items-center text-left p-6"
|
<span class="text-lg font-semibold text-white">What makes BentoPDF different from other PDF tools?</span>
|
||||||
>
|
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||||
<span class="text-lg font-semibold text-white"
|
|
||||||
>What makes BentoPDF different from other PDF tools?</span
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="chevron-down"
|
|
||||||
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<p class="p-6 pt-0 text-gray-400">
|
<p class="p-6 pt-0 text-gray-400">
|
||||||
Most PDF tools upload your files to a server for processing.
|
Most PDF tools upload your files to a server for processing.
|
||||||
BentoPDF never does that. We use secure, modern web technology to
|
BentoPDF never does that. We use secure, modern web technology to
|
||||||
@@ -618,20 +423,11 @@
|
|||||||
|
|
||||||
<!-- New FAQ: Browser-Based -->
|
<!-- New FAQ: Browser-Based -->
|
||||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||||
<button
|
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||||
class="faq-question w-full flex justify-between items-center text-left p-6"
|
<span class="text-lg font-semibold text-white">How does browser-based processing keep me safe?</span>
|
||||||
>
|
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||||
<span class="text-lg font-semibold text-white"
|
|
||||||
>How does browser-based processing keep me safe?</span
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="chevron-down"
|
|
||||||
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<p class="p-6 pt-0 text-gray-400">
|
<p class="p-6 pt-0 text-gray-400">
|
||||||
By running entirely inside your browser, BentoPDF ensures that
|
By running entirely inside your browser, BentoPDF ensures that
|
||||||
your files never leave your device. This eliminates the risks of
|
your files never leave your device. This eliminates the risks of
|
||||||
@@ -643,30 +439,16 @@
|
|||||||
|
|
||||||
<!-- New FAQ: Analytics -->
|
<!-- New FAQ: Analytics -->
|
||||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||||
<button
|
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||||
class="faq-question w-full flex justify-between items-center text-left p-6"
|
<span class="text-lg font-semibold text-white">Do you use cookies or analytics to track me?</span>
|
||||||
>
|
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||||
<span class="text-lg font-semibold text-white"
|
|
||||||
>Do you use cookies or analytics to track me?</span
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="chevron-down"
|
|
||||||
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<p class="p-6 pt-0 text-gray-400">
|
<p class="p-6 pt-0 text-gray-400">
|
||||||
We care about your privacy. BentoPDF does not track personal
|
We care about your privacy. BentoPDF does not track personal
|
||||||
information. We use
|
information. We use
|
||||||
<a
|
<a href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
|
||||||
href="https://simpleanalytics.com"
|
rel="noopener noreferrer">Simple Analytics</a>
|
||||||
class="text-indigo-400 hover:underline"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>Simple Analytics</a
|
|
||||||
>
|
|
||||||
solely to see anonymous visit counts. This means we can know how
|
solely to see anonymous visit counts. This means we can know how
|
||||||
many users visit our site, but
|
many users visit our site, but
|
||||||
<strong>we never know who you are</strong>. Simple Analytics is
|
<strong>we never know who you are</strong>. Simple Analytics is
|
||||||
@@ -682,9 +464,7 @@
|
|||||||
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
|
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
|
||||||
What Our <span class="marker-slanted">Users</span> Say
|
What Our <span class="marker-slanted">Users</span> Say
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto"
|
|
||||||
>
|
|
||||||
<div class="testimonial-card">
|
<div class="testimonial-card">
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -781,9 +561,7 @@
|
|||||||
|
|
||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
<section id="support-section" class="py-20">
|
<section id="support-section" class="py-20">
|
||||||
<div
|
<div class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl">
|
||||||
class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl"
|
|
||||||
>
|
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||||
Like My Work?
|
Like My Work?
|
||||||
</h2>
|
</h2>
|
||||||
@@ -793,12 +571,8 @@
|
|||||||
supporting its development. Every coffee helps!
|
supporting its development. Every coffee helps!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a
|
<a href="https://ko-fi.com/alio01" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://ko-fi.com/alio01"
|
class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5">
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5"
|
|
||||||
>
|
|
||||||
<i data-lucide="coffee" class="w-7 h-7"></i>
|
<i data-lucide="coffee" class="w-7 h-7"></i>
|
||||||
<span>Buy Me a Coffee</span>
|
<span>Buy Me a Coffee</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -810,16 +584,10 @@
|
|||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||||
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
|
||||||
>
|
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img
|
<img src="public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||||
src="public/images/favicon.svg"
|
|
||||||
alt="Bento PDF Logo"
|
|
||||||
class="h-10 w-10 mr-3"
|
|
||||||
/>
|
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">
|
<p class="text-gray-400 text-sm">
|
||||||
@@ -834,17 +602,13 @@
|
|||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li>
|
<li>
|
||||||
<a href="./about.html" class="hover:text-indigo-400"
|
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
>About Us</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="./contact.html" class="hover:text-indigo-400"
|
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||||
>Contact Us</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -853,14 +617,10 @@
|
|||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li>
|
<li>
|
||||||
<a href="./terms.html" class="hover:text-indigo-400"
|
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||||
>Terms and Conditions</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="./privacy.html" class="hover:text-indigo-400"
|
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||||
>Privacy Policy</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -868,72 +628,33 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a
|
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://github.com/alam00000/bentopdf"
|
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||||
target="_blank"
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
rel="noopener noreferrer"
|
<path fill-rule="evenodd"
|
||||||
class="text-gray-400 hover:text-indigo-400"
|
|
||||||
title="GitHub"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd" />
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://discord.gg/q42xWQmJ"
|
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||||
target="_blank"
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-gray-400 hover:text-indigo-400"
|
|
||||||
title="Discord"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"
|
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||||
href="https://www.instagram.com/thebentopdf/"
|
title="Instagram">
|
||||||
class="text-gray-400 hover:text-indigo-400"
|
|
||||||
title="Instagram"
|
|
||||||
>
|
|
||||||
<i data-lucide="instagram"></i>
|
<i data-lucide="instagram"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||||
href="https://www.linkedin.com/company/bentopdf/"
|
title="LinkedIn">
|
||||||
class="text-gray-400 hover:text-indigo-400"
|
|
||||||
title="LinkedIn"
|
|
||||||
>
|
|
||||||
<i data-lucide="linkedin"></i>
|
<i data-lucide="linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
|
||||||
href="https://x.com/BentoPDF"
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
class="text-gray-400 hover:text-indigo-400"
|
|
||||||
title="X (Twitter)"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -945,5 +666,6 @@
|
|||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
<script type="module" src="src/js/main.ts"></script>
|
<script type="module" src="src/js/main.ts"></script>
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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() ||
|
|
||||||
`attachment-${index + 1}`;
|
|
||||||
|
|
||||||
const ef = fileSpecDict.get('EF');
|
// let index = 0;
|
||||||
let fileSize = 0;
|
// for (const [ref, fileSpec] of embeddedFiles) {
|
||||||
if (ef) {
|
// try {
|
||||||
const fRef = ef.get('F') || ef.get('UF');
|
// const fileSpecDict = fileSpec as any;
|
||||||
if (fRef) {
|
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||||
const fileStream = state.pdfDoc.context.lookup(fRef);
|
// fileSpecDict.get('F')?.decodeText() ||
|
||||||
if (fileStream) {
|
// `attachment-${index + 1}`;
|
||||||
fileSize = (fileStream as any).getContents().length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentAttachments.push({ name: fileName, index, size: fileSize });
|
// const ef = fileSpecDict.get('EF');
|
||||||
|
// let fileSize = 0;
|
||||||
|
// if (ef) {
|
||||||
|
// const fRef = ef.get('F') || ef.get('UF');
|
||||||
|
// if (fRef) {
|
||||||
|
// const fileStream = state.pdfDoc.context.lookup(fRef);
|
||||||
|
// if (fileStream) {
|
||||||
|
// fileSize = (fileStream as any).getContents().length;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const attachmentDiv = document.createElement('div');
|
// currentAttachments.push({ name: fileName, index, size: fileSize });
|
||||||
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
|
||||||
attachmentDiv.dataset.attachmentIndex = index.toString();
|
|
||||||
|
|
||||||
const infoDiv = document.createElement('div');
|
// const attachmentDiv = document.createElement('div');
|
||||||
infoDiv.className = 'flex-1';
|
// attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
const nameSpan = document.createElement('span');
|
// attachmentDiv.dataset.attachmentIndex = index.toString();
|
||||||
nameSpan.className = 'text-white font-medium block';
|
|
||||||
nameSpan.textContent = fileName;
|
|
||||||
const sizeSpan = document.createElement('span');
|
|
||||||
sizeSpan.className = 'text-gray-400 text-sm';
|
|
||||||
sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
|
|
||||||
infoDiv.append(nameSpan, sizeSpan);
|
|
||||||
|
|
||||||
const actionsDiv = document.createElement('div');
|
// const infoDiv = document.createElement('div');
|
||||||
actionsDiv.className = 'flex items-center gap-2';
|
// infoDiv.className = 'flex-1';
|
||||||
|
// const nameSpan = document.createElement('span');
|
||||||
|
// nameSpan.className = 'text-white font-medium block';
|
||||||
|
// nameSpan.textContent = fileName;
|
||||||
|
// const sizeSpan = document.createElement('span');
|
||||||
|
// sizeSpan.className = 'text-gray-400 text-sm';
|
||||||
|
// sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
|
||||||
|
// infoDiv.append(nameSpan, sizeSpan);
|
||||||
|
|
||||||
// Remove button
|
// const actionsDiv = document.createElement('div');
|
||||||
const removeBtn = document.createElement('button');
|
// actionsDiv.className = 'flex items-center gap-2';
|
||||||
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.title = 'Remove attachment';
|
|
||||||
removeBtn.onclick = () => {
|
|
||||||
attachmentsToRemove.add(index);
|
|
||||||
attachmentDiv.classList.add('opacity-50', 'line-through');
|
|
||||||
removeBtn.disabled = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace button
|
// // Remove button
|
||||||
const replaceBtn = document.createElement('button');
|
// const removeBtn = document.createElement('button');
|
||||||
replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-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';
|
||||||
replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>';
|
// removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
replaceBtn.title = 'Replace attachment';
|
// removeBtn.title = 'Remove attachment';
|
||||||
replaceBtn.onclick = () => {
|
// removeBtn.onclick = () => {
|
||||||
const input = document.createElement('input');
|
// attachmentsToRemove.add(index);
|
||||||
input.type = 'file';
|
// attachmentDiv.classList.add('opacity-50', 'line-through');
|
||||||
input.onchange = async (e) => {
|
// removeBtn.disabled = true;
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
// };
|
||||||
if (file) {
|
|
||||||
attachmentsToReplace.set(index, file);
|
|
||||||
nameSpan.textContent = `${fileName} → ${file.name}`;
|
|
||||||
nameSpan.classList.add('text-yellow-400');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
actionsDiv.append(replaceBtn, removeBtn);
|
// // Replace button
|
||||||
attachmentDiv.append(infoDiv, actionsDiv);
|
// const replaceBtn = document.createElement('button');
|
||||||
attachmentsList.appendChild(attachmentDiv);
|
// replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm';
|
||||||
index++;
|
// replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>';
|
||||||
} catch (e) {
|
// replaceBtn.title = 'Replace attachment';
|
||||||
console.warn(`Failed to process attachment ${index}:`, e);
|
// replaceBtn.onclick = () => {
|
||||||
index++;
|
// const input = document.createElement('input');
|
||||||
}
|
// input.type = 'file';
|
||||||
}
|
// input.onchange = async (e) => {
|
||||||
} catch (e) {
|
// const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
console.error('Error loading attachments:', e);
|
// if (file) {
|
||||||
showAlert('Error', 'Failed to load attachments from PDF.');
|
// attachmentsToReplace.set(index, file);
|
||||||
}
|
// nameSpan.textContent = `${fileName} → ${file.name}`;
|
||||||
}
|
// nameSpan.classList.add('text-yellow-400');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// input.click();
|
||||||
|
// };
|
||||||
|
|
||||||
export async function editAttachments() {
|
// actionsDiv.append(replaceBtn, removeBtn);
|
||||||
if (!state.pdfDoc) {
|
// attachmentDiv.append(infoDiv, actionsDiv);
|
||||||
showAlert('Error', 'PDF is not loaded.');
|
// attachmentsList.appendChild(attachmentDiv);
|
||||||
return;
|
// index++;
|
||||||
}
|
// } catch (e) {
|
||||||
|
// console.warn(`Failed to process attachment ${index}:`, e);
|
||||||
|
// index++;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error('Error loading attachments:', e);
|
||||||
|
// showAlert('Error', 'Failed to load attachments from PDF.');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
showLoader('Updating attachments...');
|
// export async function editAttachments() {
|
||||||
try {
|
// if (!state.pdfDoc) {
|
||||||
// Create a new PDF document
|
// showAlert('Error', 'PDF is not loaded.');
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
// Copy all pages
|
// showLoader('Updating attachments...');
|
||||||
const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices());
|
// try {
|
||||||
pages.forEach((page: any) => newPdfDoc.addPage(page));
|
// // Create a new PDF document
|
||||||
|
// const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
// Handle attachments
|
// // Copy all pages
|
||||||
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
// const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices());
|
||||||
.filter(([ref, obj]: any) => {
|
// pages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||||
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
|
||||||
return dict && dict.get('Type')?.toString() === '/Filespec';
|
|
||||||
});
|
|
||||||
|
|
||||||
let attachmentIndex = 0;
|
// // Handle attachments
|
||||||
for (const [ref, fileSpec] of embeddedFiles) {
|
// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
||||||
if (attachmentsToRemove.has(attachmentIndex)) {
|
// .filter(([ref, obj]: any) => {
|
||||||
attachmentIndex++;
|
// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
||||||
continue; // Skip removed attachments
|
// return dict && dict.get('Type')?.toString() === '/Filespec';
|
||||||
}
|
// });
|
||||||
|
|
||||||
if (attachmentsToReplace.has(attachmentIndex)) {
|
// let attachmentIndex = 0;
|
||||||
// Replace attachment
|
// for (const [ref, fileSpec] of embeddedFiles) {
|
||||||
const replacementFile = attachmentsToReplace.get(attachmentIndex)!;
|
// if (attachmentsToRemove.has(attachmentIndex)) {
|
||||||
const fileBytes = await readFileAsArrayBuffer(replacementFile);
|
// attachmentIndex++;
|
||||||
await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, {
|
// continue; // Skip removed attachments
|
||||||
mimeType: replacementFile.type || 'application/octet-stream',
|
// }
|
||||||
description: `Attached file: ${replacementFile.name}`,
|
|
||||||
creationDate: new Date(),
|
|
||||||
modificationDate: new Date(replacementFile.lastModified),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Keep existing attachment - copy it
|
|
||||||
try {
|
|
||||||
const fileSpecDict = fileSpec as any;
|
|
||||||
const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
|
||||||
fileSpecDict.get('F')?.decodeText() ||
|
|
||||||
`attachment-${attachmentIndex + 1}`;
|
|
||||||
|
|
||||||
const ef = fileSpecDict.get('EF');
|
// if (attachmentsToReplace.has(attachmentIndex)) {
|
||||||
if (ef) {
|
// // Replace attachment
|
||||||
const fRef = ef.get('F') || ef.get('UF');
|
// const replacementFile = attachmentsToReplace.get(attachmentIndex)!;
|
||||||
if (fRef) {
|
// const fileBytes = await readFileAsArrayBuffer(replacementFile);
|
||||||
const fileStream = state.pdfDoc.context.lookup(fRef);
|
// await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, {
|
||||||
if (fileStream) {
|
// mimeType: replacementFile.type || 'application/octet-stream',
|
||||||
const fileData = (fileStream as any).getContents();
|
// description: `Attached file: ${replacementFile.name}`,
|
||||||
await newPdfDoc.attach(fileData, fileName, {
|
// creationDate: new Date(),
|
||||||
mimeType: 'application/octet-stream',
|
// modificationDate: new Date(replacementFile.lastModified),
|
||||||
description: `Attached file: ${fileName}`,
|
// });
|
||||||
});
|
// } else {
|
||||||
}
|
// // Keep existing attachment - copy it
|
||||||
}
|
// try {
|
||||||
}
|
// const fileSpecDict = fileSpec as any;
|
||||||
} catch (e) {
|
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||||
console.warn(`Failed to copy attachment ${attachmentIndex}:`, e);
|
// fileSpecDict.get('F')?.decodeText() ||
|
||||||
}
|
// `attachment-${attachmentIndex + 1}`;
|
||||||
}
|
|
||||||
attachmentIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdfBytes = await newPdfDoc.save();
|
// const ef = fileSpecDict.get('EF');
|
||||||
downloadFile(
|
// if (ef) {
|
||||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
// const fRef = ef.get('F') || ef.get('UF');
|
||||||
`edited-attachments-${state.files[0].name}`
|
// if (fRef) {
|
||||||
);
|
// const fileStream = state.pdfDoc.context.lookup(fRef);
|
||||||
showAlert('Success', 'Attachments updated successfully!');
|
// if (fileStream) {
|
||||||
} catch (e) {
|
// const fileData = (fileStream as any).getContents();
|
||||||
console.error(e);
|
// await newPdfDoc.attach(fileData, fileName, {
|
||||||
showAlert('Error', 'Failed to edit attachments.');
|
// mimeType: 'application/octet-stream',
|
||||||
} finally {
|
// description: `Attached file: ${fileName}`,
|
||||||
hideLoader();
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// console.warn(`Failed to copy attachment ${attachmentIndex}:`, e);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// attachmentIndex++;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const pdfBytes = await newPdfDoc.save();
|
||||||
|
// downloadFile(
|
||||||
|
// new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
// `edited-attachments-${state.files[0].name}`
|
||||||
|
// );
|
||||||
|
// showAlert('Success', 'Attachments updated successfully!');
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error(e);
|
||||||
|
// showAlert('Error', 'Failed to edit attachments.');
|
||||||
|
// } finally {
|
||||||
|
// hideLoader();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
// Get attachment name
|
// // Extract attachments
|
||||||
const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
// const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
fileSpecDict.get('F')?.decodeText() ||
|
// for (let i = 0; i < embeddedFiles.length; i++) {
|
||||||
`attachment-${i + 1}`;
|
// try {
|
||||||
|
// const [ref, fileSpec] = embeddedFiles[i];
|
||||||
|
// const fileSpecDict = fileSpec as any;
|
||||||
|
|
||||||
// Get embedded file stream
|
// // Get attachment name
|
||||||
const ef = fileSpecDict.get('EF');
|
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||||
if (ef) {
|
// fileSpecDict.get('F')?.decodeText() ||
|
||||||
const fRef = ef.get('F') || ef.get('UF');
|
// `attachment-${i + 1}`;
|
||||||
if (fRef) {
|
|
||||||
const fileStream = pdfDoc.context.lookup(fRef);
|
|
||||||
if (fileStream) {
|
|
||||||
const fileData = (fileStream as any).getContents();
|
|
||||||
zip.file(`${baseName}_${fileName}`, fileData);
|
|
||||||
totalAttachments++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalAttachments === 0) {
|
// // Get embedded file stream
|
||||||
showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
|
// const ef = fileSpecDict.get('EF');
|
||||||
hideLoader();
|
// if (ef) {
|
||||||
return;
|
// const fRef = ef.get('F') || ef.get('UF');
|
||||||
}
|
// if (fRef) {
|
||||||
|
// const fileStream = pdfDoc.context.lookup(fRef);
|
||||||
|
// if (fileStream) {
|
||||||
|
// const fileData = (fileStream as any).getContents();
|
||||||
|
// zip.file(`${baseName}_${fileName}`, fileData);
|
||||||
|
// totalAttachments++;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
// if (totalAttachments === 0) {
|
||||||
downloadFile(zipBlob, 'extracted-attachments.zip');
|
// showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
|
||||||
showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`);
|
// hideLoader();
|
||||||
} catch (e) {
|
// return;
|
||||||
console.error(e);
|
// }
|
||||||
showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
|
|
||||||
} finally {
|
// const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
hideLoader();
|
// downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||||
}
|
// showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`);
|
||||||
}
|
// } catch (e) {
|
||||||
|
// console.error(e);
|
||||||
|
// showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
|
||||||
|
// } finally {
|
||||||
|
// hideLoader();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
if (allPages.length === 0) {
|
||||||
|
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);
|
const indices = Array.from({ length: allPages.length }, (_, i) => i);
|
||||||
await downloadPagesAsPdf(indices, 'all-pages.pdf');
|
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');
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
};
|
||||||
@@ -32,71 +32,89 @@
|
|||||||
<!-- 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">
|
||||||
|
<div class="flex flex-wrap items-center gap-1 sm:gap-2 min-w-max">
|
||||||
<button id="upload-pdfs-btn"
|
<button id="upload-pdfs-btn"
|
||||||
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2">
|
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="upload" class="w-4 h-4"></i> Upload PDFs
|
<i data-lucide="upload" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden sm:inline">Upload PDFs</span>
|
||||||
|
<span class="sm:hidden">Upload</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||||
<button id="add-blank-page-btn"
|
<button id="add-blank-page-btn"
|
||||||
class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm">
|
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">
|
||||||
<i data-lucide="file-plus" class="w-4 h-4"></i> Add Blank
|
<i data-lucide="file-plus" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden sm:inline">Add Blank</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||||
<button id="undo-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-ccw" class="w-4 h-4"></i> Undo
|
<i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden lg:inline">Undo</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="redo-btn"
|
<button id="redo-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-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden lg:inline">Redo</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="reset-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="refresh-ccw" class="w-4 h-4"></i> Reset
|
<i data-lucide="refresh-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden lg:inline">Reset</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||||
<span class="text-gray-400 text-sm">Selection:</span>
|
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Selection:</span>
|
||||||
<button id="select-all-btn"
|
<button id="select-all-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="check-square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden lg:inline">Select All</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="deselect-all-btn"
|
<button id="deselect-all-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="square" class="w-4 h-4"></i> Deselect All
|
<i data-lucide="square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden lg:inline">Deselect All</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||||
<span class="text-gray-400 text-sm">Bulk Actions:</span>
|
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Bulk:</span>
|
||||||
<button id="bulk-rotate-left-btn"
|
<button id="bulk-rotate-left-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-ccw" class="w-4 h-4"></i> Rotate Left
|
<i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden xl:inline">Rotate Left</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="bulk-rotate-btn"
|
<button id="bulk-rotate-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> Rotate Right
|
<i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden xl:inline">Rotate Right</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="bulk-delete-btn"
|
<button id="bulk-delete-btn"
|
||||||
class="btn bg-red-600 hover:bg-red-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
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">
|
||||||
<i data-lucide="trash-2" class="w-4 h-4"></i> Delete
|
<i data-lucide="trash-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden xl:inline">Delete</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="bulk-duplicate-btn"
|
<button id="bulk-duplicate-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="copy" class="w-4 h-4"></i> Duplicate
|
<i data-lucide="copy" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden xl:inline">Duplicate</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="bulk-split-btn"
|
<button id="bulk-split-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="scissors" class="w-4 h-4"></i> Split
|
<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>
|
||||||
<button id="bulk-download-btn"
|
<button id="bulk-download-btn"
|
||||||
class="btn bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
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-4 h-4"></i> Download Selected
|
<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>
|
</button>
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1 min-w-[20px]"></div>
|
||||||
<button id="export-pdf-btn"
|
<button id="export-pdf-btn"
|
||||||
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2">
|
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-4 h-4"></i> Export PDF
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Content Area -->
|
<!-- Content Area -->
|
||||||
<div class="flex-1 overflow-auto p-4">
|
<div class="flex-1 overflow-auto p-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user