Center Image & Video

Improved controls for images & videos opened directly with your browser - hotkeys & resizing & visuals

安装此脚本
作者推荐脚本

您可能也喜欢Universal Dark Theme Maker

安装此脚本
  1. // ==UserScript==
  2. // @name Center Image & Video
  3. // @namespace CenterImage
  4. // @author Owyn
  5. // @version 2024.02.01
  6. // @description Improved controls for images & videos opened directly with your browser - hotkeys & resizing & visuals
  7. // @supportURL https://github.com/Owyn/Center_Image/issues
  8. // @homepage https://github.com/Owyn/Center_Image
  9. // @run-at document-end
  10. // @noframes
  11. // @grant GM.getValue
  12. // @grant GM.setValue
  13. // @grant GM_addElement
  14. // @grant GM_registerMenuCommand
  15. // @sandbox JavaScript
  16. // @match http://*/*
  17. // @match https://*/*
  18. // @match file:///*/*
  19. // ==/UserScript==
  20.  
  21. "use strict";
  22.  
  23. let type = document.contentType.substring(0,document.contentType.indexOf("/"));
  24. let i, is_video;
  25. if (type === "image")
  26. {
  27. is_video = false;
  28. i = document.images[0];
  29. }
  30. else if(type === "video")
  31. {
  32. is_video = true;
  33. i = document.querySelector("video");
  34. }
  35. else
  36. {
  37. return false;
  38. }
  39.  
  40. if (typeof GM_registerMenuCommand !== "undefined")
  41. {
  42. GM_registerMenuCommand("Center Image Configuration", cfg, "n");
  43. }
  44.  
  45. const AddElementToPage = typeof GM_addElement === "function" ? GM_addElement : function GM_addElement(node, type, content)
  46. {
  47. let el = document.createElement(type);
  48. el.textContent = content.textContent;
  49. node.appendChild(el);
  50. };
  51.  
  52. let rescaled = 0;
  53. let iot = 0, iol = 0;
  54. let skip_by = 5;
  55.  
  56. //let theStyle;
  57. let title;
  58. function makeimage()
  59. {
  60. if(typeof cfg_js !== "string") {setTimeout(function() { makeimage(); }, 11); return false;} // lets wait for async
  61. title = document.title;
  62. if(cfg_bgclr)
  63. {
  64. if(document.head){document.head.innerHTML = "";} // remove FireFox background
  65. }
  66. i.removeAttribute('class');
  67. i.removeAttribute('style'); // chrome
  68. document.body.removeAttribute('style');
  69. if(!is_video)
  70. {
  71. i.addEventListener("click", rescale, true);
  72. i.addEventListener("mousedown", onmousedown, true);
  73. }
  74. else
  75. {
  76. i.volume = cfg_vol;
  77. i.addEventListener("volumechange", onvolumechange, true);
  78. i.controls = true;
  79. i.loop = true;
  80. }
  81. i.addEventListener("auxclick", rescale, true);
  82. window.addEventListener("keydown", onkeydown, true);
  83. window.addEventListener("scroll", onscroll, false);
  84. window.addEventListener("resize", onresize, true);
  85. onVisibilityChange();
  86. }
  87.  
  88. function onvolumechange()
  89. {
  90. GM.setValue("vid_volume", (i.muted? "0" : i.volume));
  91. }
  92.  
  93. function onVisibilityChange()
  94. {
  95. if (document.visibilityState === 'visible')
  96. {
  97. //console.log('visible');
  98. window.removeEventListener('visibilitychange', onVisibilityChange);
  99. autoresize();
  100. }
  101. }
  102. window.addEventListener("visibilitychange", onVisibilityChange);
  103.  
  104. var scrolledY, scrolledX;
  105. function onscroll()
  106. {
  107. scrolledY = window.pageYOffset;
  108. scrolledX = window.pageXOffset;
  109. }
  110.  
  111. var FireFox = ((navigator.userAgent.indexOf('Firefox') != -1) ? true : false);
  112. function onresize() // doesn't let image change back to "fit to window" in chrome & in both helps keep scroll progress
  113. {
  114. if(rescaled != 1)
  115. {
  116. let wasFilled = (rescaled === 2 ? true : false);
  117. rescaled = 1;
  118. let chromeplsstopX, chromeplsstopY;
  119. if(!FireFox){ chromeplsstopX = window.pageXOffset; chromeplsstopY = window.pageYOffset;}
  120. rescale(null, wasFilled);
  121. if(!FireFox){ window.scrollTo(chromeplsstopX,chromeplsstopY);}
  122. }
  123. }
  124.  
  125. function changeCursor()
  126. {
  127. if(i.scrollHeight > window.innerHeight) // image pushing out-of-screen at top fix
  128. {
  129. i.classList.add("center_H");
  130. //theStyle.sheet.rules[0].style.margin = "0px auto";
  131. }
  132. else
  133. {
  134. i.classList.add("center");
  135. //theStyle.sheet.rules[0].style.margin = "auto";
  136. }
  137. if(is_video) return;
  138.  
  139. //i.style.margin = "auto";
  140. if(rescaled === 0) // original
  141. {
  142. if((orgImgWidth == window.innerWidth && orgImgHeight <= window.innerHeight) || (orgImgWidth <= window.innerWidth && orgImgHeight == window.innerHeight)) // perfect fit on one side, can't resize
  143. {
  144. i.style.cursor = "";
  145. }
  146. else if (orgImgWidth > window.innerWidth || orgImgHeight > window.innerHeight)
  147. {
  148. i.style.cursor = "zoom-out";
  149. }
  150. else
  151. {
  152. i.style.cursor = "zoom-in";
  153. }
  154. }
  155. else if(rescaled === 2) // fill
  156. {
  157. if(orgImgWidth == window.innerWidth && orgImgHeight == window.innerHeight) // perfect fit, can't resize
  158. {
  159. i.style.cursor = "";
  160. }
  161. else if (orgImgWidth > i.width)
  162. {
  163. i.style.cursor = "zoom-in";
  164. }
  165. else
  166. {
  167. i.style.cursor = "zoom-out";
  168. }
  169. }
  170. else // if(rescaled === 1) // fit
  171. {
  172. if((orgImgWidth == window.innerWidth && orgImgHeight <= window.innerHeight) || (orgImgWidth <= window.innerWidth && orgImgHeight == window.innerHeight)) // perfect fit on one side, can't resize
  173. {
  174. i.style.cursor = "";
  175. }
  176. else if (orgImgWidth > i.width)
  177. {
  178. i.style.cursor = "zoom-in";
  179. }
  180. else
  181. {
  182. i.style.cursor = "zoom-out";
  183. }
  184. }
  185. }
  186.  
  187. function onmousedown(event)
  188. {
  189. if(i.offsetLeft > 0){iol = i.offsetLeft;}
  190. if(i.offsetTop > 0){iot = i.offsetTop;}
  191.  
  192. if(event.which === 2) // middle mouse Chrome fix
  193. {
  194. event.preventDefault();
  195. event.stopPropagation();
  196. return;
  197. }
  198. }
  199.  
  200. function rescale(event, fill)
  201. {
  202. let ex,ey;
  203. if(event) // mouse click
  204. {
  205. if (typeof event.y === "undefined") // Firefox
  206. {
  207. ex = event.clientX;
  208. ey = event.clientY;
  209. }
  210. else
  211. {
  212. ex = event.x;
  213. ey = event.y;
  214. }
  215. ex -= iol;
  216. ey -= iot;
  217. if(event.which === 2) // middle mouse
  218. {
  219. fill = true;
  220. }
  221. else if(event.which === 3) // right mouse
  222. {
  223. return;
  224. }
  225. event.preventDefault();
  226. event.stopPropagation();
  227. }
  228.  
  229. //document.body.style.overflowX = '';
  230. //document.body.style.overflowY = '';
  231. document.body.removeAttribute('style');
  232.  
  233. let scrollMax_Y = window.scrollMaxY || ((document.body.scrollHeight || document.documentElement.scrollHeight)- document.documentElement.clientHeight);
  234. let scrollMax_X = window.scrollMaxX || ((document.body.scrollWidth || document.documentElement.scrollWidth)- document.documentElement.clientWidth);
  235.  
  236. let scrollProgressY = scrolledY / scrollMax_Y;
  237. let scrollProgressX = scrolledX / scrollMax_X;
  238.  
  239. let unFilling = false;
  240.  
  241. let sidesCMP;
  242. i.removeAttribute('class'); // clear classes
  243. if(fill)
  244. {
  245. if(rescaled === 2) // to original
  246. {
  247. rescaled = 0;
  248. i.classList.add("org");
  249. //theStyle.sheet.rules[0].style.width = orgImgWidth + "px";
  250. //theStyle.sheet.rules[0].style.height = orgImgHeight + "px";
  251. }
  252. else // fill
  253. {
  254. sidesCMP = (orgImgWidth / orgImgHeight) < (window.innerWidth / window.innerHeight);
  255. rescaled = 2;
  256. }
  257. }
  258. else
  259. {
  260. if(rescaled != 0) // to original
  261. {
  262. if(rescaled === 2) {unFilling = true;}
  263. rescaled = 0;
  264. i.classList.add("org");
  265. //theStyle.sheet.rules[0].style.width = orgImgWidth + "px";
  266. //theStyle.sheet.rules[0].style.height = orgImgHeight + "px";
  267. }
  268. else // fit
  269. {
  270. sidesCMP = (orgImgWidth / orgImgHeight) > (window.innerWidth / window.innerHeight);
  271. rescaled = 1;
  272. }
  273. }
  274.  
  275. if(rescaled != 0)
  276. {
  277. if(sidesCMP)
  278. {
  279. i.classList.add("fill_H");
  280. //theStyle.sheet.rules[0].style.width = "100%";
  281. //theStyle.sheet.rules[0].style.height = "auto";
  282. document.body.style.overflowX = 'hidden'; // we don't need unscrollable scrollbars if they appear
  283. }
  284. else
  285. {
  286. i.classList.add("fill_V");
  287. //theStyle.sheet.rules[0].style.height = "100%";
  288. //theStyle.sheet.rules[0].style.width = "auto";
  289. document.body.style.overflowY = 'hidden'; // we don't need unscrollable scrollbars if they appear
  290. }
  291. }
  292.  
  293. changeCursor();
  294.  
  295. if(event && (!unFilling && (!fill || (fill && (scrollMax_Y <= 0 && scrollMax_X <= 0))))) // left mouse click (fill-click with no scrollbars and not left click after middle click - else preserve scroll percentage)
  296. {
  297. let scale = Math.min((window.innerWidth / i.width), (window.innerHeight / i.height));
  298. window.scrollTo((ex / scale) - (window.innerWidth / 2), (ey / scale) - (window.innerHeight / 2));
  299. }
  300. else // keep percentage scroll progress for KB hotkeys
  301. {
  302. scrollMax_Y = window.scrollMaxY || ((document.body.scrollHeight || document.documentElement.scrollHeight)- document.documentElement.clientHeight);
  303. scrollMax_X = window.scrollMaxX || ((document.body.scrollWidth || document.documentElement.scrollWidth)- document.documentElement.clientWidth);
  304.  
  305. window.scrollTo(Math.round(scrollProgressX * scrollMax_X), Math.round(scrollProgressY * scrollMax_Y));
  306.  
  307. scrolledY = window.pageYOffset;
  308. scrolledX = window.pageXOffset;
  309. }
  310. }
  311.  
  312. var orgImgWidth;
  313. var orgImgHeight;
  314.  
  315. var ARC = 0;
  316. function autoresize()
  317. {
  318. if(!((!is_video && i.naturalHeight) || i.videoHeight))
  319. {
  320. ARC++;
  321. if(ARC < 500)
  322. {
  323. setTimeout(autoresize, 10);
  324. }
  325. else
  326. {
  327. console.warn("Center Image: Gave up waiting for a working image, it is broken");
  328. }
  329. return;
  330. }
  331. if(is_video) // chrome & FF don't show video resolution, + chrome doesn't show video name
  332. {
  333. if(!FireFox)
  334. {
  335. title = i.src.substr(i.src.lastIndexOf("/")+1);
  336. if(title.indexOf("?") != -1)
  337. {
  338. title = title.substr(0, title.indexOf("?"));
  339. }
  340. }
  341. title = title + " (" + i.videoWidth + "x" + i.videoHeight + ")";
  342. //title = title + " (" + i.naturalWidth + "x" + i.naturalHeight + ")"; // for images
  343. //title = decodeURIComponent(title); // browser already decoded it
  344. }
  345. else if(FireFox)
  346. {
  347. let f = title.indexOf(" — Scaled (");
  348. if(f !== -1)
  349. {
  350. title = title.substring(0,f);
  351. }
  352. }
  353. document.title = title;
  354. orgImgWidth = Math.round((is_video ? i.videoWidth : i.naturalWidth) / window.devicePixelRatio);
  355. orgImgHeight = Math.round((is_video ? i.videoHeight : i.naturalHeight) / window.devicePixelRatio);
  356.  
  357. let css = (is_video? "video" : "img") +`{position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: `+cfg_bgclr+` !important; outline: none; max-width: unset; max-height: unset;}
  358. body {margin: 0px !important; background-color: `+cfg_bgclr+` !important;}
  359. .center {margin: auto !important;}
  360. .center_H {margin: 0px auto !important;}
  361. .fill_H {width: 100% !important; height: auto !important;}
  362. .fill_V {width: auto !important; height: 100% !important;}
  363. .org {width: `+ orgImgWidth + `px !important; height: `+ orgImgHeight + `px !important; }`;
  364. AddElementToPage(document.documentElement, 'style', {textContent: css});
  365.  
  366. let InitRescale = false; // directly opened image is already fit to window if it is bigger by the browser
  367. if(cfg_fitWH && orgImgHeight > window.innerHeight && orgImgWidth > window.innerWidth) // both scrollbars
  368. {
  369. InitRescale = true;
  370. }
  371. else if(cfg_fitB && (orgImgHeight > window.innerHeight || orgImgWidth > window.innerWidth)) // one scrollbar
  372. {
  373. InitRescale = true;
  374. }
  375. else if(cfg_fitS && orgImgHeight <= window.innerHeight && orgImgWidth <= window.innerWidth) // no scrollbars
  376. {
  377. InitRescale = true;
  378. }
  379. if(InitRescale)
  380. {
  381. rescale(null, cfg_fill ? true : false);
  382. }
  383. else
  384. {
  385. i.classList.add("org");
  386. changeCursor();
  387. }
  388.  
  389. if(cfg_js){eval(cfg_js);}
  390. }
  391.  
  392. // hotkeys
  393. if (typeof KeyEvent === "undefined")
  394. {
  395. var KeyEvent = {
  396. DOM_VK_SPACE: 32,
  397. DOM_VK_LEFT: 37,
  398. DOM_VK_UP: 38,
  399. DOM_VK_RIGHT: 39,
  400. DOM_VK_DOWN: 40,
  401. DOM_VK_A: 65,
  402. DOM_VK_D: 68,
  403. DOM_VK_P: 80,
  404. DOM_VK_Q: 81,
  405. DOM_VK_S: 83,
  406. DOM_VK_W: 87,
  407. DOM_VK_NUMPAD2: 98,
  408. DOM_VK_NUMPAD4: 100,
  409. DOM_VK_NUMPAD5: 101,
  410. DOM_VK_NUMPAD6: 102,
  411. DOM_VK_NUMPAD8: 104,
  412. DOM_VK_F5: 116,
  413. DOM_VK_TAB: 9,
  414. DOM_VK_ENTER: 13
  415. };
  416. }
  417.  
  418. function cancelEvent(a)
  419. {
  420. a = a ? a : window.event;
  421. if (a.stopPropagation)
  422. {
  423. a.stopPropagation();
  424. }
  425. if (a.preventDefault)
  426. {
  427. a.preventDefault();
  428. }
  429. a.cancelBubble = true;
  430. a.cancel = true;
  431. a.returnValue = false;
  432. return false;
  433. }
  434.  
  435. function scroll_space(a, b)
  436. {
  437. var by = Math.round((b ? window.innerHeight : window.innerWidth) * 0.50 * (a ? -1 : 1));
  438. if(!b)
  439. {
  440. window.scrollBy(0, by);
  441. }
  442. else
  443. {
  444. window.scrollBy(by, 0);
  445. }
  446. }
  447.  
  448. function onkeydown (b)
  449. {
  450. var a = (window.event) ? b.keyCode : b.which;
  451. if (a != KeyEvent.DOM_VK_SPACE && (b.altKey || b.ctrlKey || b.metaKey))
  452. {
  453. return;
  454. }
  455. var by = Math.round(window.innerHeight * 0.10);
  456. switch (a)
  457. {
  458. case KeyEvent.DOM_VK_RIGHT:
  459. case KeyEvent.DOM_VK_D:
  460. case KeyEvent.DOM_VK_NUMPAD6:
  461. if(!is_video)
  462. {
  463. window.scrollBy(by, 0);
  464. }
  465. else
  466. {
  467. i.currentTime += skip_by;
  468. }
  469. cancelEvent(b);
  470. break;
  471. case KeyEvent.DOM_VK_LEFT:
  472. case KeyEvent.DOM_VK_A:
  473. case KeyEvent.DOM_VK_NUMPAD4:
  474. if(!is_video)
  475. {
  476. window.scrollBy(by * -1, 0);
  477. }
  478. else
  479. {
  480. i.currentTime -= skip_by;
  481. }
  482. cancelEvent(b);
  483. break;
  484. case KeyEvent.DOM_VK_W:
  485. case KeyEvent.DOM_VK_NUMPAD8:
  486. window.scrollBy(0, by * -1);
  487. cancelEvent(b);
  488. break;
  489. case KeyEvent.DOM_VK_S:
  490. case KeyEvent.DOM_VK_NUMPAD2:
  491. window.scrollBy(0, by);
  492. cancelEvent(b);
  493. break;
  494. case KeyEvent.DOM_VK_SPACE:
  495. if(!is_video)
  496. {
  497. scroll_space(b.shiftKey, b.ctrlKey);
  498. }
  499. else if(i.paused || i.ended)
  500. {
  501. i.play();
  502. }
  503. else
  504. {
  505. i.pause();
  506. }
  507. cancelEvent(b);
  508. break;
  509. case KeyEvent.DOM_VK_TAB:
  510. case KeyEvent.DOM_VK_ENTER:
  511. rescale(null, true);
  512. cancelEvent(b);
  513. break;
  514. case KeyEvent.DOM_VK_Q:
  515. case KeyEvent.DOM_VK_NUMPAD5:
  516. rescale(null, false);
  517. cancelEvent(b);
  518. break;
  519. case KeyEvent.DOM_VK_P:
  520. cfg();
  521. cancelEvent(b);
  522. }
  523. }
  524.  
  525. var cfg_bgclr = "grey";
  526. var cfg_fitWH = true;
  527. var cfg_fitB = false;
  528. var cfg_fitS = true;
  529. var cfg_fill = false;
  530. var cfg_js;
  531. var cfg_vol = "0.5";
  532.  
  533. function cfg(){}
  534. if (typeof GM !== "undefined" && typeof GM.getValue !== "undefined")
  535. {
  536. async function loadCfg()
  537. {
  538. cfg_bgclr = await GM.getValue("bgColor", "grey");
  539. cfg_fitWH = await GM.getValue("fitWH", true);
  540. cfg_fitB = await GM.getValue("fitB", false);
  541. cfg_fitS = await GM.getValue("fitS", true);
  542. cfg_fill = await GM.getValue("fill", false);
  543. cfg_js = await GM.getValue("js", "");
  544. cfg_vol = await GM.getValue("vid_volume", "0.5");
  545. }
  546. loadCfg();
  547.  
  548. function $(id) {return document.getElementById(id);}
  549.  
  550. cfg = function ()
  551. {
  552. function saveCfg()
  553. {
  554. GM.setValue("bgColor", $("ci_cfg_2_bgclr").value);
  555. GM.setValue("fitWH", $("ci_cfg_3_fitWH").checked);
  556. GM.setValue("fitB", $("ci_cfg_4_fitB").checked);
  557. GM.setValue("fitS", $("ci_cfg_5_fitS").checked);
  558. GM.setValue("fill", $("ci_cfg_7_fill").checked);
  559. GM.setValue("js", $("ci_cfg_6_js").value);
  560. alert("Configuration Saved");
  561. if($("ci_cfg_2_bgclr").value){document.body.bgColor = $("ci_cfg_2_bgclr").value;}else{document.body.removeAttribute("bgColor");}
  562. }
  563. if(i){
  564. i.removeEventListener("click", rescale, true);
  565. i.removeEventListener("auxclick", rescale, true);
  566. i.removeEventListener("mousedown", onmousedown, true);
  567. }
  568. window.removeEventListener("keydown", onkeydown, true);
  569. if(document.head){document.head.innerHTML = "";}
  570. document.body.innerHTML = "";
  571. var div = document.createElement("div");
  572. div.style = "margin: auto; width: fit-content; height: fit-content; border: 1px solid black; color: black; background: silver; position: absolute; top: 0; right: 0; bottom: 0; left: 0;";
  573. div.innerHTML = "<b><center>Configuration</center></b>"
  574. + "<br><input id='ci_cfg_2_bgclr' type='text' size='6'> Background color (empty = default)"
  575. + "<br><br>Fit to window images:" + " ( Fill to window instead <input id='ci_cfg_7_fill' type='checkbox'> )"
  576. + "<br><br><input id='ci_cfg_3_fitWH' type='checkbox'> Larger than window both vertically and horizontally"
  577. + "<br><br><input id='ci_cfg_4_fitB' type='checkbox'> Larger than window either vertically or horizontally"
  578. + "<br><br><input id='ci_cfg_5_fitS' type='checkbox'> Smaller than window"
  579. + "<br><br><center>Custom JS Action:<textarea id='ci_cfg_6_js' style='margin: 0px; width: 400px; height: 50px;'></textarea>"
  580. + "<br><input id='ci_cfg_save' type='button' value='Save configuration'></center>";
  581. document.body.appendChild(div);
  582. $("ci_cfg_2_bgclr").value = cfg_bgclr;
  583. $("ci_cfg_3_fitWH").checked = cfg_fitWH;
  584. $("ci_cfg_4_fitB").checked = cfg_fitB;
  585. $("ci_cfg_5_fitS").checked = cfg_fitS;
  586. $("ci_cfg_7_fill").checked = cfg_fill;
  587. $("ci_cfg_6_js").value = cfg_js;
  588. $("ci_cfg_save").addEventListener("click", saveCfg, true);
  589. }
  590. }
  591. else
  592. {
  593. cfg_js = "";
  594. cfg = function ()
  595. {
  596. alert("Sorry, Chrome userscripts in native mode can't have configurations! You need to install TamperMonkey userscript manager extension. (it's very good)");
  597. }
  598. }
  599. makeimage();