Enhanced Google Drive PDF Downloader

Download protected PDF files from Google Drive with quality options

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Enhanced Google Drive PDF Downloader
// @namespace    GoogleDrivePDFDownloader
// @version      2
// @description  Download protected PDF files from Google Drive with quality options
// @author       akvabhi
// @match        https://drive.google.com/*
// @grant        none
// @homepage     https://github.com/Akv2021/Enhanced-Google-Drive-PDF-Downloader
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    const COLORS = {
        fast: '#2ecc71', // Green for fast mode
        slow: '#e74c3c', // Red for slow mode
        hover: '#3367d6',
        default: '#4285f4'
    };
    // Utility functions
    const log = (message, type = 'info') => {
        const timestamp = new Date().toLocaleTimeString();
        const logMethod = type === 'error' ? console.error : console.log;
        logMethod(`[PDF Downloader ${timestamp}] ${message}`);
    };

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

    // Global quality setting
    window.pdfQualityMode = 'FAST'; // Default to fast mode

    const progressIndicator = {
        element: null,

        create() {
            const indicator = document.createElement('div');
            indicator.style.cssText = `
                position: fixed;
                top: 65px;
                right: 20px;
                z-index: 9999;
                padding: 8px 16px;
                background-color: #4285f4;
                color: white;
                border-radius: 4px;
                font-family: Arial, sans-serif;
                font-size: 14px;
                display: none;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                line-height: 20px;
                min-height: 20px;
                display: flex;
                align-items: center;
                transition: all 0.3s ease;
            `;
            document.body.appendChild(indicator);
            this.element = indicator;
        },

        show(message) {
            if (!this.element) this.create();
            this.element.style.display = 'block';
            this.element.style.opacity = '0';
            setTimeout(() => {
                this.element.style.opacity = '1';
                this.element.textContent = message;
            }, 10);
            const downloadContainer = document.querySelector('#pdfDownloadContainer');
            if (downloadContainer) downloadContainer.style.display = 'none';
        },

        hide() {
            if (this.element) {
                this.element.style.opacity = '0';
                setTimeout(() => {
                    this.element.style.display = 'none';
                    const downloadContainer = document.querySelector('#pdfDownloadContainer');
                    if (downloadContainer) {
                        downloadContainer.style.display = 'flex';
                        downloadContainer.style.opacity = '1';
                    }
                }, 300);
            }
        },

        updateProgress(current, total) {
            const percentage = Math.floor((current / total) * 100);
            this.show(`Processing: ${percentage}% (${current}/${total} pages)`);
        }
    };
    async function loadJsPDF() {
        return new Promise((resolve, reject) => {
            log('Loading jsPDF library...');
            progressIndicator.show('Loading PDF library...');

            const script = document.createElement('script');
            const scriptURL = 'https://unpkg.com/jspdf@latest/dist/jspdf.umd.min.js';

            if (window.trustedTypes && trustedTypes.createPolicy) {
                const policy = trustedTypes.createPolicy('pdfDownloaderPolicy', {
                    createScriptURL: (input) => input
                });
                script.src = policy.createScriptURL(scriptURL);
            } else {
                script.src = scriptURL;
            }

            script.onload = () => {
                log('jsPDF library loaded successfully');
                resolve();
            };
            script.onerror = (error) => {
                log('Failed to load jsPDF library', 'error');
                reject(error);
            };
            document.body.appendChild(script);
        });
    }

    async function convertImageToBase64(img) {
        try {
            const canvas = document.createElement('canvas');
            canvas.width = img.naturalWidth;
            canvas.height = img.naturalHeight;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight);
            return canvas.toDataURL('image/png', 1.0);
        } catch (error) {
            log(`Error converting image to base64: ${error.message}`, 'error');
            throw error;
        }
    }

    function getValidPdfPages() {
        const images = Array.from(document.getElementsByTagName('img'));
        return images.filter(img =>
            img.src.startsWith('blob:https://drive.google.com/') &&
            img.naturalWidth > 0 &&
            img.naturalHeight > 0
        );
    }

    async function generatePDF(images) {
        log(`Starting PDF generation with ${images.length} pages`);
        const {
            jsPDF
        } = window.jspdf;
        let pdf = null;

        for (let i = 0; i < images.length; i++) {
            const img = images[i];
            const orientation = img.naturalWidth > img.naturalHeight ? 'l' : 'p';

            if (!pdf) {
                pdf = new jsPDF({
                    orientation: orientation,
                    unit: 'px',
                    format: [img.naturalWidth, img.naturalHeight],
                    hotfixes: ['px_scaling']
                });
            }

            if (i > 0) {
                pdf.addPage([img.naturalWidth, img.naturalHeight], orientation);
            }

            progressIndicator.show(`Converting page ${i + 1}/${images.length}...`);
            const imgData = await convertImageToBase64(img);
            pdf.addImage(imgData, 'PNG', 0, 0, img.naturalWidth, img.naturalHeight, '', window.pdfQualityMode);

            progressIndicator.updateProgress(i + 1, images.length);
            await delay(50);
        }

        return pdf;
    }

    async function downloadPDF() {
        try {
            const button = document.querySelector('#pdfDownloadButton');
            button.disabled = true;
            button.textContent = '⏳ Processing...';

            log('Starting PDF download process...');
            progressIndicator.show('Initializing...');

            await loadJsPDF();

            const validPages = getValidPdfPages();
            if (validPages.length === 0) {
                throw new Error('No valid PDF pages found. Please scroll through the document first.');
            }
            log(`Found ${validPages.length} valid pages`);

            const pdf = await generatePDF(validPages);

            const fileName = document.querySelector('meta[itemprop="name"]')?.content || 'download.pdf';
            const finalFileName = fileName.toLowerCase().endsWith('.pdf') ? fileName : `${fileName}.pdf`;

            progressIndicator.show(`Saving as ${finalFileName}...`);
            await pdf.save(finalFileName, {
                returnPromise: true
            });

            log('PDF downloaded successfully!');
            progressIndicator.show('Download complete!');

            button.disabled = false;
            button.textContent = window.pdfQualityMode === 'SLOW' ?
                'Download PDF (Best Quality)' : 'Download PDF (Fast)';

            setTimeout(() => progressIndicator.hide(), 3000);

        } catch (error) {
            log(`Failed to generate PDF: ${error.message}`, 'error');
            progressIndicator.show(`Error: ${error.message}`);

            const button = document.querySelector('#pdfDownloadButton');
            button.disabled = false;
            button.textContent = window.pdfQualityMode === 'SLOW' ?
                'Download PDF (Best Quality)' : 'Download PDF (Fast)';

            alert(`Failed to generate PDF: ${error.message}`);
        }
    }

    function updateDownloadButtonText(isHighQuality) {
        const button = document.querySelector('#pdfDownloadButton');
        if (button) {
            const newText = isHighQuality ? 'Download PDF (Best Quality)' : 'Download PDF (Fast)';
            button.style.opacity = '0';
            setTimeout(() => {
                button.textContent = newText;
                button.style.opacity = '1';
            }, 150);
        }
    }

    function createToggleSwitch() {
        const toggleContainer = document.createElement('div');
        toggleContainer.style.cssText = `
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 12px 0;
            width: 100%;
        `;

        const switchLabel = document.createElement('div');
        switchLabel.textContent = 'Quality Mode:';
        switchLabel.style.marginBottom = '8px';

        const switchControl = document.createElement('div');
        switchControl.style.cssText = `
            display: flex;
            flex-direction: column;
            align-items: center;
            width: 100%;
        `;

        const toggleSwitch = document.createElement('label');
        toggleSwitch.style.cssText = `
            position: relative;
            display: inline-block;
            width: 46px;
            height: 24px;
        `;

        const toggleInput = document.createElement('input');
        toggleInput.type = 'checkbox';
        toggleInput.checked = false; // Default to fast mode
        toggleInput.style.cssText = `
            opacity: 0;
            width: 0;
            height: 0;
        `;

        const toggleSlider = document.createElement('span');
        toggleSlider.style.cssText = `
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: ${COLORS.fast};
            transition: .4s;
            border-radius: 24px;
        `;

        const sliderBall = document.createElement('span');
        sliderBall.style.cssText = `
            position: absolute;
            content: '';
            height: 18px;
            width: 18px;
            left: 3px;
            bottom: 3px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
            transform: translateX(0);
        `;

        const labelsContainer = document.createElement('div');
        labelsContainer.style.cssText = `
            display: flex;
            justify-content: space-between;
            width: 200px;
            margin-top: 8px;
            font-size: 12px;
            color: #888;
        `;

        const lowLabel = document.createElement('span');
        lowLabel.textContent = 'Low (Fast)';
        lowLabel.style.cssText = `
            transition: color 0.3s ease;
            color: #4285f4;
        `;

        const highLabel = document.createElement('span');
        highLabel.textContent = 'High (Slow)';
        highLabel.style.cssText = `
            transition: color 0.3s ease;
            color: #888;
        `;

        toggleInput.addEventListener('change', (e) => {
            const isHighQuality = e.target.checked;
            sliderBall.style.transform = isHighQuality ? 'translateX(22px)' : 'translateX(0)';
            toggleSlider.style.backgroundColor = isHighQuality ? COLORS.slow : COLORS.fast;
            window.pdfQualityMode = isHighQuality ? 'SLOW' : 'FAST';
            lowLabel.style.color = isHighQuality ? '#888' : '#fff';
            highLabel.style.color = isHighQuality ? '#fff' : '#888';
            updateDownloadButtonText(isHighQuality);
        });

        toggleSlider.appendChild(sliderBall);
        toggleSwitch.appendChild(toggleInput);
        toggleSwitch.appendChild(toggleSlider);
        labelsContainer.appendChild(lowLabel);
        labelsContainer.appendChild(highLabel);

        switchControl.appendChild(toggleSwitch);
        switchControl.appendChild(labelsContainer);

        toggleContainer.appendChild(switchLabel);
        toggleContainer.appendChild(switchControl);

        return toggleContainer;
    }

    function addDownloadButton() {
        const container = document.createElement('div');
        container.id = 'pdfDownloadContainer';
        container.style.cssText = `
            position: fixed;
            top: 65px;
            right: 20px;
            z-index: 9999;
            display: flex;
            align-items: center;
            gap: 8px;
            transition: opacity 0.3s ease;
        `;

        const button = document.createElement('button');
        button.id = 'pdfDownloadButton';
        button.textContent = 'Download PDF (Fast)';
        button.style.cssText = `
            padding: 8px 16px;
            background-color: #4285f4;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-family: Arial, sans-serif;
            font-size: 14px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            height: 36px;
            display: flex;
            align-items: center;
            transition: all 0.3s ease;
            opacity: 1;
        `;

        const infoIcon = document.createElement('div');
        infoIcon.id = 'pdfInfoIcon'; // Add ID for click-outside handling
        infoIcon.innerHTML = 'ℹ️';
        infoIcon.style.cssText = `
            cursor: help;
            font-size: 16px;
            position: relative;
            width: 36px;
            height: 36px;
            background-color: #4285f4;
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            transition: background-color 0.3s ease;
        `;

        const tooltip = document.createElement('div');
        tooltip.id = 'pdfDownloadTooltip'; // Add ID for click-outside handling
        tooltip.style.cssText = `
            position: absolute;
            top: calc(100% + 8px);
            right: 0;
            background-color: #333;
            color: white;
            padding: 16px;
            border-radius: 4px;
            font-size: 13px;
            width: 280px;
            display: none;
            z-index: 10000;
            font-family: Arial, sans-serif;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            transition: opacity 0.3s ease;
        `;

        // Info content
        const scrollInfo = document.createElement('div');
        scrollInfo.textContent = '⚠️ If some pages are missing, scroll to bottom to load all pages and retry.';
        scrollInfo.style.marginBottom = '8px';

        const qualityInfo = document.createElement('div');
        qualityInfo.style.cssText = `
            margin-bottom: 2px;
            padding: 8px;
            background-color: rgba(255,255,255,0.1);
            border-radius: 4px;
        `;
        qualityInfo.innerHTML = `
            <div style="margin-bottom: 8px; font-weight: bold;">Processing Modes:</div>
            <div style="font-size: 12px; line-height: 1.4;">
                • Fast: Quick processing (few seconds)<br>
                • Slow: Detailed processing (may take longer)
            </div>
        `;

        // Add toggle switch
        const toggleSwitch = createToggleSwitch();

        // Footer with author and GitHub link
        const footer = document.createElement('div');
        footer.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: center;
            margin-top: 4px;
            padding-top: 12px;
            border-top: 1px solid rgba(255,255,255,0.1);
        `;

        const authorText = document.createElement('span');
        authorText.textContent = 'Track Issues ';
        authorText.style.marginRight = '8px';

        const githubLink = document.createElement('a');
        githubLink.href = 'https://github.com/Akv2021/Enhanced-Google-Drive-PDF-Downloader/issues';
        githubLink.target = '_blank';
        githubLink.style.cssText = `
            color: white;
            text-decoration: none;
            display: flex;
            align-items: center;
            transition: opacity 0.3s ease;
        `;

        githubLink.innerHTML = `
            <svg height="20" width="20" viewBox="0 0 16 16" style="fill: white;">
                <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
            </svg>
        `;

        // Tooltip persistence
        let tooltipTimer = null;
        let isTooltipHovered = false;

        function startTooltipTimer() {
            if (tooltipTimer) {
                clearTimeout(tooltipTimer);
            }
            tooltipTimer = setTimeout(() => {
                if (!isTooltipHovered) {
                    tooltip.style.opacity = '0';
                    setTimeout(() => tooltip.style.display = 'none', 300);
                    infoIcon.style.backgroundColor = '#4285f4';
                }
            }, 60000); // 60 second timeout
        }

        tooltip.addEventListener('mouseenter', () => {
            isTooltipHovered = true;
            if (tooltipTimer) {
                clearTimeout(tooltipTimer);
            }
        });

        tooltip.addEventListener('mouseleave', () => {
            isTooltipHovered = false;
            startTooltipTimer();
        });

        infoIcon.addEventListener('mouseenter', () => {
            tooltip.style.display = 'block';
            tooltip.style.opacity = '0';
            setTimeout(() => tooltip.style.opacity = '1', 10);
            infoIcon.style.backgroundColor = '#3367d6';
            startTooltipTimer();
        });

        infoIcon.addEventListener('mouseleave', () => {
            if (!isTooltipHovered) {
                startTooltipTimer();
            }
        });

        button.addEventListener('mouseover', () => {
            if (!button.disabled) button.style.backgroundColor = '#3367d6';
        });

        button.addEventListener('mouseout', () => {
            if (!button.disabled) button.style.backgroundColor = '#4285f4';
        });

        button.addEventListener('click', downloadPDF);

        // Assemble tooltip
        tooltip.appendChild(scrollInfo);
        tooltip.appendChild(qualityInfo);
        tooltip.appendChild(toggleSwitch);
        footer.appendChild(authorText);
        footer.appendChild(githubLink);
        tooltip.appendChild(footer);

        // Assemble final container
        infoIcon.appendChild(tooltip);
        container.appendChild(button);
        container.appendChild(infoIcon);
        document.body.appendChild(container);
    }

    function initialize() {
        log('Initializing PDF downloader...');
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                addDownloadButton();
                setupClickOutside();
            });
        } else {
            addDownloadButton();
            setupClickOutside();
        }
    }

    // Add this new function for click-outside handling
    function setupClickOutside() {
        document.addEventListener('click', (event) => {
            const tooltip = document.querySelector('#pdfDownloadTooltip');
            const infoIcon = document.querySelector('#pdfInfoIcon');

            if (tooltip &&
                !tooltip.contains(event.target) &&
                !infoIcon.contains(event.target)) {
                tooltip.style.opacity = '0';
                setTimeout(() => tooltip.style.display = 'none', 300);
                infoIcon.style.backgroundColor = COLORS.default;
            }
        });
    }


    initialize();
})();