// ==UserScript== // @name 4chan X Link Hover Preview // @namespace lig // @license GNU GPLv3 // @description Adds hover preview capability to image/video links // @version 1.0.3 // @match *://boards.4chan.org/*/*/* // @match *://boards.4channel.org/*/*/* // @run-at document-start // @grant GM.info // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAAkUlEQVQ4T6WQAQ6AMAgD9yf/5Gv906QYSBGGRk1OXFuaxbEf2y/0Necc8uCl4FxRZa4WEWQ6HPDgIhNuUAUePRFSwIDeeV6gH02YsRx2QoELTQkvel6MIKhYlFTL4NUNoDEhzwUw78vAdJ6poFqGZtiZvbbAAuxVmVTAsyswP/0DngbOXMB+KFiBBSZ4KPjONk6FWw9BoS3PSwAAAABJRU5ErkJggg== // @downloadURL none // ==/UserScript== const whitelist = [ "4cdn.org", "catbox.moe", "cockfile.com", "derpicdn.net", "deviantart.net", "discordapp.net", "discordapp.com", "fileditch.com", "pomf.cat", "pomf.se", "puu.sh", "tumblr.com", "uguu.se", ]; const delay = ms => new Promise(res => setTimeout(res, ms)); (async () => { let style = document.createElement('style'); style.innerHTML = ` a.linkify.loading { cursor: progress; } a.linkify.hideCursor { cursor: none; } `; while(document.head === null) { await delay(100); } document.head.appendChild(style); })(); function bind(link, type) { if(link.hoverified) return; link.hoverified = true; //console.log(link); let hostname = new URL(link.href).hostname; if(!whitelist.some(w => hostname.endsWith(w))) return; link.addEventListener('mouseenter', async e => { link.classList.add('loading'); let widthProp, heightProp; switch(type) { case 'image': document.querySelector('#hoverUI').innerHTML = ``; widthProp = 'naturalWidth'; heightProp = 'naturalHeight'; break; case 'video': document.querySelector('#hoverUI').innerHTML = ` `; widthProp = 'videoWidth'; heightProp = 'videoHeight'; break; default: return; } let ihover = document.querySelector('#ihover'); let x0 = e.clientX, y0 = e.clientY, w0 = ihover[widthProp], h0 = ihover[heightProp], initiated; function updatePosition() { let x = 0, y = 0, w = w0, h = h0; let docWidth = document.documentElement.offsetWidth; w = Math.min(w, docWidth); h = Math.min(h, window.innerHeight); let factor = Math.min(w/w0, h/h0); w = factor*w0; h = factor*h0; if(w < docWidth) { x = x0 + 45; if(x + w > docWidth) x = docWidth - w; } if(h < window.innerHeight) { y = Math.max(y0 - h/2, 0); if(y + h > window.innerHeight) y = window.innerHeight - h; } ihover.style.left = x+'px'; ihover.style.top = y+'px'; ihover.style.width = w+'px'; ihover.style.height = h+'px'; link.classList.remove('hideCursor'); clearTimeout(hideCursorTimeout); if(initiated && x0 >= x) { hideCursorTimeout = setTimeout(() => { link.classList.add('hideCursor'); }, 1000); } //console.log(`left: ${x}, top: ${y}, width: ${w}, height: ${h}`); } let hideCursorTimeout; function move(e) { x0 = e.clientX; y0 = e.clientY; updatePosition(); } link.addEventListener('mousemove', move) link.addEventListener('mouseleave', function unbind() { ihover.remove() link.removeEventListener('mouseleave', unbind); link.removeEventListener('mousemove', move); clearTimeout(hideCursorTimeout); }); let init = e => { if(initiated) return; w0 = w0 || ihover[widthProp]; h0 = h0 || ihover[heightProp]; initiated = h0 || w0; if(initiated) link.classList.remove('loading'); //console.log(`[${e.type}] Width: ${w0}, Height: ${h0}`); updatePosition(); } ihover.onloadedmetadata = init; ihover.onloadeddata = init; ihover.onloadstart = init; ihover.onloadend = init; ihover.onload = init; }); } ['PostsInserted', '4chanXInitFinished'].forEach(event => document.addEventListener(event, e => { //console.log(`[${event}]`, e.target); if(e.target.matches && e.target.matches('#qp')) return; ['image', 'video'].forEach(type => { [...e.target.querySelectorAll(`a.linkify.${type}`)] .forEach(link => bind(link, type)); }); }));