// ==UserScript==
// @name MassiveFap
// @author Ryan Thaut + patches from Elandoris
// @description A complete ImageFap.com gallery conversion script featuring customization options and multiple viewing modes. Original author is Ryan Thaut, but it looks like abandoned.
// @include https://*imagefap.com/*
// @version 1.7.7
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @namespace https://greasyfork.org/users/705794
// ==/UserScript==
/* ===== Integration =====
This is where the initial/basic eventListeners are added.
*/
window.addEventListener('load', init, false);
document.addEventListener('DOMContentLoaded', init, false);
/* ===== Global Variables =====
Changing these is a terrible idea; they are NOT for configuration
*/
var fullscreen = false;
var loaded = false;
var images = [];
var imagesMap = new Map();
var head, body, title, addFavLink;
var author = {name: '', url: ''};
var activeImage = (parseInt(getHashParam('image'), 10) - 1) || 0;
var totalImages = 0;
var hotkeys = [
{
action: displayHelp,
codes: [191],
keys: '?',
label: 'Display Help',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: true}
},
{
action: displayAbout,
codes: [90],
keys: 'z',
label: 'Display About',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
},
{
action: toggleFullScreen,
codes: [70],
keys: 'f',
label: 'Toggle Full Screen',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
},
{
action: switchGalleryMode,
codes: [71],
keys: 'g',
label: 'Switch Gallery Mode',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
},
{
action: changeSettings,
codes: [83],
keys: 's',
label: 'Change Settings',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
},
{
action: toggleThumbs,
codes: [84],
keys: 't',
label: 'Change Thumbnails',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
},
{
action: hideDialog,
codes: [27],
keys: 'esc',
label: 'Hide Dialog Window',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
},
{
action: prevImage,
codes: [37, 38, 65, 87],
keys: '← ↑ a w',
label: 'Previous Image',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
},
{
action: nextImage,
codes: [39, 40, 68, 83],
keys: '→ ↓ d s',
label: 'Next Image',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
},
{
action: toggleAutoplay,
codes: [32],
keys: '[space]',
label: 'Start/Stop Autoplay',
modif: {altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
}
];
var settings = {
'autoplay': {
name: 'autoplayDelay',
label: 'Auto Play Delay',
hint: 'in seconds',
type: 'integer',
size: 3,
min: 0,
max: null,
def: 2
},
'inifiteScrolling': {
name: 'inifiteScrolling',
label: 'Infinite Scrolling',
type: 'boolean',
def: true
},
'mode': {
name: 'galleryMode',
label: 'Gallery Mode',
type: 'select',
opts: ['scrolling', 'slideshow'],
def: 'slideshow'
},
'pagination': {
name: 'imageLimit',
label: 'Pagination Limit',
hint: 'use <b>0</b> to disable',
type: 'integer',
size: 3,
min: 0,
max: null,
def: 0
},
'preloading': {
name: 'preloadingEnabled',
label: 'Preloading reach',
hint: 'use <b>0</b> to disable preloading',
type: 'integer',
size: 3,
def: 10
},
'minLoadingTime': {
name: 'autoplayDelay',
label: 'Min loading time',
hint: 'in ms',
type: 'integer',
size: 4,
min: 0,
max: null,
def: 4
},
'theme': {
name: 'theme',
label: 'Gallery Theme',
type: 'select',
opts: ['default', 'classic', 'green', 'blue'],
def: 'default'
},
'thumbnails': {
name: 'showThumbs',
label: 'Show Thumbnails',
type: 'boolean',
def: true
},
'thumbnailsSize': {
name: 'thumbnailsSize',
label: 'Thumbnails Size',
type: 'select',
opts: ['small', 'medium', 'large'],
def: 'medium'
}
};
// objects for features that need multiple settings and calculated properties
var autoplay = {
active: false,
count: parseInt(GM_getValue(settings.autoplay.name, settings.autoplay.def), 10),
delay: parseInt(GM_getValue(settings.autoplay.name, settings.autoplay.def), 10),
paused: false,
timer: undefined
};
var pagination = {
append: GM_getValue(settings.inifiteScrolling.name, settings.inifiteScrolling.def),
active: false,
limit: parseInt(GM_getValue(settings.pagination.name, settings.pagination.def), 10),
page: parseInt(getHashParam('page'), 10) || 1
};
var preloading = {
active: GM_getValue(settings.preloading.name, settings.preloading.def) > 0,
pos: activeImage,
reach: parseInt(GM_getValue(settings.preloading.name, settings.preloading.def), 10),
};
/* ===== Core Functions =====
Where the magic happens...
*/
/**
* Crawls the normal gallery page and finds all thumbnail images
* @return Array locations of all thumbnail images
*/
function findImages() {
var imgs = document.getElementById('gallery').getElementsByTagName('img');
var thumbRegex = /^(.*\/images\/)(thumb)(\/.*\/)(\d+)(\..*)$/i;
var count = 0;
var ret = [];
var match;
var image;
for (var i = 0; i < imgs.length; i++) {
if (thumbRegex.test(imgs[i].src)) {
match = thumbRegex.exec(imgs[i].src);
image = {
id: match[4],
pos: count++,
thumb: match[0],
loadedUrl: null,
singlePageUrl: imgs[i].parentNode.href,
fullUrlRequest: null,
callback: () => {
},
full(callback) {
if (this.loadedUrl) {
callback(this.loadedUrl);
} else {
let old = this.callback;
this.callback = url => {
old(url);
callback(url);
}
fetchFullImageUrl(this);
}
}
};
ret.push(image);
imagesMap.set(image.id, image);
}
}
totalImages = ret.length;
return ret;
}
function fetchFullImageUrl(image) {
var fullImgRegex = /^(.*\/images\/)(full)(\/.*\/)(\d+)(\..*)$/i;
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
var el = document.createElement( 'html' );
el.innerHTML = xmlHttp.responseText;
let srcs = el.querySelector( '#navigation')
.getElementsByTagName('a')
let requestedReference
for (let i in srcs) {
let href = srcs[i].href;
let match = fullImgRegex.exec(href);
if (match && match.length >= 2) {
let id = match[4];
if (id === image.id) {
requestedReference = href;
}
let otherImage = imagesMap.get(id);
if (otherImage && !otherImage.loadedUrl) {
otherImage.loadedUrl = href;
if (otherImage.callback) {
otherImage.callback(otherImage.loadedUrl);
}
}
}
}
if (requestedReference) {
image.loadedUrl = requestedReference;
if (image.callback) {
image.callback(image.loadedUrl)
}
}
}
};
xmlHttp.open("GET", image.singlePageUrl, true); // true for asynchronous
xmlHttp.send(null);
return xmlHttp;
}
function smartUrlLoading() {
}
/**
* Sets the global variables needed for pagination in other functions
*/
function initPagination() {
initSetting('pagination');
// reset pagination object properties to default
pagination = {
append: settings.inifiteScrolling.value,
active: (settings.mode.value === 'slideshow'),
limit: parseInt(settings.pagination.value, 10),
page: parseInt(getHashParam('page'), 10) || 1
};
if ((pagination.limit <= 0) || (settings.mode.value === 'slideshow')) {
// in slideshow mode pagination is handled as if it is disabled
pagination.active = false;
pagination.limit = totalImages;
pagination.page = 1;
} else {
pagination.active = true;
if (!pagination.page || (pagination.page < 1))
pagination.page = 1;
}
// ensure the user is on the correct page
var page = findImagePage();
if (page !== pagination.page) {
setHashParam('page', page);
pagination.page = page;
}
}
/**
* Finds the page number that the active image should be on
* @param Int (Optional) The number of the image to find (default: value of activeImage internal variable)
* @return Int The page number containing the active image
*/
function findImagePage(pos) {
if ((typeof activeImage === 'undefined') || !activeImage || (activeImage <= 0))
return 1;
return parseInt(((activeImage / pagination.limit) + 1), 10);
}
/**
* Returns the images that will be used on the current page
* @param Array Objects representing all images from the original gallery
* @return Array Multi-dimensional array of objects representing all images on each page
*/
function paginateImages(imgs) {
// if images have been paginated previously, they must first be un-paginated
if (typeof imgs[0] === 'object' && typeof imgs[0][0] === 'object')
imgs = resetImages(imgs);
var page = 0;
var ret = [];
for (var i = 0; i < imgs.length; i++) {
if (typeof ret[page] === 'undefined')
ret[page] = [];
ret[page].push(imgs[i]);
if (((i + 1) % pagination.limit) === 0)
page++;
}
return ret;
}
/** Flattens a paginated multi-dimensional array of images
* @param Array Multi-dimensional array of objects representing all images on multiple page
* @return Array Multi-dimensional array of objects representing all images on one page
*/
function resetImages(imgs) {
var ret = [];
for (var i = 0; i < imgs.length; i++) {
for (var j = 0; j < imgs[i].length; j++) {
ret.push(imgs[i][j]);
}
}
// if the supplied array was only 1-dimensional, then the new array will be empty
if (ret.length === 0)
ret = imgs;
return ret;
}
/**
* Loads the next "page" of images in Infinite Scrolling mode
* Updates the position text and the pagination links
*/
function loadNextPage() {
if (pagination.page < images.length) {
pagination.page++;
showNotification('Loading images from page ' + pagination.page);
updatePosition(undefined, (pagination.limit * pagination.page), undefined, undefined);
updatePagination(pagination.page);
populateScrollingGallery(images[(pagination.page - 1)], false);
populateThumbnails(images[(pagination.page - 1)], false);
setHashParam('page', pagination.page);
} else {
var loader = document.getElementById('loader');
if (loader)
loader.parentNode.removeChild(loader);
}
}
/**
* Generates HTML for the help dialog
* @return String HTML to be placed in the dialog
*/
function getAbout() {
var about = '';
about += '<p>' + GM_info.script.name + ' v' + GM_info.script.version + '. Automatic script updates are ' + ((GM_info.scriptWillUpdate) ? 'enabled' : 'disabled') + '. </p>';
about += '<p>' + GM_info.script.description + '</p>';
return about;
}
/**
* Generates HTML for the help dialog
* @return String HTML to be placed in the dialog
*/
function getHelp() {
var help = '';
help += '<table>';
help += '<tr><th></th><th>Hotkeys</th></tr>';
for (var h in hotkeys)
help += '<tr><td class="key"><b>' + hotkeys[h].keys + '</b></td><td class="command">' + hotkeys[h].label + '</td></tr>';
help += '</table>';
return help;
}
/**
* Generates the DOM objects needed for the thumbnail images
* @param Array The objects of all images to be displayed
* @param Bool (Optional) If the existing thumbnails should be removed (default: false)
*/
function populateThumbnails(imgs, reset) {
reset = (typeof reset === "undefined") ? false : reset;
var thumbs = document.getElementById('thumbnails');
if (!thumbs)
return false;
if (reset)
thumbs.innerHTML = '';
thumbs.className = settings.thumbnailsSize.value;
var img, link;
for (var i = 0; i < imgs.length; i++) {
img = document.createElement('img');
img.src = imgs[i].thumb;
link = document.createElement('a');
link.addEventListener('click', clickThumbnail);
link.className = 'thumbnail';
link.id = 'thumb_' + imgs[i].pos;
link.rel = imgs[i].pos;
link.appendChild(img);
thumbs.appendChild(link);
}
}
/**
* Generates the DOM objects needed for the scrolling mode
* @param Array The objects of all images to be displayed
* @param Bool (Optional) If the existing gallery images should be removed (default: false)
*/
function populateScrollingGallery(imgs, reset) {
reset = (typeof reset === "undefined") ? false : reset;
var gallery = document.getElementById('gallery');
if (!gallery)
return false;
if (reset)
gallery.innerHTML = '';
var container, spinner;
for (var i = 0; i < imgs.length; i++) {
const img = document.createElement('img');
img.alt = '';
img.rel = imgs[i].pos;
imgs[i].full(url => img.src = url);
spinner = document.createElement('span');
spinner.className = 'spinner';
const link = document.createElement('a');
link.className = 'image';
imgs[i].full(url => link.href = url);
link.id = 'full_' + (((pagination.page - 1) * pagination.limit) + i);
link.appendChild(img);
link.appendChild(spinner);
container = document.createElement('p');
container.appendChild(link);
gallery.appendChild(container);
}
if (pagination.append && (imgs.length < totalImages)) {
var loader = document.getElementById('loader');
if (!loader) {
loader = document.createElement('a');
loader.id = 'loader'
loader.innerHTML = 'Load Next Page of Images';
loader.addEventListener('click', loadNextPage);
}
gallery.appendChild(loader);
}
}
/**
* Generates the DOM objects needed for the slideshow mode
* @param Object The object representing the active image
*/
function buildSlideshowGallery(active) {
var gallery = document.getElementById('gallery');
if (!gallery)
return false;
gallery.innerHTML = '';
var prev = document.createElement('a');
prev.addEventListener('click', prevImage);
prev.id = 'prev';
prev.className = 'nav';
prev.innerHTML = '<span class="arrow"><</span>';
var next = document.createElement('a');
next.addEventListener('click', nextImage);
next.id = 'next';
next.className = 'nav';
next.innerHTML = '<span class="arrow">></span>';
var img = document.createElement('img');
img.alt = '';
img.id = 'slideshowImage';
active.full(url => img.src = url);
var spinner = document.createElement('span');
spinner.className = 'spinner';
var link = document.createElement('a');
link.className = 'image';
active.full(url => link.href = url);
link.id = 'slideshowLink';
link.appendChild(img);
link.appendChild(spinner);
gallery.appendChild(link);
gallery.appendChild(prev);
gallery.appendChild(next);
// resize the slideshow area
resizeSlideshowGallery();
}
/**
* Resizes the DOM object containing the slideshow image to accomodate non-fixed header and footer sizes
*/
function resizeSlideshowGallery() {
var content = document.getElementById('content');
if (!content)
return false;
// header
var header = document.getElementById('header');
content.style.top = (header) ? header.offsetHeight + 'px' : '';
// footer
var footer = document.getElementById('footer');
content.style.bottom = (footer) ? footer.offsetHeight + 'px' : '';
}
/**
* Generates the DOM objects for the header of rebuilt pages
*/
function buildHeader() {
var header = document.getElementById('header');
if (!header)
return false;
header.innerHTML = '';
// logo
var logo = document.createElement('a');
logo.href = window.location.protocol + '//' + window.location.host;
logo.id = 'logo';
logo.innerHTML = '<span class="image">Image</span><span class="fap">Fap</span>';
header.appendChild(logo);
// heading
var heading = document.createElement('h2');
heading.id = 'heading';
heading.innerHTML = '"<span class="title">' + stripslashes(title) + '</span>"';
if (author.name && author.url)
heading.innerHTML += ' <small>by</small> <a href="' + author.url + '">' + unescape(stripslashes(author.name)) + '</a>';
header.appendChild(heading);
// description
if (desc) {
var description = document.createElement('p');
description.id = 'description';
description.innerHTML = stripslashes(desc);
header.appendChild(description);
// if the description spans multiple lines, left-align the text
if (description.offsetHeight > 16)
description.style.textAlign = 'left';
}
// sub-heading
var subheading = document.createElement('p');
header.appendChild(subheading);
// sub-heading > position
var position = document.createElement('span');
position.className = settings.mode.value;
position.id = 'position';
subheading.appendChild(position);
// sub-heading > spacer
var separator = document.createElement('span');
separator.innerHTML = ' | ';
subheading.appendChild(separator);
// sub-heading > "toggle thumbnails" link
var toggle = document.createElement('a');
toggle.addEventListener('click', toggleThumbs);
toggle.innerHTML = 'Toggle Thumbnails';
subheading.appendChild(toggle);
// sub-heading > spacer
var separator = document.createElement('span');
separator.innerHTML = ' | ';
subheading.appendChild(separator);
// sub-heading > "change settings" link
var openSettings = document.createElement('a');
openSettings.addEventListener('click', changeSettings);
openSettings.innerHTML = 'Change Settings';
subheading.appendChild(openSettings);
// pagination
if (settings.mode.value === 'scrolling' && pagination.active)
buildPagination('header');
// search form
var form = document.createElement('form');
form.id = 'search';
form.method = 'POST';
form.action = window.location.protocol + '//' + window.location.host + '/gallery.php';
header.appendChild(form);
// search form > text input
var search = document.createElement('input');
search.type = 'text';
search.value = 'Enter search term(s)...';
search.name = 'search';
search.addEventListener('focus', function () {
if (this.value === 'Enter search term(s)...') this.value = '';
});
search.addEventListener('blur', function () {
if (this.value === '') this.value = 'Enter search term(s)...';
});
form.appendChild(search);
// search form > submit button
var submit = document.createElement('input');
submit.type = 'submit';
submit.value = 'Search';
submit.name = 'submit';
form.appendChild(submit);
}
/**
* Generates the DOM objects for the footer of rebuilt pages
*/
function buildFooter() {
var footer = document.getElementById('footer');
if (!footer)
return false;
footer.innerHTML = '';
footer.appendChild(addFavLink);
if (settings.mode.value === 'scrolling' && pagination.active)
buildPagination('footer');
buildInfo();
buildAutoplay();
}
/**
* Updates the HTML for the position of the current image(s) within the gallery
* @param Int (Optional) The lower limit image number (default: use existing value from DOM)
* @param Int (Optional) The upper limit image number (default: use existing value from DOM)
* @param Int (Optional) The total image number (default: use existing value from DOM)
* @param Int (Optional) The active image number (default: use existing value from DOM)
*/
function updatePosition(lower, upper, total, active) {
var position = document.getElementById('position');
if (!position)
return false;
if (position.className !== settings.mode.value) {
position.className = settings.mode.value;
position.innerHTML = '';
}
if (settings.mode.value === 'scrolling') {
if (position.innerHTML === undefined || position.innerHTML === '')
position.innerHTML = 'Viewing image(s) <span id="position_lower">' + lower + '</span>-<span id="position_upper">' + upper + '</span> of <span id="position_total">' + total + '</span>';
lower = (typeof lower === "undefined") ? parseInt(document.getElementById('position_lower').innerHTML, 10) : lower;
upper = (typeof upper === "undefined") ? parseInt(document.getElementById('position_upper').innerHTML, 10) : upper;
total = (typeof total === "undefined") ? parseInt(document.getElementById('position_total').innerHTML, 10) : total;
if (upper > totalImages)
upper = totalImages;
document.getElementById('position_lower').innerHTML = lower;
document.getElementById('position_upper').innerHTML = upper;
document.getElementById('position_total').innerHTML = total;
} else if (settings.mode.value === 'slideshow') {
if (position.innerHTML === undefined || position.innerHTML === '')
position.innerHTML = 'Viewing image <span id="position_active">' + lower + '</span> of <span id="position_total">' + total + '</span>';
active = (typeof active === "undefined") ? parseInt(document.getElementById('position_active').innerHTML, 10) : active;
total = (typeof total === "undefined") ? parseInt(document.getElementById('position_total').innerHTML, 10) : total;
document.getElementById('position_active').innerHTML = active;
document.getElementById('position_total').innerHTML = total;
}
}
/**
* Generates the DOM objects for the pagination of rebuilt gallery pages
* @param String The ID of the DOM object of which to insert the pagination controls
*/
function buildPagination(location) {
var container = document.getElementById(location);
if (!container)
return false;
var pages = Math.ceil(totalImages / pagination.limit);
if (pages <= 1)
return false;
var wrapper = document.createElement('div');
wrapper.className = 'pagination';
// previous page
var prev = document.createElement('a');
prev.innerHTML = '« Prev';
if (pagination.page > 1) {
prev.addEventListener('click', clickPagination);
prev.rel = (pagination.page - 1);
} else {
prev.className = 'disabled';
}
wrapper.appendChild(prev);
// individual pages
var link, lower, upper;
for (var i = 1; i <= pages; i++) {
link = document.createElement('a');
link.rel = i;
lower = (pagination.limit * (i - 1) + 1);
upper = (i < pages) ? (pagination.limit * i) : totalImages;
link.innerHTML = (lower === upper) ? lower : lower + '-' + upper;
if (i === pagination.page) {
link.className = 'current';
} else {
link.addEventListener('click', clickPagination);
}
wrapper.appendChild(link);
}
// next page
var next = document.createElement('a');
next.innerHTML = 'Next »';
if (pagination.page < pages) {
next.addEventListener('click', clickPagination);
next.rel = (pagination.page + 1);
} else {
next.className = 'disabled';
}
wrapper.appendChild(next);
container.appendChild(wrapper);
}
/**
* Updates the pagination controls
* @param Int The number of the current page
* @param Bool (Optional) If all pagination controls should be reset (default: false)
*/
function updatePagination(page, reset) {
reset = (typeof reset === "undefined") ? false : reset;
var containers = document.getElementsByClassName('pagination');
if (containers.length === 0)
return false;
var links, rel;
for (var i = 0; i < containers.length; i++) {
links = containers[i].getElementsByTagName('a');
// first handle all of the inner links (i.e. the numbered ones)
if (reset) {
// this is for when a page is loaded by itself
// activate the target link and reset all of the other links to default
for (var j = 1; j < (links.length - 1); j++) {
if (j === page) {
links[j].className = 'current';
links[j].removeEventListener('click', clickPagination);
} else {
links[j].className = '';
links[j].addEventListener('click', clickPagination);
}
}
} else {
// this is for when a page is appended
// simply activate the target link
links[page].className = 'current';
links[page].removeEventListener('click', clickPagination);
}
// the first link is the "Prev" link, which needs to point to the page BEFORE the current page
var prev = links[0];
if (prev.nextSibling && prev.nextSibling.className === 'current') {
prev.rel = page;
prev.className = 'disabled';
prev.removeEventListener('click', clickPagination);
} else {
prev.rel = (page - 1);
prev.className = trim(links[0].className.replace('disabled', ''));
prev.addEventListener('click', clickPagination);
}
// the last link is the "Next" link, which needs to point to the page AFTER the current page
var next = links[(links.length - 1)];
if (next.previousSibling && next.previousSibling.className === 'current') {
next.rel = page;
next.className = 'disabled';
next.removeEventListener('click', clickPagination);
} else {
next.rel = (page + 1);
next.className = trim(next.className.replace('disabled', ''));
next.addEventListener('click', clickPagination);
}
}
}
/**
* Generates the DOM objects for the information text at the bottom of the footer
*/
function buildInfo() {
var footer = document.getElementById('footer');
if (!footer)
return false;
var info = document.createElement('p');
info.id = 'info';
// "about" link
var about = document.createElement('a');
about.addEventListener('click', displayAbout);
about.innerHTML = GM_info.script.name + ' v' + GM_info.script.version;
info.appendChild(about);
// spacer
var separator = document.createElement('span');
separator.innerHTML = ' | ';
info.appendChild(separator);
// "help" link
var help = document.createElement('a');
help.addEventListener('click', displayHelp);
help.innerHTML = 'Help (?)';
info.appendChild(help);
// spacer
var separator = document.createElement('span');
separator.innerHTML = ' | ';
info.appendChild(separator);
// "settings" link
var openSettings = document.createElement('a');
openSettings.addEventListener('click', changeSettings);
openSettings.innerHTML = 'Settings';
info.appendChild(openSettings);
footer.appendChild(info);
}
/**
* Generates the DOM objects for the autoplay indicator in the footer
*/
function buildAutoplay() {
var footer = document.getElementById('footer');
if (!footer)
return false;
var autoplay = document.createElement('div');
autoplay.id = 'autoplay';
if (settings.mode.value === 'scrolling') {
var disabled = document.createElement('span');
disabled.innerHTML = 'Autoplay is only available in slideshow mode';
autoplay.appendChild(disabled);
} else if (settings.mode.value === 'slideshow') {
// autoplay counter
var counter = document.createElement('span');
counter.id = 'counter';
counter.innerHTML = (autoplay.count > 0) ? 'Advancing image in ' + autoplay.count + ' seconds' : 'Autoplay is disabled';
autoplay.appendChild(counter);
// spacer
var separator = document.createElement('span');
separator.innerHTML = ' | ';
autoplay.appendChild(separator);
// autplay control link
var control = document.createElement('a');
control.addEventListener('click', toggleAutoplay);
control.id = 'control';
control.innerHTML = 'Start Autoplay';
autoplay.appendChild(control);
}
footer.appendChild(autoplay);
}
/**
* Clears out and re-initializes the DOM with the basic HTML needed for the gallery
*/
function initDOM() {
document.removeChild(document.getElementsByTagName('html')[0]);
var html = document.createElement('html');
head = document.createElement('head');
body = document.createElement('body');
html.appendChild(head);
html.appendChild(body);
document.appendChild(html);
head.innerHTML = '<title>' + stripslashes(title) + '</title>';
// build the basic HTML structure to prevent missing elements
body.innerHTML = '<div id="header"></div><div id="content"><div id="gallery"></div></div><div id="footer"></div>'
}
/**
* Registers the GreaseMonkey Menu commands
* Must be run after the gallery page is initially built
*/
function initMenuCommands() {
GM_registerMenuCommand('[' + GM_info.script.name + '] Help', displayHelp);
GM_registerMenuCommand('[' + GM_info.script.name + '] About', displayAbout);
GM_registerMenuCommand('[' + GM_info.script.name + '] Settings', changeSettings);
}
/**
* Rebuilds the actual gallery page piece by piece
*/
function rebuildGalleryPage() {
hideDialog();
// re-initialize all settings and feature packages
initSettings();
initAutoplay();
initPagination();
initPreloading();
// manually remove existing CSS and apply the chosen theme's CSS
var styles = head.getElementsByTagName('style');
for (var i = 0; i < styles.length; i++) {
head.removeChild(styles[i]);
}
GM_addStyle(getCSS());
// paginate the images using current pagination settings
images = paginateImages(images);
// header
var header = document.getElementById('header');
if (!header) {
header = document.createElement('div');
header.id = 'header';
body.appendChild(header);
}
buildHeader();
// thumbnails
var thumbs = document.getElementById('thumbnails');
if (!thumbs) {
thumbs = document.createElement('div');
thumbs.id = 'thumbnails';
header.appendChild(thumbs);
}
// content (gallery wrapper)
var content = document.getElementById('content');
if (!content) {
content = document.createElement('div');
content.id = 'content';
body.appendChild(content);
}
// main gallery
var gallery = document.getElementById('gallery');
if (!gallery) {
gallery = document.createElement('div');
gallery.id = 'gallery';
content.appendChild(gallery);
}
// footer
var footer = document.getElementById('footer');
if (!footer) {
footer = document.createElement('div');
footer.id = 'footer';
body.appendChild(footer);
}
buildFooter();
// populate the thumbnails
if (settings.thumbnails.value === false)
thumbs.style.display = 'none';
if (settings.mode.value === 'slideshow')
thumbs.addEventListener('DOMMouseScroll', scrollThumbs, false);
populateThumbnails(images[(pagination.page - 1)], true);
// populate the gallery
if (settings.mode.value === 'scrolling') {
gallery.style.marginBottom = (footer.offsetHeight + 10) + 'px';
body.className = 'scrolling';
var lower = ((pagination.limit * (pagination.page - 1)) + 1);
var upper = (pagination.limit * pagination.page);
updatePosition(lower, upper, totalImages, undefined);
populateScrollingGallery(images[(pagination.page - 1)], true);
} else if (settings.mode.value === 'slideshow') {
body.className = 'slideshow';
updatePosition(undefined, undefined, totalImages, activeImage);
buildSlideshowGallery(images[(pagination.page - 1)][activeImage]);
refreshNavigation();
}
// remove the page hash for slideshow mode
if (settings.mode.value === 'slideshow')
unsetHashParam('page');
// show the active image, unless it is the first image of a scrolling gallery
if ((settings.mode.value === 'slideshow') || (activeImage > 0))
showImage();
// start preloading images if preloading is enabled
if ((settings.mode.value === 'slideshow') && preloading.active)
preloadImage();
}
/**
* main function; executes functionality based on page (via URL)
* Redirects to the one-page version of galleries (if not already in one-page mode)
*/
function init() {
// prevent the initialization function from running multiple times
if (loaded) {
return false;
} else {
loaded = true;
}
var loc = window.location.href;
if ((loc.indexOf('/gallery/') !== -1) || (loc.indexOf('/pictures/') !== -1)) {
// this is a gallery page; make sure it is in "One Page" mode and then go to work
if (loc.indexOf('view') === -1) {
window.location.href += ((loc.indexOf('?') === -1) ? '?' : '&') + 'view=2';
} else {
// populate the global variables from the original gallery page
title = document.title;
if (title)
title = trim(title.replace('Porn pics of ', '').replace(' (Page 1)', ''));
var links = document.getElementsByTagName('a');
if (links) {
var i = 0;
for (var i = 0; i < links.length; i++) {
if (links[i].href.indexOf('profile.php?') !== -1) {
author.name = links[i].href.split('=')[1];
author.url = links[i].href;
break;
}
}
}
desc = document.getElementById('cnt_description');
if (desc)
desc = trim(desc.textContent);
addFavLink = document.getElementById('favorites_container');
// FINALLY! grab the images, build the gallery, enable the hotkeys, and listen for changes to settings
images = findImages();
initDOM();
rebuildGalleryPage();
initMenuCommands();
checkSettings(); // must run after the gallery page is built (otherwise the notification gets wiped out)
document.addEventListener('keydown', onKeyDown, false);
window.addEventListener('focus', onWindowFocus, false);
window.addEventListener('scroll', onWindowScroll, false);
window.addEventListener('mozfullscreenchange', onFullScreenChange, false);
window.addEventListener('webkitfullscreenchange', onFullScreenChange, false);
}
} else {
// this might be a page with links to galleries; change all of the links to galleries to "One Page" mode
var links = document.getElementsByTagName('a');
if (links) {
for (var i = 0; i < links.length; i++) {
if ((links[i].href.indexOf('gallery') !== -1) || (links[i].href.indexOf('pictures') !== -1)) {
links[i].href += ((links[i].href.indexOf('?') === -1) ? '?' : '&') + 'view=2';
}
}
}
}
}
/**
* Toggles visibility of the navigational arrows by adding/removing a "disabled" class
*/
function refreshNavigation() {
var nav = document.getElementsByClassName('nav');
for (var i = 0; i < nav.length; i++) {
nav[i].className = trim(nav[i].className.replace('disabled', ''));
}
if (activeImage === 0) {
var prev = document.getElementById('prev');
if (prev)
prev.className += ' disabled';
} else if (activeImage === (pagination.limit - 1)) {
var next = document.getElementById('next');
if (next)
next.className += ' disabled';
}
}
/**
* Handles the click event for the pagination links
* @param Event The click event
*/
function clickPagination(evt) {
if (this.rel) {
var page = parseInt(this.rel, 10);
activeImage = (((page - 1) * pagination.limit) + 1);
unsetHashParam('image');
updatePagination(page, true);
goToPage(page);
}
}
/**
* Sets the values needed to change pages and rebuilds the gallery for the specified page
* @param Int The number of the page to display
*/
function goToPage(page) {
pagination.page = page;
var lower = ((pagination.limit * (pagination.page - 1)) + 1);
var upper = (pagination.limit * pagination.page);
setHashParam('page', page);
setHashParam('image', lower);
updatePosition(lower, upper, totalImages, undefined);
populateScrollingGallery(images[(page - 1)], true);
populateThumbnails(images[(page - 1)], true);
window.scrollTo(0, 0);
}
/**
* Captures mouse scrolling on the thumbnails and scrolls the images horizontally
* @param Event mouse wheel scroll event
*/
function scrollThumbs(evt) {
if (!evt)
evt = this;
evt.preventDefault(); // prevent vertical scrolling on the page
var delta = (evt.detail) ? evt.detail : 0;
window.document.getElementById('thumbnails').scrollLeft += (delta * 10);
evt.returnValue = false;
}
/**
* Sets the active image based on the thumbnail image that was clicked
* Expects to be executed as a click event for a DOM object with a "rel" value
* @param Event The click event
*/
function clickThumbnail(evt) {
if (this.rel) {
activeImage = parseInt(this.rel, 10);
preloading.pos = activeImage;
}
showImage();
}
/**
* Sets the active image to the next image
* Ensures the active image is not the last one already
*/
function nextImage() {
if (settings.mode.value === 'slideshow' && autoplay.active) {
autoplay.count = autoplay.delay;
}
if (activeImage < ((pagination.page * pagination.limit) - 1)) {
activeImage++;
showImage();
}
}
/**
* Sets the active image to the previous image
* Ensures the active image is not the first one already
*/
function prevImage() {
if (settings.mode.value === 'slideshow' && autoplay.active) {
stopAutoplay();
}
if (activeImage > ((pagination.page - 1) * pagination.limit)) {
activeImage--;
showImage();
}
}
/**
* Sets the internal activeImage pointer and sets the image hash parameter
* Activates the correct thumbnail for the active image
* @param Int (Optional) The number of the active image (default: value of activeImage internal variable)
*/
function setActiveImage(pos) {
pos = (typeof pos === "undefined") ? activeImage : pos;
setHashParam('page', findImagePage());
setHashParam('image', (pos + 1));
showActiveThumb(pos);
}
/**
* Highlights the thumbnail corresponding to the current image
* and scrolls it into view in slideshow mode
* @param Int (Optional) The number of the active image (default: value of activeImage internal variable)
*/
function showActiveThumb(pos) {
pos = (typeof pos === "undefined") ? activeImage : pos;
var thumbs = document.getElementsByClassName('thumbnail');
for (var i = 0; i < thumbs.length; i++) {
thumbs[i].className = trim(thumbs[i].className.replace('active', ''));
}
var thumb = document.getElementById('thumb_' + pos);
if (thumb) {
thumb.className += ' active';
if (settings.mode.value === 'slideshow')
thumb.scrollIntoView();
}
}
/**
* Displays/navigates to the "active" image
* In scrolling mode, this simply scrolls the page up/down to the active image,
* but in slideshow mode, this shows the active image and hides the others
* @param Int (Optional) The number of the active image (default: value of activeImage internal variable)
*/
function showImage(pos) {
pos = (typeof pos === "undefined") ? activeImage : pos;
setActiveImage(pos);
if (settings.mode.value === 'scrolling') {
var target = document.getElementById('full_' + pos);
if (target)
target.scrollIntoView();
} else if (settings.mode.value === 'slideshow') {
var link = document.getElementById('slideshowLink');
var img = document.getElementById('slideshowImage');
if (link && img) {
showNotification("Loading image " + (pos + 1), 1000);
// first blank out the current image and link target from the previous image
// then use a slight delay to set the next image and link target;
// this causes the loading animation to be played while the image is loaded;
// if the delay is removed, the previous image will remain visible until the new image is loaded
// without any indication that the image has changed and the next image is loading
img.src = link.href = '';
setTimeout(function () {
images[pagination.page - 1][pos].full(url => img.src = link.href = url);
},
settings.minLoadingTime.value
);
// always remove the autoplay function from load, and then re-add again if autoplay is still enabled
img.removeEventListener('load', startAutoplay);
if (autoplay.active && !autoplay.paused) {
img.addEventListener('load', startAutoplay);
}
updatePosition(undefined, undefined, totalImages, (pos + 1));
refreshNavigation();
preloadImage();
if (preloading.active && !preloading.done && (preloading.pos < activeImage))
preloading.pos = activeImage;
}
}
}
/**
* Event handler for keypresses
* Used to handle hotkeys
* @param Event The keypress event
*/
function onKeyDown(evt) {
if (!evt)
evt = this;
if ((evt.target.nodeName === 'INPUT') || (evt.target.nodeName === 'SELECT') || (evt.target.nodeName === 'TEXTAREA'))
return false;
var correct = true,
hotkey;
for (var h in hotkeys) {
hotkey = hotkeys[h];
for (var c in hotkey.codes) {
if (evt.keyCode === hotkey.codes[c]) {
for (var m in hotkey.modif) {
correct = (evt[m] === hotkey.modif[m]) ? correct : false;
}
if (correct) {
evt.preventDefault();
return (typeof hotkey.action === 'function') ? hotkey.action.call() : eval(hotkey.action);
}
}
}
}
}
/**
* Event handler for window scroll
* Used to update the active image when manually scrolling in the scrolling gallery
* and to determine if the next page of images should be loaded
* @param Event The scroll event
*/
function onWindowScroll(evt) {
if (settings.mode.value === 'scrolling') {
var imgs = document.getElementById('gallery').getElementsByTagName('img');
var target = 0;
// loop backwards through the images until the currently visible image is found
for (var i = (imgs.length - 1); i >= 0; i--) {
var current = imgs[i].parentNode.offsetTop;
if (document.body.scrollTop >= current) {
target = parseInt(imgs[i].rel, 10);
break;
}
}
// only update the active image if it changed
if (target !== activeImage) {
activeImage = target;
setActiveImage();
}
if (pagination.append) {
var last = imgs[(imgs.length - 1)];
if ((last.parentNode.offsetTop - window.innerHeight) <= window.pageYOffset) {
loadNextPage();
}
}
}
}
/**
* Event handler for window focus
* Used to monitor setting changes and refresh the gallery when needed
* @param Event The focus event
*/
function onWindowFocus(evt) {
var prevMode = settings.mode.value;
var prevPagination = settings.pagination.value;
initSettings();
var refresh = false;
var thumbs = document.getElementById('thumbnails');
if (thumbs) {
// thumbnails can be toggled without rebuilding the entire gallery page
thumbs.className = settings.thumbnailsSize.value;
thumbs.style.display = (settings.thumbnails.value === true) ? 'block' : 'none';
if (settings.mode.value === 'slideshow')
resizeSlideshowGallery();
}
if (prevMode !== settings.mode.value) {
// current gallery mode does not match the stored preferences
refresh = true;
}
if (settings.mode.value === 'scrolling') {
if (prevPagination !== settings.pagination.value) {
// current gallery pagination does not match the stored preferences
refresh = true;
}
}
if (refresh)
rebuildGalleryPage();
}
/**
* Displays the about dialog
*/
function displayAbout() {
showDialog(getAbout(), 'About');
}
/**
* Displays the help dialog
*/
function displayHelp() {
showDialog(getHelp(), 'Help');
}
/**
* Re-initialize the autoplay settings
*/
function initAutoplay() {
if (autoplay.timer)
window.clearTimeout(autoplay.timer);
initSetting('autoplay');
// reset autoplay object properties to default
autoplay = {
active: false,
count: parseInt(settings.autoplay.value, 10),
delay: parseInt(settings.autoplay.value, 10),
paused: false,
timer: undefined
};
}
/**
* Starts the autoplay used timer for advancing to the next image
* If the delay is not set correctly, the user is prompted to set it
*/
function startAutoplay() {
if (!autoplay.active)
return false;
if (autoplay.delay > 1000) {
// the delay is likely in milliseconds, so convert it to seconds and save
autoplay.delay /= 1000;
GM_setValue(settings.autoplay.name, autoplay.delay);
initAutoplay();
}
if (autoplay.delay < 0) {
// the delay is invalid; inform the user
var buttons = {0: {text: 'Change Settings', action: changeSettings}};
showDialog('<p>The current value for the ' + settings.autoplay.label + ' is not valid.</p>', 'Invalid ' + settings.autoplay.label, buttons);
} else {
autoplay.count = autoplay.delay;
if (activeImage < totalImages) {
autoplayTimer();
} else {
stopAutoplay();
}
}
}
/**
* Stops the autoplay timer used for advancing to the next image
*/
function stopAutoplay() {
window.clearTimeout(autoplay.timer);
autoplay.active = false;
autoplay.count = autoplay.delay;
var counter = document.getElementById('counter');
if (counter)
counter.innerHTML = 'Autoplay is disabled';
var control = document.getElementById('control');
if (control)
control.innerHTML = 'Start Autoplay';
}
/**
* Pauses the autoplay timer
*/
function pauseAutoplay() {
window.clearTimeout(autoplay.timer);
autoplay.paused = true;
var counter = document.getElementById('counter');
if (counter)
counter.innerHTML = 'Autoplay is paused';
var control = document.getElementById('control');
if (control)
control.innerHTML = 'Resume Autoplay';
}
/**
* Resumes the autoplay timer
*/
function resumeAutoplay() {
var counter = document.getElementById('counter');
if (counter)
counter.innerHTML = 'Resuming autoplay...';
var control = document.getElementById('control');
if (control)
control.innerHTML = 'Pause Autoplay';
autoplay.paused = false;
autoplayTimer();
}
/**
* Starts the counter indicator for advancing to the next image
*/
function autoplayTimer() {
window.clearTimeout(autoplay.timer);
var counter = document.getElementById('counter');
if (!counter)
return false;
if (activeImage < (totalImages - 1)) {
if (autoplay.count > 0) {
autoplay.timer = window.setTimeout(autoplayTimer, 1000);
counter.innerHTML = 'Advancing image in <b>' + autoplay.count + '</b> seconds';
} else {
counter.innerHTML = 'Loading image...';
nextImage();
}
autoplay.count--;
} else {
counter.innerHTML = 'End of gallery';
}
}
/**
* Toggles autoplay (start and pause/resume)
*/
function toggleAutoplay() {
autoplay.active = !autoplay.active;
if (autoplay.active) {
resumeAutoplay();
} else {
pauseAutoplay();
}
if (fullscreen)
showAutoplayIndicator();
}
/**
* Displays an indicator when toggling autoplay
*/
function showAutoplayIndicator() {
var gallery = document.getElementById('gallery');
if (!gallery)
return false;
var indicator = document.getElementById('indicator');
if (indicator)
hideAutoplayIndicator(0);
indicator = document.createElement('div');
indicator.id = 'indicator';
indicator.style.opacity = 1;
gallery.appendChild(indicator);
if (autoplay.active) {
indicator.innerHTML = '<span class="symbol play">►</span>';
} else {
indicator.innerHTML = '<span class="symbol pause">||</span>';
}
setTimeout(function () {
hideAutoplayIndicator(100);
}, 1000);
}
/**
* Fades the autoplay indicator
*/
function hideAutoplayIndicator(duration) {
duration = (typeof duration === "undefined") ? 100 : duration;
var indicator = document.getElementById('indicator');
if (!indicator)
return false;
var timer = setInterval(function () {
if (indicator === null || indicator.parentNode === null) {
clearInterval(timer);
} else {
indicator.style.opacity -= 0.1
if (indicator.style.opacity <= 0)
indicator.parentNode.removeChild(indicator);
}
}, duration);
}
/**
* Toggles the slideshow mode for gallery pages
* A dialog is shown after toggling to allow the user to apply the change immediately
* @param Bool (Optional) If a nofitication should be displayed (default: true)
*/
function switchGalleryMode(notify) {
notify = (typeof notify === "undefined") ? true : notify;
if (settings.mode.value === 'slideshow')
GM_setValue(settings.mode.name, 'scrolling');
else if (settings.mode.value === 'scrolling')
GM_setValue(settings.mode.name, 'slideshow');
initSetting('mode');
if (notify) {
var buttons = {
0: {text: 'Apply Change Now', action: rebuildGalleryPage},
1: {text: 'Close', action: hideDialog}
};
showDialog('<p>Gallery mode has been changed to ' + settings.mode.value + '.</p>', 'Gallery Mode', buttons);
}
}
/**
* Toggles visibilty on thumbnails and stores the preference
*/
function toggleThumbs() {
GM_setValue(settings.thumbnails.name, !GM_getValue(settings.thumbnails.name, settings.thumbnails.def));
initSetting('thumbnails');
var visible = settings.thumbnails.value;
var thumbs = document.getElementById('thumbnails');
if (thumbs) {
var thumbsHeight = thumbs.offsetHeight;
thumbs.style.display = (visible) ? 'block' : 'none';
showNotification('Thumbnails are now ' + ((visible) ? 'visible' : 'hidden'), 1000);
if (settings.mode.value === 'slideshow') {
resizeSlideshowGallery();
} else if (settings.mode.value === 'scrolling') {
if (visible) {
// scroll the window up to the top of the page when thumbnails are visible
window.scrollTo(0, 0);
} else {
// attempt to prevent the page from scrolling too much when thumbnails are hidden
window.scrollTo(0, (window.pageYOffset - thumbsHeight - 10));
}
}
}
}
/**
* Re-initialize the preloading settings
*/
function initPreloading() {
initSetting('preloading');
// reset autoplay object properties to default
preloading = {
active: settings.preloading.value > 0,
pos: activeImage,
reach: settings.preloading.value
};
}
function performPreload(imageData) {
const image = document.createElement('img');
image.setAttribute('alt', "preloadin...");
imageData.full(url => image.setAttribute('src', url));
image.addEventListener('load', preloadImage);
}
/**
* Preloads the next image in slideshow mode
* Will be called continuously until the last image is loaded
*/
function preloadImage() {
if (!preloading.active)
return false;
if (preloading.pos < (activeImage + preloading.reach)) {
showNotification("Preloading image " + (preloading.pos + 1), 1000);
const imageData = images[pagination.page - 1][preloading.pos];
if (imageData) {
performPreload(imageData);
}
preloading.pos++;
}
}
/**
* Toggles full screen view in supported browsers
* Full screen view is only available in slideshow modepreloading.pos <
*/
function toggleFullScreen() {
if (settings.mode.value !== 'slideshow')
return false;
if (!document.mozFullScreenElement && !document.webkitFullscreenElement) {
var target = document.getElementById('content');
if (target.mozRequestFullScreen) {
target.mozRequestFullScreen();
} else if (target.webkitRequestFullscreen) {
target.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else {
showDialog('Sorry, but it seems your browser does not support customized fullscreen HTML.', 'Fullscreen Error');
}
} else {
if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
}
}
}
/**
* Event handler for fullscreen enter/exit
* Used to move the thumbnails container into the c#ontent container in fullscreen mode
* and back to the #header container when not in fullscreen mode
* @param Event fullscreenchange event
*/
function onFullScreenChange(evt) {
fullscreen = !(document.mozFullScreenElement === null || document.webkitFullscreenElement === null);
if (fullscreen) {
document.getElementById('content').insertBefore(document.getElementById('thumbnails'), document.getElementById('gallery'));
} else {
document.getElementById('header').appendChild(document.getElementById('thumbnails'));
}
resizeSlideshowGallery();
}
/* ===== Dialog Functions =====
Rather than using boring alert() and prompt() boxes, which are finicky in Greasemonkey,
this script creates a div, styled with CSS, to replicate that functionality.
*/
/**
* Generates the DOM elements for the dialog boxes
*/
function buildDialog() {
var dialog = document.getElementById('dialogBox');
if (dialog) {
resetDialog();
return;
}
dialog = document.createElement('div');
dialog.id = 'dialogBox';
dialog.innerHTML = '<h3 id="dialogTitle"></h3><div id="dialogMessage"></div><div id="dialogButtons"></div>';
var closeButton = document.createElement('a');
closeButton.addEventListener('click', hideDialog, false);
closeButton.id = 'dialogClose';
closeButton.innerHTML = '✖';
dialog.appendChild(closeButton);
var dialogContainer = document.createElement('div');
dialogContainer.addEventListener('click', function (e) {
hideDialog(e, true);
}, false);
dialogContainer.id = 'dialogContainer';
dialogContainer.style.display = 'none';
dialogContainer.appendChild(dialog);
body.appendChild(dialogContainer);
}
/**
* Hides the dialog box
* @param Event The click event
* @param Bool (Optional) If true, will only hide the dialog if the target of the click event is the dialogContainer
*/
function hideDialog(e, containerOnly) {
containerOnly = (typeof containerOnly === "undefined") ? false : containerOnly;
if (containerOnly && (typeof e !== "undefined") && (e.target.id !== 'dialogContainer'))
return false;
var dialogContainer = document.getElementById('dialogContainer');
if (dialogContainer)
dialogContainer.style.display = 'none';
}
/**
* Vertically centers the dialog box on the screen
*/
function positionDialog() {
var dialogBox = document.getElementById('dialogBox');
var dialogContainer = document.getElementById('dialogContainer');
dialogContainer.style.display = 'block';
var top = ((window.innerHeight / 2) - (dialogBox.offsetHeight / 2) - 20);
dialogBox.style.top = (top < 0) ? '0' : top + 'px';
dialogContainer.scrollTop = 0;
}
/**
* Clears out the contents of the dialog box
*/
function resetDialog() {
document.getElementById('dialogTitle').innerHTML = '';
document.getElementById('dialogMessage').innerHTML = '';
document.getElementById('dialogButtons').innerHTML = '';
}
/**
* Preloads the next image in slideshow mode
* Will be called continuously until the last image is loaded
* @param String The HTML content of the dialog box
* @param String (Optional) A title for the dialog box (always prefixed with '[Script Name]')
* @param Object (Optional) An object representing the buttons to display and their actions (default: close button)
*/
function showDialog(content, title, buttons) {
buttons = (typeof buttons === "undefined") ? {0: {text: 'Close', action: hideDialog}} : buttons;
title = (typeof title === "undefined") ? '' : title;
buildDialog();
var dialogContainer = document.getElementById('dialogContainer');
var titleContainer = document.getElementById('dialogTitle');
var messageContainer = document.getElementById('dialogMessage');
var buttonContainer = document.getElementById('dialogButtons');
titleContainer.innerHTML = '[<b>' + GM_info.script.name + '</b>] ' + title;
messageContainer.innerHTML = content;
var btn;
for (var button in buttons) {
btn = document.createElement('button');
btn.addEventListener('click', buttons[button].action, false);
btn.innerHTML = buttons[button].text;
if (button == 0)
btn.className = 'default';
buttonContainer.appendChild(btn);
}
positionDialog();
buttonContainer.childNodes[0].focus();
}
/* ===== Notication Message Functions =====
The are notification messages that display in the lower right corner and are
automatically removed after a short duration without needing user interaction.
*/
/**
* Generates and displays a notification message
* @param String The notification message text
* @param Int (Optional) The delay (in milliseconds) before the message should be hidden (default: 5000)
* @param Int (Optional) The duration (in milliseconds) over which the message should fade (default: 100)
* @return Object The DOM object of the created notification
*/
function showNotification(text, delay, duration) {
delay = (typeof delay === "undefined") ? 5000 : delay;
var notificationContainer = document.getElementById('notificationContainer');
if (!notificationContainer) {
notificationContainer = document.createElement('div');
notificationContainer.id = 'notificationContainer';
document.getElementById('content').appendChild(notificationContainer);
}
notificationContainer.style.bottom = (document.getElementById('footer').offsetHeight + 10) + 'px';
var notifications = notificationContainer.getElementsByClassName('notification');
var notification = document.createElement('div');
notification.className = 'notification';
notification.id = 'notification' + notifications.length;
notification.innerHTML = text;
if (notifications.length > 0) {
notificationContainer.insertBefore(notification, notifications[0]);
} else {
notificationContainer.appendChild(notification);
}
if (delay > 0)
setTimeout(function () {
hideNotification(notification, duration);
}, delay);
return notification;
}
/**
* Fades a notification message out and then removes it
* @param Object The notification message to hide
* @param Int (Optional) The duration (in milliseconds) over which the message should fade (default: 100)
*/
function hideNotification(notification, duration) {
duration = (typeof duration === "undefined") ? 100 : duration;
if (!notification)
return false;
notification.style.opacity = 1;
var timer = setInterval(function () {
notification.style.opacity -= 0.1
if (notification.style.opacity <= 0) {
notification.parentNode.removeChild(notification);
clearInterval(timer);
}
}, duration);
}
/* ===== Setting Functions =====
Functions used for working with the individual settings in bulk
*/
/**
* Saves the values for all settings from the Settings Form Dialog Box
* Verifies values are valid based on criteria of the individual settings (min, max, etc.)
*/
function applySettings() {
var setting, field, value;
for (s in settings) {
setting = settings[s];
if (setting && (typeof setting === 'object')) {
value = null;
field = document.getElementById('setting-' + s);
if (field) {
switch (setting.type) {
case 'integer':
value = parseInt(field.value, 10);
if (isNaN(value)) {
var buttons = {0: {text: 'Try Again', action: changeSettings}};
showDialog('<p>A numeric value must be provided for <b>' + setting.label + '</b>.</p>', 'Error', buttons);
return false;
}
if ((typeof setting.min !== 'undefined') && (setting.min !== null) && (value < setting.min)) {
var buttons = {0: {text: 'Try Again', action: changeSettings}};
showDialog('<p>The value provided (<span class="error">' + value + '</span>) for <b>' + setting.label + '</b> must be greater than or equal to <b>' + setting.min + '</b>.</p>', 'Error', buttons);
return false;
}
if ((typeof setting.max !== 'undefined') && (setting.max !== null) && (value > setting.max)) {
var buttons = {0: {text: 'Try Again', action: changeSettings}};
showDialog('<p>The value provided for (<span class="error">' + value + '</span>) <b>' + setting.label + '</b> must be less than or equal to <b>' + setting.max + '</b>.</p>', 'Error', buttons);
return false;
}
break;
case 'select':
value = field.options[field.selectedIndex].value;
break;
case 'boolean':
value = field.checked;
break;
default:
value = field.value;
break;
}
}
if (value !== null) {
settings[s].value = value;
}
}
}
saveSettings();
var buttons = {0: {text: 'Apply Changes Now', action: rebuildGalleryPage}, 1: {text: 'Close', action: hideDialog}};
showDialog('<p>Your changes have been saved successfully!</p>', 'Success', buttons);
}
/**
* Checks if settings have been saved for the current script version.
* For new installs, a notice is displayed forcing the user to save the settings for the first time;
* for updates, the user can view the settings or dismiss the notice.
*/
function checkSettings() {
var lastSavedVersion = GM_getValue('lastSavedVersion', false);
if (!lastSavedVersion) {
var buttons = {0: {text: 'Continue', action: changeSettings}};
showDialog('<p>Since this is your first time using ' + GM_info.script.name + ', you need to view (and modify) the settings to meet your needs.</p><p>Note that all settings have a default value set already for your convenience; you can simply click the "Save Settings" button on the next dialog box to continue.', 'First Run', buttons);
} else if (lastSavedVersion !== GM_info.script.version) {
GM_setValue('lastSavedVersion', GM_info.script.version);
var buttons = {0: {text: 'Change Settings', action: changeSettings}, 1: {text: 'Close', action: hideDialog}};
showDialog('<p>The version of this script has changed from ' + lastSavedVersion + ' to ' + GM_info.script.version + '. There may be new settings for you to utilize.', 'Version Change', buttons);
}
}
/**
* Initializes all settings from saved preferences
* Uses the default values for settings that have not yet been saved
*/
function initSettings() {
for (s in settings) {
initSetting(s)
}
}
/**
* Initializes the specified setting from saved preferences
* Uses the default value for settings that have not yet been saved
* @param String name (key) of the setting to initialize
*/
function initSetting(s) {
if (settings[s] && (typeof settings[s] === 'object')) {
if (settings[s].name)
settings[s].value = GM_getValue(settings[s].name, settings[s].def);
if (settings[s].type === 'integer')
settings[s].value = parseInt(settings[s].value, 10);
else if ((settings[s].type === 'select') && (settings[s].opts.indexOf(settings[s].value) === -1))
settings[s].value = settings[s].def;
}
}
/**
* Generates the HTML for the Settings Form that will be displayed via dialog box
*/
function changeSettings() {
initSettings();
var setting;
var html = '<form id="settingsForm">';
for (s in settings) {
setting = settings[s];
if (setting && (typeof setting === 'object')) {
html += '<fieldset><legend>' + setting.label + '</legend>';
switch (setting.type) {
case 'integer':
case 'text':
html += '<label for="setting-' + s + '">Enter a value for the ' + setting.label;
if (setting.hint)
html += ' (' + setting.hint + ')';
html += ':<br/><span class="default">Default value: <b>' + setting.def + '</b></span></label>';
html += '<input type="text" id="setting-' + s + '" name="' + s + '" value="' + setting.value + '" size="' + setting.size + '" maxlength="' + setting.size + '"/>';
break;
case 'select':
html += '<label for="setting-' + s + '">Select a value for the ' + setting.label;
if (setting.hint)
html += ' (' + setting.hint + ')';
html += ':<br/><span class="default">Default value: <b>' + capitalize(setting.def) + '</b></span></label>';
html += '<select id="setting-' + s + '" name="' + s + '">';
for (opt in setting.opts) {
html += '<option value="' + setting.opts[opt] + '"' + ((setting.value === setting.opts[opt]) ? ' selected="selected"' : '') + '>' + capitalize(setting.opts[opt]) + '</option>';
}
html += '</select>';
break;
case 'boolean':
html += '<input type="checkbox" id="setting-' + s + '" name="' + s + '" value="true"' + ((setting.value) ? ' checked="checked"' : '') + '/>';
html += '<label for="setting-' + s + '">Enable ' + setting.label;
if (setting.hint)
html += ' (' + setting.hint + ')';
html += '<br/><span class="default">Default value: <b>' + ((setting.def) ? 'Enabled' : 'Disabled') + '</b></span></label>';
break;
}
html += '</fieldset>';
}
}
showDialog(html, 'Settings', {
0: {text: 'Save Settings', action: applySettings},
1: {text: 'Cancel', action: hideDialog}
});
}
/**
* Saves the current value for each setting to local storage
*/
function saveSettings() {
var setting;
for (s in settings) {
setting = settings[s];
if (setting && (typeof setting === 'object'))
GM_setValue(setting.name, setting.value);
}
initSettings();
GM_setValue('lastSavedVersion', GM_info.script.version);
}
/* ===== String Functions =====
Utility functions for manipulating strings
*/
/**
* Escapes quotes and double-quotes in a string
* @param String The string to escape
* @return String The escaped string
*/
function addslashes(str) {
return str.replace(/\\/g, '\\\\').replace(/\'/g, '\\\'').replace(/\"/g, '\\"').replace(/\0/g, '\\0');
}
/**
* Unescapes quotes and double-quotes in a string
* @param String The escaped string
* @return String The string to escape
*/
function stripslashes(str) {
return str.replace(/\\'/g, '\'').replace(/\\"/g, '"').replace(/\\0/g, '\0').replace(/\\\\/g, '\\');
}
/**
* Removes leading and trailing whitepsace from a string
* @param String The string to trim
* @return String The trimmed string
*/
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
/**
* Capitalizes the first character of a string
* @param String The string to capitalize
* @return String The capitalized string
*/
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
/* ===== Hash Parameter Functions =====
Utility functions for setting and retrieving data from the URL location hash
*/
/**
* Returns all key/value pairs stored in the location hash
* @return Object The location hash parameters indexed by key name
*/
function getHashParams() {
var params = window.location.hash.replace('#!', '').split('/');
var ret = [];
for (var i = 0; i < params.length; i = i + 2) {
if (params[(i + 1)] && params[(i + 2)])
ret[params[(i + 1)]] = params[(i + 2)];
}
return ret;
}
/**
* Returns the value of the supplied hash key
* @param String The name of the hash key
* @return String The value of the hash key
*/
function getHashParam(key) {
var params = getHashParams();
return params[key] || undefined;
}
/**
* Sets the value of the supplied hash key
* @param String The name of the hash key
* @param String The value of the hash key
*/
function setHashParam(key, val) {
var hashString = '#!';
var params = getHashParams();
params[key] = val;
for (key in params) {
hashString += '/' + key + '/' + params[key];
}
history.replaceState(null, null, hashString);
}
/**
* Removes a hash key/value pair from the location hash
* @param String The name of the hash key
*/
function unsetHashParam(key) {
var current = getHashParam(key);
if (typeof current !== 'undefined')
history.replaceState(null, null, window.location.hash.replace('/' + key + '/' + current, ''));
}
/* ===== Gallery CSS =====
The CSS for the rebuilt gallery page with theme support
*/
/**
* Generates the CSS used for the rebuilt gallery page
* @param String (Optional) Additional CSS
* @return String The CSS for the rebuilt gallery page
*/
function getCSS(css) {
css = (typeof css === "undefined") ? '' : css;
initSettings('theme');
switch (getHashParam('theme') || settings.theme.value) {
case 'blue':
var bg1 = '#060D1A';
var bg2 = '#03060D';
var fg1 = '#557799';
var fg2 = '#557799';
var links = '#AABBCC';
var accent1 = '#6699CC';
var accent2 = '#6699CC';
break;
case 'classic':
var bg1 = '#FFFFFF';
var bg2 = '#3366CC';
var fg1 = '#666666';
var fg2 = '#AACCEE';
var links = '#AACCEE';
var accent1 = '#3366CC';
var accent2 = '#FFFFFF';
break;
case 'green':
var bg1 = '#FFFFFF';
var bg2 = '#222222';
var fg1 = '#888888';
var fg2 = '#888888';
var links = '#AAAAAA';
var accent1 = '#33AA00';
var accent2 = '#66CC33';
break;
case 'default':
default:
var bg1 = '#222222';
var bg2 = '#111111';
var fg1 = '#888888';
var fg2 = '#888888';
var links = '#AAAAAA';
var accent1 = '#3380CC';
var accent2 = '#3380CC';
break;
}
/**
* Darkens a color by a specified amount
* @param String The RGB hex color code
* @param Int The amount (decimal-format pertentage) by which to darken the color
* @return String The color code for the darkened color
*/
function darken(color, amount) {
color = splitColor(color);
var ret = [];
for (var i = 0; i < color.length; i++) {
ret[i] = (color[i] - Math.ceil(255 * amount));
if (ret[i] < 0)
ret[i] = 0;
if (ret[i] > 255)
ret[i] = 255;
ret[i] = ret[i].toString(16);
if (ret[i].length < 2)
ret[i] = '0' + ret[i];
}
return '#' + ret.join('').toUpperCase();
}
/**
* Lightens a color by a specified amount
* @param String The RGB hex color code
* @param Int The amount (decimal-format pertentage) by which to lighten the color
* @return String The color code for the lightened color
*/
function lighten(color, amount) {
color = splitColor(color);
var ret = [];
for (var i = 0; i < color.length; i++) {
ret[i] = (color[i] + Math.ceil(255 * amount));
if (ret[i] < 0)
ret[i] = 0;
if (ret[i] > 255)
ret[i] = 255;
ret[i] = ret[i].toString(16);
if (ret[i].length < 2)
ret[i] = '0' + ret[i];
}
return '#' + ret.join('').toUpperCase();
}
/**
* Converts a color code into a usable array for math-based functions
* @param String The RGB hex color code
* @return Int[] The array containing the decimal values of each color
*/
function splitColor(color) {
color = color.replace('#', '');
var offset = Math.floor(color.length / 3);
var ret = [];
for (var i = 0; i < color.length; i += offset) {
ret.push(parseInt(color.substring(i, (i + offset)), 16));
}
return ret;
}
/**
* Returns the CSS for a vertical background gradient (with vendor-specific prefixes)
* @param String The RGB hex color code of the top color
* @param String The RGB hex color code of the bottom color
* @return String The CSS for the background gradient
*/
function gradient(top, bottom) {
var ret = '';
ret += 'background: -moz-linear-gradient(top, ' + top + ' 0%, ' + bottom + ' 100%);';
ret += 'background: -webkit-linear-gradient(top, ' + top + ' 0%, ' + bottom + ' 100%);';
return ret;
}
/**
* Returns the CSS for rounded corners (with vendor-specific prefixes)
* @param String The radius value (syntax: '#px' or '#px #px #px #px')
* @return String The CSS for the rounded corners
*/
function borderRadius(radius) {
radius = 'border-radius: ' + radius;
// returns: -moz-border-radius: <radius>; -webkit-border-radius: <radius>; border-radius: <radius>;
return '-moz-' + radius + '; ' + '-webkit-' + radius + '; ' + radius + ';';
}
/**
* Returns the CSS for box shadows (with vendor-specific prefixes)
* @param String The shadow value (syntax: '#px #px [#px] [#px] color [inset]')
* @return String The CSS for the box shadows
*/
function boxShadow(shadow) {
shadow = 'box-shadow: ' + shadow;
// returns: -moz-box-shadow: <shadow>; -webkit-box-shadow: <shadow>; box-shadow: <shadow>;
return '-moz-' + shadow + '; ' + '-webkit-' + shadow + '; ' + shadow + ';';
}
// basics
css += '* { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; margin: 0; padding: 0; }';
css += 'body { background-color: ' + bg1 + '; color: ' + fg1 + '; font: 13px Helvetica, Arial, sans-serif; }';
css += 'a { color: ' + links + '; cursor: pointer; text-decoration: underline; }';
css += 'a:hover, a:hover { color: ' + lighten(links, 0.133) + '; text-decoration: none; }';
css += 'p { margin: 0 0 10px; }';
css += 'table { font: 13px Helvetica, Arial, sans-serif; margin: auto; width: 100%; }';
// layout
css += '#header { background-color: ' + bg2 + '; border-bottom: 1px solid ' + lighten(bg1, 0.066) + '; color: ' + fg2 + '; padding: 10px 0 0; text-align: center; }';
css += '.scrolling #header { margin-bottom: 10px; }';
css += '.slideshow #header { min-height: 60px; position: fixed; top: 0; width: 100%; z-index: 2; }';
css += '#header .title { color: ' + accent2 + '; }';
css += '#header small { font-variant: small-caps; }';
css += '#header p { margin: 10px 0; }';
css += '#header #description { margin: 10px auto; max-width: 60%; text-align: center; }';
css += '#search { position: absolute; right: 10px; top: 10px; }';
css += '#footer { background-color: ' + bg2 + '; border-top: 1px solid ' + lighten(bg1, 0.066) + '; color: ' + fg2 + '; min-height: 60px; padding: 10px 0; position: relative; text-align: center; }';
css += '.scrolling #footer { margin-top: 10px; position: fixed; bottom: 0; left: 0; right: 0; }';
css += '.slideshow #footer { bottom: 0; height: 60px; position: fixed; width: 100%; z-index: 2; }';
css += '#favorites_container { height: 40px; line-height: 40px; margin-bottom: 0 !important; position: absolute; left: 25px; bottom: 10px; }';
css += '#autoplay { height: 20px; line-height: 20px; position: absolute; right: 25px; bottom: 20px; }';
css += '#info { font-size: 11px; margin: 10px 0 0; }';
// logo
css += '#logo { text-decoration: none; position: absolute; left: 10px; top: 10px; }';
css += '#logo span { font: 18px "Comic Sans MS"; padding: 0 2px; }';
css += '#logo .image { background-color: ' + bg1 + '; color: ' + accent1 + '; }';
css += '#logo:hover .image { background-color: ' + accent1 + '; color: ' + bg1 + '; }';
css += '#logo .fap { background-color: ' + accent1 + '; color: ' + bg1 + '; }';
css += '#logo:hover .fap { background-color: ' + bg1 + '; color: ' + accent1 + '; }';
// forms
css += 'form { margin: 0 0 10px; }';
css += 'input[type="text"], select { background: ' + bg1 + '; border: 1px solid ' + fg1 + '; color: ' + fg1 + '; margin-right: 5px; padding: 5px; }';
css += 'input[type="text"]:focus, select:focus { border-color: ' + accent1 + '; color: ' + fg1 + '; }';
css += 'button, input[type="button"], input[type="submit"] { background: ' + lighten(bg2, 0.066) + '; ' + gradient(lighten(bg2, 0.133), lighten(bg2, 0.066)) + '; border: 1px solid ' + lighten(bg2, 0.200) + '; ' + borderRadius('5px') + ' color: ' + links + '; cursor: pointer; margin-left: 5px; padding: 5px; ' + boxShadow('0 0 0 1px ' + bg2) + '; }';
css += 'button:hover, input[type="button"]:hover, input[type="submit"]:hover { ' + gradient(lighten(bg2, 0.200), lighten(bg2, 0.066)) + '; border: 1px solid ' + lighten(bg2, 0.266) + '; color: ' + lighten(links, 0.066) + '; }';
css += 'button:focus, input[type="button"]:focus, input[type="submit"]:focus { ' + gradient(lighten(bg2, 0.266), lighten(bg2, 0.066)) + '; border: 2px solid ' + lighten(bg2, 0.333) + '; color: ' + lighten(links, 0.133) + '; padding: 4px; }';
css += '#favorites_container input[type="button"], button.default, input[type="submit"] { border: 1px solid ' + accent1 + '; color: ' + lighten(links, 0.133) + '; }';
css += '#favorites_container input[type="button"]:hover, button.default:hover, input[type="submit"]:hover { border: 1px solid ' + lighten(accent1, 0.066) + '; color: ' + lighten(links, 0.200) + '; }';
css += '#favorites_container input[type="button"]:focus, button.default:focus, input[type="submit"]:focus { border: 2px solid ' + lighten(accent1, 0.066) + '; color: ' + lighten(links, 0.266) + '; padding: 4px; }';
// pagination
css += '.pagination { margin: 0; }';
css += '.pagination a { border: 1px solid ' + darken(links, 0.133) + '; color: ' + darken(links, 0.133) + '; display: inline-block; margin: 0 2px 10px; padding: 2px 6px; text-decoration: none; }';
css += '.pagination a:hover { border-color: ' + links + '; color: ' + links + '; display: inline-block; margin: 0 2px; padding: 2px 6px; text-decoration: none; }';
css += '.pagination a.current { border: 1px solid ' + accent2 + '; color: ' + accent2 + '; }';
css += '.pagination a.disabled { border: 1px solid ' + darken(fg2, 0.266) + '; color: ' + darken(fg2, 0.133) + '; cursor: default; display: inline-block; margin: 0 2px; padding: 2px 6px; }';
// thumbnails
css += '#thumbnails { margin-bottom: 10px; text-align: center; z-index: 2; }';
css += '#thumbnails .thumbnail { border: 1px solid ' + darken(links, 0.133) + '; display: inline-block; margin: 2px; padding: 4px; vertical-align: middle; }';
css += '#thumbnails .thumbnail:hover { border-color: ' + links + '; }';
css += '#thumbnails .thumbnail.active { border: 2px solid ' + accent2 + '; padding: 3px; }';
css += '#thumbnails.small img { max-height: 100px; }';
css += '#thumbnails.medium img { max-height: 150px; }';
css += '#thumbnails.large img { max-height: 200px; }';
css += '.slideshow #thumbnails { background-color: ' + bg2 + '; padding-bottom: 10px; overflow-y: hidden; width: 100%; white-space: nowrap; }';
css += '.slideshow #thumbnails .thumbnail { margin: 0 5px; }';
// gallery basics
css += '#gallery { position: relative; text-align: center; }';
css += '#gallery .image img { border: 1px solid ' + darken(links, 0.133) + '; padding: 4px; min-height: 100px; min-width: 100px; }';
css += '#gallery .image:hover img { border-color: ' + accent1 + '; }';
css += '#gallery .image .spinner { border: 10px solid ' + darken(fg2, 0.266) + '; border-left-color: ' + accent1 + '; position: absolute; left: calc(100% / 2 - 80px / 2); top: calc(100% / 2 - 80px / 2); -webkit-animation: spinning 1s infinite linear; animation: spinning 1s infinite linear; }';
css += '#gallery .image .spinner, #gallery .image .spinner:after { ' + borderRadius('50%') + '; width: 80px; height: 80px; z-index: -1; }';
css += '@-webkit-keyframes spinning { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }';
css += '@keyframes spinning { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }';
// scrolling gallery
css += '.scrolling #gallery { max-width: 100%; }';
css += '.scrolling #gallery .image { clear: both; display: inline-block; max-width: 98%; position: relative; }';
css += '.scrolling #gallery .image img { display: inline-block; max-width: 100%; }';
css += '#loader { background: ' + lighten(bg2, 0.066) + '; ' + gradient(lighten(bg2, 0.133), lighten(bg2, 0.066)) + '; border: 1px solid ' + accent1 + '; border-radius: 5px; ' + boxShadow('0 0 0 1px ' + bg2) + '; color: ' + lighten(links, 0.133) + '; display: inline-block; margin-top: 10px; padding: 8px 16px; text-decoration: none; }';
css += '#loader:hover { ' + gradient(lighten(bg2, 0.200), lighten(bg2, 0.066)) + '; border-color: ' + lighten(accent1, 0.066) + '; color: ' + lighten(links, 0.200) + '; }';
// slideshow gallery
css += '.slideshow #content { bottom: 81px; left: 0; padding: 10px; position: absolute; right: 0; top: 81px; }';
css += '.slideshow #gallery { height: 100%; width: 100%; overflow: hidden; }';
css += '.slideshow #gallery .image img { bottom: 0; left: 0; margin: auto; max-height: 100%; max-width: 100%; position: absolute; right: 0; top: 0; }';
css += '.slideshow #gallery .nav { color: #FFFFFF; display: block; text-decoration: none; opacity: 0.5; position: absolute; top: 5px; bottom: 5px; height: 100%; width: 160px; z-index: 1; }';
css += '.slideshow #gallery .nav.disabled { display: none; }';
css += '.slideshow #gallery #next { min-height: 60px; min-width: 60px; right: 5px; }';
css += '.slideshow #gallery #prev { min-height: 60px; min-width: 60px; left: 5px; }';
css += '.slideshow #gallery .arrow { display: block; font-size: 120px; height: 160px; line-height: 160px; margin-top: -80px; position: relative; text-align: center; text-shadow: 0 0 5px #000000, 0 0 20px #FFFFFF; top: 50%; }';
css += '.slideshow #gallery .nav:hover { background-color: rgba(0,0,0,0.5); opacity: 1.0; }';
css += '.slideshow #gallery #indicator { background-color: rgba(0,0,0,0.5); ' + borderRadius('40px') + '; display: block; position: absolute; top: 50%; left: 50%; height: 160px; margin-top: -80px; margin-left: -80px; width: 160px; z-index: 1; }';
css += '.slideshow #gallery #indicator .symbol { color: #FFFFFF; font-size: 120px; position: relative; text-align: center; text-shadow: 0 0 5px #000000, 0 0 20px #FFFFFF; }';
css += '.slideshow #gallery #indicator .symbol.pause { font-weight: bold; }';
css += '.slideshow #gallery #indicator .symbol.play { line-height: 160px; }';
// full screen:: WebKit
css += ':-webkit-full-screen { background-color: #000000; }';
css += '#content:-webkit-full-screen { padding: 0; top: 0 !important; bottom: 0 !important; height: 100%; width: 100%; }';
css += '#content:-webkit-full-screen .nav { ' + borderRadius('40px') + '; height: 160px; margin-top: -80px; top: 50%; }';
css += '#content:-webkit-full-screen .image img { border: 0; padding: 0; }';
css += '#content:-webkit-full-screen #thumbnails { background-color: #000000; position: absolute; top: 0; }';
// full screen:: Mozilla
css += '#content:-moz-full-screen { padding: 0; top: 0 !important; bottom: 0 !important; height: 100%; width: 100%; }';
css += '#content:-moz-full-screen .nav { ' + borderRadius('40px') + '; height: 160px; margin-top: -80px; top: 50%; }';
css += '#content:-moz-full-screen .image img { border: 0; padding: 0; }';
css += '#content:-moz-full-screen #thumbnails { background-color: #000000; position: absolute; top: 0; }';
// add to favorites
css += '#favorites_container table { width: auto; }';
// notification messages
css += '#notificationContainer { bottom: 70px; position: fixed; right: 10px; }';
css += '.notification { background: rgba(0, 0, 0, 0.8); border: 1px solid rgba(255, 255, 255, 0.2); ' + borderRadius('5px') + '; color: rgba(255, 255, 255, 0.5); display: block; font-size: 11px; margin-top: 10px; padding: 9px; }';
// dialog box layout
css += '#dialogContainer { background: rgba(0,0,0,0.8); display: block; text-align: center; position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 3; overflow-y: auto; }';
css += '#dialogBox { background: ' + bg1 + '; border: 1px solid ' + lighten(bg2, 0.200) + '; ' + boxShadow('0 0 20px 0 #000000') + '; color: ' + fg1 + '; display: inline-block; margin: 20px auto; min-width: 300px; text-align: left; position: relative; z-index: 10; }';
css += '#dialogClose { border-left: 1px solid ' + lighten(bg2, 0.200) + '; color: ' + links + '; font-size: 14px; line-height: 28px; position: absolute; right: 0; text-align: center; text-decoration: none; top: 0; width: 30px; }';
css += '#dialogClose:hover { color: ' + lighten(links, 0.200) + '; }';
css += '#dialogTitle { background: ' + bg1 + '; ' + gradient(lighten(bg2, 0.066), bg2) + '; border-bottom: 1px solid ' + lighten(bg2, 0.200) + '; color: ' + links + '; display: block; margin: 0; padding: 5px 10px; }';
css += '#dialogTitle b { color: ' + accent2 + '; }';
css += '#dialogMessage { display: block; padding: 10px; }';
css += '#dialogButtons { clear: both; display: block; padding: 10px; text-align: right; }';
// dialog box content
css += '#dialogMessage table { margin: 0; }';
css += '#dialogMessage table th { color: ' + accent1 + '; font-size: 14px; text-align: left; }';
css += '#dialogMessage table b { color: ' + lighten(accent1, 0.066) + '; }';
css += '#dialogMessage table td.name { padding-right: 5%; text-align: right; width: 20%; }';
css += '#dialogMessage table td.key { text-align: center; width: 25%; }';
// dialog box form elements
css += '#dialogBox #settingsForm { width: 820px }';
css += '#dialogBox .error { color: #C43131; font-weight: bold; }';
css += '#dialogBox button { margin-right: 5px; }';
css += '#dialogBox fieldset { border: 0; border-bottom: 1px solid ' + lighten(bg2, 0.200) + '; margin: 0 0 15px; padding-bottom: 10px; min-width: 400px; }';
css += '#dialogBox fieldset:nth-child(1n) { float: left; }';
css += '#dialogBox fieldset:nth-child(2n) { float: right; }';
css += '#dialogBox legend { color: ' + accent1 + '; font-weight: bold; margin-bottom: 10px; }';
css += '#dialogBox label { float: left; line-height: 20px; }';
css += '#dialogBox label b { color: ' + accent1 + '; }';
css += '#dialogBox label span.default { font-size: 11px; font-variant: small-caps; }';
css += '#dialogBox input[type="text"], #dialogBox select { float: right; }';
css += '#dialogBox input[type="checkbox"] { float: left; margin: 4px 10px 0 ; }';
// insert the line breaks automatically before returning
return css.replace(/}/g, "}\n");
}