// ==UserScript==
// @name Hentai Foundry - Image Hover
// @namespace https://github.com/Kayla355
// @version 0.3.2
// @description Fetches a larger version of the image upon hovering over a thumbnail.
// @author Kayla355
// @match http://www.hentai-foundry.com/*
// @match https://www.hentai-foundry.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @icon http://img.hentai-foundry.com/themes/Hentai/favicon.ico
// @require http://code.jquery.com/jquery-2.1.3.min.js
// @require http://cdn.jsdelivr.net/jquery.visible/1.1.0/jquery.visible.min.js
// @require https://cdn.rawgit.com/Kayla355/MonkeyConfig/d152bb448db130169dbd659b28375ae96e4c482d/monkeyconfig.js
// @history 0.2 Fixed an issue with smartPreload not loading in the image correctly. Also fixed an issue with flash files.
// @history 0.2.1 Fixed some issues with loading getting stuck.
// @history 0.2.2 Added more image positions.
// @history 0.2.3 Fixed the images not loading with the new HF theme. Some issues still remain with screen boundries, works when combined with my CSS fixes script.
// @history 0.2.4 Fixed the image boundries and also fixed an issue with the title showing up over the image somtimes.
// @history 0.2.5 Fixed an issue where images were still attempting to cache even after having already been cached.
// @history 0.3.0 Added MonkeyConfig options config, for actually storing options.
// @history 0.3.1 Quick bug fix from some changes to the website.
// @history 0.3.2 Fixed an issue with the "smart-preload" option.
// ==/UserScript==
// Options //
var imagePosition;
var hoverSize;
var smartPreload;
var preloadAll;
cfg = new MonkeyConfig({
title: 'Hentai Foundry - Image Hover Configuration',
menuCommand: true,
params: {
image_position: {
type: 'select',
choices: [ 'top-left', 'top-right', 'bottom-left', 'bottom-right', 'middle-left', 'middle-right'],
default: 'middle-right'
},
hover_size: {
type: 'number',
default: 512
},
preload_option: {
type: 'select',
choices: [ 'Smart Preload', 'Preload All', 'none'],
default: 'Smart Preload'
}
},
onSave: setOptions
});
function setOptions() {
imagePosition = cfg.get('image_position');
hoverSize = cfg.get('hover_size');
smartPreload = false;
preloadAll = false;
switch(cfg.get('preload_option')) {
case "Smart Preload":
smartPreload = true;
preloadAll = false;
break;
case "Preload All":
smartPreload = false;
preloadAll = true;
break;
default:
smartPreload = false;
preloadAll = false;
}
}
setOptions();
switch(cfg.get('preload_option')) {
case "Smart Preload":
smartPreload = true;
preloadAll = false;
break;
case "Preload All":
smartPreload = false;
preloadAll = true;
break;
default:
smartPreload = false;
preloadAll = false;
}
// Styles //
GM_addStyle(".image-hover {"
+"position: absolute;"
+"z-index: 9999;"
+"box-shadow: 5px 5px 10px 0px rgba(50, 50, 50, 0.75);"
+"pointer-events: none;"
+"}"
+".loader {"
+"position: absolute;"
+"margin: 8px 0px 0px 8px;"
+"border-bottom: 6px solid rgba(255, 255, 255, 0.4);"
+"border-left: 6px solid rgba(255, 255, 255, 0.4);"
+"border-right: 6px solid rgba(255, 255, 255, 0.4);"
+"border-top: 6px solid rgba(0, 0, 0, 0.8);"
+"border-radius: 100%;"
+"height: 25px;"
+"width: 25px;"
+"animation: rot 0.6s infinite linear;"
+"}"
+"@keyframes rot {"
+"from {transform: rotate(0deg);}"
+"to {transform: rotate(359deg);}"
+"}"
+"#pl-background {"
+"position: absolute;"
+"background-color: white;"
+"height: 20px;"
+"width: 125px;"
+"border-radius: 25px;"
+"}"
+"#pl-fill {"
+"display: inline-block;"
+"background-color: red;"
+"height: 20px;"
+"border-radius: 25px;"
+"}"
+"#pl-background center {"
+"position: absolute;"
+"top: 0px;"
+"left: 0px;"
+"width: 125px;"
+"text-align: center;"
+"font-size: 10px;"
+"font-weight: 900;"
+"line-height: 20px;"
+"}"
+".thumb:hover {"
+"position: relative !important;"
+"padding: 0;"
+"margin: 0;"
+"background-size: cover;"
+"border: 0;"
+"}");
// Variables //
var hovering = false;
var mouse = {X: 0, Y: 0};
var imageExt = [".jpg", ".jpeg", ".png", ".gif"];
loaded = {};
var plProgress = {current: 0, total: 0, percent: "0%"};
var loadingStatus = "inactive";
var oldTitle = "";
var done;
// Timers
var hoverTimer;
var hoverTimerStart;
var scrollTimer;
// Code //
// Event Listeners //
// Start preloading images
if(preloadAll || smartPreload) {
if(preloadAll) {
smartPreload = false;
}
loadImages();
}
// Listen for Events on thumbnails
$(".thumb").on({
mousemove: function(e) {
// Get mouse location
if(e.pageY && e.pageX) {
mouse.Y = e.pageY + 2;
mouse.X = e.pageX + 5;
}
// Run function to keep image inside of window.
if(hovering) {
keepInside();
}
//console.log("X:", e.pageX, ", Y:", e.pageY);
},
mouseenter: function(e) {
// Create links, id, etc.
var link = e.target.parentNode.href.match(/(https?:\/\/www.hentai-foundry.com\/pictures\/user)(\/.*\/?)/)[2];
var id = link.match(/(?:\/.*\/)(.*)(?:\/)/)[1];
var cat = link.slice(1, 2).toLowerCase();
if(cat.match(/-/)) {
cat = "_";
} else if(cat.match(/[^a-z]/)) {
cat = "0";
}
var src = "http://pictures.hentai-foundry.com/" + cat + "/" + link.slice(0, -1);
var obj = {id: id, src: src, target: e.target, from: "hover"};
// Title issue fix
oldTitle = this.title;
this.title = "";
// Create content div
$('<div class="image-hover">'
+'<div id="hoverLoader" class="loader"></div>'
+'<div id="'+ id +'" style="display:none"></div>'
+'</div>').appendTo("body");
// Check if user has recently hovered over an object, thereby triggering the hover mode.
if(!hovering) {
clearTimeout(hoverTimerStart);
hoverTimerStart = setTimeout(function() {
hovering = true;
hoverFunc(obj);
}, 500);
} else {
hoverFunc(obj);
}
// Clear timer to exit "hovermode"
clearTimeout(hoverTimer);
},
mouseleave: function(e) {
// Clear timeouts and remove divs.
clearTimeout(hoverTimerStart);
$("div.image-hover").remove();
hoverTimer = setTimeout(function() { hovering = false; }, 500);
// Title issue fix
this.title = oldTitle;
oldTitle = "";
}
});
// If smartPreload is enabled, Listen for when the document is scrolled
if(smartPreload) {
$(document).on('scroll', function() {
console.log("Scrolled, loading is", loadingStatus);
if(preloadAll || smartPreload) {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(function() {
if(loadingStatus === "active") {
$(document).on("stoppedLoading", function() {
console.log("stopped loading, starting again");
loadImages();
});
} else {
loadImages();
}
}, 1000);
}
});
}
// Listen for update to the pre-load progress.
// Re-usable Functions //
// Function that is run when hovering over an image.
function hoverFunc(obj) {
var target = obj.target;
var id = obj.id;
$('#'+id).on("imageLoaded", function() {
$('#hoverLoader').remove();
// Set size of image
loaded[id].image.style.maxHeight = hoverSize +"px";
loaded[id].image.style.maxWidth = hoverSize +"px";
$('.image-hover div#'+ obj.id).css("background-color", "#a3a3ab").append(loaded[id].image).show();
$(obj.target).trigger("mousemove");
});
$(target).trigger("mousemove");
if(plProgress.current != plProgress.total) {
$('#hoverLoader').remove();
$('.image-hover').append('<div id="pl-background"><div id="pl-fill"></div><center></center></div>');
} else if (validateImage(id)) {
if(loaded[id].status === "done") {
loaded[id].from = "hover";
createImages(loaded[id]);
}
} else {
imageExt.eachImage(obj);
}
$(document).trigger("plStatusChange");
}
// Function for creating and loading the images before showing.
function loadImages() {
var thumbs = $('.thumb');
var from = "preload";
loadingStatus = "active";
done = 0;
if(smartPreload) {
from = "smartload";
console.log("Filtering!");
thumbs = $('.thumb').filter(function(e) {
var id = parseInt(this.style["background-image"].replace(/.*pid=([0-9]+).*/, '$1'));
if(!id || typeof id !== "number") return false;
if($(this).visible( true )) {
if(validateImage(id)) {
console.info("["+id+"]", "Image already loaded:", loaded[id].image.src);
}
//console.log("Visible:",$(this).visible( true ));
return true;
} else {
//console.log("Visible:", $(this).visible( true ), "Loaded:", loaded[id]);
return false;
}
});
}
if(thumbs.length === 0) {
console.log("Finished loading images");
loadingStatus = "inactive";
$(document).trigger("loadingReady");
}
thumbs.each(function(i) {
var e = {target: this};
var link = e.target.parentNode.href.match(/(https?:\/\/www.hentai-foundry.com\/pictures\/user)(\/.*\/?)/)[2];
var id = link.match(/(?:\/.*\/)(.*)(?:\/)/)[1];
var cat = link.slice(1, 2).toLowerCase();
if(cat.match(/-/)) {
cat = "_";
} else if(cat.match(/[^a-z]/)) {
cat = "0";
}
var imgSrc = "http://pictures.hentai-foundry.com/" + cat + "/" + link.slice(0, -1);
loaded[id] = {};
var fail = 0;
imageExt.forEach(function(ext) {
imageExists(imgSrc + ext, function(exists) {
if(exists) {
loaded[id].id = id;
loaded[id].src = imgSrc;
loaded[id].ext = ext;
loaded[id].target = e.target;
loaded[id].from = from;
if(loaded[id].ext && from === "preload") {
plProgress.realtotal++;
//return;
}
createImages(loaded[id], thumbs.length);
} else {
fail++;
if(fail === imageExt.length) {
done++;
console.log("Loading Progress: ", done +" / "+ thumbs.length);
loaded[id].ext = "failed";
console.error("Could not determine file type:", imgSrc);
}
}
});
});
});
}
// Create the image and load it before attaching it to the div.
function createImages(obj, total) {
var image = new Image();
if(obj.from === "preload") {
plProgress.total = total;
if(plProgress.realtotal > plProgress.total && obj.from === "preload") {
plProgress.total = plProgress.realtotal;
}
}
if(obj.status === "done") {
if(loaded[obj.id].image) {
if($('#'+obj.id+' img').length === 0) {
$('#'+obj.id).trigger("imageLoaded");
}
return;
}
}
if(obj.from != "preload" && obj.status !== "done") {
image.onload = function () {
obj.image = image;
if($('#'+obj.id+' img').length === 0) {
obj.status = "done";
if(obj.from === "smartload") {
loaded[obj.id].status = obj.status;
loaded[obj.id].image = image;
} else {
loaded[obj.id] = obj;
}
console.info("["+obj.id+"]", "Image loaded:", obj.image.src);
$('#'+obj.id).trigger("imageLoaded");
}
done++;
console.log("Loading Progress: ", done +" / "+ total);
if(done === total) {
console.log("Finished loading images");
loadingStatus = "inactive";
$(document).trigger("loadingReady");
} else if((total - done) <= Math.round(total/3)) {
console.log("Accepting new Images");
loadingStatus = "ready";
$(document).trigger("loadingReady");
}
};
} else if(obj.from === "preload") {
image.onload = function () {
plProgress = {current: plProgress.current+1, total: plProgress.total, percent: Math.round((plProgress.current / plProgress.total) * 100)};
$(document).trigger("plStatusChange");
};
}
image.onerror = function () {
obj.status = "failed";
console.error("Cannot load image");
};
obj.status = "loading";
image.src = obj.src + obj.ext;
}
// Prototype for checking each of the image extensions listed in 'imageExt'.
Array.prototype.eachImage = function(obj) {
var fail = 0;
this.forEach(function(ext) {
imageExists(obj.src + ext, function(exists) {
if(exists) {
obj.ext = ext;
createImages(obj);
} else {
fail++;
if(fail === imageExt.length) {
done++;
console.log("Loading Progress: ", done +" / "+ thumbs.length);
loaded[id].ext = "failed";
console.error("Could not determine file type:", imgSrc);
}
}
});
});
};
// Checks if the given image url exists
function imageExists(url, callback) {
GM_xmlhttpRequest({
url: url,
method: "HEAD",
onload: function(response) {
callback(response.status < 400);
}
});
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
// Validate the existing image object
function validateImage(id) {
if(loaded[id] == null) return false;
if(loaded[id].image) return true;
if(loaded[id].length > 0) return true;
if(loaded[id].length === 0) return false;
if(typeof loaded[id] !== "object") return false;
for(var key in loaded[id]) {
if (hasOwnProperty.call(loaded[id], key)) return true;
}
return false;
}
// Function for keeping the image inside the window borders.
function keepInside() {
var image = {};
try {
image = {
naturalHeight: $('.image-hover img')[0].height,
naturalWidth: $('.image-hover img')[0].width
};
} catch(e) {
image = {
naturalHeight: 0,
naturalWidth: 0
};
}
var screen = {
height: window.pageYOffset + $(window).height() - 2,
width: window.pageXOffset + $(window).width() - 0,
naturalHeight: $(window).height(),
naturalWidth: $(window).width(),
margin: {
height: parseFloat(($(document).height() - $("body").height()) / 2),
width: parseFloat(($(document).width() - $("body").width()) / 2)
}
};
// Get image height, relative to mouse position.
try {
if(imagePosition === "bottom-left" || imagePosition === "bottom-right") {
image.height = (mouse.Y - 2) + image.naturalHeight;
} else if(imagePosition === "middle-left" || imagePosition === "middle-right") {
image.height = {
top: (mouse.Y - 2) - (image.naturalHeight / 2), // For checking if colliding with top
bottom: (mouse.Y - 2) + (image.naturalHeight / 2) // For checking if colliding with bottom
};
} else {
image.height = (mouse.Y - 2) - image.naturalHeight;
}
} catch(e) {
image.height = 0;
}
// Get image width, relative to mouse position.
try {
if(imagePosition === "top-right" || imagePosition === "bottom-right" || imagePosition === "middle-right") {
image.width = (mouse.X + 2) + image.naturalWidth;
} else {
image.width = (mouse.X + 2) - image.naturalWidth;
}
} catch(e) {
image.width = 0;
}
// Check if image height is outside of screen
// If on bottom
if(imagePosition === "bottom-left" || imagePosition === "bottom-right") {
if(screen.height <= image.height) {
mouse.Y = mouse.Y - (image.height - screen.height);
}
// ELSE IF in middle
} else if(imagePosition === "middle-left" || imagePosition === "middle-right") {
if(screen.height <= image.height.bottom) {
mouse.Y = mouse.Y - (image.height.bottom - screen.height);
} else if(image.height.top + screen.naturalHeight - 1 <= screen.height) {
mouse.Y = mouse.Y - image.height.top + (screen.height - screen.naturalHeight) + 1;
}
// ELSE on top
} else {
if(image.height + screen.naturalHeight - 1 <= screen.height) {
mouse.Y = mouse.Y - image.height + (screen.height - screen.naturalHeight) + 1;
}
}
// Check if image width is outside of screen
// IF on right side
if(imagePosition === "top-right" || imagePosition === "bottom-right" || imagePosition === "middle-right") {
if(screen.width <= image.width) {
mouse.X = mouse.X - (image.width - screen.width);
}
// ELSE on left side
} else {
if (image.width <= 3){
mouse.X = mouse.X + ~image.width + 5;
}
}
// Offset depending image position relative to mouse set in options
switch(imagePosition) {
case "top-left":
image.Y = mouse.Y - (image.naturalHeight);
image.X = mouse.X - (image.naturalWidth);
break;
case "top-right":
image.Y = mouse.Y - (image.naturalHeight);
image.X = mouse.X;
break;
case "bottom-left":
image.Y = mouse.Y;
image.X = mouse.X - (image.naturalWidth);
break;
case "bottom-right":
image.Y = mouse.Y;
image.X = mouse.X;
break;
case "middle-left":
image.Y = mouse.Y - (image.naturalHeight / 2);
image.X = mouse.X - (image.naturalWidth);
break;
case "middle-right":
image.Y = mouse.Y - (image.naturalHeight / 2);
image.X = mouse.X;
break;
default:
image.Y = mouse.Y;
image.X = mouse.X;
break;
}
// Margin offsets
image.Y = image.Y - screen.margin.height;
image.X = image.X - screen.margin.width;
// Set image position
$("div.image-hover").css({
"top": image.Y,
"left": image.X
});
}