// ==UserScript== // @name Arena.ai Lightbox Enhanced + Batch Download // @namespace http://tampermonkey.net/ // @version 6.1.1 // @description Lightbox with original/edited detection, EXIF metadata, zoom, touch + Batch download with progress overlay - Enhanced & Secure // @author TheSicknesszar // @match https://arena.ai/* // @match https://chat.lmsys.org/* // @match https://lmarena.ai/* // @match https://www.lmarena.ai/* // @icon https://arena.ai/favicon.ico // @grant GM_addStyle // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_download // @grant GM_openInTab // @grant GM_info // @connect self // @connect *.arena.ai // @connect *.lmsys.org // @connect *.lmarena.ai // @connect *.cloudflarestream.com // @connect *.gstatic.com // @connect *.googleusercontent.com // @run-at document-idle // @updateURL https://raw.githubusercontent.com/TheSicknesszar/my-tampermonkey-userscripts/main/Arena-AI_Lightbox_Enhanced.js // @downloadURL https://raw.githubusercontent.com/TheSicknesszar/my-tampermonkey-userscripts/main/Arena-AI_Lightbox_Enhanced.js // ==/UserScript== (function() { 'use strict'; // ================== CONFIGURATION ================== const DEFAULT_CONFIG = { // Image Detection MIN_IMG_WIDTH: 50, PRELOAD_NEIGHBORS: 2, TOUCH_SWIPE_THRESHOLD: 50, LOADING_TIMEOUT: 10000, DEBOUNCE_DELAY: 250, ZOOM_STEP: 0.2, MAX_ZOOM: 4, MIN_ZOOM: 0.1, // Batch Download Settings BATCH_DELAY_MS: 600, DOWNLOAD_PREFIX: '', MAX_FILENAME_LENGTH: 150, RETRY_COUNT: 2, RETRY_DELAY_MS: 1000, // Image Selectors IMAGE_SELECTORS: [ 'img[src*="blob:"]', 'img[src*="data:image"]', 'img[src*="storage"]', 'img[src*="cdn"]', 'img[src*="upload"]', 'img[src*="image"]', '.image-container img', '.chat-image img', '.message-content img', '.response-image img', '[data-testid*="image"] img', '[class*="image"] img', '[class*="Image"] img', 'figure img', '.markdown img', '.prose img', 'img' ], // Exclude Selectors EXCLUDE_SELECTORS: [ '.avatar', '.icon', '.logo', '.emoji', '[class*="avatar"]', '[class*="icon"]', '[class*="logo"]', '[class*="emoji"]', 'nav img', 'header img', 'footer img' ], // Original/Edited Detection EXIF_CHECK_ENABLED: true, FILENAME_PATTERNS: { ORIGINAL: [ /original/i, /source/i, /input/i, /reference/i, /base/i, /initial/i, /^img_/i, /^photo_/i, /^DSC_/i, /^IMG_\d+/i, /^PXL_/i, /before/i, /raw/i ], EDITED: [ /edited/i, /modified/i, /enhanced/i, /output/i, /result/i, /final/i, /processed/i, /_edit/i, /edit_/i, /_processed/i, /_enhanced/i, /_ai_/i, /_generated/i, /_styled/i, /_stablediffusion/i, /_midjourney/i, /_dalle/i, /_ai_generated/i, /_generated_by/i, /_by_ai/i, /_ai_art/i, /_neural/i, /_ml_/i, /_gan_/i, /_photoshop/i, /_ps_/i, /_retouched/i, /_filtered/i, /_style_transfer/i, /after/i, /v\d+/i ] }, USER_PATTERNS: { ORIGINAL: [], EDITED: [] } }; // Load user config with persistence const CONFIG = loadConfig(); function loadConfig() { const saved = GM_getValue('lma_userConfig', {}); const merged = { ...DEFAULT_CONFIG, ...saved }; if (saved.USER_PATTERNS) { merged.USER_PATTERNS = { ORIGINAL: (saved.USER_PATTERNS.ORIGINAL || []).map(p => typeof p === 'string' ? new RegExp(p, 'i') : p), EDITED: (saved.USER_PATTERNS.EDITED || []).map(p => typeof p === 'string' ? new RegExp(p, 'i') : p) }; } return merged; } function saveConfig(partialConfig) { const updated = { ...DEFAULT_CONFIG, ...partialConfig }; GM_setValue('lma_userConfig', updated); Object.assign(CONFIG, updated); } // ================== STATE ================== let state = { allImages: [], currentIndex: 0, isOpen: false, isLoading: false, observer: null, preloadedImages: new Map(), // src -> blobUrl imageMetadata: new Map(), // imgElement -> metadata exifCache: new Map(), // urlHash -> metadata touchStartX: 0, touchStartY: 0, metadataPanelVisible: false, zoomState: { scale: 1, rotation: 0, panX: 0, panY: 0 }, lastFocusedElement: null, isDragging: false, dragStart: { x: 0, y: 0 }, initialized: false, // Batch Download State batchCancelled: false, batchInProgress: false, batchSelectionMode: false, selectedBatchIndices: new Set() }; // ================== DOM ELEMENTS ================== const elements = {}; // ================== UTILITY FUNCTIONS ================== function log(message, type = 'info') { const prefix = '[LMA Lightbox]'; switch(type) { case 'error': console.error(prefix, message); break; case 'warn': console.warn(prefix, message); break; default: console.log(prefix, message); } } function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function escapeHtml(str) { if (typeof str !== 'string') return String(str); const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } async function simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36); } function sanitizeFilename(str) { return str .replace(/[<>:"/\\|?*]/g, '_') .replace(/\s+/g, '_') .replace(/_{2,}/g, '_') .replace(/^_+|_+$/g, '') .substring(0, CONFIG.MAX_FILENAME_LENGTH); } function getPageEntityName() { const og = document.querySelector('meta[property="og:title"]'); if (og?.content) return sanitizeFilename(og.content.split(' - ')[0].trim()); if (document.title) { const t = document.title.replace(/\s*[–—-]\s*(Official Site|Photos|Gallery).*$/i, '').trim(); if (t) return sanitizeFilename(t); } return 'arena_images'; } function getThumbnailUrl(url) { if (!url) return url; if (url.startsWith('data:') || url.startsWith('blob:')) return url; const hasQuery = url.includes('?'); return url + (hasQuery ? '&' : '?') + 'w=100&h=100&fit=crop'; } // Blob URL Management - Prevent Memory Leaks function createBlobUrl(blob) { return URL.createObjectURL(blob); } function revokeBlobUrl(url) { if (url && url.startsWith('blob:')) { try { URL.revokeObjectURL(url); } catch(e) { /* ignore */ } } } function cleanupAllBlobUrls() { for (const [src, blobUrl] of state.preloadedImages) { revokeBlobUrl(blobUrl); } state.preloadedImages.clear(); } // Shared fetchAsBlob helper - reduces GM_xmlhttpRequest duplication function fetchAsBlob(url) { return new Promise((resolve, reject) => { if (!url || url.startsWith('data:')) { resolve(null); return; } GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(resp) { if (resp.status >= 200 && resp.status < 300) { const blobUrl = createBlobUrl(resp.response); resolve(blobUrl); } else { resolve(null); } }, onerror: function() { resolve(null); } }); }); } // ================== CSS STYLES (with CSS Variables & Fallbacks) ================== const css = ` :root { --lma-bg-overlay: rgba(10, 10, 10, 0.97); --lma-bg-panel: rgba(0, 0, 0, 0.9); --lma-bg-card: #1a1a2e; --lma-bg-thumb: #0d0d1a; --lma-text-primary: #fff; --lma-text-secondary: #aaa; --lma-text-muted: #888; --lma-border: rgba(255,255,255,0.15); --lma-primary: #4CAF50; --lma-primary-dim: rgba(76, 175, 80, 0.15); --lma-warning: #FF9800; --lma-warning-dim: rgba(255, 152, 0, 0.15); --lma-error: #F44336; --lma-error-dim: rgba(244, 67, 54, 0.15); --lma-accent: #2196F3; --lma-accent-dim: rgba(33, 150, 243, 0.15); --lma-gold: #f5c518; --lma-zindex-base: 100000; --lma-zindex-overlay: 100002; --lma-transition-fast: 0.2s ease; --lma-transition-normal: 0.3s ease; } /* === LIGHTBOX CORE === */ #lma-lightbox { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: var(--lma-bg-overlay); z-index: var(--lma-zindex-base); display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity var(--lma-transition-normal); backdrop-filter: blur(8px); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; overflow: hidden; } @supports not (backdrop-filter: blur(8px)) { #lma-lightbox { background: rgba(10, 10, 10, 0.97); } } #lma-lightbox.active { opacity: 1; pointer-events: auto; } .lma-image-container { position: relative; max-width: 90%; max-height: 80vh; cursor: grab; user-select: none; } .lma-image-container.dragging { cursor: grabbing; } #lma-main-img { max-width: 100%; max-height: 80vh; object-fit: contain; box-shadow: 0 0 40px rgba(0,0,0,0.6); border-radius: 6px; opacity: 1; transition: opacity var(--lma-transition-normal), transform var(--lma-transition-normal); transform-origin: center center; } #lma-main-img.loading { opacity: 0.3; } /* === NAVIGATION BUTTONS === */ .lma-nav-btn { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(255, 255, 255, 0.08); color: #eee; border: 1px solid var(--lma-border); padding: 0; font-size: 26px; cursor: pointer; border-radius: 50%; width: 64px; height: 64px; display: flex; align-items: center; justify-content: center; transition: all var(--lma-transition-fast); user-select: none; z-index: 100; } .lma-nav-btn:hover, .lma-nav-btn:focus { background: rgba(255, 255, 255, 0.2); color: white; border-color: white; transform: translateY(-50%) scale(1.05); outline: 2px solid rgba(255,255,255,0.3); } .lma-nav-btn:active { transform: translateY(-50%) scale(0.95); } .lma-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; transform: translateY(-50%); } #lma-prev { left: 30px; } #lma-next { right: 30px; } /* === TOOLBAR === */ .lma-toolbar { position: absolute; top: 25px; display: flex; gap: 15px; z-index: 100; } .lma-toolbar-left { left: 35px; } .lma-toolbar-right { right: 35px; } .lma-tool-btn { background: rgba(0, 0, 0, 0.6); color: var(--lma-text-primary); border: 1px solid var(--lma-border); border-radius: 10px; cursor: pointer; padding: 12px 18px; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all var(--lma-transition-fast); } .lma-tool-btn:hover, .lma-tool-btn:focus { background: white; color: black; outline: 2px solid rgba(255,255,255,0.5); } .lma-tool-btn svg { width: 18px; height: 18px; fill: currentColor; } #lma-close { font-size: 28px; padding: 8px 18px; line-height: 1; } .lma-tool-btn:disabled { opacity: 0.5; cursor: not-allowed; } /* === ZOOM CONTROLS === */ .lma-zoom-controls { position: absolute; bottom: 120px; right: 35px; display: flex; gap: 10px; z-index: 100; } .lma-zoom-btn { background: rgba(0, 0, 0, 0.6); color: var(--lma-text-primary); border: 1px solid var(--lma-border); border-radius: 50%; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 20px; transition: all var(--lma-transition-fast); } .lma-zoom-btn:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.1); } .lma-zoom-btn:active { transform: scale(0.95); } /* === INFO AREA === */ .lma-info-area { position: absolute; bottom: 35px; display: flex; flex-direction: column; align-items: center; gap: 10px; width: 100%; pointer-events: none; } #lma-filename { color: var(--lma-text-primary); font-size: 16px; background: rgba(0, 0, 0, 0.75); padding: 10px 20px; border-radius: 8px; max-width: 80%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: auto; border: 1px solid var(--lma-border); } #lma-counter { color: var(--lma-text-secondary); font-size: 14px; background: rgba(0,0,0,0.7); padding: 6px 14px; border-radius: 20px; } /* === METADATA PANEL === */ #lma-metadata-panel { position: absolute; top: 85px; left: 35px; background: var(--lma-bg-panel); border: 1px solid var(--lma-border); border-radius: 12px; padding: 20px; color: var(--lma-text-primary); font-size: 13px; max-width: 320px; max-height: 70vh; overflow-y: auto; z-index: 99; display: none; backdrop-filter: blur(10px); box-shadow: 0 10px 30px rgba(0,0,0,0.5); animation: lma-slide-in 0.2s ease-out; } @supports not (backdrop-filter: blur(10px)) { #lma-metadata-panel { background: rgba(0, 0, 0, 0.95); } } @keyframes lma-slide-in { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } #lma-metadata-panel.active { display: block; } .lma-metadata-title { font-weight: bold; margin-bottom: 15px; color: var(--lma-primary); font-size: 15px; display: flex; align-items: center; gap: 8px; } .lma-metadata-section { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid var(--lma-border); } .lma-metadata-section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; } .lma-metadata-section-title { font-size: 12px; color: var(--lma-text-secondary); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; } .lma-metadata-item { margin-bottom: 8px; display: flex; justify-content: space-between; align-items: flex-start; } .lma-metadata-label { color: #ccc; min-width: 120px; font-size: 12px; } .lma-metadata-value { color: var(--lma-text-primary); font-family: 'SF Mono', Monaco, Consolas, monospace; text-align: right; max-width: 180px; overflow: hidden; text-overflow: ellipsis; font-size: 12px; line-height: 1.4; } /* === TYPE INDICATORS === */ .lma-type-indicator-metadata { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-left: 8px; border: 2px solid currentColor; } .lma-type-indicator-metadata.original { background: var(--lma-primary-dim); color: var(--lma-primary); border-color: var(--lma-primary); } .lma-type-indicator-metadata.edited { background: var(--lma-accent-dim); color: var(--lma-accent); border-color: var(--lma-accent); } .lma-type-indicator-metadata.unknown { background: rgba(158, 158, 158, 0.15); color: #9E9E9E; border-color: #9E9E9E; } /* === CONFIDENCE METER === */ .lma-confidence-meter { width: 100%; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; margin-top: 5px; overflow: hidden; } .lma-confidence-fill { height: 100%; border-radius: 3px; transition: width var(--lma-transition-normal); } .lma-confidence-fill.high { background: linear-gradient(90deg, var(--lma-primary), #8BC34A); } .lma-confidence-fill.medium { background: linear-gradient(90deg, var(--lma-warning), #FFC107); } .lma-confidence-fill.low { background: linear-gradient(90deg, var(--lma-error), var(--lma-warning)); } /* === LOADING SPINNER === */ .lma-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 50px; height: 50px; border: 3px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: white; animation: lma-spin 1s linear infinite; z-index: 10; display: none; } .lma-loading.active { display: block; } @keyframes lma-spin { to { transform: translate(-50%, -50%) rotate(360deg); } } /* === HELP MODAL === */ #lma-help-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.95); color: var(--lma-text-primary); padding: 30px; border-radius: 12px; z-index: calc(var(--lma-zindex-base) + 1); max-width: 450px; width: 90%; border: 1px solid var(--lma-border); box-shadow: 0 20px 60px rgba(0,0,0,0.5); display: none; max-height: 80vh; overflow-y: auto; } #lma-help-modal.active { display: block; animation: lma-fade-in 0.2s ease-out; } @keyframes lma-fade-in { from { opacity: 0; transform: translate(-50%, -45%); } to { opacity: 1; transform: translate(-50%, -50%); } } .lma-help-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid var(--lma-border); } .lma-help-title { font-size: 20px; font-weight: 600; color: var(--lma-primary); } .lma-help-close { background: none; border: none; color: var(--lma-text-primary); font-size: 28px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .lma-help-close:hover { background: rgba(255,255,255,0.1); } .lma-help-section { margin-bottom: 20px; } .lma-help-section-title { font-size: 14px; color: var(--lma-text-secondary); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1px; } .lma-help-shortcuts { display: grid; grid-template-columns: 1fr; gap: 8px; } .lma-help-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--lma-border); } .lma-help-key { font-family: 'SF Mono', Monaco, Consolas, monospace; background: rgba(255,255,255,0.1); padding: 4px 8px; border-radius: 4px; } /* === BATCH DOWNLOAD OVERLAY === */ @keyframes lma-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.3 } } @keyframes lma-pop { 0% { transform: scale(1) } 50% { transform: scale(1.12) } 100% { transform: scale(1) } } @keyframes lma-glow { 0% { box-shadow: 0 0 10px rgba(39,174,96,0.8) } 100% { box-shadow: none } } #lma-batch-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.85); z-index: var(--lma-zindex-overlay); display: flex; align-items: center; justify-content: center; font-family: system-ui, sans-serif; } .lma-batch-card { background: var(--lma-bg-card); color: #eee; padding: 24px 28px; border-radius: 14px; width: 560px; max-width: 92vw; box-shadow: 0 12px 40px rgba(0,0,0,0.6); max-height: 88vh; display: flex; flex-direction: column; } .lma-batch-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-shrink: 0; } .lma-batch-header h3 { margin: 0; color: var(--lma-gold); } .lma-batch-folder { font-size: 11px; color: var(--lma-text-muted); background: #111; padding: 3px 10px; border-radius: 6px; } #lma-batch-status { margin-bottom: 8px; flex-shrink: 0; font-size: 14px; } .lma-batch-bar-container { background: #333; border-radius: 8px; overflow: hidden; height: 22px; margin-bottom: 6px; flex-shrink: 0; } #lma-batch-bar { height: 100%; width: 0%; border-radius: 8px; background: linear-gradient(90deg, var(--lma-gold), #e6b800); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: #000; transition: width var(--lma-transition-normal); } #lma-batch-file { font-size: 11px; color: var(--lma-text-muted); word-break: break-all; min-height: 16px; margin-bottom: 10px; flex-shrink: 0; } #lma-batch-grid { display: flex; flex-wrap: wrap; gap: 5px; overflow-y: auto; margin-bottom: 14px; padding: 8px; border-radius: 8px; background: var(--lma-bg-thumb); min-height: 80px; max-height: 45vh; align-content: flex-start; } .lma-batch-thumb { position: relative; width: 68px; height: 68px; border-radius: 6px; overflow: hidden; border: 2px solid #333; opacity: 0.35; transition: all var(--lma-transition-normal); flex-shrink: 0; background: #222; cursor: pointer; } .lma-batch-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } .lma-batch-thumb-badge { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.75); text-align: center; font-size: 9px; padding: 2px 0; color: #777; font-weight: 600; transition: all var(--lma-transition-fast); } .lma-batch-thumb.active { opacity: 1; border-color: var(--lma-gold); box-shadow: 0 0 10px rgba(245,197,24,0.4); } .lma-batch-thumb.active .lma-batch-thumb-badge { background: rgba(245,197,24,0.85); color: #000; } .lma-batch-thumb.done { opacity: 1; border-color: #27ae60; animation: lma-glow 0.6s ease; } .lma-batch-thumb.done .lma-batch-thumb-badge { background: rgba(39,174,96,0.85); color: #fff; } .lma-batch-thumb.fail { opacity: 0.55; border-color: var(--lma-error); } .lma-batch-thumb.fail .lma-batch-thumb-badge { background: rgba(231,76,60,0.85); color: #fff; } .lma-batch-thumb.selected { border-color: var(--lma-accent); box-shadow: 0 0 0 2px var(--lma-accent-dim); } .lma-batch-thumb.selected .lma-batch-thumb-badge { background: var(--lma-accent); color: #fff; } .lma-batch-footer { display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } .lma-batch-stats { font-size: 13px; color: var(--lma-text-muted); } .lma-batch-stats span { font-weight: 700; } #lma-batch-ok { color: #27ae60; } #lma-batch-fail { color: var(--lma-error); } #lma-batch-cancel { background: var(--lma-error); color: #fff; border: none; padding: 8px 22px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; transition: background var(--lma-transition-fast); } #lma-batch-cancel:hover { background: #c0392b; } /* === CONFIRM MODAL === */ #lma-confirm-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.85); z-index: calc(var(--lma-zindex-overlay) + 2); display: flex; align-items: center; justify-content: center; font-family: system-ui, sans-serif; } .lma-confirm-card { background: var(--lma-bg-card); color: #eee; padding: 24px 28px; border-radius: 14px; width: 400px; max-width: 90vw; box-shadow: 0 12px 40px rgba(0,0,0,0.6); display: flex; flex-direction: column; gap: 16px; } .lma-confirm-card p { margin: 0; font-size: 15px; line-height: 1.5; } .lma-confirm-actions { display: flex; justify-content: flex-end; gap: 10px; } .lma-confirm-actions button { padding: 8px 22px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; border: none; transition: background var(--lma-transition-fast); } .lma-confirm-cancel { background: #555; color: #fff; } .lma-confirm-cancel:hover { background: #777; } .lma-confirm-ok { background: var(--lma-primary); color: #fff; } .lma-confirm-ok:hover { background: #388E3C; } /* === BATCH BUTTON IN LIGHTBOX === */ #lma-batch-btn { background: linear-gradient(135deg, #3498db, #2980b9); border: 1px solid var(--lma-border); } #lma-batch-btn:hover { background: linear-gradient(135deg, #5dade2, #3498db); } .lma-live-dot { display: inline-block; width: 8px; height: 8px; background: #2ecc71; border-radius: 50%; animation: lma-pulse 1.5s infinite; margin-left: 6px; vertical-align: middle; } .lma-count-pop { animation: lma-pop 0.3s ease; } /* === BATCH SELECTION TOGGLE === */ #lma-batch-select-toggle { background: rgba(33, 150, 243, 0.2); border-color: var(--lma-accent); color: var(--lma-accent); } #lma-batch-select-toggle.active { background: var(--lma-accent); color: white; } /* === RESPONSIVE === */ @media (max-width: 768px) { .lma-nav-btn { width: 52px; height: 52px; font-size: 22px; } #lma-prev { left: 15px; } #lma-next { right: 15px; } .lma-toolbar-left { left: 15px; top: 15px; } .lma-toolbar-right { right: 15px; top: 15px; } .lma-tool-btn { padding: 10px 14px; font-size: 13px; } .lma-zoom-controls { bottom: 100px; right: 15px; } .lma-zoom-btn { width: 38px; height: 38px; font-size: 18px; } #lma-filename { font-size: 14px; padding: 8px 16px; } #lma-counter { font-size: 13px; padding: 5px 12px; } #lma-metadata-panel { top: 70px; left: 15px; max-width: 280px; padding: 15px; } .lma-batch-card { width: 95vw; padding: 18px; } .lma-batch-thumb { width: 56px; height: 56px; } } @media (max-width: 480px) { .lma-nav-btn { width: 44px; height: 44px; font-size: 20px; } .lma-tool-btn { padding: 8px 12px; font-size: 12px; } #lma-metadata-panel { max-width: 250px; padding: 12px; } .lma-zoom-controls { flex-direction: column; bottom: 150px; } #lma-batch-grid { max-height: 30vh; } } `; GM_addStyle(css); // ================== IMAGE COLLECTION ================== function isValidTargetImage(img) { if (!img.src) return false; if (img.offsetWidth < CONFIG.MIN_IMG_WIDTH && img.offsetHeight < CONFIG.MIN_IMG_WIDTH) return false; for (let exclude of CONFIG.EXCLUDE_SELECTORS) { if (img.closest(exclude)) return false; } return true; } function getAllImages() { log('Scanning for images...'); const candidates = new Set(); CONFIG.IMAGE_SELECTORS.forEach(selector => { document.querySelectorAll(selector).forEach(img => { if (isValidTargetImage(img)) { candidates.add(img); } }); }); const images = Array.from(candidates); log(`Found ${images.length} images`); return images; } // ================== EXIF PARSER ================== const ExifParser = { parse(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = e => { try { const dv = new DataView(e.target.result); const tags = this.readEXIF(dv); resolve(tags); } catch (err) { reject(err); } }; reader.onerror = reject; reader.readAsArrayBuffer(blob); }); }, readEXIF(dv) { if (dv.getUint16(0) !== 0xFFD8) return {}; let offset = 2; const length = dv.byteLength; while (offset < length) { const marker = dv.getUint16(offset); offset += 2; if (marker === 0xFFE1) { const segmentLength = dv.getUint16(offset); offset += 2; if (dv.getUint32(offset) === 0x45786966) { offset += 6; return this.parseTIFF(dv, offset); } else { offset += segmentLength - 2; } } else if (marker >= 0xFFC0 && marker <= 0xFFDA) { const segLength = dv.getUint16(offset); offset += segLength; } else if (marker === 0xFFD9) break; } return {}; }, parseTIFF(dv, offset) { const littleEndian = dv.getUint16(offset) === 0x4949; offset += 2; if (dv.getUint16(offset, littleEndian) !== 0x002A) return {}; offset += 2; const ifdOffset = dv.getUint32(offset, littleEndian); offset = offset + ifdOffset - 8; const tags = {}; const numEntries = dv.getUint16(offset, littleEndian); offset += 2; for (let i = 0; i < numEntries; i++) { const tag = dv.getUint16(offset, littleEndian); const type = dv.getUint16(offset + 2, littleEndian); const count = dv.getUint32(offset + 4, littleEndian); const valueOffset = dv.getUint32(offset + 8, littleEndian); offset += 12; const dataOffset = offset + valueOffset - 12; let value; if (type === 2) { if (count <= 4) { value = String.fromCharCode( valueOffset & 0xFF, (valueOffset >> 8) & 0xFF, (valueOffset >> 16) & 0xFF, (valueOffset >> 24) & 0xFF ).replace(/\0/g, ''); } else { let str = ''; for (let j = 0; j < count - 1; j++) { const charCode = dv.getUint8(dataOffset + j); if (charCode === 0) break; str += String.fromCharCode(charCode); } value = str; } } else if (type === 3) { value = dv.getUint16(dataOffset, littleEndian); } else if (type === 4) { value = dv.getUint32(dataOffset, littleEndian); } else continue; const tagNames = { 0x010E: 'ImageDescription', 0x010F: 'Make', 0x0110: 'Model', 0x0132: 'ModifyDate', 0x8298: 'Copyright', 0x9003: 'DateTimeOriginal', 0x9004: 'DateTimeDigitized', 0x9201: 'ShutterSpeed', 0x9202: 'Aperture', 0x9204: 'ExposureBias', 0x9207: 'MeteringMode', 0x9209: 'Flash', 0x920A: 'FocalLength', 0xA002: 'PixelXDimension', 0xA003: 'PixelYDimension' }; const tagName = tagNames[tag] || '0x' + tag.toString(16); tags[tagName] = value; } return tags; } }; // ================== IMAGE TYPE DETECTION ================== function detectImageType(imgElement, metadata) { let score = { original: 0, edited: 0, reasons: [] }; const src = imgElement.src || ''; const alt = imgElement.alt || ''; const filename = src.split('/').pop().split('?')[0]; for (let pattern of CONFIG.FILENAME_PATTERNS.ORIGINAL) { if (pattern.test(filename) || pattern.test(alt)) { score.original += 30; score.reasons.push('Filename matches original: ' + pattern); } } for (let pattern of CONFIG.FILENAME_PATTERNS.EDITED) { if (pattern.test(filename) || pattern.test(alt)) { score.edited += 30; score.reasons.push('Filename matches edited: ' + pattern); } } for (let pattern of CONFIG.USER_PATTERNS.ORIGINAL) { if (pattern.test(filename) || pattern.test(alt)) { score.original += 20; score.reasons.push('User pattern matches original: ' + pattern); } } for (let pattern of CONFIG.USER_PATTERNS.EDITED) { if (pattern.test(filename) || pattern.test(alt)) { score.edited += 20; score.reasons.push('User pattern matches edited: ' + pattern); } } if (metadata && Object.keys(metadata).length > 0) { if (metadata.Make || metadata.Model) { score.original += 25; score.reasons.push('EXIF contains camera make/model'); } if (metadata.Software && /photoshop|lightroom|editor/i.test(metadata.Software)) { score.edited += 35; score.reasons.push('EXIF software indicates editing: ' + metadata.Software); } if (metadata.DateTimeOriginal && metadata.ModifyDate && metadata.DateTimeOriginal !== metadata.ModifyDate) { score.edited += 15; score.reasons.push('ModifyDate differs from DateTimeOriginal'); } } else { score.edited += 5; score.reasons.push('No EXIF metadata found (may be generated)'); } const total = score.original + score.edited; if (total === 0) return { type: 'unknown', confidence: 0, reasons: ['No patterns matched'] }; const confidence = Math.max(score.original, score.edited) / total * 100; const type = score.original > score.edited ? 'original' : (score.edited > score.original ? 'edited' : 'unknown'); return { type, confidence: Math.round(confidence), reasons: score.reasons }; } // ================== METADATA FETCHING (with caching) ================== async function fetchMetadata(imgElement) { const src = imgElement.src; if (!src || src.startsWith('data:') || src.startsWith('blob:')) { return { note: 'Cannot fetch EXIF for data/blob URLs' }; } const urlHash = await simpleHash(src); if (state.exifCache.has(urlHash)) { return state.exifCache.get(urlHash); } return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: src, responseType: 'blob', onload: function(resp) { if (resp.status >= 200 && resp.status < 300) { ExifParser.parse(resp.response) .then(tags => { state.exifCache.set(urlHash, tags); resolve(tags); }) .catch(err => { const result = { error: 'EXIF parse failed', details: err.message }; state.exifCache.set(urlHash, result); resolve(result); }); } else { const result = { error: 'HTTP ' + resp.status }; state.exifCache.set(urlHash, result); resolve(result); } }, onerror: function(err) { const result = { error: 'Network error' }; state.exifCache.set(urlHash, result); resolve(result); } }); }); } // ================== LIGHTBOX DOM CREATION (with ARIA) ================== function createLightbox() { if (document.getElementById('lma-lightbox')) return; const html = `
' + escapeHtml(message) + '
' + '