| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- <!DOCTYPE html>
- <html lang="zh">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>EPUB CSS 清理工具 (Web版)</title>
- <style>
- body {
- font-family: sans-serif;
- line-height: 1.6;
- padding: 20px;
- max-width: 600px;
- margin: 20px auto;
- background-color: #f4f4f4;
- border: 1px solid #ccc;
- border-radius: 8px;
- }
- h1 {
- text-align: center;
- color: #333;
- }
- #drop-area {
- border: 2px dashed #ccc;
- border-radius: 5px;
- padding: 30px;
- text-align: center;
- background-color: #fff;
- margin-bottom: 20px;
- cursor: pointer;
- }
- #drop-area.highlight {
- border-color: dodgerblue;
- }
- #drop-area p {
- margin: 0;
- color: #555;
- }
- #fileInput {
- display: none; /* Hide default input, use drop area or label */
- }
- label.file-label {
- display: inline-block;
- padding: 10px 15px;
- background-color: dodgerblue;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- margin-top: 10px;
- transition: background-color 0.2s;
- }
- label.file-label:hover {
- background-color: #007ae5;
- }
- #status {
- margin-top: 15px;
- padding: 10px;
- background-color: #eee;
- border-radius: 4px;
- min-height: 40px; /* Ensure space for messages */
- text-align: center;
- font-weight: bold;
- }
- .status-details {
- font-size: 0.9em;
- color: #555;
- margin-top: 5px;
- }
- .warning {
- color: #a00;
- font-weight: bold;
- text-align: center;
- margin-top: 20px;
- padding: 10px;
- background-color: #fdd;
- border: 1px solid #fbb;
- border-radius: 4px;
- }
- progress {
- width: 100%;
- margin-top: 10px;
- display: none; /* Hidden by default */
- }
- </style>
- <!-- Include JSZip library -->
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
- <!-- Include FileSaver.js library -->
- <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
- </head>
- <body>
- <h1>EPUB CSS 清理工具 (Web版)</h1>
- <p>将单个 EPUB 文件或包含多个 EPUB 的 ZIP 文件拖拽到下方区域,或点击按钮选择文件,即可移除指定的 CSS 样式。</p>
- <div id="drop-area">
- <!-- Accept both .epub and .zip -->
- <input type="file" id="fileInput" accept=".epub,.zip">
- <p>将 EPUB 或 ZIP 文件拖拽到这里</p>
- <label for="fileInput" class="file-label">或者选择文件</label>
- </div>
- <progress id="progressBar" value="0" max="100"></progress>
- <div id="status">请选择或拖拽一个 EPUB 或 ZIP 文件。</div>
- <div id="statusDetails" class="status-details"></div>
- <div class="warning">
- <strong>重要提示:</strong> 此工具在你的浏览器中处理文件,不会上传到服务器。处理后的文件需要你手动下载。建议在处理前备份原始文件!处理 ZIP 文件时,会生成一个包含所有处理后(或原始)EPUB 的新 ZIP 文件。
- </div>
- <script>
- // --- Check if libraries loaded ---
- if (typeof JSZip === 'undefined' || typeof saveAs === 'undefined') {
- const errorMsg = '错误:无法加载必需的库 (JSZip 或 FileSaver)。请检查您的网络连接,并确保没有浏览器插件(如广告拦截器)阻止从 cdnjs.cloudflare.com 加载脚本。然后请刷新页面重试。';
- document.getElementById('status').textContent = errorMsg;
- document.getElementById('status').style.color = '#a00';
- document.getElementById('drop-area').style.display = 'none';
- document.querySelector('.warning').textContent = errorMsg;
- } else {
- // --- Global Variables & Constants ---
- const dropArea = document.getElementById('drop-area');
- const fileInput = document.getElementById('fileInput');
- const statusDiv = document.getElementById('status');
- const statusDetailsDiv = document.getElementById('statusDetails');
- const progressBar = document.getElementById('progressBar');
- const CSS_REMOVE_PATTERN = /((text-indent|line-height|font-size|height|font-family|color)\s*:\s*[^;]*;|display\s*:\s*block\s*;)/ig;
- // --- Event Handlers ---
- setupDragDropHandlers();
- fileInput.addEventListener('change', handleFileSelect, false);
- function setupDragDropHandlers() {
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
- dropArea.addEventListener(eventName, preventDefaults, false);
- document.body.addEventListener(eventName, preventDefaults, false);
- });
- ['dragenter', 'dragover'].forEach(eventName => {
- dropArea.addEventListener(eventName, () => dropArea.classList.add('highlight'), false);
- });
- ['dragleave', 'drop'].forEach(eventName => {
- dropArea.addEventListener(eventName, () => dropArea.classList.remove('highlight'), false);
- });
- dropArea.addEventListener('drop', handleDrop, false);
- }
- function preventDefaults(e) {
- e.preventDefault();
- e.stopPropagation();
- }
- function handleDrop(e) {
- handleFiles(e.dataTransfer.files);
- }
- function handleFileSelect(e) {
- handleFiles(e.target.files);
- }
- // --- Main File Handling Logic ---
- function handleFiles(files) {
- if (files.length === 0) return;
- const file = files[0]; // Process only the first file
- // Check file type
- const isEpub = file.name.toLowerCase().endsWith('.epub');
- const isZip = file.name.toLowerCase().endsWith('.zip');
- if (!isEpub && !isZip) {
- updateStatus('错误:请选择一个 .epub 或 .zip 文件。', true);
- resetInput();
- return;
- }
- updateStatus(`正在读取文件: ${file.name}...`);
- clearStatusDetails();
- progressBar.style.display = 'block';
- progressBar.value = 0;
- const reader = new FileReader();
- reader.onload = async (e) => {
- const fileContent = e.target.result; // ArrayBuffer
- try {
- if (isEpub) {
- updateStatus(`正在处理 EPUB 文件: ${file.name}...`);
- const result = await processEpub(fileContent, file.name);
- if (result.blob) {
- saveAs(result.blob, result.filename);
- updateStatus(`处理完成!已生成 ${result.filename}。`);
- } else if (result.modified === false) {
- updateStatus('信息:文件无需修改。');
- }
- resetInput();
- } else if (isZip) {
- updateStatus(`正在处理 ZIP 压缩包: ${file.name}...`);
- await processZipArchive(fileContent, file.name);
- }
- } catch (error) {
- console.error("处理失败:", error);
- updateStatus(`处理失败:${error.message}`, true);
- resetInput();
- }
- };
- reader.onprogress = updateReadProgress;
- reader.onerror = handleReadError;
- reader.readAsArrayBuffer(file);
- }
- function updateReadProgress(e) {
- if (e.lengthComputable) {
- progressBar.value = Math.round((e.loaded / e.total) * 10); // Reading is ~10%
- }
- }
- function handleReadError() {
- updateStatus('错误:读取文件时出错。', true);
- resetInput();
- }
- // --- ZIP Archive Processing ---
- async function processZipArchive(zipFileContent, zipFilename) {
- let outputZip = new JSZip();
- let processedCount = 0;
- let failedCount = 0;
- let foundCount = 0;
- let modifiedCount = 0;
- let filePromises = [];
- updateStatus(`正在打开 ZIP 压缩包...`);
- progressBar.value = 15;
- const inputZip = await JSZip.loadAsync(zipFileContent);
- const totalFilesInZip = Object.keys(inputZip.files).length;
- updateStatus(`在 ZIP 中查找 EPUB 文件...`);
- inputZip.forEach((relativePath, zipEntry) => {
- if (!zipEntry.dir && relativePath.toLowerCase().endsWith('.epub')) {
- foundCount++;
- updateStatusDetails(`找到 EPUB: ${relativePath}`);
- // Add a promise to process this EPUB entry
- const promise = zipEntry.async('arraybuffer')
- .then(async (innerEpubContent) => {
- try {
- // Process the inner EPUB
- const result = await processEpub(innerEpubContent, relativePath);
- if (result.blob) {
- // Add modified EPUB to output ZIP
- outputZip.file(result.filename, result.blob);
- modifiedCount++;
- console.log(`Added cleaned ${result.filename} to output zip.`);
- } else {
- // Add original EPUB back if not modified
- outputZip.file(relativePath, innerEpubContent);
- console.log(`Added original ${relativePath} (no changes needed) to output zip.`);
- }
- processedCount++;
- } catch (epubError) {
- // Add original EPUB back if processing failed
- console.error(`Error processing inner EPUB ${relativePath}:`, epubError);
- updateStatusDetails(`处理 ${relativePath} 时出错,将保留原文件。`);
- outputZip.file(relativePath, innerEpubContent); // Add original back
- failedCount++;
- }
- })
- .catch(extractError => {
- // Handle error extracting the inner EPUB content
- console.error(`Error extracting inner EPUB ${relativePath} from zip:`, extractError);
- updateStatusDetails(`无法从 ZIP 中提取 ${relativePath},已跳过。`);
- failedCount++;
- })
- .finally(() => {
- // Update progress based on entries processed (success or fail)
- const progress = 15 + Math.round(((processedCount + failedCount) / foundCount) * 60); // Processing EPUBs is ~60%
- progressBar.value = Math.min(progress, 75); // Cap at 75 before final packing
- });
- filePromises.push(promise);
- }
- // We are currently NOT adding non-EPUB files from the source ZIP to the output ZIP.
- // To do so, handle the 'else' case here and add zipEntry data to outputZip.
- });
- // Wait for all inner EPUB processing promises to complete
- await Promise.all(filePromises);
- progressBar.value = 80; // Done processing files inside zip
- if (foundCount === 0) {
- updateStatus('错误:在 ZIP 文件中未找到任何 EPUB 文件。', true);
- resetInput();
- return;
- }
- updateStatus(`处理了 ${foundCount} 个 EPUB 文件。正在生成输出 ZIP...`);
- statusDetailsDiv.innerHTML += `<br>成功: ${processedCount}, 修改: ${modifiedCount}, 失败/跳过: ${failedCount}`;
- progressBar.value = 90;
- const outputZipBlob = await outputZip.generateAsync({
- type: "blob",
- compression: "DEFLATE",
- platform: "browser"
- }, (metadata) => {
- progressBar.value = 90 + Math.round(metadata.percent * 0.1);
- });
- progressBar.value = 100;
- const baseName = zipFilename.substring(0, zipFilename.lastIndexOf('.')) || zipFilename;
- const outputZipFilename = `${baseName}_cleaned.zip`;
- saveAs(outputZipBlob, outputZipFilename);
- updateStatus(`处理完成!已生成包含处理后 EPUB 的 ${outputZipFilename}。`);
- resetInput();
- }
- // --- Single EPUB Processing (Modified to return result) ---
- async function processEpub(fileContent, originalFilename) {
- // This function now returns an object:
- // { blob: Blob, filename: string } if modified
- // { modified: false } if no modification needed
- // Throws error if processing fails critically
- console.log(`Processing EPUB: ${originalFilename}`);
- // Note: Progress bar updates inside this function are less meaningful now,
- // as the overall progress is handled by the caller (handleFiles or processZipArchive).
- // We could pass a progress callback if detailed inner progress is needed.
- const zip = await JSZip.loadAsync(fileContent);
- const newZip = new JSZip();
- let cssFilesFound = false;
- let modified = false;
- let filePromises = [];
- zip.forEach((relativePath, zipEntry) => {
- if (zipEntry.dir) { newZip.folder(relativePath); return; }
- const options = {
- date: zipEntry.date,
- unixPermissions: zipEntry.unixPermissions,
- dosPermissions: zipEntry.dosPermissions,
- comment: zipEntry.comment,
- dir: zipEntry.dir,
- compression: zipEntry.options?.compression === "STORE" ? "STORE" : "DEFLATE"
- };
- if (relativePath.toLowerCase() === 'mimetype') {
- const promise = zipEntry.async('uint8array').then(data => {
- newZip.file(relativePath, data, { ...options, compression: "STORE" });
- });
- filePromises.push(promise);
- } else if (relativePath.toLowerCase().endsWith('.css')) {
- cssFilesFound = true;
- const promise = zipEntry.async('string').then(originalCss => {
- const cleanedCss = originalCss.replace(CSS_REMOVE_PATTERN, '');
- let contentToAdd = originalCss;
- if (cleanedCss !== originalCss) {
- modified = true;
- contentToAdd = cleanedCss;
- console.log(` Cleaned CSS: ${relativePath}`);
- }
- newZip.file(relativePath, contentToAdd, options);
- }).catch(err => {
- console.warn(` Could not process CSS ${relativePath} as text, keeping original. Error: ${err}`);
- return zipEntry.async('uint8array').then(originalData => {
- newZip.file(relativePath, originalData, options);
- });
- });
- filePromises.push(promise);
- } else {
- const promise = zipEntry.async('uint8array').then(data => {
- newZip.file(relativePath, data, options);
- });
- filePromises.push(promise);
- }
- });
- await Promise.all(filePromises);
- if (!modified) {
- console.log(` EPUB ${originalFilename} - No modifications needed.`);
- return { modified: false }; // Indicate no changes were made
- }
- console.log(` Repacking modified EPUB: ${originalFilename}`);
- const newEpubBlob = await newZip.generateAsync({
- type: "blob",
- mimeType: "application/epub+zip",
- compression: "DEFLATE",
- platform: "browser"
- });
- const baseName = originalFilename.substring(0, originalFilename.lastIndexOf('.')) || originalFilename;
- const newFilename = `${baseName}_cleaned.epub`;
- return { blob: newEpubBlob, filename: newFilename }; // Return data for the caller
- } // end of processEpub function
- // --- UI Update Functions ---
- function updateStatus(message, isError = false) {
- statusDiv.textContent = message;
- statusDiv.style.color = isError ? '#a00' : '#000';
- if (isError) {
- progressBar.style.display = 'none';
- clearStatusDetails();
- }
- }
- function updateStatusDetails(message) {
- statusDetailsDiv.innerHTML += message + "<br>";
- }
- function clearStatusDetails() {
- statusDetailsDiv.innerHTML = "";
- }
- function resetInput() {
- fileInput.value = '';
- // Hide progress bar after a delay
- setTimeout(() => { progressBar.style.display = 'none'; progressBar.value = 0; }, 5000);
- }
- } // End of the 'else' block for library check
- </script>
- </body>
- </html>
|