LearnableMeta to Anki Exporter

Export LearnableMeta maps to Anki txt files

当前为 2025-06-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         LearnableMeta to Anki Exporter
// @namespace    https://learnablemeta.com/
// @version      1.0.0
// @description  Export LearnableMeta maps to Anki txt files
// @match        https://learnablemeta.com/maps/*
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      *
// @run-at       document-end
// @author       BennoGHG
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    function $(s) { return document.querySelector(s); }
    function $$(s) { return Array.from(document.querySelectorAll(s)); }
    function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); }

    // Enhanced CSS styles - Production Ready
    GM_addStyle([
        '@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");',
        '#lm-window { position: fixed; top: 50px; left: calc(100vw - 380px); width: 360px; min-width: 320px; min-height: 450px; max-width: 600px; max-height: 80vh;',
        'background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%); color: #f0f0f0; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
        'border-radius: 16px; box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08);',
        'z-index: 2147483647; overflow: hidden; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);',
        'backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.1); user-select: none; resize: none; }',

        '#lm-window.dragging { transition: none; cursor: move; }',
        '#lm-window.resizing { transition: none; }',
        '#lm-window.hidden { transform: translateX(420px) scale(0.9); opacity: 0; pointer-events: none; }',

        '#lm-header { background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); padding: 16px 20px;',
        'cursor: move; user-select: none; display: flex; align-items: center; justify-content: space-between;',
        'border-radius: 16px 16px 0 0; position: relative; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2); }',
        '#lm-header:active { cursor: grabbing; }',

        '#lm-title { font-size: 15px; font-weight: 600; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.2); }',

        '#lm-hide-btn { background: rgba(255,255,255,0.15); border: none; color: white; width: 28px; height: 28px;',
        'border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center;',
        'font-size: 14px; transition: all 0.2s ease; font-weight: 500; }',
        '#lm-hide-btn:hover { background: rgba(255,255,255,0.25); transform: scale(1.05); }',

        '#lm-content { padding: 20px; display: flex; flex-direction: column; gap: 16px; height: calc(100% - 64px); overflow-y: auto; }',
        '#lm-content::-webkit-scrollbar { width: 6px; }',
        '#lm-content::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 3px; }',
        '#lm-content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }',
        '#lm-content::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }',

        '#lm-show-btn { position: fixed; top: 50px; right: 20px; background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);',
        'border: none; color: white; width: 48px; height: 48px; border-radius: 50%; cursor: pointer;',
        'display: none; align-items: center; justify-content: center; font-size: 18px; z-index: 2147483646;',
        'box-shadow: 0 8px 20px rgba(59, 130, 246, 0.3); transition: all 0.3s ease;',
        'border: 2px solid rgba(255,255,255,0.1); }',
        '#lm-show-btn:hover { transform: scale(1.05); box-shadow: 0 12px 30px rgba(59, 130, 246, 0.4); }',

        '.lm-section { background: rgba(255,255,255,0.03); padding: 16px; border-radius: 10px;',
        'border: 1px solid rgba(255,255,255,0.08); backdrop-filter: blur(10px); }',

        '.lm-section label { font-size: 12px; margin-bottom: 8px; display: block; color: #a0a0a0; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }',

        '.lm-section input[type=text] { width: 100%; padding: 10px 12px; border-radius: 6px;',
        'border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); color: #f0f0f0;',
        'box-sizing: border-box; font-family: Inter; transition: all 0.2s ease; font-size: 14px; }',
        '.lm-section input[type=text]:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); background: rgba(255,255,255,0.08); }',

        '.lm-section input[type=range] { width: 100%; margin: 8px 0; accent-color: #3b82f6; }',

        '.lm-button { padding: 10px 16px; border: none; border-radius: 8px; font-size: 12px; cursor: pointer;',
        'margin-bottom: 8px; width: 100%; font-family: Inter; font-weight: 500; transition: all 0.2s ease;',
        'text-transform: uppercase; letter-spacing: 0.5px; position: relative; overflow: hidden; }',

        '.lm-button.primary { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; }',
        '.lm-button.primary:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(139, 92, 246, 0.3); }',

        '.lm-button.secondary { background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); color: white; }',
        '.lm-button.secondary:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(59, 130, 246, 0.3); }',

        '.lm-button.warning { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; }',
        '.lm-button.warning:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3); }',

        '.lm-button:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; }',

        '.lm-progress-bar { width: 100%; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin: 12px 0; }',
        '.lm-progress-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #8b5cf6); width: 0%; transition: width 0.3s ease; border-radius: 3px; }',

        '.lm-slider-container { display: flex; align-items: center; gap: 12px; margin-top: 8px; }',
        '.lm-slider-value { background: linear-gradient(135deg, #3b82f6, #8b5cf6); color: white; padding: 4px 10px;',
        'border-radius: 12px; font-size: 11px; font-weight: 600; min-width: 20px; text-align: center; }',

        '#lm-status { font-size: 11px; color: #10b981; background: rgba(16, 185, 129, 0.1); padding: 12px; border-radius: 8px;',
        'border: 1px solid rgba(16, 185, 129, 0.2); font-weight: 500; }',

        '#lm-meta-count { font-size: 10px; color: #9ca3af; text-align: center; margin-top: 6px; }',

        '.lm-resize-handle { position: absolute; background: transparent; transition: background 0.2s ease; }',
        '.lm-resize-se { bottom: 0; right: 0; width: 16px; height: 16px; cursor: se-resize; }',
        '.lm-resize-s { bottom: 0; left: 16px; right: 16px; height: 4px; cursor: s-resize; }',
        '.lm-resize-e { right: 0; top: 16px; bottom: 16px; width: 4px; cursor: e-resize; }',
        '.lm-resize-se:hover { background: rgba(59, 130, 246, 0.2); }',
        '.lm-resize-s:hover, .lm-resize-e:hover { background: rgba(59, 130, 246, 0.15); }',

        'body.lm-dragging { cursor: move !important; user-select: none !important; }',
        'body.lm-resizing { user-select: none !important; cursor: inherit !important; }',
        'body.lm-resizing * { pointer-events: none !important; }',

        '@keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }',
        '#lm-window { animation: fadeIn 0.3s ease-out; }',

        '@media (max-width: 768px) { #lm-window { width: calc(100vw - 40px); left: 20px; right: 20px; } }'
    ].join('\n'));

    function updateStatus(message) {
        var status = document.getElementById('lm-status');
        if (status) status.textContent = message;
    }

    function updateProgress(current, total) {
        var progressFill = document.querySelector('.lm-progress-fill');
        var metaCount = document.getElementById('lm-meta-count');

        if (progressFill) {
            var percent = total > 0 ? (current / total) * 100 : 0;
            progressFill.style.width = percent + '%';
        }

        if (metaCount) {
            metaCount.textContent = 'Processing: ' + current + '/' + total;
        }
    }

    function sanitizeFilename(filename) {
        if (!filename) return 'LearnableMeta_Export';
        return filename
            .replace(/[<>:"/\\|?*]/g, '')
            .replace(/[^\w\s\-\.]/g, '')
            .replace(/\s+/g, '_')
            .replace(/_{2,}/g, '_')
            .replace(/^_+|_+$/g, '')
            .substring(0, 100) || 'LearnableMeta_Export';
    }

    function downloadFile(content, filename, mimeType) {
        var sanitizedFilename = sanitizeFilename(filename);
        console.log('📥 Downloading:', sanitizedFilename);

        try {
            var blob = new Blob([content], { type: mimeType });
            var url = URL.createObjectURL(blob);

            var link = document.createElement('a');
            link.href = url;
            link.download = sanitizedFilename;
            link.style.display = 'none';

            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);

            setTimeout(() => URL.revokeObjectURL(url), 1000);
            updateStatus('✅ Downloaded: ' + sanitizedFilename);
        } catch (error) {
            console.error('❌ Download failed:', error);
            updateStatus('❌ Download failed: ' + error.message);
        }
    }

    function waitForTable() {
        return new Promise(function(resolve) {
            var attempts = 0;
            var maxAttempts = 20; // 10 seconds max

            function checkTable() {
                attempts++;
                var table = document.querySelector('table');
                if (table && table.querySelectorAll('td').length > 0) {
                    resolve({
                        cells: table.querySelectorAll('td'),
                        hasCheckboxes: document.querySelectorAll('input[type="checkbox"]').length > 0,
                        checkboxes: document.querySelectorAll('input[type="checkbox"]')
                    });
                } else if (attempts >= maxAttempts) {
                    throw new Error('Table not found after ' + maxAttempts + ' attempts');
                } else {
                    setTimeout(checkTable, 500);
                }
            }
            checkTable();
        });
    }

    function isMetaSelected(metaName, checkboxes) {
        if (!checkboxes || checkboxes.length === 0) return true;
        for (var i = 0; i < checkboxes.length; i++) {
            var parent = checkboxes[i].closest('tr, div, li') || checkboxes[i].parentElement;
            if (parent && parent.textContent.includes(metaName)) {
                return checkboxes[i].checked;
            }
        }
        return true;
    }

    function countSelectedMetas(tableCells, checkboxes) {
        var selectedCount = 0, totalCount = 0;
        for (var i = 0; i < tableCells.length; i++) {
            var metaName = tableCells[i].textContent.trim();
            if (metaName) {
                totalCount++;
                if (isMetaSelected(metaName, checkboxes)) selectedCount++;
            }
        }
        return { selected: selectedCount, total: totalCount };
    }

    function findContentDiv(metaName) {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < divs.length; i++) {
            if (divs[i].textContent.includes(metaName) &&
                (divs[i].querySelector('img') || divs[i].querySelector('p'))) {
                return divs[i];
            }
        }
        return null;
    }

    function extractImages(container, maxImages) {
        var imgs = container.querySelectorAll('img');
        var result = [];
        for (var i = 0; i < imgs.length && result.length < maxImages; i++) {
            var src = imgs[i].src;
            if (!src || src.indexOf('http') !== 0) continue;
            if (src.includes('logo') || src.includes('icon') || src.includes('nav') ||
                src.includes('menu') || src.includes('header') || src.includes('_app/')) continue;
            if ((imgs[i].width > 0 && imgs[i].width < 50) ||
                (imgs[i].height > 0 && imgs[i].height < 50)) continue;
            result.push(src);
        }
        return result;
    }

    function extractDescription(container) {
        var elements = container.querySelectorAll('p, li, div, span');
        var bestDescription = '';
        var bestScore = 0;

        for (var i = 0; i < elements.length; i++) {
            var text = elements[i].textContent.trim();
            if (text.length < 20 || text.length > 1000) continue;

            var lower = text.toLowerCase();
            if (lower.includes('meta list') || lower.includes('home') ||
                lower.includes('plonkit.net') || lower.includes('www.') ||
                text === 'Play' || text === 'Maps') continue;

            var score = 0;
            if (text.includes('.') || text.includes('!')) score += 10;
            if (lower.includes('note:')) score += 20;
            if (text.length > 50) score += text.length / 10;
            if (lower.includes('used') || lower.includes('typically') ||
                lower.includes('common') || lower.includes('found')) score += 5;

            if (score > bestScore) {
                bestScore = score;
                bestDescription = text;
            }
        }

        return bestDescription.replace(/Meta List[^.]*\./gi, '')
                           .replace(/Play\s*/gi, '')
                           .replace(/\s+/g, ' ')
                           .trim();
    }

    function cleanDescription(description) {
        if (!description) return '';
        return description.replace(/",LearnableMeta/g, '')
                         .replace(/,LearnableMeta/g, '')
                         .replace(/LearnableMeta$/g, '')
                         .replace(/\s+/g, ' ')
                         .trim();
    }

    function cleanMetaTitle(title) {
        return title ? title.replace(/\s*\(\d+\)\s*$/, '').trim() : '';
    }

    function setButtonsEnabled(enabled) {
        var buttons = document.querySelectorAll('.lm-button');
        for (var i = 0; i < buttons.length; i++) {
            buttons[i].disabled = !enabled;
        }
    }

    function scrapeMetas(maxImages) {
        updateStatus('⏳ Waiting for meta table...');
        setButtonsEnabled(false);

        return waitForTable().then(function(tableData) {
            var tableCells = tableData.cells;
            var checkboxes = tableData.hasCheckboxes ? Array.from(tableData.checkboxes) : null;
            var metas = [], processedCount = 0;
            var counts = countSelectedMetas(tableCells, checkboxes);

            updateStatus(checkboxes ?
                '🔍 Processing ' + counts.selected + '/' + counts.total + ' selected metas...' :
                '🔍 Processing all ' + counts.total + ' metas...');

            function processNextCell(index) {
                if (index >= tableCells.length) {
                    updateStatus('✅ Found ' + metas.length + ' metas with content');
                    setButtonsEnabled(true);
                    return Promise.resolve(metas);
                }

                var cell = tableCells[index];
                var metaName = cell.textContent.trim();
                if (!metaName || !isMetaSelected(metaName, checkboxes)) {
                    return processNextCell(index + 1);
                }

                processedCount++;
                updateProgress(processedCount, counts.selected || counts.total);
                updateStatus('📝 Processing (' + processedCount + '): ' + metaName);

                cell.scrollIntoView({ behavior: 'smooth', block: 'center' });
                cell.click();

                return sleep(800).then(function() {
                    try {
                        var contentDiv = findContentDiv(metaName);
                        if (contentDiv) {
                            var images = extractImages(contentDiv, maxImages);
                            var description = cleanDescription(extractDescription(contentDiv));
                            var cleanTitle = cleanMetaTitle(metaName);

                            if (images.length > 0 || description) {
                                metas.push({
                                    title: cleanTitle,
                                    images: images,
                                    description: description || cleanTitle
                                });
                            }
                        }
                    } catch (error) {
                        console.warn('⚠️ Error processing meta:', metaName, error);
                    }
                    return processNextCell(index + 1);
                });
            }

            return processNextCell(0);
        }).catch(function(error) {
            setButtonsEnabled(true);
            throw error;
        });
    }

    function createPerfectTxtExport(deckName, metas) {
        updateStatus('📝 Creating production-ready Anki file...');

        var timestamp = new Date().toLocaleString();
        var instructions = [
            '# 🎯 PRODUCTION ANKI IMPORT FILE',
            '# ==============================',
            '# Deck: ' + deckName,
            '# Cards: ' + metas.length,
            '# Created: ' + timestamp,
            '# Format: Premium styled cards with responsive design',
            '# Quality: Production ready with error handling',
            '#',
            '# 📥 IMPORT INSTRUCTIONS:',
            '# 1. Open Anki Desktop',
            '# 2. File → Import',
            '# 3. Select this TXT file',
            '# 4. Import Settings:',
            '#    • Type: "Text separated by tabs or semicolons"',
            '#    • Field separator: Tab',
            '#    • Field 1 → Front',
            '#    • Field 2 → Back',
            '#    • Field 3 → Tags',
            '#    • ✅ Allow HTML in fields',
            '#    • Deck: "' + deckName + '"',
            '# 5. Click Import',
            '#',
            '# 🎨 CARD DESIGN:',
            '# • Mobile-responsive layout',
            '# • High-quality image display',
            '# • Professional typography',
            '# • Optimized for learning',
            '#'
        ].join('\n') + '\n';

        var csvContent = instructions + 'Front\tBack\tTags\n';

        for (var i = 0; i < metas.length; i++) {
            var meta = metas[i];

            var front = '';
            if (meta.images.length > 0) {
                var imageStyles = 'max-width: 100%; max-height: 400px; width: auto; height: auto; display: block; margin: 15px auto; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); object-fit: contain;';

                var imageHtml = meta.images.map(function(img) {
                    return '<img src="' + img + '" alt="' + meta.title + '" style="' + imageStyles + '" loading="lazy">';
                }).join('');

                front = '<div style="text-align: center; padding: 25px; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-radius: 16px; margin: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.08); min-height: 200px; display: flex; flex-direction: column; justify-content: center;">' + imageHtml + '</div>';
            } else {
                front = '<div style="text-align: center; padding: 60px 20px; background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); border-radius: 16px; margin: 12px; color: #1e40af; font-size: 48px; min-height: 200px; display: flex; align-items: center; justify-content: center;">🗺️<div style="font-size: 16px; margin-top: 10px; color: #64748b;">No image available</div></div>';
            }

            var back = '<div style="font-family: Inter, -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; max-width: 700px; margin: 0 auto; background: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 20px 40px rgba(0,0,0,0.1); border: 1px solid #e5e7eb;"><div style="background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); color: white; padding: 32px 24px; text-align: center;"><h1 style="margin: 0; font-size: 28px; font-weight: 700; text-shadow: 0 2px 4px rgba(0,0,0,0.2); line-height: 1.2;">' + meta.title + '</h1></div><div style="padding: 32px 24px; line-height: 1.7; color: #374151;"><div style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-left: 4px solid #3b82f6; padding: 24px; border-radius: 0 12px 12px 0; font-size: 16px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); margin-bottom: 16px;">' + meta.description + '</div><div style="font-size: 12px; color: #9ca3af; text-align: center; padding-top: 16px; border-top: 1px solid #e5e7eb;">LearnableMeta Export</div></div></div>';

            var tags = 'LearnableMeta ' + deckName.replace(/\s+/g, '_') + ' geography visual_learning production_ready';

            csvContent += front + '\t' + back + '\t' + tags + '\n';
        }

        var filename = sanitizeFilename(deckName) + '.txt';
        downloadFile(csvContent, filename, 'text/plain;charset=utf-8');
    }

    function checkSelection() {
        updateStatus('🔍 Analyzing selection...');
        setButtonsEnabled(false);

        waitForTable().then(function(tableData) {
            var counts = countSelectedMetas(tableData.cells, tableData.checkboxes);
            setButtonsEnabled(true);

            if (tableData.hasCheckboxes) {
                updateStatus('📊 Selection: ' + counts.selected + ' of ' + counts.total + ' metas');
                alert('Selection Status:\n\n' +
                      '✅ Selected: ' + counts.selected + ' metas\n' +
                      '📊 Total available: ' + counts.total + ' metas\n\n' +
                      (counts.selected === 0 ?
                       '⚠️ Please select some metas to export!' :
                       '🚀 Ready to export ' + counts.selected + ' selected metas!'));
            } else {
                updateStatus('📊 Will process all ' + counts.total + ' metas');
                alert('Export Status:\n\n' +
                      '📊 Found: ' + counts.total + ' metas\n' +
                      '🚀 Will process all metas when exported\n\n' +
                      'No selection controls detected.');
            }
        }).catch(function(error) {
            setButtonsEnabled(true);
            updateStatus('❌ Error: ' + error.message);
            alert('Error checking selection:\n' + error.message);
        });
    }

    function exportData() {
        var deckName = document.getElementById('lm-deck').value.trim() || 'LearnableMeta';
        var maxImages = parseInt(document.getElementById('lm-range').value) || 2;

        updateStatus('🚀 Starting export process...');

        scrapeMetas(maxImages).then(function(metas) {
            if (metas.length === 0) {
                updateStatus('⚠️ No content found to export');
                alert('Export Failed:\n\nNo metas with content were found.\n\nTips:\n• Make sure metas are loaded\n• Check if any metas are selected\n• Verify the page has content');
                return;
            }

            updateStatus('📦 Creating download file...');
            createPerfectTxtExport(deckName, metas);

            // Success notification
            setTimeout(function() {
                updateStatus('🎉 Export completed successfully!');
            }, 1000);

        }).catch(function(error) {
            console.error('💥 Export failed:', error);
            updateStatus('❌ Export failed: ' + error.message);
            alert('Export Error:\n\n' + error.message + '\n\nPlease try again or check the console for details.');
        });
    }

    function createPanel() {
        var mapTitle = (document.querySelector('h1, h2, title') || {}).textContent || 'LearnableMeta';

        // Clean up the map title
        mapTitle = mapTitle.replace(/LearnableMeta\s*[-|]\s*/gi, '').trim();

        var window = document.createElement('div');
        window.id = 'lm-window';

        var showBtn = document.createElement('button');
        showBtn.id = 'lm-show-btn';
        showBtn.innerHTML = '📚';
        showBtn.title = 'Show Anki Exporter';

        window.innerHTML = [
            '<div id="lm-header">',
            '<div id="lm-title">📚 Anki Exporter</div>',
            '<button id="lm-hide-btn" title="Hide Window">×</button>',
            '</div>',
            '<div id="lm-content">',
            '<div class="lm-section">',
            '<label>Deck Name</label>',
            '<input id="lm-deck" type="text" value="' + sanitizeFilename(mapTitle) + '" placeholder="Enter deck name">',
            '</div>',
            '<div class="lm-section">',
            '<label>Images per Card (0-5)</label>',
            '<div class="lm-slider-container">',
            '<input id="lm-range" type="range" min="0" max="5" value="2">',
            '<span id="lm-slider-value" class="lm-slider-value">2</span>',
            '</div></div>',
            '<div class="lm-section">',
            '<button id="lm-export-main" class="lm-button primary">🚀 Export</button>',
            '<button id="lm-check-selection" class="lm-button warning">📊 Check Selection</button>',
            '<div class="lm-progress-bar"><div class="lm-progress-fill"></div></div>',
            '<div id="lm-meta-count"></div>',
            '</div>',
            '<div id="lm-status">✅ Production ready! Click Export to begin.</div>',
            '</div>',
            '<div class="lm-resize-handle lm-resize-se"></div>',
            '<div class="lm-resize-handle lm-resize-s"></div>',
            '<div class="lm-resize-handle lm-resize-e"></div>'
        ].join('');

        document.body.appendChild(window);
        document.body.appendChild(showBtn);

        // FIXED DRAG FUNCTIONALITY
        var isDragging = false;
        var dragOffset = { x: 0, y: 0 };

        var header = document.getElementById('lm-header');
        header.addEventListener('mousedown', startDrag);

        function startDrag(e) {
            if (e.target.id === 'lm-hide-btn') return;

            isDragging = true;
            var rect = window.getBoundingClientRect();

            dragOffset.x = e.clientX - rect.left;
            dragOffset.y = e.clientY - rect.top;

            window.classList.add('dragging');
            document.body.classList.add('lm-dragging');

            document.addEventListener('mousemove', onDrag);
            document.addEventListener('mouseup', stopDrag);
            e.preventDefault();
        }

        function onDrag(e) {
            if (!isDragging) return;

            var newX = e.clientX - dragOffset.x;
            var newY = e.clientY - dragOffset.y;

            var maxX = document.documentElement.clientWidth - window.offsetWidth;
            var maxY = document.documentElement.clientHeight - window.offsetHeight;

            newX = Math.max(0, Math.min(newX, maxX));
            newY = Math.max(0, Math.min(newY, maxY));

            window.style.left = newX + 'px';
            window.style.top = newY + 'px';
        }

        function stopDrag() {
            isDragging = false;
            window.classList.remove('dragging');
            document.body.classList.remove('lm-dragging');
            document.removeEventListener('mousemove', onDrag);
            document.removeEventListener('mouseup', stopDrag);
        }

        // FIXED RESIZE FUNCTIONALITY
        var isResizing = false;
        var resizeType = '';
        var resizeStart = { x: 0, y: 0, width: 0, height: 0 };

        // Use window.querySelector instead of document.querySelector
        var resizeSE = window.querySelector('.lm-resize-se');
        var resizeS = window.querySelector('.lm-resize-s');
        var resizeE = window.querySelector('.lm-resize-e');

        if (resizeSE) resizeSE.addEventListener('mousedown', function(e) { startResize(e, 'se'); });
        if (resizeS) resizeS.addEventListener('mousedown', function(e) { startResize(e, 's'); });
        if (resizeE) resizeE.addEventListener('mousedown', function(e) { startResize(e, 'e'); });

        function startResize(e, type) {
            isResizing = true;
            resizeType = type;

            var rect = window.getBoundingClientRect();
            resizeStart.x = e.clientX;
            resizeStart.y = e.clientY;
            resizeStart.width = rect.width;
            resizeStart.height = rect.height;

            window.classList.add('resizing');
            document.body.classList.add('lm-resizing');

            document.addEventListener('mousemove', onResize);
            document.addEventListener('mouseup', stopResize);
            e.preventDefault();
            e.stopPropagation();
        }

        function onResize(e) {
            if (!isResizing) return;

            var deltaX = e.clientX - resizeStart.x;
            var deltaY = e.clientY - resizeStart.y;

            var newWidth = resizeStart.width;
            var newHeight = resizeStart.height;

            if (resizeType.includes('e')) {
                newWidth = Math.max(320, Math.min(600, resizeStart.width + deltaX));
            }

            if (resizeType.includes('s')) {
                newHeight = Math.max(450, Math.min(window.innerHeight * 0.9, resizeStart.height + deltaY));
            }

            window.style.width = newWidth + 'px';
            window.style.height = newHeight + 'px';
        }

        function stopResize() {
            isResizing = false;
            window.classList.remove('resizing');
            document.body.classList.remove('lm-resizing');
            document.removeEventListener('mousemove', onResize);
            document.removeEventListener('mouseup', stopResize);
        }

        // Hide/Show functionality
        var hideBtn = document.getElementById('lm-hide-btn');
        var isHidden = false;

        hideBtn.addEventListener('click', function() {
            if (!isHidden) {
                window.classList.add('hidden');
                showBtn.style.display = 'flex';
                isHidden = true;
            }
        });

        showBtn.addEventListener('click', function() {
            if (isHidden) {
                window.classList.remove('hidden');
                showBtn.style.display = 'none';
                isHidden = false;
            }
        });

        // Event listeners
        document.getElementById('lm-range').addEventListener('input', function(e) {
            document.getElementById('lm-slider-value').textContent = e.target.value;
        });

        document.getElementById('lm-export-main').addEventListener('click', exportData);
        document.getElementById('lm-check-selection').addEventListener('click', checkSelection);

        // Keyboard shortcuts
        document.addEventListener('keydown', function(e) {
            if (e.ctrlKey || e.metaKey) {
                if (e.key === 'h' && !isHidden) {
                    e.preventDefault();
                    hideBtn.click();
                } else if (e.key === 'e' && !isHidden) {
                    e.preventDefault();
                    exportData();
                }
            }
        });
    }

    // Initialize with error handling
    setTimeout(function() {
        try {
            createPanel();
            updateStatus('✅ Ready! Select Metas for Export.');
            console.log('✅ LearnableMeta Anki Exporter v1.0 loaded successfully');
        } catch (error) {
            console.error('❌ Failed to initialize Anki Exporter:', error);
        }
    }, 1000);

})();