您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export LearnableMeta maps to Anki txt files with offline images
// ==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); })();