4chan X Link Hover Preview

Adds hover preview capability to image/video links

  1. // ==UserScript==
  2. // @name 4chan X Link Hover Preview
  3. // @namespace lig
  4. // @license GNU GPLv3
  5. // @description Adds hover preview capability to image/video links
  6. // @version 1.0.3
  7. // @match *://boards.4chan.org/*/*/*
  8. // @match *://boards.4channel.org/*/*/*
  9. // @run-at document-start
  10. // @grant GM.info
  11. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAAkUlEQVQ4T6WQAQ6AMAgD9yf/5Gv906QYSBGGRk1OXFuaxbEf2y/0Necc8uCl4FxRZa4WEWQ6HPDgIhNuUAUePRFSwIDeeV6gH02YsRx2QoELTQkvel6MIKhYlFTL4NUNoDEhzwUw78vAdJ6poFqGZtiZvbbAAuxVmVTAsyswP/0DngbOXMB+KFiBBSZ4KPjONk6FWw9BoS3PSwAAAABJRU5ErkJggg==
  12. // ==/UserScript==
  13.  
  14. const whitelist = [
  15. "4cdn.org",
  16. "catbox.moe",
  17. "cockfile.com",
  18. "derpicdn.net",
  19. "deviantart.net",
  20. "discordapp.net",
  21. "discordapp.com",
  22. "fileditch.com",
  23. "pomf.cat",
  24. "pomf.se",
  25. "puu.sh",
  26. "tumblr.com",
  27. "uguu.se",
  28. ];
  29.  
  30. const delay = ms => new Promise(res => setTimeout(res, ms));
  31.  
  32. (async () => {
  33. let style = document.createElement('style');
  34. style.innerHTML = `
  35. a.linkify.loading {
  36. cursor: progress;
  37. }
  38. a.linkify.hideCursor {
  39. cursor: none;
  40. }
  41. `;
  42. while(document.head === null) {
  43. await delay(100);
  44. }
  45. document.head.appendChild(style);
  46. })();
  47.  
  48.  
  49. function bind(link, type) {
  50. if(link.hoverified) return;
  51. link.hoverified = true;
  52. //console.log(link);
  53.  
  54. let hostname = new URL(link.href).hostname;
  55. if(!whitelist.some(w => hostname.endsWith(w)))
  56. return;
  57.  
  58. link.addEventListener('mouseenter', async e => {
  59. link.classList.add('loading');
  60. let widthProp, heightProp;
  61. switch(type) {
  62. case 'image':
  63. document.querySelector('#hoverUI').innerHTML = `<img id='ihover' src='${link.href}' style='max-height: 100vh !important;'>`;
  64. widthProp = 'naturalWidth';
  65. heightProp = 'naturalHeight';
  66. break;
  67. case 'video':
  68. document.querySelector('#hoverUI').innerHTML = `
  69. <video id='ihover' src='${link.href}' loop='' autoplay='true'></video>
  70. `;
  71. widthProp = 'videoWidth';
  72. heightProp = 'videoHeight';
  73. break;
  74. default: return;
  75. }
  76. let ihover = document.querySelector('#ihover');
  77.  
  78. let x0 = e.clientX, y0 = e.clientY, w0 = ihover[widthProp], h0 = ihover[heightProp], initiated;
  79. function updatePosition() {
  80. let x = 0, y = 0, w = w0, h = h0;
  81. let docWidth = document.documentElement.offsetWidth;
  82. w = Math.min(w, docWidth);
  83. h = Math.min(h, window.innerHeight);
  84. let factor = Math.min(w/w0, h/h0);
  85. w = factor*w0;
  86. h = factor*h0;
  87. if(w < docWidth) {
  88. x = x0 + 45;
  89. if(x + w > docWidth)
  90. x = docWidth - w;
  91. }
  92. if(h < window.innerHeight) {
  93. y = Math.max(y0 - h/2, 0);
  94. if(y + h > window.innerHeight)
  95. y = window.innerHeight - h;
  96. }
  97. ihover.style.left = x+'px';
  98. ihover.style.top = y+'px';
  99. ihover.style.width = w+'px';
  100. ihover.style.height = h+'px';
  101.  
  102. link.classList.remove('hideCursor');
  103. clearTimeout(hideCursorTimeout);
  104. if(initiated && x0 >= x) {
  105. hideCursorTimeout = setTimeout(() => {
  106. link.classList.add('hideCursor');
  107. }, 1000);
  108. }
  109. //console.log(`left: ${x}, top: ${y}, width: ${w}, height: ${h}`);
  110. }
  111. let hideCursorTimeout;
  112. function move(e) {
  113. x0 = e.clientX;
  114. y0 = e.clientY;
  115. updatePosition();
  116. }
  117. link.addEventListener('mousemove', move)
  118. link.addEventListener('mouseleave', function unbind() {
  119. ihover.remove()
  120. link.removeEventListener('mouseleave', unbind);
  121. link.removeEventListener('mousemove', move);
  122. clearTimeout(hideCursorTimeout);
  123. });
  124. let init = e => {
  125. if(initiated) return;
  126. w0 = w0 || ihover[widthProp];
  127. h0 = h0 || ihover[heightProp];
  128. initiated = h0 || w0;
  129. if(initiated) link.classList.remove('loading');
  130. //console.log(`[${e.type}] Width: ${w0}, Height: ${h0}`);
  131. updatePosition();
  132. }
  133. ihover.onloadedmetadata = init;
  134. ihover.onloadeddata = init;
  135. ihover.onloadstart = init;
  136. ihover.onloadend = init;
  137. ihover.onload = init;
  138. });
  139. }
  140.  
  141. ['PostsInserted', '4chanXInitFinished'].forEach(event => document.addEventListener(event, e => {
  142. //console.log(`[${event}]`, e.target);
  143. if(e.target.matches && e.target.matches('#qp')) return;
  144. ['image', 'video'].forEach(type => {
  145. [...e.target.querySelectorAll(`a.linkify.${type}`)]
  146. .forEach(link => bind(link, type));
  147. });
  148. }));