Hover Zoom Minus --

image popup: zoom, pan, scroll

当前为 2024-03-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Hover Zoom Minus --
  3. // @namespace Hover Zoom Minus --
  4. // @version 1.0.2
  5. // @description image popup: zoom, pan, scroll
  6. // @author Ein, Copilot AI
  7. // @match *://*/*
  8. // @license MIT
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. // 1. to use hover over the image(container) to view a popup of the target image
  13. // 2. to zoom in/out use wheel up/down. to reset press the scroll button
  14. // 3. click left mouse to lock popup this will make it move along with the mouse, click again to release (indicated by green border)
  15. // 4. while being locked the wheel up/down will act as scroll up/down
  16. // 5. double click will lock it on screen preventing it from being hidden
  17. // 6. while locked at screen (indicated by red outline) a single click with the blurred background will unblur it, only one popup per time, so the locked popup will prevent other popup to spawn
  18. // 7. double clicking on blurred background will de-spawn popup
  19. // 8. to turn on/off hover at the bottom of the page
  20.  
  21. (function() {
  22. 'use strict';
  23.  
  24. // Configuration ----------------------------------------------------------
  25.  
  26. // Define regexp of web page you want HoverZoomMinus to run with,
  27. // 1st array value - default status at start of page: '1' for on, '0' for off
  28. // 2nd array value - spawn position for popup: 'center' for center of screen, '' for cursor position
  29. // 3rd array value - allowed interval for spawning popup; i.e. when exited on popup but immediately touches an img container thus making it "blink spawn blink spawn", experiment it with the right number
  30.  
  31. const siteConfig = {
  32. 'reddit.com*': [1, 'center', '0'],
  33. '9gag.com*': [1, 'center', '0'],
  34. 'feedly.com*': [1, 'center', '200'],
  35. '4chan.org*': [1, '', '400'],
  36. 'deviantart.com*': [0, 'center', '300']
  37. };
  38.  
  39. // image container [hover box where popup triggers]
  40. const imgContainers = `
  41. /* ------- reddit */ ._3Oa0THmZ3f5iZXAQ0hBJ0k > div, ._35oEP5zLnhKEbj5BlkTBUA, ._1ti9kvv_PMZEF2phzAjsGW > div, ._28TEYBuEdOuE3kN6UyoKMa div, ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx div, ._3m20hIKOhTTeMgPnfMbVNN,
  42. /* --------- 9gag */ .post-container .post-view > picture,
  43. /* ------- feedly */ .PinableImageContainer, .entryBody,
  44. /* -------- 4chan */ div.post div.file a,
  45. /* --- deviantart */ ._3_LJY, ._2e1g3, ._2SlAD, ._1R2x6
  46. `;
  47. // target img
  48. const imgElements = `
  49. /* ------- reddit */ ._2_tDEnGMLxpM6uOa2kaDB3, ._1dwExqTGJH2jnA-MYGkEL-, ._2_tDEnGMLxpM6uOa2kaDB3._1XWObl-3b9tPy64oaG6fax,
  50. /* --------- 9gag */ .post-container .post-view > picture > img,
  51. /* ------- feedly */ .pinable, .entryBody img,
  52. /* -------- 4chan */ div.post div.file img:nth-child(1), ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx img, div.post div.file .fileThumb img,
  53. /* --- deviantart */ ._3_LJY img, ._2e1g3 img, ._2SlAD img, ._1R2x6 img
  54. `;
  55. // excluded element
  56. const nopeElements = `
  57. /* ------- reddit */ ._2ED-O3JtIcOqp8iIL1G5cg
  58. `;
  59. // specialElements were if targeted, will call it's paired function
  60. const specialElements = [
  61. /* -------- 4chan */ { selector: 'div.post div.file .fileThumb img', func: SP1 }
  62. ];
  63.  
  64. function SP1(imageElement) {
  65. let src = imageElement.getAttribute('src');
  66. if (src && src.includes('s.jpg')) {
  67. let newSrc = src.replace('s.jpg', '.jpg');
  68. imageElement.setAttribute('src', newSrc);
  69. }
  70. }
  71.  
  72. //-------------------------------------------------------------------------
  73.  
  74. // Variables
  75. const currentHref = window.location.href;
  76. let enableP, positionP, intervalP, URLmatched;
  77. Object.keys(siteConfig).some((config) => {
  78. const regex = new RegExp(config);
  79. if (currentHref.match(regex)) {
  80. [enableP, positionP, intervalP] = siteConfig[config];
  81. URLmatched = true;
  82. return true;
  83. }
  84. });
  85.  
  86.  
  87. // The HoverZoomMinus Function---------------------------------------------
  88. function HoverZoomMinus() {
  89. let isshowPopupEnabled = true;
  90. isshowPopupEnabled = true;
  91. // Style for the popup image
  92. const style = document.createElement('style');
  93. style.type = 'text/css';
  94. style.innerHTML = `
  95. .popup-image {
  96. max-height: calc(90vh - 10px);
  97. z-index: 1000;
  98. display: none;
  99. cursor: move;}`;
  100. document.head.appendChild(style);
  101.  
  102. // Creating the popup image and backdrop
  103. const popup = document.createElement('img');
  104. popup.className = 'popup-image';
  105. document.body.appendChild(popup);
  106. const backdrop = document.createElement('div');
  107. backdrop.className = 'popup-backdrop';
  108. backdrop.style.cssText = `
  109. position: fixed;
  110. top: 0;
  111. left: 0;
  112. width: 100vw;
  113. height: 100vh;
  114. display: none;`;
  115. document.body.appendChild(backdrop);
  116.  
  117. //-------------------------------------------------------------------------
  118.  
  119. // Zoom, Pan, Scroll
  120. let isLocked = false;
  121. let offsetX, offsetY;
  122. let scale = 1;
  123. const ZOOM_SPEED = 0.005;
  124.  
  125. let clickTimeout;
  126.  
  127. popup.addEventListener('click', function(event) {
  128. // Clear the timeout if it's set
  129. if (clickTimeout) clearTimeout(clickTimeout);
  130. // Set a timeout to execute the single click logic
  131. clickTimeout = setTimeout(function() {
  132. isLocked = !isLocked;
  133. if (isLocked) {
  134.  
  135. popup.style.borderLeft = '3px solid #00ff00';
  136. popup.style.borderRight = '3px solid #00ff00';
  137.  
  138. let rect = popup.getBoundingClientRect();
  139. offsetX = event.clientX - rect.left - (rect.width / 2);
  140. offsetY = event.clientY - rect.top - (rect.height / 2);
  141. } else {
  142. popup.style.borderLeft = '';
  143. popup.style.borderRight = '';
  144. }
  145. }, 300); // Delay for the single click logic
  146. });
  147.  
  148. document.addEventListener('mousemove', function(event) {
  149. if (isLocked) {
  150. popup.style.left = (event.clientX - offsetX) + 'px';
  151. popup.style.top = (event.clientY - offsetY) + 'px';
  152. }
  153. });
  154.  
  155. function ZoomOrScroll(event) {
  156. event.preventDefault();
  157. if (isLocked) {
  158. let deltaY = event.deltaY * -ZOOM_SPEED;
  159. let newTop = parseInt(popup.style.top) || 0;
  160. newTop += deltaY * 100;
  161. popup.style.top = newTop + 'px';
  162.  
  163. offsetY -= deltaY * 100;
  164. } else {
  165. scale += event.deltaY * -ZOOM_SPEED;
  166. scale = Math.min(Math.max(0.125, scale), 10);
  167. popup.style.transform = `translate(-50%, -50%) scale(${scale})`;
  168. }
  169. }
  170.  
  171. popup.addEventListener('wheel', ZoomOrScroll);
  172.  
  173. //-------------------------------------------------------------------------
  174.  
  175. // show popup
  176. function showPopup(src, mouseX, mouseY) {
  177. if (!isshowPopupEnabled) return;
  178. popup.src = src;
  179. popup.style.display = 'block';
  180. popup.style.position = 'fixed';
  181. popup.style.transform = 'translate(-50%, -50%)';
  182. backdrop.style.display = 'block';
  183. backdrop.style.zIndex = '999';
  184. backdrop.style.backdropFilter = 'blur(10px)';
  185.  
  186. if (positionP === 'center') {
  187. popup.style.top = '50%';
  188. popup.style.left = '50%';
  189. } else {
  190. popup.style.top = `${mouseY}px`;
  191. popup.style.left = `${mouseX}px`;
  192. }
  193. }
  194.  
  195. let popupTimer;
  196. document.addEventListener('mouseover', function(e) {
  197. if (popupTimer) return;
  198. let target = e.target.closest(imgContainers);
  199. if (!target) return;
  200. if (target.querySelector(nopeElements)) return;
  201. let currentContainer = target;
  202. const imageElement = currentContainer.querySelector(imgElements);
  203.  
  204. specialElements.forEach(pair => {
  205. if (imageElement.matches(pair.selector)) {
  206. pair.func(imageElement);
  207. }
  208.  
  209. });
  210.  
  211. if (imageElement) {
  212. if (intervalP === '') {
  213. if (positionP === 'center') {
  214. showPopup(imageElement.src);
  215. } else {
  216. showPopup(imageElement.src, e.clientX, e.clientY);
  217. }
  218. } else {
  219. popupTimer = setTimeout(() => {
  220. if (positionP === 'center') {
  221. showPopup(imageElement.src);
  222. } else {
  223. showPopup(imageElement.src, e.clientX, e.clientY);
  224. }
  225. popupTimer = null;
  226. }, parseInt(intervalP));
  227. }
  228. }
  229. });
  230.  
  231. //-------------------------------------------------------------------------
  232.  
  233. // hide popup
  234. function hidePopup() {
  235. if (!ishidePopupEnabled) return;
  236. isshowPopupEnabled = true;
  237. if (popupTimer) {
  238. clearTimeout(popupTimer);
  239. }
  240. document.body.appendChild(backdrop);
  241. popup.style.display = 'none';
  242. popup.style.left = '50%';
  243. popup.style.top = '50%';
  244. popup.style.position = 'fixed';
  245. popup.style.transform = 'translate(-50%, -50%) scale(1)';
  246. popup.style.borderLeft = '';
  247. popup.style.borderRight = '';
  248. popup.style.outline = '';
  249. backdrop.style.zIndex = '';
  250. backdrop.style.display = 'none';
  251. backdrop.style.backdropFilter = '';
  252. isLocked = false;
  253. }
  254.  
  255. document.addEventListener('mouseout', function(e) {
  256. let target = e.target.closest('.imgContainers');
  257. let relatedTarget = e.relatedTarget;
  258.  
  259. if (target && (!relatedTarget || (!relatedTarget.matches('.popup-image') && !relatedTarget.closest('.imgContainers')))) {
  260. hidePopup();
  261. if (intervalP !== '') {
  262. popupTimer = setTimeout(() => {
  263. popupTimer = null;
  264. }, parseInt(intervalP));
  265. }
  266. }
  267. });
  268.  
  269. popup.addEventListener('mouseout', function() {
  270. hidePopup();
  271. if (intervalP !== '') {
  272. popupTimer = setTimeout(() => {
  273. popupTimer = null;
  274. }, parseInt(intervalP));
  275. }
  276. });
  277.  
  278. document.addEventListener('keydown', function(event) {
  279. if (event.key === "Escape") {
  280. event.preventDefault();
  281. hidePopup();
  282. }
  283. });
  284.  
  285.  
  286. //-------------------------------------------------------------------------
  287.  
  288. // lock popup in screen
  289. let ishidePopupEnabled = true;
  290.  
  291. function togglehidePopup(event) {
  292. ishidePopupEnabled = !ishidePopupEnabled;
  293. popup.style.outline = ishidePopupEnabled ? '' : '3px solid #ae0001';
  294. }
  295.  
  296. popup.addEventListener('dblclick', function(event) {
  297. clearTimeout(clickTimeout);
  298. togglehidePopup();
  299. });
  300. backdrop.addEventListener('dblclick', function(event) {
  301. clearTimeout(clickTimeout);
  302. ishidePopupEnabled = true;
  303. hidePopup();
  304. });
  305.  
  306.  
  307. backdrop.addEventListener('click', function(event) {
  308. if (clickTimeout) clearTimeout(clickTimeout);
  309. clickTimeout = setTimeout(function() {
  310. backdrop.style.zIndex = '';
  311. backdrop.style.display = 'none';
  312. backdrop.style.backdropFilter = '';
  313. isshowPopupEnabled = false;
  314. }, 300);
  315. });
  316. }
  317.  
  318. //-------------------------------------------------------------------------
  319.  
  320. // Is to be run -----------------------------------------------------------
  321. if (URLmatched) {
  322. const indicatorBar = document.createElement('div');
  323. indicatorBar.style.cssText = `
  324. position: fixed;
  325. bottom: 0;
  326. left: 50%;
  327. transform: translateX(-50%);
  328. z-index: 9999;
  329. height: 30px;
  330. width: 50vw;
  331. background: #0000;`;
  332. document.body.appendChild(indicatorBar);
  333.  
  334. function toggleIndicator() {
  335. enableP = 1 - enableP;
  336. indicatorBar.style.background = enableP ? 'linear-gradient(to right, rgba(50, 190, 152, 0) 0%, rgba(50, 190, 152, 0.5) 25%, rgba(50, 190, 152, 0.5) 75%, rgba(50, 190, 152, 0) 100%)' : 'linear-gradient(to right, rgba(174, 0, 1, 0) 0%, rgba(174, 0, 1, 0.5) 25%, rgba(174, 0, 1, 0.5) 75%, rgba(174, 0, 1, 0) 100%)';
  337. setTimeout(() => {
  338. indicatorBar.style.background = '#0000';
  339. }, 1000);
  340. if (enableP === 1) {
  341. HoverZoomMinus();
  342. } else {
  343. const existingPopup = document.body.querySelector('.popup-image');
  344. if (existingPopup) document.body.removeChild(existingPopup);
  345. const existingBackdrop = document.body.querySelector('.popup-backdrop');
  346. if (existingBackdrop) document.body.removeChild(existingBackdrop);
  347. }
  348. }
  349.  
  350. let hoverTimeout;
  351. indicatorBar.addEventListener('mouseenter', () => {
  352. hoverTimeout = setTimeout(toggleIndicator, 500);
  353. });
  354. indicatorBar.addEventListener('mouseleave', () => {
  355. clearTimeout(hoverTimeout);
  356. indicatorBar.style.background = '#0000';
  357. });
  358. if (enableP === 1) {
  359. HoverZoomMinus();
  360. }
  361. } else {
  362. return;
  363. }
  364.  
  365. })();