// ==UserScript== // @name Hide Image Overlays // @namespace http://tampermonkey.net/ // @version 0.1 // @description Adds a button to hide images when hovered over. // @match *://*/* // @grant GM_xmlHttpRequest // ==/UserScript== (function () { 'use strict'; if (window.top !== window.self) return; // Don't run in iframes // --- STORAGE --- function getStoredRecords() { return JSON.parse(localStorage.getItem('hiddenImageRecords') || '[]'); } function saveStoredRecords(records) { localStorage.setItem('hiddenImageRecords', JSON.stringify(records)); } function storeRecord(checksum, url) { const records = getStoredRecords(); if (!records.some(r => r.checksum === checksum)) { // Create thumbnail and store record createThumbnail(url).then(thumbnail => { records.push({ checksum, url, thumbnail }); saveStoredRecords(records); }).catch(error => { console.error('Failed to create thumbnail:', error); // Store without thumbnail if creation fails records.push({ checksum, url, thumbnail: null }); saveStoredRecords(records); }); } } function removeRecord(checksum) { const records = getStoredRecords().filter(r => r.checksum !== checksum); saveStoredRecords(records); } // --- HASHING --- async function hashString(input) { try { // Fetch the actual image data const response = await new Promise((resolve, reject) => { GM_xmlHttpRequest({ method: 'GET', url: input, responseType: 'arraybuffer', onload: function(response) { resolve(response.response); }, onerror: function(error) { reject(error); } }); }); // Hash the actual image data const hashBuffer = await crypto.subtle.digest('SHA-256', response); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } catch (error) { console.error('Error fetching image for hashing:', error); // Fallback to URL hashing if image fetch fails const encoder = new TextEncoder(); const data = encoder.encode(input); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } } // --- BUTTON CREATION --- function createHideButton(image) { const btn = document.createElement('button'); btn.textContent = '✕'; // Use a more robust styling approach btn.style.setProperty('position', 'fixed', 'important'); btn.style.setProperty('background', 'rgba(0,0,0,0.9)', 'important'); btn.style.setProperty('color', 'white', 'important'); btn.style.setProperty('border', 'none', 'important'); btn.style.setProperty('borderRadius', '3px', 'important'); btn.style.setProperty('padding', '2px 6px', 'important'); btn.style.setProperty('cursor', 'pointer', 'important'); btn.style.setProperty('zIndex', '999999', 'important'); btn.style.setProperty('fontSize', '12px', 'important'); btn.style.setProperty('display', 'none', 'important'); btn.style.setProperty('userSelect', 'none', 'important'); btn.style.setProperty('pointerEvents', 'auto', 'important'); btn.style.setProperty('margin', '0', 'important'); btn.style.setProperty('transform', 'none', 'important'); btn.style.setProperty('float', 'none', 'important'); btn.style.setProperty('clear', 'none', 'important'); btn.style.setProperty('boxShadow', '0 1px 3px rgba(0,0,0,0.5)', 'important'); btn.style.setProperty('fontFamily', 'Arial, sans-serif', 'important'); btn.style.setProperty('fontWeight', 'bold', 'important'); btn.style.setProperty('textAlign', 'center', 'important'); btn.style.setProperty('lineHeight', '1', 'important'); btn.style.setProperty('minWidth', 'auto', 'important'); btn.style.setProperty('minHeight', 'auto', 'important'); btn.style.setProperty('maxWidth', 'none', 'important'); btn.style.setProperty('maxHeight', 'none', 'important'); btn.style.setProperty('width', 'auto', 'important'); btn.style.setProperty('height', 'auto', 'important'); // Store reference to image for positioning btn._targetImage = image; btn.addEventListener('click', async (e) => { // Only prevent default if we're actually going to hide the image e.stopPropagation(); const checksum = await hashString(image.src); storeRecord(checksum, image.src); image.style.display = 'none'; btn.remove(); // Remove from DOM since it's attached to body }); return btn; } // --- DECORATE IMAGE --- function decorateImage(image) { console.log('Attempting to decorate image:', image.src); if (image._hideImageHandled) { console.log('Image already handled, skipping:', image.src); return; } // Only add hide buttons to images larger than a certain size const rect = image.getBoundingClientRect(); console.log('Image size check:', image.src, 'size:', rect.width + 'x' + rect.height, 'visible:', rect.width > 0 && rect.height > 0); if (rect.width < 100 || rect.height < 100) { console.log('Image too small, skipping:', image.src, 'size:', rect.width + 'x' + rect.height); return; // Skip small images (likely UI elements, thumbnails, etc.) } console.log('Processing image:', image.src); image._hideImageHandled = true; const btn = createHideButton(image); document.body.appendChild(btn); // Ensure image can receive mouse events image.style.setProperty('pointer-events', 'auto', 'important'); // Use a more robust hover detection let isHovering = false; // Try multiple event types for better compatibility const showButton = () => { console.log('Hover event fired for image:', image.src); isHovering = true; const rect = image.getBoundingClientRect(); // Debug positioning issues console.log('Image rect:', rect); console.log('Image computed style:', getComputedStyle(image)); if (btn._setPosition) { btn._setPosition(rect); } else { // fallback: top-left btn.style.setProperty('top', (rect.top + 2) + 'px', 'important'); btn.style.setProperty('left', (rect.left + 2) + 'px', 'important'); } btn.style.setProperty('display', 'block', 'important'); }; const hideButton = () => { isHovering = false; setTimeout(() => { if (!isHovering) { btn.style.setProperty('display', 'none', 'important'); } }, 50); }; // Use passive listeners to avoid interfering with site events image.addEventListener('mouseenter', showButton, { passive: true }); image.addEventListener('mouseover', showButton, { passive: true }); image.addEventListener('mouseleave', hideButton, { passive: true }); image.addEventListener('mouseout', hideButton, { passive: true }); btn.addEventListener('mouseenter', (e) => { e.stopPropagation(); isHovering = true; }, { passive: false }); btn.addEventListener('mouseleave', () => { isHovering = false; btn.style.setProperty('display', 'none', 'important'); }, { passive: true }); if (image.parentElement) { let el = image.parentElement; while (el) { el.style.setProperty('pointer-events', 'auto', 'important'); el = el.parentElement; } } } // --- PROCESS IMAGES WITH BATCHING --- async function processImagesBatch(images, checksumSet) { console.log('Processing batch of', images.length, 'images'); const batchSize = 5; for (let i = 0; i < images.length; i += batchSize) { const batch = images.slice(i, i + batchSize); await Promise.all(batch.map(async (img) => { if (img._hideImageHandled) return; try { const checksum = await hashString(img.src); if (checksumSet.has(checksum)) { img.style.display = 'none'; img._hideImageHandled = true; } else { decorateImage(img); } } catch (e) { console.error("Hash error:", e); } })); } } // --- DEBOUNCED OBSERVER --- let observerTimeout; const observer = new MutationObserver(() => { clearTimeout(observerTimeout); observerTimeout = setTimeout(() => { const records = getStoredRecords(); const checksumSet = new Set(records.map(r => r.checksum)); const imgs = Array.from(document.querySelectorAll('img')); processImagesBatch(imgs, checksumSet); }, 300); }); // --- MANAGER PANEL --- function createManagerPanel() { const toggleBtn = document.createElement('button'); toggleBtn.textContent = '👁 Show Hidden Images'; Object.assign(toggleBtn.style, { position: 'fixed', bottom: '10px', left: '10px', zIndex: '9999999', padding: '8px 12px', background: '#333', color: '#fff', border: '2px solid white', borderRadius: '8px', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer', boxShadow: '2px 2px 8px rgba(0,0,0,0.3)', userSelect: 'none', }); // --- Make the toggle button draggable --- let isDragging = false; let dragOffsetX = 0; let dragOffsetY = 0; let wasDragged = false; function positionPanelAboveButton() { const btnRect = toggleBtn.getBoundingClientRect(); panel.style.left = btnRect.left + 'px'; const gap = 8; panel.style.bottom = (window.innerHeight - btnRect.top + gap) + 'px'; panel.style.top = 'auto'; } toggleBtn.addEventListener('mousedown', function(e) { isDragging = true; wasDragged = false; const rect = toggleBtn.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!isDragging) return; toggleBtn.style.left = (e.clientX - dragOffsetX) + 'px'; toggleBtn.style.top = (e.clientY - dragOffsetY) + 'px'; toggleBtn.style.bottom = 'auto'; positionPanelAboveButton(); wasDragged = true; }); document.addEventListener('mouseup', function() { isDragging = false; positionPanelAboveButton(); }); // --- End draggable --- const panel = document.createElement('div'); Object.assign(panel.style, { position: 'fixed', bottom: '50px', left: '10px', maxHeight: '60vh', width: '320px', overflowY: 'auto', background: '#f9f9f9', color: '#111', border: '1px solid #ccc', padding: '12px', fontFamily: 'sans-serif', lineHeight: '1.5', boxShadow: '2px 2px 10px rgba(0,0,0,0.2)', zIndex: 999998, display: 'none', fontSize: '13px', userSelect: 'text', }); function refreshPanel() { panel.innerHTML = 'Hidden Images
None
'; return; } for (const rec of records) { const item = document.createElement('div'); item.style.marginBottom = '12px'; item.style.padding = '6px 0'; item.style.borderBottom = '1px solid #ddd'; item.style.display = 'flex'; item.style.justifyContent = 'space-between'; item.style.alignItems = 'center'; item.style.gap = '10px'; // Filename link const link = document.createElement('a'); link.href = rec.url; link.style.fontSize = '13px'; link.style.wordBreak = 'break-word'; link.style.flex = '1'; link.target = '_blank'; link.rel = 'noopener noreferrer'; try { const urlObj = new URL(rec.url); const parts = urlObj.pathname.split('/'); const filename = decodeURIComponent(parts[parts.length - 1]); link.textContent = filename || '(Unnamed Image)'; } catch (e) { link.textContent = '(Invalid URL)'; } // Thumbnail const thumbnail = document.createElement('img'); thumbnail.style.width = '55px'; thumbnail.style.height = '45px'; thumbnail.style.objectFit = 'cover'; thumbnail.style.borderRadius = '4px'; thumbnail.style.border = '1px solid #ddd'; if (rec.thumbnail) { thumbnail.src = rec.thumbnail; } else { thumbnail.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTUiIGhlaWdodD0iNDUiIHZpZXdCb3g9IjAgMCA1NSA0NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjU1IiBoZWlnaHQ9IjQ1IiBmaWxsPSIjZjBmMGYwIi8+CjxwYXRoIGQ9Ik0yNy41IDIyLjVMMzIuNSAxNy41TDM3LjUgMjIuNUwyNy41IDMyLjVMMTcuNSAyMi41TDIyLjUgMTcuNUwyNy41IDIyLjVaIiBmaWxsPSIjY2NjIi8+Cjwvc3ZnPgo='; } // Restore button const restoreBtn = document.createElement('button'); restoreBtn.textContent = 'Restore'; Object.assign(restoreBtn.style, { fontSize: '12px', padding: '4px 8px', cursor: 'pointer', background: '#4CAF50', color: '#fff', border: 'none', borderRadius: '4px', boxShadow: '1px 1px 3px rgba(0,0,0,0.2)', flexShrink: '0' }); restoreBtn.addEventListener('click', async () => { // Remove the record from storage removeRecord(rec.checksum); // Find and restore the image on the page const imgs = Array.from(document.querySelectorAll('img')); for (const img of imgs) { try { const imgChecksum = await hashString(img.src); if (imgChecksum === rec.checksum) { // Show the image img.style.display = ''; img._hideImageHandled = false; // Re-decorate the image with hide button decorateImage(img); break; // Found and restored, no need to continue } } catch (e) { console.error("Hash error during restore:", e); } } // Update the panel refreshPanel(); }); item.appendChild(link); item.appendChild(thumbnail); item.appendChild(restoreBtn); panel.appendChild(item); } } toggleBtn.addEventListener('click', (e) => { if (wasDragged) { // Prevent menu toggle if this click was part of a drag e.preventDefault(); return; } if (panel.style.display === 'none') { refreshPanel(); // Always position the panel above the button before showing positionPanelAboveButton(); panel.style.display = 'block'; } else { panel.style.display = 'none'; } }); // --- Settings for hide button position --- const HIDE_BTN_POS_KEY = 'hideBtnPosition'; const defaultHideBtnPos = 'top-left'; function getHideBtnPosition() { return localStorage.getItem(HIDE_BTN_POS_KEY) || defaultHideBtnPos; } function setHideBtnPosition(pos) { localStorage.setItem(HIDE_BTN_POS_KEY, pos); } // --- Helper function to assign position method to button --- function assignHideBtnPosition(btn) { // Remove all position props btn.style.removeProperty('top'); btn.style.removeProperty('left'); btn.style.removeProperty('right'); btn.style.removeProperty('bottom'); // Set position based on user setting const pos = getHideBtnPosition(); if (pos === 'top-left') { btn._setPosition = (rect) => { btn.style.setProperty('top', (rect.top + 2) + 'px', 'important'); btn.style.setProperty('left', (rect.left + 2) + 'px', 'important'); }; } else if (pos === 'top-right') { btn._setPosition = (rect) => { btn.style.setProperty('top', (rect.top + 2) + 'px', 'important'); btn.style.setProperty('left', (rect.right - btn.offsetWidth - 2) + 'px', 'important'); }; } else if (pos === 'bottom-left') { btn._setPosition = (rect) => { btn.style.setProperty('top', (rect.bottom - btn.offsetHeight - 2) + 'px', 'important'); btn.style.setProperty('left', (rect.left + 2) + 'px', 'important'); }; } else if (pos === 'bottom-right') { btn._setPosition = (rect) => { btn.style.setProperty('top', (rect.bottom - btn.offsetHeight - 2) + 'px', 'important'); btn.style.setProperty('left', (rect.right - btn.offsetWidth - 2) + 'px', 'important'); }; } } // --- Settings button --- const settingsBtn = document.createElement('button'); settingsBtn.textContent = '⚙️'; Object.assign(settingsBtn.style, { marginLeft: '8px', fontSize: '16px', cursor: 'pointer', background: 'transparent', border: 'none', color: '#fff', verticalAlign: 'middle', }); toggleBtn.appendChild(settingsBtn); // --- Settings popup --- const settingsPopup = document.createElement('div'); Object.assign(settingsPopup.style, { position: 'fixed', left: '50%', top: '30%', transform: 'translate(-50%, 0)', background: '#fff', color: '#111', border: '1px solid #ccc', borderRadius: '8px', padding: '18px 24px', zIndex: 10000000, boxShadow: '2px 2px 10px rgba(0,0,0,0.2)', display: 'none', fontSize: '15px', }); settingsPopup.innerHTML = `