您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Manually start/stop downloading images from PDFTron viewer iframe, prevents duplicate UI.
// ==UserScript== // @name PDFTron Image Extractor (v1.2 - Manual Control, Linter Fixes, EN) // @namespace http://tampermonkey.net/ // @version 1.2 // @description Manually start/stop downloading images from PDFTron viewer iframe, prevents duplicate UI. // @author Tinaut1986 // @match https://pdftron-viewer-quasar.pro.iberley.net/webviewer/ui/index.html* // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @connect pdftron.pro.iberley.net // @run-at document-end // ==/UserScript== (function() { 'use strict'; // --- Constants --- const IMAGE_PATTERN_KEY = 'pdfTronImagePattern'; const DEFAULT_IMAGE_PATTERN = /\/pageimg\d+\.jpg/i; // Default pattern const UI_ID = 'pdftron-downloader-ui-9k4h'; // UI container ID const STATUS_ID = 'pdftron-status-display-a8fj'; // Status text ID const START_BUTTON_ID = 'pdftron-start-button-x7gt';// Start/Stop button ID const FOLDER_KEY = 'pdfTronDestinationFolder'; const DEFAULT_FOLDER = 'PDFTron_Images'; // Default subfolder const VERIFICATION_INTERVAL = 5000; // ms // --- Global Variables --- let latestImages = new Set(); let destinationFolder = GM_getValue(FOLDER_KEY, DEFAULT_FOLDER); let imagePattern; let observer = null; let cacheCheckInterval = null; let isDownloadingActive = false; // --- Initialize Image Pattern --- function initializeImagePattern() { const savedPattern = GM_getValue(IMAGE_PATTERN_KEY, DEFAULT_IMAGE_PATTERN.source); try { imagePattern = new RegExp(savedPattern, 'i'); log(`Image pattern initialized: ${imagePattern.source}`); } catch (e) { log(`Error creating RegExp from saved pattern "${savedPattern}". Using default. Error: ${e.message}`, 'error'); imagePattern = DEFAULT_IMAGE_PATTERN; GM_setValue(IMAGE_PATTERN_KEY, DEFAULT_IMAGE_PATTERN.source); } } // --- Logging Utility --- function log(message, type = 'info') { const timestamp = new Date().toISOString(); const formattedMessage = `[PDFTron Extractor][${timestamp}] ${message}`; switch (type) { case 'error': console.error(formattedMessage); break; case 'warn': console.warn(formattedMessage); break; default: console.log(formattedMessage); } } // --- Configuration Functions --- function configureFolder() { const message = `Enter the subfolder name.\nThis folder will be created inside your browser's main download location.\n\nExample: PDFTron_Images\nCurrent: ${destinationFolder}`; const newFolder = prompt(message, destinationFolder); if (newFolder !== null) { destinationFolder = newFolder.replace(/[\\/]/g, '').trim(); if (!destinationFolder) { destinationFolder = DEFAULT_FOLDER; alert(`Folder name cannot be empty. Using default: ${DEFAULT_FOLDER}`); } GM_setValue(FOLDER_KEY, destinationFolder); log(`Destination subfolder set to: ${destinationFolder}`); updateInterface(); } } function configureImagePattern() { const currentSource = imagePattern ? imagePattern.source : DEFAULT_IMAGE_PATTERN.source; const message = `Enter a regular expression (RegExp) pattern to find the image URLs.\nThis allows you to match specific filenames, such as those containing page numbers.\n\nExample: /pageimg\\d+\\.jpg/i\n(This matches filenames starting with 'pageimg', followed by numbers, ending in '.jpg', case-insensitive)\n\nIf you're not familiar with regular expressions, you may want to look up online guides on how to create them.\n\nCurrent pattern: ${currentSource}`; const newPatternSource = prompt(message, currentSource); if (newPatternSource === null) return; try { const testRegExp = new RegExp(newPatternSource, 'i'); imagePattern = testRegExp; GM_setValue(IMAGE_PATTERN_KEY, newPatternSource); log(`Image pattern configured manually: ${imagePattern.source}`); latestImages.clear(); log("Detected images list cleared due to pattern change."); if (isDownloadingActive) { stopDownloading(); startDownloading(); log("Download process restarted with new pattern."); } updateInterface(); } catch (error) { log(`Invalid pattern format: ${error.message}`, 'error'); alert(`Invalid pattern format: ${error.message}\nPlease enter a valid pattern (like the example).`); } } // --- User Interface --- function createInterface() { if (document.getElementById(UI_ID)) { log(`UI with ID ${UI_ID} already exists in this iframe. Ensuring button state is correct.`); updateInterface(); return; } log(`Creating UI (ID: ${UI_ID}) inside the iframe...`); const container = document.createElement('div'); container.id = UI_ID; container.style.cssText = ` position: fixed; top: 10px; right: 10px; z-index: 2147483647; padding: 12px; background: rgba(240, 240, 240, 0.95); border: 1px solid #ccc; border-radius: 5px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); font-family: Arial, sans-serif; font-size: 14px; color: #333; display: flex; flex-direction: column; gap: 8px; max-width: 250px; `; const title = document.createElement('h3'); title.textContent = 'PDFTron Extractor'; title.style.cssText = 'margin: 0 0 5px 0; font-size: 16px; text-align: center;'; const startButton = document.createElement('button'); startButton.id = START_BUTTON_ID; startButton.textContent = '▶️ Start Download'; startButton.onclick = toggleDownloadState; startButton.style.cssText = 'padding: 8px 10px; font-size: 1em; margin-bottom: 5px; cursor: pointer; border-radius: 3px; border: 1px solid #bbb; background-color: #e7e7e7;'; const configButtonContainer = document.createElement('div'); configButtonContainer.style.cssText = 'display: flex; justify-content: space-around; gap: 5px;'; const folderButton = document.createElement('button'); folderButton.textContent = 'Folder'; folderButton.title = 'Configure download subfolder'; folderButton.onclick = configureFolder; folderButton.style.cssText = 'padding: 5px 10px; flex-grow: 1; border-radius: 3px; border: 1px solid #bbb; background-color: #e7e7e7; cursor: pointer;'; const patternButton = document.createElement('button'); patternButton.textContent = 'Pattern'; patternButton.title = 'Configure image URL pattern'; patternButton.onclick = configureImagePattern; patternButton.style.cssText = 'padding: 5px 10px; flex-grow: 1; border-radius: 3px; border: 1px solid #bbb; background-color: #e7e7e7; cursor: pointer;'; configButtonContainer.append(folderButton, patternButton); const status = document.createElement('div'); status.id = STATUS_ID; status.style.cssText = 'margin-top: 8px; font-size: 0.9em; white-space: pre-wrap; word-wrap: break-word; background: #fff; border: 1px solid #ddd; padding: 5px; border-radius: 3px;'; container.append(title, startButton, configButtonContainer, status); try { document.body.appendChild(container); log(`UI added to the iframe body.`); updateInterface(); } catch (e) { log(`Error adding UI to iframe body: ${e.message}.`, 'error'); } } function updateInterface() { const statusElement = document.getElementById(STATUS_ID); const startButton = document.getElementById(START_BUTTON_ID); if (statusElement) { const patternSource = imagePattern ? imagePattern.source : 'N/A (Error?)'; const activeStatus = isDownloadingActive ? '🟢 Active' : '🔴 Idle'; statusElement.textContent = `Status: ${activeStatus}\nFolder: ${destinationFolder || '(None)'}\nPattern: ${patternSource}\nDownloaded: ${latestImages.size}`; } if (startButton) { startButton.textContent = isDownloadingActive ? '⏹️ Stop Download' : '▶️ Start Download'; startButton.style.backgroundColor = isDownloadingActive ? '#ffdddd' : '#ddffdd'; } } // --- Download Control Functions --- function toggleDownloadState() { if (isDownloadingActive) { stopDownloading(); } else { startDownloading(); } updateInterface(); } function startDownloading() { if (isDownloadingActive) return; log("Starting download process..."); isDownloadingActive = true; setupPerformanceObserver(); if (cacheCheckInterval) clearInterval(cacheCheckInterval); log("Running initial cache check..."); verifyCache().then(() => { log("Initial cache check complete."); cacheCheckInterval = setInterval(verifyCache, VERIFICATION_INTERVAL); log(`Periodic cache check started (interval: ${VERIFICATION_INTERVAL}ms).`); }).catch(err => { log(`Error during initial cache check: ${err.message}`, 'error'); cacheCheckInterval = setInterval(verifyCache, VERIFICATION_INTERVAL); log(`Periodic cache check started DESPITE initial error (interval: ${VERIFICATION_INTERVAL}ms).`); }); log("Detection and download system ACTIVATED."); updateInterface(); } function stopDownloading() { if (!isDownloadingActive) return; log("Stopping download process..."); isDownloadingActive = false; if (observer) { observer.disconnect(); log("PerformanceObserver stopped."); } if (cacheCheckInterval) { clearInterval(cacheCheckInterval); cacheCheckInterval = null; log("Periodic cache check stopped."); } log("Detection and download system DEACTIVATED."); updateInterface(); } // --- Image Processing and Downloading --- function processImage(url) { if (!isDownloadingActive) { return; } if (!url || typeof url !== 'string') { log(`[processImage] Invalid URL provided: ${url}`, 'warn'); return; } if (!imagePattern) { log(`[processImage] Image pattern not initialized. Aborting process for ${url}.`, 'error'); return; } if (!imagePattern.test(url)) { log(`[processImage] URL unexpectedly failed pattern test inside processImage: ${url}`, 'warn'); return; } const cleanUrl = url.split('?')[0]; const name = cleanUrl.split('/').pop(); if (latestImages.has(cleanUrl)) { return; } log(`[processImage] New image URL detected: ${cleanUrl}`); latestImages.add(cleanUrl); const fullPath = destinationFolder ? `${destinationFolder}/${name}` : name; log(`[processImage] Preparing direct download for: ${name} to path: ${fullPath} from URL: ${url}`); try { GM_download({ url: url, name: fullPath, saveAs: false, headers: { 'Referer': location.href }, timeout: 20000, onload: () => { log(`✅ [GM_download] Download successful: ${fullPath}`); updateInterface(); }, onerror: (error) => { let errorDetails = error?.error || 'unknown'; let finalUrl = error?.details?.finalUrl || url; let httpStatus = error?.details?.httpStatus; log(`❌ [GM_download] Error: ${errorDetails}. Status: ${httpStatus || 'N/A'}. Final URL: ${finalUrl}. Path: ${fullPath}`, 'error'); latestImages.delete(cleanUrl); updateInterface(); }, ontimeout: () => { log(`❌ [GM_download] Timeout downloading: ${fullPath}`, 'error'); latestImages.delete(cleanUrl); updateInterface(); } }); log(`[processImage] GM_download call initiated for ${name}. Waiting for callbacks...`); } catch (e) { log(`❌ [processImage] CRITICAL Exception calling GM_download: ${e.message}`, 'error'); latestImages.delete(cleanUrl); updateInterface(); } } // --- Resource Detection Mechanisms --- function setupPerformanceObserver() { try { if (observer) { observer.disconnect(); log('[Observer] Disconnected existing observer.'); } log('[Observer] Setting up PerformanceObserver...'); observer = new PerformanceObserver((list) => { if (!isDownloadingActive) return; list.getEntriesByType('resource').forEach(entry => { const url = entry.name; if (imagePattern && imagePattern.test(url)) { log(`[Observer] MATCHED pattern: ${url}`); processImage(url); } else { if (!url.startsWith('data:') && !url.endsWith('.css') && !url.endsWith('.js') && !url.includes('favicon')) { // log(`[Observer] Ignored resource (no pattern match): ${url}`); } } }); }); observer.observe({ type: 'resource', buffered: true }); log('[Observer] PerformanceObserver started and listening.'); } catch (e) { log('[Observer] Error starting PerformanceObserver: ' + e.message, 'error'); } } // --- Cache Verification (FIXED) --- async function verifyCache() { if (!isDownloadingActive) { return; } // log('[Cache] Verifying cache (active)...'); // Can be verbose try { const keys = await caches.keys(); // Get all cache storage keys for (const key of keys) { // Loop through cache keys (outer loop) try { const cache = await caches.open(key); // Open specific cache const requests = await cache.keys(); // Get all request objects (keys) in this cache // *** FIXED: Use for...of instead of forEach to avoid linter warnings *** for (const request of requests) { // Loop through requests in *this* cache (inner loop) const url = request.url; // Check if the cached request URL matches the pattern if (imagePattern && imagePattern.test(url)) { log(`[Cache] MATCHED pattern: ${url}`); processImage(url); // Hand off to processing function } } } catch (cacheError) { // Log errors accessing specific caches if needed for debugging // log(`[Cache] Could not access/read cache '${key}': ${cacheError.message}`, 'warn'); } } } catch (error) { log('[Cache] General error verifying cache: ' + error.message, 'error'); } } // --- Main Initialization --- function initialize() { initializeImagePattern(); log(`Initializing script in IFRAME: ${location.href}`); createInterface(); try { GM_registerMenuCommand('🖼️ Configure Download Subfolder', configureFolder); GM_registerMenuCommand('🔍 Configure Image URL Pattern', configureImagePattern); } catch (e) { log(`Error registering menu commands (already registered?): ${e.message}`, 'warn'); } log('Script ready. Press "Start Download" to begin.'); } // --- Run Initialization --- if (document.readyState === 'interactive' || document.readyState === 'complete') { initialize(); } else { document.addEventListener('DOMContentLoaded', initialize); } })(); // End of userscript