Enhanced Google Drive PDF Downloader

Download protected PDF files from Google Drive with quality options

// ==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();
})();