// ==UserScript==
// @name Center Image & Video
// @namespace CenterImage
// @author Owyn
// @version 2024.02.01
// @description Improved controls for images & videos opened directly with your browser - hotkeys & resizing & visuals
// @supportURL https://github.com/Owyn/Center_Image/issues
// @homepage https://github.com/Owyn/Center_Image
// @run-at document-end
// @noframes
// @grant GM.getValue
// @grant GM.setValue
// @grant GM_addElement
// @grant GM_registerMenuCommand
// @sandbox JavaScript
// @match http://*/*
// @match https://*/*
// @match file:///*/*
// ==/UserScript==
"use strict";
let type = document.contentType.substring(0,document.contentType.indexOf("/"));
let i, is_video;
if (type === "image")
{
is_video = false;
i = document.images[0];
}
else if(type === "video")
{
is_video = true;
i = document.querySelector("video");
}
else
{
return false;
}
if (typeof GM_registerMenuCommand !== "undefined")
{
GM_registerMenuCommand("Center Image Configuration", cfg, "n");
}
const AddElementToPage = typeof GM_addElement === "function" ? GM_addElement : function GM_addElement(node, type, content)
{
let el = document.createElement(type);
el.textContent = content.textContent;
node.appendChild(el);
};
let rescaled = 0;
let iot = 0, iol = 0;
let skip_by = 5;
//let theStyle;
let title;
function makeimage()
{
if(typeof cfg_js !== "string") {setTimeout(function() { makeimage(); }, 11); return false;} // lets wait for async
title = document.title;
if(cfg_bgclr)
{
if(document.head){document.head.innerHTML = "";} // remove FireFox background
}
i.removeAttribute('class');
i.removeAttribute('style'); // chrome
document.body.removeAttribute('style');
if(!is_video)
{
i.addEventListener("click", rescale, true);
i.addEventListener("mousedown", onmousedown, true);
}
else
{
i.volume = cfg_vol;
i.addEventListener("volumechange", onvolumechange, true);
i.controls = true;
i.loop = true;
}
i.addEventListener("auxclick", rescale, true);
window.addEventListener("keydown", onkeydown, true);
window.addEventListener("scroll", onscroll, false);
window.addEventListener("resize", onresize, true);
onVisibilityChange();
}
function onvolumechange()
{
GM.setValue("vid_volume", (i.muted? "0" : i.volume));
}
function onVisibilityChange()
{
if (document.visibilityState === 'visible')
{
//console.log('visible');
window.removeEventListener('visibilitychange', onVisibilityChange);
autoresize();
}
}
window.addEventListener("visibilitychange", onVisibilityChange);
var scrolledY, scrolledX;
function onscroll()
{
scrolledY = window.pageYOffset;
scrolledX = window.pageXOffset;
}
var FireFox = ((navigator.userAgent.indexOf('Firefox') != -1) ? true : false);
function onresize() // doesn't let image change back to "fit to window" in chrome & in both helps keep scroll progress
{
if(rescaled != 1)
{
let wasFilled = (rescaled === 2 ? true : false);
rescaled = 1;
let chromeplsstopX, chromeplsstopY;
if(!FireFox){ chromeplsstopX = window.pageXOffset; chromeplsstopY = window.pageYOffset;}
rescale(null, wasFilled);
if(!FireFox){ window.scrollTo(chromeplsstopX,chromeplsstopY);}
}
}
function changeCursor()
{
if(i.scrollHeight > window.innerHeight) // image pushing out-of-screen at top fix
{
i.classList.add("center_H");
//theStyle.sheet.rules[0].style.margin = "0px auto";
}
else
{
i.classList.add("center");
//theStyle.sheet.rules[0].style.margin = "auto";
}
if(is_video) return;
//i.style.margin = "auto";
if(rescaled === 0) // original
{
if((orgImgWidth == window.innerWidth && orgImgHeight <= window.innerHeight) || (orgImgWidth <= window.innerWidth && orgImgHeight == window.innerHeight)) // perfect fit on one side, can't resize
{
i.style.cursor = "";
}
else if (orgImgWidth > window.innerWidth || orgImgHeight > window.innerHeight)
{
i.style.cursor = "zoom-out";
}
else
{
i.style.cursor = "zoom-in";
}
}
else if(rescaled === 2) // fill
{
if(orgImgWidth == window.innerWidth && orgImgHeight == window.innerHeight) // perfect fit, can't resize
{
i.style.cursor = "";
}
else if (orgImgWidth > i.width)
{
i.style.cursor = "zoom-in";
}
else
{
i.style.cursor = "zoom-out";
}
}
else // if(rescaled === 1) // fit
{
if((orgImgWidth == window.innerWidth && orgImgHeight <= window.innerHeight) || (orgImgWidth <= window.innerWidth && orgImgHeight == window.innerHeight)) // perfect fit on one side, can't resize
{
i.style.cursor = "";
}
else if (orgImgWidth > i.width)
{
i.style.cursor = "zoom-in";
}
else
{
i.style.cursor = "zoom-out";
}
}
}
function onmousedown(event)
{
if(i.offsetLeft > 0){iol = i.offsetLeft;}
if(i.offsetTop > 0){iot = i.offsetTop;}
if(event.which === 2) // middle mouse Chrome fix
{
event.preventDefault();
event.stopPropagation();
return;
}
}
function rescale(event, fill)
{
let ex,ey;
if(event) // mouse click
{
if (typeof event.y === "undefined") // Firefox
{
ex = event.clientX;
ey = event.clientY;
}
else
{
ex = event.x;
ey = event.y;
}
ex -= iol;
ey -= iot;
if(event.which === 2) // middle mouse
{
fill = true;
}
else if(event.which === 3) // right mouse
{
return;
}
event.preventDefault();
event.stopPropagation();
}
//document.body.style.overflowX = '';
//document.body.style.overflowY = '';
document.body.removeAttribute('style');
let scrollMax_Y = window.scrollMaxY || ((document.body.scrollHeight || document.documentElement.scrollHeight)- document.documentElement.clientHeight);
let scrollMax_X = window.scrollMaxX || ((document.body.scrollWidth || document.documentElement.scrollWidth)- document.documentElement.clientWidth);
let scrollProgressY = scrolledY / scrollMax_Y;
let scrollProgressX = scrolledX / scrollMax_X;
let unFilling = false;
let sidesCMP;
i.removeAttribute('class'); // clear classes
if(fill)
{
if(rescaled === 2) // to original
{
rescaled = 0;
i.classList.add("org");
//theStyle.sheet.rules[0].style.width = orgImgWidth + "px";
//theStyle.sheet.rules[0].style.height = orgImgHeight + "px";
}
else // fill
{
sidesCMP = (orgImgWidth / orgImgHeight) < (window.innerWidth / window.innerHeight);
rescaled = 2;
}
}
else
{
if(rescaled != 0) // to original
{
if(rescaled === 2) {unFilling = true;}
rescaled = 0;
i.classList.add("org");
//theStyle.sheet.rules[0].style.width = orgImgWidth + "px";
//theStyle.sheet.rules[0].style.height = orgImgHeight + "px";
}
else // fit
{
sidesCMP = (orgImgWidth / orgImgHeight) > (window.innerWidth / window.innerHeight);
rescaled = 1;
}
}
if(rescaled != 0)
{
if(sidesCMP)
{
i.classList.add("fill_H");
//theStyle.sheet.rules[0].style.width = "100%";
//theStyle.sheet.rules[0].style.height = "auto";
document.body.style.overflowX = 'hidden'; // we don't need unscrollable scrollbars if they appear
}
else
{
i.classList.add("fill_V");
//theStyle.sheet.rules[0].style.height = "100%";
//theStyle.sheet.rules[0].style.width = "auto";
document.body.style.overflowY = 'hidden'; // we don't need unscrollable scrollbars if they appear
}
}
changeCursor();
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)
{
let scale = Math.min((window.innerWidth / i.width), (window.innerHeight / i.height));
window.scrollTo((ex / scale) - (window.innerWidth / 2), (ey / scale) - (window.innerHeight / 2));
}
else // keep percentage scroll progress for KB hotkeys
{
scrollMax_Y = window.scrollMaxY || ((document.body.scrollHeight || document.documentElement.scrollHeight)- document.documentElement.clientHeight);
scrollMax_X = window.scrollMaxX || ((document.body.scrollWidth || document.documentElement.scrollWidth)- document.documentElement.clientWidth);
window.scrollTo(Math.round(scrollProgressX * scrollMax_X), Math.round(scrollProgressY * scrollMax_Y));
scrolledY = window.pageYOffset;
scrolledX = window.pageXOffset;
}
}
var orgImgWidth;
var orgImgHeight;
var ARC = 0;
function autoresize()
{
if(!((!is_video && i.naturalHeight) || i.videoHeight))
{
ARC++;
if(ARC < 500)
{
setTimeout(autoresize, 10);
}
else
{
console.warn("Center Image: Gave up waiting for a working image, it is broken");
}
return;
}
if(is_video) // chrome & FF don't show video resolution, + chrome doesn't show video name
{
if(!FireFox)
{
title = i.src.substr(i.src.lastIndexOf("/")+1);
if(title.indexOf("?") != -1)
{
title = title.substr(0, title.indexOf("?"));
}
}
title = title + " (" + i.videoWidth + "x" + i.videoHeight + ")";
//title = title + " (" + i.naturalWidth + "x" + i.naturalHeight + ")"; // for images
//title = decodeURIComponent(title); // browser already decoded it
}
else if(FireFox)
{
let f = title.indexOf(" — Scaled (");
if(f !== -1)
{
title = title.substring(0,f);
}
}
document.title = title;
orgImgWidth = Math.round((is_video ? i.videoWidth : i.naturalWidth) / window.devicePixelRatio);
orgImgHeight = Math.round((is_video ? i.videoHeight : i.naturalHeight) / window.devicePixelRatio);
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;}
body {margin: 0px !important; background-color: `+cfg_bgclr+` !important;}
.center {margin: auto !important;}
.center_H {margin: 0px auto !important;}
.fill_H {width: 100% !important; height: auto !important;}
.fill_V {width: auto !important; height: 100% !important;}
.org {width: `+ orgImgWidth + `px !important; height: `+ orgImgHeight + `px !important; }`;
AddElementToPage(document.documentElement, 'style', {textContent: css});
let InitRescale = false; // directly opened image is already fit to window if it is bigger by the browser
if(cfg_fitWH && orgImgHeight > window.innerHeight && orgImgWidth > window.innerWidth) // both scrollbars
{
InitRescale = true;
}
else if(cfg_fitB && (orgImgHeight > window.innerHeight || orgImgWidth > window.innerWidth)) // one scrollbar
{
InitRescale = true;
}
else if(cfg_fitS && orgImgHeight <= window.innerHeight && orgImgWidth <= window.innerWidth) // no scrollbars
{
InitRescale = true;
}
if(InitRescale)
{
rescale(null, cfg_fill ? true : false);
}
else
{
i.classList.add("org");
changeCursor();
}
if(cfg_js){eval(cfg_js);}
}
// hotkeys
if (typeof KeyEvent === "undefined")
{
var KeyEvent = {
DOM_VK_SPACE: 32,
DOM_VK_LEFT: 37,
DOM_VK_UP: 38,
DOM_VK_RIGHT: 39,
DOM_VK_DOWN: 40,
DOM_VK_A: 65,
DOM_VK_D: 68,
DOM_VK_P: 80,
DOM_VK_Q: 81,
DOM_VK_S: 83,
DOM_VK_W: 87,
DOM_VK_NUMPAD2: 98,
DOM_VK_NUMPAD4: 100,
DOM_VK_NUMPAD5: 101,
DOM_VK_NUMPAD6: 102,
DOM_VK_NUMPAD8: 104,
DOM_VK_F5: 116,
DOM_VK_TAB: 9,
DOM_VK_ENTER: 13
};
}
function cancelEvent(a)
{
a = a ? a : window.event;
if (a.stopPropagation)
{
a.stopPropagation();
}
if (a.preventDefault)
{
a.preventDefault();
}
a.cancelBubble = true;
a.cancel = true;
a.returnValue = false;
return false;
}
function scroll_space(a, b)
{
var by = Math.round((b ? window.innerHeight : window.innerWidth) * 0.50 * (a ? -1 : 1));
if(!b)
{
window.scrollBy(0, by);
}
else
{
window.scrollBy(by, 0);
}
}
function onkeydown (b)
{
var a = (window.event) ? b.keyCode : b.which;
if (a != KeyEvent.DOM_VK_SPACE && (b.altKey || b.ctrlKey || b.metaKey))
{
return;
}
var by = Math.round(window.innerHeight * 0.10);
switch (a)
{
case KeyEvent.DOM_VK_RIGHT:
case KeyEvent.DOM_VK_D:
case KeyEvent.DOM_VK_NUMPAD6:
if(!is_video)
{
window.scrollBy(by, 0);
}
else
{
i.currentTime += skip_by;
}
cancelEvent(b);
break;
case KeyEvent.DOM_VK_LEFT:
case KeyEvent.DOM_VK_A:
case KeyEvent.DOM_VK_NUMPAD4:
if(!is_video)
{
window.scrollBy(by * -1, 0);
}
else
{
i.currentTime -= skip_by;
}
cancelEvent(b);
break;
case KeyEvent.DOM_VK_W:
case KeyEvent.DOM_VK_NUMPAD8:
window.scrollBy(0, by * -1);
cancelEvent(b);
break;
case KeyEvent.DOM_VK_S:
case KeyEvent.DOM_VK_NUMPAD2:
window.scrollBy(0, by);
cancelEvent(b);
break;
case KeyEvent.DOM_VK_SPACE:
if(!is_video)
{
scroll_space(b.shiftKey, b.ctrlKey);
}
else if(i.paused || i.ended)
{
i.play();
}
else
{
i.pause();
}
cancelEvent(b);
break;
case KeyEvent.DOM_VK_TAB:
case KeyEvent.DOM_VK_ENTER:
rescale(null, true);
cancelEvent(b);
break;
case KeyEvent.DOM_VK_Q:
case KeyEvent.DOM_VK_NUMPAD5:
rescale(null, false);
cancelEvent(b);
break;
case KeyEvent.DOM_VK_P:
cfg();
cancelEvent(b);
}
}
var cfg_bgclr = "grey";
var cfg_fitWH = true;
var cfg_fitB = false;
var cfg_fitS = true;
var cfg_fill = false;
var cfg_js;
var cfg_vol = "0.5";
function cfg(){}
if (typeof GM !== "undefined" && typeof GM.getValue !== "undefined")
{
async function loadCfg()
{
cfg_bgclr = await GM.getValue("bgColor", "grey");
cfg_fitWH = await GM.getValue("fitWH", true);
cfg_fitB = await GM.getValue("fitB", false);
cfg_fitS = await GM.getValue("fitS", true);
cfg_fill = await GM.getValue("fill", false);
cfg_js = await GM.getValue("js", "");
cfg_vol = await GM.getValue("vid_volume", "0.5");
}
loadCfg();
function $(id) {return document.getElementById(id);}
cfg = function ()
{
function saveCfg()
{
GM.setValue("bgColor", $("ci_cfg_2_bgclr").value);
GM.setValue("fitWH", $("ci_cfg_3_fitWH").checked);
GM.setValue("fitB", $("ci_cfg_4_fitB").checked);
GM.setValue("fitS", $("ci_cfg_5_fitS").checked);
GM.setValue("fill", $("ci_cfg_7_fill").checked);
GM.setValue("js", $("ci_cfg_6_js").value);
alert("Configuration Saved");
if($("ci_cfg_2_bgclr").value){document.body.bgColor = $("ci_cfg_2_bgclr").value;}else{document.body.removeAttribute("bgColor");}
}
if(i){
i.removeEventListener("click", rescale, true);
i.removeEventListener("auxclick", rescale, true);
i.removeEventListener("mousedown", onmousedown, true);
}
window.removeEventListener("keydown", onkeydown, true);
if(document.head){document.head.innerHTML = "";}
document.body.innerHTML = "";
var div = document.createElement("div");
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;";
div.innerHTML = "<b><center>Configuration</center></b>"
+ "<br><input id='ci_cfg_2_bgclr' type='text' size='6'> Background color (empty = default)"
+ "<br><br>Fit to window images:" + " ( Fill to window instead <input id='ci_cfg_7_fill' type='checkbox'> )"
+ "<br><br><input id='ci_cfg_3_fitWH' type='checkbox'> Larger than window both vertically and horizontally"
+ "<br><br><input id='ci_cfg_4_fitB' type='checkbox'> Larger than window either vertically or horizontally"
+ "<br><br><input id='ci_cfg_5_fitS' type='checkbox'> Smaller than window"
+ "<br><br><center>Custom JS Action:<textarea id='ci_cfg_6_js' style='margin: 0px; width: 400px; height: 50px;'></textarea>"
+ "<br><input id='ci_cfg_save' type='button' value='Save configuration'></center>";
document.body.appendChild(div);
$("ci_cfg_2_bgclr").value = cfg_bgclr;
$("ci_cfg_3_fitWH").checked = cfg_fitWH;
$("ci_cfg_4_fitB").checked = cfg_fitB;
$("ci_cfg_5_fitS").checked = cfg_fitS;
$("ci_cfg_7_fill").checked = cfg_fill;
$("ci_cfg_6_js").value = cfg_js;
$("ci_cfg_save").addEventListener("click", saveCfg, true);
}
}
else
{
cfg_js = "";
cfg = function ()
{
alert("Sorry, Chrome userscripts in native mode can't have configurations! You need to install TamperMonkey userscript manager extension. (it's very good)");
}
}
makeimage();