Quizlet Enhancer

Download flashcards (JSON/CSV/TXT) • Remove blur on definitions • Hide upgrade buttons • Remove sign-up banners • Works with light & dark mode

// ==UserScript==
// @name         Quizlet Enhancer
// @namespace    https://greasyfork.org/en/users/1528865-blati
// @version      1.1
// @description  Download flashcards (JSON/CSV/TXT) • Remove blur on definitions • Hide upgrade buttons • Remove sign-up banners • Works with light & dark mode
// @author       Blati
// @license      MIT
// @match        https://quizlet.com/*
// @icon         data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='g1' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%234255ff'/%3E%3Cstop offset='100%25' style='stop-color:%233147e0'/%3E%3C/linearGradient%3E%3ClinearGradient id='g2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%2366d9ff'/%3E%3Cstop offset='100%25' style='stop-color:%234255ff'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='48' fill='url(%23g1)'/%3E%3Crect x='20' y='30' width='60' height='40' rx='4' fill='white' opacity='0.95'/%3E%3Crect x='25' y='35' width='50' height='8' rx='2' fill='url(%23g2)' opacity='0.4'/%3E%3Crect x='25' y='47' width='40' height='4' rx='1' fill='%23666' opacity='0.3'/%3E%3Crect x='25' y='54' width='35' height='4' rx='1' fill='%23666' opacity='0.3'/%3E%3Cpath d='M75 20 L78 23 L85 16' stroke='%23FFD700' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3Ccircle cx='82' cy='30' r='2' fill='%23FFD700'/%3E%3Ccircle cx='88' cy='25' r='1.5' fill='%23FFD700'/%3E%3Cpath d='M15 25 L18 28 L25 21' stroke='%23FFD700' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3Ccircle cx='12' cy='32' r='1.8' fill='%23FFD700'/%3E%3Cpath d='M18 75 L20 78 L25 73' stroke='%23FFD700' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3Ccircle cx='82' cy='72' r='2' fill='%23FFD700'/%3E%3C/svg%3E
// @grant        none
// ==/UserScript==

// MIT License
//
// Copyright (c) 2025 Blati
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

(function() {
    'use strict';

    // This script automatically adapts to Quizlet's theme (light/dark mode)
    // by dynamically detecting colors from the page at runtime.

    // Wait for the page to load completely
    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            const checkInterval = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(checkInterval);
                    resolve(element);
                } else if (Date.now() - startTime > timeout) {
                    clearInterval(checkInterval);
                    reject(new Error('Element not found'));
                }
            }, 100);
        });
    }

    // Extract flashcards from the page
    function extractFlashcards() {
        // Use more specific selector to avoid matching ad containers
        const termCards = document.querySelectorAll('[class*="SetPageTermsList-term"]');
        const flashcards = [];

        termCards.forEach(card => {
            const sides = card.querySelectorAll('[data-testid="set-page-term-card-side"]');

            // Only add if we have both term and definition
            if (sides.length >= 2) {
                const term = sides[0].textContent.trim();
                const definition = sides[1].textContent.trim();

                // Skip if term or definition is empty
                if (term && definition) {
                    flashcards.push({ term, definition });
                }
            }
        });

        return flashcards;
    }

    // Get the set title from the page
    function getSetTitle() {
        const titleElement = document.querySelector('h1, [class*="SetPage-title"]');
        return titleElement ? titleElement.textContent.trim() : 'quizlet-flashcards';
    }

    // Download as JSON
    function downloadJSON(flashcards, filename) {
        const data = JSON.stringify(flashcards, null, 2);
        downloadFile(data, filename + '.json', 'application/json');
    }

    // Download as CSV
    function downloadCSV(flashcards, filename) {
        let csv = 'Term,Definition\n';
        flashcards.forEach(card => {
            const term = '"' + card.term.replace(/"/g, '""') + '"';
            const definition = '"' + card.definition.replace(/"/g, '""') + '"';
            csv += term + ',' + definition + '\n';
        });
        downloadFile(csv, filename + '.csv', 'text/csv');
    }

    // Download as TXT
    function downloadTXT(flashcards, filename) {
        let txt = '';
        flashcards.forEach((card, index) => {
            txt += `Card ${index + 1}:\n`;
            txt += `Term: ${card.term}\n`;
            txt += `Definition: ${card.definition}\n`;
            txt += '\n---\n\n';
        });
        downloadFile(txt, filename + '.txt', 'text/plain');
    }

    // Generic download function
    function downloadFile(content, filename, mimeType) {
        const blob = new Blob([content], { type: mimeType });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    // Get Quizlet's theme colors dynamically
    function getThemeColors() {
        const bodyStyles = window.getComputedStyle(document.body);
        const bgColor = bodyStyles.backgroundColor;

        // Check if dark mode is active by examining background color
        const isDarkMode = bgColor.includes('18, 18, 18') ||
                          bgColor.includes('31, 31, 31') ||
                          bgColor === 'rgb(0, 0, 0)' ||
                          bgColor === 'rgb(18, 18, 18)' ||
                          bgColor === 'rgb(31, 31, 31)';

        const colors = {
            background: bgColor,
            text: isDarkMode ? 'rgb(255, 255, 255)' : 'rgb(40, 46, 62)',
            primary: 'rgb(66, 85, 255)',
            primaryHover: 'rgb(49, 71, 224)',
            secondary: isDarkMode ? 'rgb(42, 46, 54)' : 'rgb(246, 247, 251)',
            secondaryText: isDarkMode ? 'rgb(180, 183, 192)' : 'rgb(88, 99, 128)',
            cardBg: isDarkMode ? 'rgb(31, 35, 41)' : 'rgb(255, 255, 255)',
            border: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
            overlay: isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)'
        };

        return colors;
    }

    // Check if scroll-to-top button exists and adjust position
    function adjustDownloadButtonPosition(button) {
        const arrowUpButton = document.querySelector('button[aria-label="arrow up"]');

        if (arrowUpButton) {
            // Scroll-to-top button exists, move download button to the left
            button.style.right = '100px';
        } else {
            // No scroll-to-top button, use default position
            button.style.right = '24px';
        }
    }

    // Create download button with Quizlet styling
    function createDownloadButton() {
        const colors = getThemeColors();
        const button = document.createElement('button');

        // Create download icon SVG
        const icon = `
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-right: 6px; display: inline-block; vertical-align: middle;">
                <path d="M8 1v10M4 7l4 4 4-4M2 14h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
        `;

        button.innerHTML = icon + '<span>Download</span>';
        button.style.cssText = `
            position: fixed;
            bottom: 24px;
            right: 24px;
            z-index: 10000;
            padding: 10px 16px;
            background-color: ${colors.primary};
            color: white;
            border: 2px solid transparent;
            border-radius: 200px;
            font-size: 14px;
            font-weight: 600;
            font-family: hurme_no2-webfont, -apple-system, BlinkMacSystemFont, sans-serif;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(66, 85, 255, 0.3);
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
            display: flex;
            align-items: center;
            gap: 4px;
        `;

        button.onmouseover = () => {
            button.style.backgroundColor = colors.primaryHover;
            button.style.transform = 'translateY(-2px)';
            button.style.boxShadow = '0 6px 16px rgba(66, 85, 255, 0.4)';
        };

        button.onmouseout = () => {
            button.style.backgroundColor = colors.primary;
            button.style.transform = 'translateY(0)';
            button.style.boxShadow = '0 4px 12px rgba(66, 85, 255, 0.3)';
        };

        button.onclick = () => {
            showDownloadModal();
        };

        document.body.appendChild(button);

        // Adjust position based on scroll-to-top button
        adjustDownloadButtonPosition(button);

        // Watch for scroll-to-top button appearing/disappearing
        const observer = new MutationObserver(() => {
            adjustDownloadButtonPosition(button);
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        return button;
    }

    // Create modal for format selection with Quizlet styling
    function showDownloadModal() {
        const flashcards = extractFlashcards();

        if (flashcards.length === 0) {
            alert('No flashcards found on this page. Make sure you are on a Quizlet set page.');
            return;
        }

        const title = getSetTitle();
        const colors = getThemeColors();

        const modal = document.createElement('div');
        modal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: ${colors.overlay};
            z-index: 10001;
            display: flex;
            justify-content: center;
            align-items: center;
            backdrop-filter: blur(4px);
            animation: fadeIn 0.2s ease;
        `;

        const modalContent = document.createElement('div');
        modalContent.style.cssText = `
            background-color: ${colors.cardBg};
            padding: 32px;
            border-radius: 16px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
            max-width: 440px;
            width: 90%;
            font-family: hurme_no2-webfont, -apple-system, BlinkMacSystemFont, sans-serif;
            animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        `;

        // Create format buttons with SVG icons
        const formatButtons = [
            {
                id: 'json',
                label: 'JSON',
                icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" style="color: inherit;"><path d="M6 4C6 2.89543 5.10457 2 4 2C2.89543 2 2 2.89543 2 4V8C2 9.88562 2.59489 10.9248 3.60825 11.6421C2.59489 12.3594 2 13.3986 2 15.2V16C2 17.1046 2.89543 18 4 18C5.10457 18 6 17.1046 6 16M14 4C14 2.89543 14.8954 2 16 2C17.1046 2 18 2.89543 18 4V8C18 9.88562 17.4051 10.9248 16.3918 11.6421C17.4051 12.3594 18 13.3986 18 15.2V16C18 17.1046 17.1046 18 16 18C14.8954 18 14 17.1046 14 16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
                desc: 'Machine-readable format'
            },
            {
                id: 'csv',
                label: 'CSV',
                icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" style="color: inherit;"><rect x="3" y="3" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/><line x1="3" y1="7" x2="17" y2="7" stroke="currentColor" stroke-width="1.5"/><line x1="3" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5"/><line x1="10" y1="7" x2="10" y2="17" stroke="currentColor" stroke-width="1.5"/></svg>',
                desc: 'Spreadsheet compatible'
            },
            {
                id: 'txt',
                label: 'Text',
                icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" style="color: inherit;"><path d="M6 3H14C15.1046 3 16 3.89543 16 5V15C16 16.1046 15.1046 17 14 17H6C4.89543 17 4 16.1046 4 15V5C4 3.89543 4.89543 3 6 3Z" stroke="currentColor" stroke-width="1.5"/><line x1="7" y1="7" x2="13" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="7" y1="10" x2="13" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="7" y1="13" x2="10" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
                desc: 'Plain text format'
            }
        ];

        const buttonsHTML = formatButtons.map(btn => `
            <button id="download-${btn.id}" style="
                padding: 16px 20px;
                background-color: ${colors.secondary};
                color: ${colors.text};
                border: 2px solid ${colors.border};
                border-radius: 12px;
                cursor: pointer;
                font-size: 14px;
                font-weight: 600;
                font-family: inherit;
                transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
                display: flex;
                align-items: center;
                gap: 12px;
                text-align: left;
            " onmouseover="this.style.backgroundColor='${colors.primary}'; this.style.color='white'; this.style.borderColor='${colors.primary}'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(66, 85, 255, 0.2)'; this.querySelector('.label-text').style.color='white'; this.querySelector('.desc-text').style.color='rgba(255,255,255,0.8)';" onmouseout="this.style.backgroundColor='${colors.secondary}'; this.style.color='${colors.text}'; this.style.borderColor='${colors.border}'; this.style.transform='translateY(0)'; this.style.boxShadow='none'; this.querySelector('.label-text').style.color='${colors.text}'; this.querySelector('.desc-text').style.color='${colors.secondaryText}';">
                <span style="flex-shrink: 0; display: flex; align-items: center;">${btn.icon}</span>
                <div style="flex: 1;">
                    <div class="label-text" style="font-weight: 600; color: ${colors.text}; transition: color 0.2s;">${btn.label}</div>
                    <div class="desc-text" style="font-size: 12px; color: ${colors.secondaryText}; margin-top: 2px; transition: color 0.2s;">${btn.desc}</div>
                </div>
            </button>
        `).join('');

        modalContent.innerHTML = `
            <style>
                @keyframes fadeIn {
                    from { opacity: 0; }
                    to { opacity: 1; }
                }
                @keyframes slideUp {
                    from {
                        opacity: 0;
                        transform: translateY(20px);
                    }
                    to {
                        opacity: 1;
                        transform: translateY(0);
                    }
                }
            </style>
            <h2 style="margin: 0 0 8px 0; color: ${colors.text}; font-size: 24px; font-weight: 700;">Download Flashcards</h2>
            <p style="color: ${colors.secondaryText}; margin: 0 0 24px 0; font-size: 14px;">
                Found ${flashcards.length} flashcard${flashcards.length !== 1 ? 's' : ''} • Choose a format
            </p>
            <div style="display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px;">
                ${buttonsHTML}
            </div>
            <button id="cancel-download" style="
                width: 100%;
                padding: 10px 16px;
                background-color: transparent;
                color: ${colors.secondaryText};
                border: 2px solid ${colors.border};
                border-radius: 200px;
                cursor: pointer;
                font-size: 14px;
                font-weight: 600;
                font-family: inherit;
                transition: all 0.2s ease;
            " onmouseover="this.style.backgroundColor='${colors.secondary}';" onmouseout="this.style.backgroundColor='transparent';">
                Cancel
            </button>
        `;

        modal.appendChild(modalContent);
        document.body.appendChild(modal);

        // Add event listeners
        document.getElementById('download-json').onclick = () => {
            downloadJSON(flashcards, title);
            document.body.removeChild(modal);
        };

        document.getElementById('download-csv').onclick = () => {
            downloadCSV(flashcards, title);
            document.body.removeChild(modal);
        };

        document.getElementById('download-txt').onclick = () => {
            downloadTXT(flashcards, title);
            document.body.removeChild(modal);
        };

        document.getElementById('cancel-download').onclick = () => {
            document.body.removeChild(modal);
        };

        // Close modal when clicking outside
        modal.onclick = (e) => {
            if (e.target === modal) {
                document.body.removeChild(modal);
            }
        };
    }

    // Check if we're on a Quizlet set page
    function isQuizletSetPage() {
        return window.location.pathname.match(/^\/[a-z]{2}\/\d+\//);
    }

    // Remove upgrade/free trial buttons from header
    function removeUpgradeButtons() {
        const upgradeButtons = Array.from(document.querySelectorAll('a[data-testid="assembly-button-upgrade"]'));
        upgradeButtons.forEach(button => {
            const text = button.textContent.trim();
            if (text.includes('Free trial') || text.includes('Upgrade') || text.includes('free 7-day trial')) {
                button.remove();
                console.log('Removed upgrade button:', text);
            }
        });
    }

    // Remove blur and sign-up banner for non-logged-in users
    function removeLoginWalls() {
        // Check if user is logged in by looking for the "Log in" button
        const loginButton = Array.from(document.querySelectorAll('a, button')).find(el =>
            el.textContent.trim() === 'Log in'
        );

        // If user is not logged in, remove blur and banner
        if (loginButton) {
            // Remove blur from all definition card sides
            const blurredElements = document.querySelectorAll('[data-testid="set-page-term-card-side"]');
            blurredElements.forEach(el => {
                const styles = window.getComputedStyle(el);
                if (styles.filter && styles.filter.includes('blur')) {
                    el.style.filter = 'none';
                }
            });

            // Remove the sticky sign-up banner
            const removeBanner = () => {
                const allElements = document.querySelectorAll('div');
                for (const el of allElements) {
                    if (el.textContent.includes('Sign up to reveal definitions') &&
                        el.textContent.length < 300) {
                        const styles = window.getComputedStyle(el);
                        if (styles.position === 'sticky' || styles.position === 'fixed') {
                            el.remove();
                            console.log('Removed sign-up banner');
                            return true;
                        }
                    }
                }
                return false;
            };

            // Try to remove banner immediately and after scroll
            removeBanner();
            setTimeout(removeBanner, 1000);
            window.addEventListener('scroll', removeBanner, { once: true });

            console.log('Removed blur from definitions for non-logged-in user');
        }
    }

    // Initialize the script
    async function init() {
        if (!isQuizletSetPage()) {
            return;
        }

        try {
            // Wait for flashcards to load
            await waitForElement('[data-testid="set-page-term-card-side"]');

            // Remove upgrade/free trial buttons
            removeUpgradeButtons();

            // Remove login walls (blur and banner)
            removeLoginWalls();

            // Create download button
            createDownloadButton();

            console.log('Quizlet Enhancer loaded successfully');
        } catch (error) {
            console.error('Error loading Quizlet Enhancer:', error);
        }
    }

    // Run the script when the page is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();