PDFTron Image Extractor (v1.2 - Manual Control, Linter Fixes, EN)

Manually start/stop downloading images from PDFTron viewer iframe, prevents duplicate UI.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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