LearnableMeta to Anki Exporter

Export LearnableMeta maps to Anki txt files with offline images

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         LearnableMeta to Anki Exporter
// @namespace    https://learnablemeta.com/
// @version      1.2.0
// @description  Export LearnableMeta maps to Anki txt files with offline images
// @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); }); }

    // Actual GeoGuessr-style dark theme
    GM_addStyle([
        '@import url("https://fonts.googleapis.com/css2?family=Neo+Sans:wght@300;400;500;600;700&display=swap");',

        /* Main Window - GeoGuessr dark theme */
        '#lm-window { position: fixed; top: 20px; right: 20px; width: 340px; min-width: 300px; max-width: 380px;',
        'background: #1a1a1a; color: #ffffff; font-family: "Neo Sans", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;',
        'border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1);',
        'z-index: 2147483647; overflow: hidden; transition: all 0.25s ease;',
        'border: none; user-select: none; }',

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

        /* Header - GeoGuessr style */
        '#lm-header { background: #2c2c2c; padding: 12px 16px; cursor: move; user-select: none;',
        'display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #3a3a3a; }',
        '#lm-header:active { cursor: grabbing; }',

        '#lm-title { font-size: 14px; font-weight: 600; color: #ffffff; letter-spacing: 0.5px; }',

        '#lm-hide-btn { background: transparent; border: 1px solid #555; color: #ccc; width: 20px; height: 20px;',
        'border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center;',
        'font-size: 12px; transition: all 0.2s ease; font-weight: 400; }',
        '#lm-hide-btn:hover { background: #404040; border-color: #777; color: #fff; }',

        /* Show Button */
        '#lm-show-btn { position: fixed; top: 20px; right: 20px; background: #1a1a1a;',
        'border: 1px solid #555; color: #fff; width: 40px; height: 40px; border-radius: 6px; cursor: pointer;',
        'display: none; align-items: center; justify-content: center; font-size: 14px; z-index: 2147483646;',
        'box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); transition: all 0.2s ease; font-weight: 600; }',
        '#lm-show-btn:hover { background: #2c2c2c; border-color: #777; }',

        /* Content */
        '#lm-content { padding: 16px; display: flex; flex-direction: column; gap: 12px; }',

        /* Sections */
        '.lm-section { background: #242424; padding: 12px; border-radius: 6px; border: 1px solid #3a3a3a; }',

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

        /* Input Fields - GeoGuessr style */
        '.lm-section input[type=text] { width: 100%; padding: 8px 10px; border-radius: 4px;',
        'border: 1px solid #555; background: #1a1a1a; color: #ffffff; box-sizing: border-box;',
        'font-family: inherit; transition: all 0.2s ease; font-size: 13px; }',
        '.lm-section input[type=text]:focus { outline: none; border-color: #4fc3f7; background: #1e1e1e; }',

        '.lm-section input[type=range] { width: 100%; margin: 6px 0; accent-color: #4fc3f7;',
        'height: 4px; border-radius: 2px; background: #3a3a3a; }',

        /* Buttons - GeoGuessr style */
        '.lm-button { padding: 10px 12px; border: 1px solid; border-radius: 4px; font-size: 12px; cursor: pointer;',
        'margin-bottom: 6px; width: 100%; font-family: inherit; font-weight: 500;',
        'transition: all 0.2s ease; text-transform: none; letter-spacing: 0.3px; }',

        '.lm-button.primary { background: #4fc3f7; color: #000; border-color: #4fc3f7; }',
        '.lm-button.primary:hover { background: #29b6f6; border-color: #29b6f6; }',

        '.lm-button.secondary { background: transparent; color: #4fc3f7; border-color: #4fc3f7; }',
        '.lm-button.secondary:hover { background: #4fc3f7; color: #000; }',

        '.lm-button.warning { background: transparent; color: #ff9800; border-color: #ff9800; }',
        '.lm-button.warning:hover { background: #ff9800; color: #000; }',

        '.lm-button:disabled { opacity: 0.4; cursor: not-allowed; }',
        '.lm-button:disabled:hover { background: transparent !important; color: inherit !important; }',

        /* Progress Bar */
        '.lm-progress-bar { width: 100%; height: 4px; background: #3a3a3a; border-radius: 2px; overflow: hidden; margin: 8px 0; }',
        '.lm-progress-fill { height: 100%; background: #4fc3f7; width: 0%; transition: width 0.3s ease; border-radius: 2px; }',

        /* Slider */
        '.lm-slider-container { display: flex; align-items: center; gap: 10px; margin-top: 6px; }',
        '.lm-slider-value { background: #4fc3f7; color: #000; padding: 2px 6px;',
        'border-radius: 3px; font-size: 11px; font-weight: 600; min-width: 18px; text-align: center; }',

        /* Status */
        '#lm-status { font-size: 11px; color: #4fc3f7; background: rgba(79, 195, 247, 0.1); padding: 8px;',
        'border-radius: 4px; border: 1px solid rgba(79, 195, 247, 0.2); font-weight: 400; }',

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

        '#lm-credits { font-size: 9px; color: #666; text-align: center; padding: 8px 0 4px 0;',
        'border-top: 1px solid #3a3a3a; margin-top: 8px; }',

        /* Drag functionality */
        'body.lm-dragging { cursor: move !important; user-select: none !important; }',

        /* Animation */
        '@keyframes slideInRight { from { opacity: 0; transform: translateX(50px); }',
        'to { opacity: 1; transform: translateX(0); } }',
        '#lm-window { animation: slideInRight 0.3s ease-out; }',

        /* Responsive */
        '@media (max-width: 768px) { #lm-window { width: calc(100vw - 40px); 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);
        }
    }

    // NEW: Function to download image and convert to base64
    function downloadImageAsBase64(url) {
        return new Promise(function(resolve, reject) {
            fetch(url)
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Failed to fetch image: ' + response.status);
                    }
                    return response.blob();
                })
                .then(blob => {
                    var reader = new FileReader();
                    reader.onload = function() {
                        resolve(reader.result); // This is the base64 data URL
                    };
                    reader.onerror = function() {
                        reject(new Error('Failed to convert image to base64'));
                    };
                    reader.readAsDataURL(blob);
                })
                .catch(error => {
                    console.warn('Failed to download image:', url, error);
                    reject(error);
                });
        });
    }

    // NEW: Function to download image as file
    function downloadImageAsFile(url, filename, folderName) {
        return fetch(url)
            .then(response => {
                if (!response.ok) {
                    throw new Error('Failed to fetch image: ' + response.status);
                }
                return response.blob();
            })
            .then(blob => {
                // Get file extension from URL or use jpg as default
                var extension = url.split('.').pop().split('?')[0] || 'jpg';
                var fullFilename = (folderName ? folderName + '/' : '') + filename + '.' + extension;

                // Download the file
                var blobUrl = URL.createObjectURL(blob);
                var link = document.createElement('a');
                link.href = blobUrl;
                link.download = fullFilename;
                link.style.display = 'none';

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

                setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);

                return fullFilename; // Return the filename for reference in Anki
            });
    }

    // NEW: Function to save files to user-selected folder
    function saveFilesToFolder(files, folderName) {
        return new Promise(function(resolve, reject) {
            console.log('Checking folder picker support...');
            console.log('showDirectoryPicker available:', 'showDirectoryPicker' in window);
            console.log('Files to save:', files.length);

            // Check if File System Access API is supported
            if ('showDirectoryPicker' in window) {
                console.log('Using File System Access API...');

                // Use modern folder picker
                window.showDirectoryPicker({
                    mode: 'readwrite',
                    startIn: 'downloads'
                }).then(function(dirHandle) {
                    console.log('Folder selected:', dirHandle.name);
                    updateStatus('📁 Creating subfolder: ' + folderName);

                    // Create subfolder with deck name
                    return dirHandle.getDirectoryHandle(folderName, { create: true });
                }).then(function(subFolderHandle) {
                    console.log('Subfolder created:', subFolderHandle.name);
                    updateStatus('💾 Saving files to folder...');

                    // Save each file to the subfolder
                    var savePromises = files.map(function(file, index) {
                        return subFolderHandle.getFileHandle(file.filename, { create: true })
                            .then(function(fileHandle) {
                                return fileHandle.createWritable();
                            })
                            .then(function(writable) {
                                return writable.write(file.blob).then(function() {
                                    return writable.close();
                                });
                            })
                            .then(function() {
                                console.log('Saved file:', file.filename);
                                updateStatus('💾 Saved ' + (index + 1) + '/' + files.length + ' files...');
                            });
                    });

                    return Promise.all(savePromises);
                }).then(function() {
                    console.log('All files saved successfully');
                    resolve(true);
                }).catch(function(error) {
                    console.error('File System Access API error:', error);
                    if (error.name === 'AbortError') {
                        reject(new Error('Folder selection was cancelled'));
                    } else {
                        console.warn('Falling back to regular downloads');
                        downloadFilesAsFallback(files, folderName).then(resolve).catch(reject);
                    }
                });
            } else {
                console.log('File System Access API not supported, using fallback');
                // Fallback: download files with folder prefix
                downloadFilesAsFallback(files, folderName).then(resolve).catch(reject);
            }
        });
    }

    // CRC32 calculation for ZIP files
    function calculateCRC32(data) {
        var crcTable = [];
        for (var i = 0; i < 256; i++) {
            var crc = i;
            for (var j = 0; j < 8; j++) {
                crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
            }
            crcTable[i] = crc;
        }

        var crc = 0 ^ (-1);
        for (var i = 0; i < data.length; i++) {
            crc = (crc >>> 8) ^ crcTable[(crc ^ data[i]) & 0xFF];
        }
        return (crc ^ (-1)) >>> 0;
    }

    // Simplified ZIP file creation with proper CRC32
    function createZipFile(files, folderName) {
        return new Promise(function(resolve, reject) {
            try {
                updateStatus('📦 Creating ZIP file...');

                var zipParts = [];
                var centralDirEntries = [];
                var offset = 0;

                // Process each file
                Promise.all(files.map(function(file, index) {
                    return file.blob.arrayBuffer().then(function(buffer) {
                        var fileData = new Uint8Array(buffer);
                        var fileName = folderName + '/' + file.filename;
                        var fileNameBytes = new TextEncoder().encode(fileName);
                        var crc32 = calculateCRC32(fileData);

                        updateStatus('📦 Adding to ZIP: ' + (index + 1) + '/' + files.length);

                        // Local file header (30 bytes + filename)
                        var localHeader = new ArrayBuffer(30 + fileNameBytes.length);
                        var view = new DataView(localHeader);

                        view.setUint32(0, 0x04034b50, true); // Local file header signature
                        view.setUint16(4, 10, true); // Version needed to extract
                        view.setUint16(6, 0, true); // General purpose bit flag
                        view.setUint16(8, 0, true); // Compression method (stored)
                        view.setUint16(10, 0, true); // Last mod file time
                        view.setUint16(12, 0, true); // Last mod file date
                        view.setUint32(14, crc32, true); // CRC-32
                        view.setUint32(18, fileData.length, true); // Compressed size
                        view.setUint32(22, fileData.length, true); // Uncompressed size
                        view.setUint16(26, fileNameBytes.length, true); // File name length
                        view.setUint16(28, 0, true); // Extra field length

                        // Add filename to header
                        new Uint8Array(localHeader, 30).set(fileNameBytes);

                        // Store for central directory
                        centralDirEntries.push({
                            fileName: fileName,
                            fileNameBytes: fileNameBytes,
                            crc32: crc32,
                            size: fileData.length,
                            offset: offset
                        });

                        var localHeaderBytes = new Uint8Array(localHeader);
                        zipParts.push(localHeaderBytes);
                        zipParts.push(fileData);

                        offset += localHeaderBytes.length + fileData.length;

                        return { localHeaderBytes, fileData };
                    });
                })).then(function() {
                    // Create central directory
                    var centralDirOffset = offset;
                    var centralDirSize = 0;

                    centralDirEntries.forEach(function(entry) {
                        var centralHeader = new ArrayBuffer(46 + entry.fileNameBytes.length);
                        var view = new DataView(centralHeader);

                        view.setUint32(0, 0x02014b50, true); // Central directory signature
                        view.setUint16(4, 10, true); // Version made by
                        view.setUint16(6, 10, true); // Version needed to extract
                        view.setUint16(8, 0, true); // General purpose bit flag
                        view.setUint16(10, 0, true); // Compression method
                        view.setUint16(12, 0, true); // Last mod file time
                        view.setUint16(14, 0, true); // Last mod file date
                        view.setUint32(16, entry.crc32, true); // CRC-32
                        view.setUint32(20, entry.size, true); // Compressed size
                        view.setUint32(24, entry.size, true); // Uncompressed size
                        view.setUint16(28, entry.fileNameBytes.length, true); // File name length
                        view.setUint16(30, 0, true); // Extra field length
                        view.setUint16(32, 0, true); // File comment length
                        view.setUint16(34, 0, true); // Disk number start
                        view.setUint16(36, 0, true); // Internal file attributes
                        view.setUint32(38, 0, true); // External file attributes
                        view.setUint32(42, entry.offset, true); // Relative offset of local header

                        // Add filename
                        new Uint8Array(centralHeader, 46).set(entry.fileNameBytes);

                        var centralHeaderBytes = new Uint8Array(centralHeader);
                        zipParts.push(centralHeaderBytes);
                        centralDirSize += centralHeaderBytes.length;
                    });

                    // End of central directory record
                    var endRecord = new ArrayBuffer(22);
                    var endView = new DataView(endRecord);

                    endView.setUint32(0, 0x06054b50, true); // End of central dir signature
                    endView.setUint16(4, 0, true); // Number of this disk
                    endView.setUint16(6, 0, true); // Number of the disk with the start of the central directory
                    endView.setUint16(8, files.length, true); // Total number of entries in the central directory on this disk
                    endView.setUint16(10, files.length, true); // Total number of entries in the central directory
                    endView.setUint32(12, centralDirSize, true); // Size of the central directory
                    endView.setUint32(16, centralDirOffset, true); // Offset of start of central directory
                    endView.setUint16(20, 0, true); // ZIP file comment length

                    zipParts.push(new Uint8Array(endRecord));

                    // Combine all parts into final ZIP
                    var totalSize = zipParts.reduce(function(sum, part) {
                        return sum + part.byteLength;
                    }, 0);

                    var zipArray = new Uint8Array(totalSize);
                    var pos = 0;

                    zipParts.forEach(function(part) {
                        zipArray.set(part, pos);
                        pos += part.byteLength;
                    });

                    updateStatus('📦 ZIP file created successfully');
                    resolve(new Blob([zipArray], { type: 'application/zip' }));
                });

            } catch (error) {
                console.error('ZIP creation error:', error);
                reject(error);
            }
        });
    }

    // Fallback function for browsers without folder picker
    function downloadFilesAsFallback(files, folderName) {
        return new Promise(function(resolve, reject) {
            updateStatus('📦 Creating ZIP file as fallback...');

            createZipFile(files, folderName)
                .then(function(zipBlob) {
                    // Download the ZIP file
                    var zipUrl = URL.createObjectURL(zipBlob);
                    var link = document.createElement('a');
                    link.href = zipUrl;
                    link.download = folderName + '.zip';
                    link.style.display = 'none';

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

                    setTimeout(() => URL.revokeObjectURL(zipUrl), 1000);

                    updateStatus('📦 ZIP file downloaded successfully');
                    resolve(false); // Indicate fallback was used
                })
                .catch(function(error) {
                    console.error('ZIP creation failed:', error);
                    // Final fallback: individual file downloads
                    files.forEach(function(file, index) {
                        setTimeout(function() {
                            var link = document.createElement('a');
                            link.href = file.url;
                            link.download = folderName + '_' + file.filename;
                            link.style.display = 'none';

                            document.body.appendChild(link);
                            link.click();
                            document.body.removeChild(link);
                        }, index * 150);
                    });
                    resolve(false);
                });
        });
    }

    // NEW: Function to convert image to PNG and return blob
    function convertImageToPNG(url, filename) {
        return fetch(url)
            .then(response => {
                if (!response.ok) {
                    throw new Error('Failed to fetch image: ' + response.status);
                }
                return response.blob();
            })
            .then(blob => {
                return new Promise(function(resolve, reject) {
                    var img = new Image();
                    img.crossOrigin = 'anonymous';

                    img.onload = function() {
                        // Create canvas to convert to PNG
                        var canvas = document.createElement('canvas');
                        var ctx = canvas.getContext('2d');

                        canvas.width = img.naturalWidth;
                        canvas.height = img.naturalHeight;

                        // Draw image on canvas preserving transparency
                        ctx.drawImage(img, 0, 0);

                        // Convert to PNG blob
                        canvas.toBlob(function(pngBlob) {
                            resolve({
                                blob: pngBlob,
                                filename: filename + '.png'
                            });
                        }, 'image/png', 0.95);
                    };

                    img.onerror = function() {
                        reject(new Error('Failed to load image for PNG conversion'));
                    };

                    // Load image from blob
                    var imageUrl = URL.createObjectURL(blob);
                    img.src = imageUrl;
                    setTimeout(() => URL.revokeObjectURL(imageUrl), 5000);
                });
            });
    }

    // NEW: Function to convert image to PNG and return file info (no download)
    function convertImageToPNGFile(url, filename) {
        return convertImageToPNG(url, filename)
            .then(function(result) {
                return {
                    blob: result.blob,
                    filename: result.filename,
                    url: URL.createObjectURL(result.blob)
                };
            });
    }

    // NEW: Function to convert image to PNG and download (legacy)
    function downloadImageAsPNG(url, filename, folderName) {
        return convertImageToPNG(url, filename)
            .then(function(result) {
                var fullFilename = (folderName ? folderName + '/' : '') + result.filename;

                // Download the PNG file
                var blobUrl = URL.createObjectURL(result.blob);
                var link = document.createElement('a');
                link.href = blobUrl;
                link.download = fullFilename;
                link.style.display = 'none';

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

                setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
                return fullFilename;
            });
    }

    // NEW: Function to download meta description as text file
    function downloadDescriptionAsText(title, description, folderName) {
        var content = description;

        var filename = (folderName ? folderName + '/' : '') + sanitizeFilename(title) + '_description.txt';

        var blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
        var url = URL.createObjectURL(blob);

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

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

        setTimeout(() => URL.revokeObjectURL(url), 1000);

        return filename;
    }

    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 = [];
        var seenUrls = {};  // Track URLs we've already added

        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;

            // Only add if we haven't seen this URL before
            if (!seenUrls[src]) {
                seenUrls[src] = true;
                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 '';

        // Stop at footer patterns instead of removing them
        var footerPatterns = [
            /more\s+Infos?:/gi,
            /Images?\s*\(\d+\)/gi,
            /Google\s+Docs?/gi,
            /AtomoMC/gi,
            /Plonk\s+it/gi
        ];

        var cleaned = description;

        // Find the earliest footer pattern and cut off there
        var earliestIndex = cleaned.length;
        for (var i = 0; i < footerPatterns.length; i++) {
            var match = cleaned.search(footerPatterns[i]);
            if (match !== -1 && match < earliestIndex) {
                earliestIndex = match;
            }
        }

        // Cut off at the footer
        if (earliestIndex < cleaned.length) {
            cleaned = cleaned.substring(0, earliestIndex);
        }

        // Basic cleanup
        cleaned = cleaned.replace(/",LearnableMeta/g, '')
                        .replace(/,LearnableMeta/g, '')
                        .replace(/LearnableMeta$/g, '')
                        .replace(/\s+/g, ' ')
                        .trim();

        // Remove trailing punctuation if it's just hanging there
        cleaned = cleaned.replace(/[,;:\s]+$/, '');

        return cleaned;
    }

    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;
        }
    }

    // MODIFIED: Enhanced scraping with offline image support
    function scrapeMetas(maxImages, downloadMode, deckName) {
        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 imageUrls = extractImages(contentDiv, maxImages);
                            var description = cleanDescription(extractDescription(contentDiv));
                            var cleanTitle = cleanMetaTitle(metaName);

                            if (imageUrls.length > 0 || description) {
                                var metaData = {
                                    title: cleanTitle,
                                    imageUrls: imageUrls,
                                    description: description || cleanTitle,
                                    images: [] // Will be populated with processed images
                                };

                                // Process images based on download mode
                                if (downloadMode === 'base64' && imageUrls.length > 0) {
                                    updateStatus('🖼️ Converting images to base64: ' + cleanTitle);
                                    return Promise.all(imageUrls.map(downloadImageAsBase64))
                                        .then(function(base64Images) {
                                            metaData.images = base64Images.filter(img => img);
                                            if (metaData.images.length > 0 || description) {
                                                metas.push(metaData);
                                            }
                                            return processNextCell(index + 1);
                                        })
                                        .catch(function(error) {
                                            console.warn('Failed to convert images for:', cleanTitle, error);
                                            metas.push(metaData); // Add without images
                                            return processNextCell(index + 1);
                                        });
                                } else if (downloadMode === 'files' && imageUrls.length > 0) {
                                    updateStatus('📁 Downloading image files: ' + cleanTitle);
                                    var folderName = sanitizeFilename(deckName || 'LearnableMeta');
                                    var promises = imageUrls.map(function(url, imgIndex) {
                                        var filename = sanitizeFilename(cleanTitle) + '_' + imgIndex;
                                        return downloadImageAsFile(url, filename, folderName)
                                            .catch(function(error) {
                                                console.warn('Failed to download image:', url, error);
                                                return null;
                                            });
                                    });

                                    return Promise.all(promises)
                                        .then(function(filenames) {
                                            metaData.imageFiles = filenames.filter(f => f);
                                            if (metaData.imageFiles.length > 0 || description) {
                                                metas.push(metaData);
                                            }
                                            return processNextCell(index + 1);
                                        });
                                } else if (downloadMode === 'png') {
                                    // PNG mode - download images as PNG and description as TXT
                                    var folderName = sanitizeFilename(deckName || 'LearnableMeta');

                                    var promises = [];

                                    // Download images if any
                                    if (imageUrls.length > 0) {
                                        updateStatus('🖼️ Converting to PNG: ' + cleanTitle);
                                        promises = imageUrls.map(function(url, imgIndex) {
                                            var filename = sanitizeFilename(cleanTitle) + '_' + imgIndex;
                                            return downloadImageAsPNG(url, filename, folderName)
                                                .catch(function(error) {
                                                    console.warn('Failed to convert image to PNG:', url, error);
                                                    return null;
                                                });
                                        });
                                    }

                                    return Promise.all(promises)
                                        .then(function(filenames) {
                                            metaData.imageFiles = filenames.filter(f => f);

                                            // Download description as text file
                                            if (description) {
                                                updateStatus('📝 Downloading description: ' + cleanTitle);
                                                try {
                                                    var descFile = downloadDescriptionAsText(cleanTitle, description, folderName);
                                                    metaData.descriptionFile = descFile;
                                                } catch (error) {
                                                    console.warn('Failed to download description:', error);
                                                }
                                            }

                                            if (metaData.imageFiles.length > 0 || description) {
                                                metas.push(metaData);
                                            }
                                            return processNextCell(index + 1);
                                        });
                                } else {
                                    // Online mode - just use URLs
                                    metaData.images = imageUrls;
                                    metas.push(metaData);
                                    return processNextCell(index + 1);
                                }
                            }
                        }
                    } catch (error) {
                        console.warn('⚠️ Error processing meta:', metaName, error);
                    }
                    return processNextCell(index + 1);
                });
            }

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

    // MODIFIED: Enhanced export with offline image support
    function createPerfectTxtExport(deckName, metas, downloadMode) {
        updateStatus('📝 Creating production-ready Anki file...');

        var timestamp = new Date().toLocaleString();
        var instructions = [
            '# 🎯 PRODUCTION ANKI IMPORT FILE (' + downloadMode.toUpperCase() + ' IMAGES)',
            '# ===============================================',
            '# Deck: ' + deckName,
            '# Cards: ' + metas.length,
            '# Created: ' + timestamp,
            '# Format: Premium styled cards with responsive design',
            '# Images: ' + (downloadMode === 'base64' ? 'Embedded offline base64' :
                           downloadMode === 'files' ? 'Downloaded files (import separately)' : 'Online URLs'),
            '# Quality: Production ready with error handling',
            '#',
            '# 📥 IMPORT INSTRUCTIONS:',
            '# 1. Open Anki Desktop',
            downloadMode === 'files' ? '# 2. Import downloaded image files to your media folder first' : '',
            '# ' + (downloadMode === 'files' ? '3' : '2') + '. File → Import',
            '# ' + (downloadMode === 'files' ? '4' : '3') + '. Select this TXT file',
            '# ' + (downloadMode === 'files' ? '5' : '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 + '"',
            '# ' + (downloadMode === 'files' ? '6' : '5') + '. Click Import',
            '#',
            '# 🎨 CARD DESIGN:',
            '# • Mobile-responsive layout',
            '# • High-quality image display',
            '# • Professional typography',
            '# • Optimized for learning',
            downloadMode === 'base64' ? '# • Fully offline capable' : '',
            '#'
        ].filter(line => line).join('\n') + '\n';

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

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

            var front = '';
            var hasImages = false;

            if (downloadMode === 'base64' && meta.images && meta.images.length > 0) {
                // Use base64 embedded images
                hasImages = true;
                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(base64Data) {
                    return '<img src="' + base64Data + '" 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 if ((downloadMode === 'files' || downloadMode === 'png') && meta.imageFiles && meta.imageFiles.length > 0) {
                // Use downloaded file references (PNG or original format)
                hasImages = true;
                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.imageFiles.map(function(filename) {
                    return '<img src="' + filename + '" 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 if (downloadMode === 'online' && meta.imageUrls && meta.imageUrls.length > 0) {
                // Use online URLs (original behavior)
                hasImages = true;
                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.imageUrls.map(function(url) {
                    return '<img src="' + url + '" 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>';
            }

            if (!hasImages) {
                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 (' + downloadMode + ')</div></div></div>';

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

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

        var filename = sanitizeFilename(deckName) + '_' + downloadMode + '.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);
        });
    }

    // MODIFIED: Export functions for different modes
    function exportWithBase64() {
        var deckName = document.getElementById('lm-deck').value.trim() || 'LearnableMeta';
        var maxImages = parseInt(document.getElementById('lm-range').value) || 2;

        updateStatus('🚀 Starting export with embedded images...');

        scrapeMetas(maxImages, 'base64', deckName).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 with embedded images...');
            createPerfectTxtExport(deckName, metas, 'base64');

            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 exportWithFiles() {
        var deckName = document.getElementById('lm-deck').value.trim() || 'LearnableMeta';
        var maxImages = parseInt(document.getElementById('lm-range').value) || 2;

        updateStatus('🚀 Starting export with separate image files...');

        scrapeMetas(maxImages, 'files', deckName).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 Anki file with image references...');
            createPerfectTxtExport(deckName, metas, 'files');

            setTimeout(function() {
                updateStatus('🎉 Export completed! Images downloaded separately.');
                alert('Export Complete!\n\nThe Anki file has been created and images have been downloaded as separate files.\n\nTo import:\n1. Copy image files to your Anki media folder\n2. Import the TXT file into Anki');
            }, 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.');
        });
    }

    // NEW: Scrape metas and collect all files for raw export
    function scrapeMetasForRawFiles(maxImages, deckName) {
        updateStatus('⏳ Waiting for meta table...');
        setButtonsEnabled(false);

        var allFiles = []; // Collect all files here

        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: metas, allFiles: allFiles });
                }

                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 imageUrls = extractImages(contentDiv, maxImages);
                            var description = cleanDescription(extractDescription(contentDiv));
                            var cleanTitle = cleanMetaTitle(metaName);

                            if (imageUrls.length > 0 || description) {
                                var metaData = {
                                    title: cleanTitle,
                                    imageUrls: imageUrls,
                                    description: description || cleanTitle,
                                    imageFiles: []
                                };

                                var promises = [];

                                // Convert images to PNG files
                                if (imageUrls.length > 0) {
                                    updateStatus('🖼️ Converting to PNG: ' + cleanTitle);
                                    promises = imageUrls.map(function(url, imgIndex) {
                                        var filename = sanitizeFilename(cleanTitle) + '_' + imgIndex;
                                        return convertImageToPNGFile(url, filename)
                                            .then(function(fileInfo) {
                                                allFiles.push(fileInfo);
                                                return fileInfo.filename;
                                            })
                                            .catch(function(error) {
                                                console.warn('Failed to convert image to PNG:', url, error);
                                                return null;
                                            });
                                    });
                                }

                                return Promise.all(promises)
                                    .then(function(filenames) {
                                        metaData.imageFiles = filenames.filter(f => f);

                                        // Create description file
                                        if (description) {
                                            updateStatus('📝 Creating description file: ' + cleanTitle);
                                            var descContent = description;
                                            var descFilename = sanitizeFilename(cleanTitle) + '_description.txt';
                                            var descBlob = new Blob([descContent], { type: 'text/plain;charset=utf-8' });

                                            allFiles.push({
                                                blob: descBlob,
                                                filename: descFilename,
                                                url: URL.createObjectURL(descBlob)
                                            });

                                            metaData.descriptionFile = descFilename;
                                        }

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

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

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

        updateStatus('🚀 Starting export with PNG conversion...');

        scrapeMetasForRawFiles(maxImages, deckName).then(function(result) {
            if (result.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 Anki file and organizing raw files...');

            // Create the Anki TXT file
            createPerfectTxtExport(deckName, result.metas, 'png');

            // Save all raw files to user-selected folder
            if (result.allFiles.length > 0) {
                updateStatus('📁 Choose folder to save raw files...');

                saveFilesToFolder(result.allFiles, sanitizeFilename(deckName))
                    .then(function(usedFolderPicker) {
                        if (usedFolderPicker) {
                            updateStatus('🎉 Export completed! Raw files saved to selected folder.');
                            alert('Raw Files Export Complete!\n\n✅ Images converted to PNG format\n✅ Descriptions saved as TXT files\n✅ All files saved to "' + deckName + '" folder in your chosen directory\n\nTo import:\n1. Copy files from the saved folder to your Anki media folder\n2. Import the TXT file into Anki');
                        } else {
                            updateStatus('🎉 Export completed! Raw files packaged in ZIP.');
                            alert('Raw Files Export Complete!\n\n✅ Images converted to PNG format\n✅ Descriptions saved as TXT files\n✅ All files packaged in "' + deckName + '.zip"\n\nNote: Your browser doesn\'t support folder picker, so files were packaged in a ZIP file.\n\nTo import:\n1. Extract the ZIP file to see the organized folder\n2. Copy files from the extracted folder to your Anki media folder\n3. Import the TXT file into Anki');
                        }
                    })
                    .catch(function(error) {
                        if (error.message.includes('cancelled')) {
                            updateStatus('❌ Export cancelled - folder not selected.');
                            alert('Export cancelled: No folder was selected for saving raw files.');
                        } else {
                            updateStatus('❌ Failed to save raw files: ' + error.message);
                            alert('Failed to save raw files:\n\n' + error.message + '\n\nPlease try again or check browser permissions.');
                        }
                    });
            } else {
                setTimeout(function() {
                    updateStatus('🎉 Export completed! No raw files to save.');
                    alert('Export Complete!\n\nAnki TXT file created 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 testFolderPicker() {
        updateStatus('🧪 Testing folder picker capability...');

        if ('showDirectoryPicker' in window) {
            updateStatus('📁 Please select a folder to test...');

            window.showDirectoryPicker({
                mode: 'readwrite',
                startIn: 'downloads'
            }).then(function(dirHandle) {
                updateStatus('✅ Folder picker works! Selected: ' + dirHandle.name);
                alert('✅ Folder Picker Test Successful!\n\nSelected folder: ' + dirHandle.name + '\n\nYour browser supports folder selection for raw file exports.');
            }).catch(function(error) {
                if (error.name === 'AbortError') {
                    updateStatus('❌ Test cancelled - no folder selected.');
                    alert('Test cancelled: No folder was selected.');
                } else {
                    updateStatus('❌ Folder picker failed: ' + error.message);
                    alert('❌ Folder Picker Test Failed!\n\nError: ' + error.message + '\n\nYour browser may not support folder selection or permissions were denied.');
                }
            });
        } else {
            updateStatus('❌ Folder picker not supported by browser.');
            alert('❌ Folder Picker Not Supported!\n\nYour browser doesn\'t support folder selection.\n\nRaw file exports will use fallback mode (files with prefixes).\n\nSupported browsers: Chrome, Edge (latest versions)');
        }
    }

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

        updateStatus('🚀 Starting export with online images...');

        scrapeMetas(maxImages, 'online', deckName).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, 'online');

            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 = 'A';
        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</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-png" class="lm-button primary">Export Raw Files ZIP</button>',
            '<button id="lm-export-online" class="lm-button secondary">Export to Anki</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">Ready to export LearnableMeta content</div>',
            '<div id="lm-credits">Made by BennoGHG</div>',
            '</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 };

        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 for NEW export modes
        document.getElementById('lm-range').addEventListener('input', function(e) {
            document.getElementById('lm-slider-value').textContent = e.target.value;
        });

        document.getElementById('lm-export-png').addEventListener('click', exportWithPNG);
        document.getElementById('lm-export-online').addEventListener('click', exportOnline);
        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();
                    exportWithPNG(); // Default to PNG export
                }
            }
        });
    }

    // Initialize with error handling
    setTimeout(function() {
        try {
            createPanel();
            updateStatus('Ready to export LearnableMeta content');
            console.log('LearnableMeta Anki Exporter v1.2 loaded successfully');
        } catch (error) {
            console.error('❌ Failed to initialize Anki Exporter:', error);
        }
    }, 1000);

})();