Se7en / Booru Downloader + Viewer

// ==UserScript==
// @name        Booru Downloader + Viewer
// @description The original fullsize images downloader, and viewer for more than 20 booru imageboards
// @namespace   https://openuserjs.org/users/Se7en
// @author      Se7en
// @homepage    https://sleazyfork.org/scripts/34175
// @homepageURL https://sleazyfork.org/scripts/34175
// @supportURL  https://sleazyfork.org/scripts/34175/feedback
// @version     1.0.6
// @copyright   2018, Se7en (https://openuserjs.org/users/Se7en)
// @license     MIT
// -------- INCLUDE
// @include     *://gelbooru.com/*
// @include     *://rule34.xxx/*
// @include     *://yande.re/*
// @include     *://*.donmai.us/*
// @include     *://*.sankakucomplex.com/*
// @include     *://behoimi.org/*
// @include     *://youhate.us/*
// @include     *://safebooru.org/*
// @include     *://uberbooru.com/*
// @include     *://bronibooru.com/*
// @include     *://www.bronibooru.com/*
// @include     *://mspabooru.com/*
// @include     *://e926.net/*
// @include     *://e621.net/*
// @include     *://*.booru.org/*
// @include     *://atfbooru.ninja/*
// @include     *://lolibooru.moe/*
// @include     *://hypnohub.net/*
// @include     *://tbib.org/*
// @include     *://konachan.net/*
// @include     *://konachan.com/*
// @include     *://rule34.paheal.net/*
// -------- EXCLUDE
// @exclude     *://simg3.gelbooru.com*//images/*
// @exclude     *://img.rule34.xxx*//images/*
// @exclude     *://files.yande.re*/images/*
// @exclude     *://files.yande.re*/jpeg/*
// @exclude     *://*.donmai.us*/data/*
// @exclude     *://*s.sankakucomplex.com*/data/*
// @exclude     *://behoimi.org*/data/*
// @exclude     *://safebooru.org*//images/*
// @exclude     *://uberbooru.com*/data/*
// @exclude     *://s3.amazonaws.com*/bronibooru/*
// @exclude     *://mspabooru.com*//images/*
// @exclude     *://static1.e926.net*/data/*
// @exclude     *://static1.e621.net*/data/*
// @exclude     *://img.booru.org*/*//images/*
// @exclude     *://atfbooru.ninja*/data/*
// @exclude     *://lolibooru.moe*/image/*
// @exclude     *://hypnohub.net*//data/image/*
// @exclude     *://tbib.org*//images/*
// @exclude     *://konachan.net*/images/*
// @exclude     *://konachan.net*/jpeg/*
// @exclude     *://konachan.com*/images/*
// @exclude     *://konachan.com*/jpeg/*
// @exclude     *://*.paheal.net*/_images/*
// -------- CONNECT
// @connect     gelbooru.com
// @connect     rule34.xxx
// @connect     yande.re
// @connect     donmai.us
// @connect     sankakucomplex.com
// @connect     behoimi.org
// @connect     safebooru.org
// @connect     uberbooru.com
// @connect     s3.amazonaws.com
// @connect     bronibooru.com
// @connect     mspabooru.org
// @connect     e926.net
// @connect     e621.net
// @connect     booru.org
// @connect     atfbooru.ninja
// @connect     lolibooru.moe
// @connect     hypnohub.net
// @connect     tbib.org
// @connect     konachan.net
// @connect     konachan.com
// @connect     paheal.net
// -------- GREASEMONKEY API
// @grant       GM_xmlhttpRequest
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_listValues
// @grant       GM_deleteValue
// @grant       GM_download
// @grant       GM_info
// -------- GREASEMONKEY 4.0+ COMPATIBILITY
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @grant       GM.xmlHttpRequest
// @grant       GM.setValue
// @grant       GM.getValue
// @grant       GM.listValues
// @grant       GM.deleteValue
// @grant       GM.download
// @grant       GM.info
// ==/UserScript==

/*
 1.0.6
	* fix viewer colors on safebooru.org
	* fix url match on donmai.us post page
	* fix sankaku viewer
 1.0.4
	* fix 'ignoredTags' option: trimmed them
 1.0.3
	* fix include's list
 1.0.2
	* fix cofiguration data on tampermonkey
 1.0.1
	* fix preloader, it fell into infinite recursion, when total amount of images was equal to 1
 1.0.0
	+ added tabs to user menu:
		+ General: autoRun, createViewer, downloadJPEG, animateProgress
		+ Filename: maxTagsInName, tagsOrder, ignoredTags, tagsDelim, addImgBrdName, prefixedName, imgIdAtNameEnd
		+ Viewer: viewOriginal, viewJPEG, viewFirst, holdCtrl, fixedThumbs, fixedTags
	+ new user options:
		+ ignored tag names [''], in the Filename tab:
			* underscored tag names, that are not included into file name
		+ fixed thumb/tag list [false]/[true], in the Viewer tab
			* if false, then fade out the thumb/tag list, when mouse is out, and fade in, when mouse is over them, otherwise fixe them
		+ view original images [false], in the Viewer tab:
			* the old option called viewSample is replaced by the viewOriginal keeping backward compatibility
		+ show progress/status bar [true], in the Viewer tab
	+ auto hide the viewer's navigation bar
	+ auto focus on the viewer's thumb/tag list when the mouse cursor is over them, thus, making much easier to start scrolling them
	+ simple image preloader
	* change the default value of the 'holdCtrl' option to [false]
	* horizontally, and vertically centered images on the viewer window
	* set controls, and loop attributes on the video elements
	* restore browser history URL after viewer exit (older versions remain history URL unchanged after manipulations)
		* this is sankaku hack: simply replaces the current history URL by the image's post URL to enable the valid image loading
	* hide main page scroll bar when viewer activated
	* fixed viewer's 1st image thumbnail (post image thumb) on post page
	* little refactoring (for further changes)
 0.7.0
	+ compatibility with Greasemonkey 4.0+
 0.6.0
	+ advanced viewer:
		+ tag list on right sidebar
		+ thumbs on left sidebar
	* removed user options: maxWidth, maxHeight
 0.5.0
	+ image status/progress bar
	+ user option:
		+ animate initialization/downloading progress [true]
	* fix main button events
 0.4.2
	+ hotkey:
		+ 'Shift+A' - download all available images
	* fix image source getter
	- known issues:
		- 'Download Mode', and 'Download All' buttons don't work on post page of rule34.xxx, use hotkeys instead
 0.4.1
	* fix exclude-list typo
	* fix konachan jpeg images extension
 0.4.0
	+ supported imageboards:
		+ atfbooru
		+ lolibooru
		+ hypnohub
		+ tbib
		+ konachan
		+ paheal
	* change name 'rule34' to 'rule34.xxx'
	* fix bug on post page due to an empty viewer div
 0.3.2
	+ user option:
		+ tag-types order in file name ['character', 'copyright', 'artist', 'species', 'model', 'idol', 'photo_set', 'circle', 'medium', 'metadata', 'general', 'faults']
 0.3.1
	+ user options:
		+ hold Ctrl key to left/right navigate when viewing [true]
		+ maximum width of image, px [1000]
		+ maximum height of image, px [700]
	* little changes
 0.3.0
	+ simple image viewer
	+ user options:
		+ create image viewer - [true]
		+ view image sample - [true]
		+ view jpeg image (yande.re option) - [false]
		+ view 1st image on viewer activation - [true]
	+ hotkeys:
		+ 'Shift+V' - switch viewer on/off
		+ 'Ctrl+left/right' arrows - view previous/next image
	+ viewer buttons:
		+ Prev
		+ Source - open image file in a new tab
		+ Number - index of the current image
		+ Download
		+ Next
	+ @connect meta-data (to silence tampermonkey)
	* fix wrong image hostname for uberbooru
 0.2.7
	* scrollable content of user menu window
	* user menu window's size fitted to client's size
	* move user menu 'close' button to the top right of the menu window (x sign)
	* other little change
 0.2.5
	* fix typos
 0.2.4
	+ user option:
		+ Image ID, and ImageBoard name at the end of the file name [true]
	+ dynamically rename images on user options change
 0.2.3
	* fix image extensions on tampermonkey
 0.2.2
	* bugfixes
	* little changes
 0.2.0
	+ image downloader for imageboards:
		+ youhate.us
		+ safebooru
		+ uberbooru
		+ bronibooru
		+ mspabooru
		+ e926/e621
		+ *.booru.org
	+ user option:
		+ prefixed imageboard name [false]
 0.1.1
	+ user option:
		+ auto initialize the script [true]
	+ hotkey:
		+ 'Shift+M' - open/close user menu dialog
	* little changes
 0.1.0
	+ user menu
 0.0.13
	* refactoring
	* fix button events 
 0.0.10
	+ behoimi downloader
 0.0.9
	+ hotkey:
		+ 'Shift+I' - (re)initializes imageBoard (usefull for the imageboards with auto paging)
	* fix yande.re jpeg image extension
 0.0.8
	+ sankaku downloader
		+ chan.sankakucomplex.com
		+ idol.sankakucomplex.com
 0.0.7
	+ hotkey:
		+ 'Shift+D' - toggle the Download Mode on/off
	+ donmai downloader
		+ safebooru.donmai.us
		+ danbooru.donmai.us
		+ sonohara.donmai.us
		+ hijiribe.donmai.us
 0.0.6
	+ yande.re downloader
	+ user option:
		+ download jpeg image on yande.re [false]
 0.0.5
	+ rule34 downloader
	+ user option:
		+ add the imageboard name to the image name [true]
 0.0.3
	+ gelbooru downloader
	+ user options:
		+ maximum tags in the image name [10]
		+ tags delimeter in the image name ['-']
*/
if( window.self !== window.top )
	return;
var RANDOM = '1681238';//Math.floor(Math.random()*1e6 + 1e6);
console.log('start ' + GM.info.script.name + ' v' + GM.info.script.version + '..');
(async function(){
	function consoleLog(){window.console.log.apply(this, arguments);}
	function blank(){}
	var clog = consoleLog;
	clog = blank;
	var userOptions = await initOptions(),
	    methodsObject = initMethodsObject(),
		imageBoard = initImageBoard();
	newCssClasses();
	
	//------------------------------------------------------------------------------------//
	//------------------------------------ IMAGE BOARD -----------------------------------//
	function initImageBoard( d )
	{
		/*
		var elmClass = initImageBoardClasses(d),
			elmData  = initImageBoardDataset(d),
			siteList = initSiteList(),
			download = initImageBoardDownloader(d),
			userMenu = initUserMenu(),
			view     = initImageBoardViewer(d),
			state    = {'viewMode': false, 'userMenu': false, 'downloadMode': false},
			divID    = 'image-board-div-' + RANDOM;
		*/
		var imgBrdCl = initImageBoardClasses(d),
			imgBrdDt = initImageBoardDataset(d),
			siteList = initSiteList(),
			imgBrdDw = initImageBoardDownloader(d),
			userMenu = initUserMenu(),
			imgBrdVw = initImageBoardViewer(d),
			imgBrdSt = {'viewMode': false, 'userMenu': false, 'downloadMode': false},
			imgBrdId = 'image-board-div-' + RANDOM;
		var retVal   = {
			get siteList(){return siteList;},
			get imgBrdCl(){return imgBrdCl;},
			get imgBrdDt(){return imgBrdDt;},
			get imgBrdId(){return imgBrdId;},
			get imgBrdDw(){return imgBrdDw;},
			get userMenu(){return userMenu;},
			get imgBrdVw(){return imgBrdVw;},
			get imgBrdSt(){return imgBrdSt;},
			get images(){return this.data.images;},
			get downloader(){return this.data.downloader;},
			get viewer(){return this.data.viewer;},
			data: {
				'images': {
					list: null,
					init: function( doc, type ){
						clog("imageBoard init..");
						siteList.init(type);
						imgBrdDt.init(doc);
						imgBrdCl.init(doc);
						this.list = this.list || [];
						this.doc = doc || document;
						var siteObj = siteList.val(type),
							isPost = siteObj.isPost(),
							imgD;
						if( isPost )
						{
							var img = siteObj.getPostImage();
							if( img && !imgBrdCl.hasClass( img, 'counted') )
								imgD = this.addNewImage( img, isPost, siteObj );
						}
						var thumbs = siteObj.getImageThumbs( this.doc ),
							_3ParentTypes = ['yande.re', 'lolibooru', 'hypnohub', 'konachan'],
							name = siteObj.name,
							num = (_3ParentTypes.indexOf(name) != -1 ? 3 : 2);
						clog("thumbs.length: ", thumbs.length);
						for( var i = 0, len = thumbs.length, thumb, par, h; i < len; ++i )
						{
							thumb = thumbs[i];
							if( imgBrdCl.hasClass( thumb, 'counted' ) )
								continue;
							imgD = this.addNewImage( thumb, false, siteObj );
							par = parent( thumb, num );
							par.appendChild( this.createProgressBar(imgD.index) );
							if( par.tagName === 'ARTICLE' )
							{
								try{
									h = par.style.height;
									h = parseInt(h.match(/\d+/)[0], 10);
									h += 15;
									h += 'px';
								}catch(er){
									console.error(er);
									h = null;
								}
								par.style.height = h || '170px';
							}
						}
					},
					addNewImage: function( img, isPost, siteObj ){
						this.list.push({});
						var imgD = last(this.list), pdiv;
						imgD.state = 'empty';
						imgD.index = this.list.length - 1;
						imgD.type = siteObj.name;
						if( isPost )
						{
							imgD.postId = siteObj.getPostId();
							imgD.postUrl = window.location.href;
							siteObj.setImageDataDoc(imgD);
							pdiv = this.createProgressBar(imgD.index);
							if( img.parentNode.tagName != 'A' )
								img.parentNode.insertBefore(pdiv, img.nextSibling);
							else
								img.parentNode.parentNode.appendChild(pdiv);
						}else
							siteObj.setImageDataThumb( imgD, img );
						imgBrdDt.val( img, 'index', imgD.index);
						imgBrdCl.addClass( img, 'counted' );
						if( imgD.state === 'ready' )
						{
							siteObj.createDiv( imgBrdId, this.doc);
							imgBrdDw.init(imgBrdId, this.doc);
							setReadyImage( imgD, imgBrdCl, imgBrdDt, imgBrdDw, imgBrdVw );
						}
						return imgD;
					},
					createProgressBar: function(index ){
						var div = document.createElement('div'),
							html = '<div id="progress-stripe-' + index + '" ' +
							'class="progress-stripe progress-counted"></div>';
						div.setAttribute('class', 'progress-bar');
						div.insertAdjacentHTML('beforeend', html);
						return div;
					},
					getEmpty: function(){
						var empty = [];
						for( var i = 0; i < this.list.length; ++i )
						{
							if( this.list[i].state === 'empty' )
								empty.push(i);
						}
						return empty;
					},
					fix: function()
					{
						var empty = this.getEmpty(), animate = userOptions.val('animateProgress');
						clog("fix start..", empty.length);
						for( var i = 0, idx, imgD; i < empty.length; ++i )
						{
							idx = empty[i];
							imgD = this.list[idx];
							imgD.state = 'busy';
							this.getImageData(imgD, animate);
						}
					},
					getImageData: function(imgD, animate)
					{
						if( siteList.needXHR(imgD.type) )
						{
							if( animate )
								addClass(document.querySelectorAll('#progress-stripe-' + imgD.index), 'progress-animated');
							GM.xmlHttpRequest({
								url: imgD.postUrl,
								method: 'GET',
								context: {
									'index': imgD.index,
									'url': imgD.postUrl,
								},
								onload: xhrImageData,
							});
						}else{
							console.log("TODO :D");
							var siteObj = siteList.val(imgD.type);
							//siteObj.setImageDataFull(imgD);// TODO (yande.re, donmai)
						}
					},
				},
				'downloader': {
					init: function(doc, type){
						clog("downloader init..");
						siteList.init(type);
						var siteObj = siteList.val(type);
						siteObj.createDiv( imgBrdId, doc);
						imgBrdDw.init(imgBrdId, doc);
					},
					isActive: function(){
						//return imgBrdDw && imgBrdDw.isActive() || false;
						return imgBrdSt.downloadMode;
					},
					activateImage: function(thumb){
						if( !thumb )
							return;
						var a = thumb.parentNode;
						if( !imgBrdCl.hasClass(thumb, 'ready' ) )
							return;
						else if( !imgBrdCl.hasClass( a, 'downloadAttach' ) )
						{
							a.addEventListener('click', handleDownloadEvent, false);
							imgBrdCl.addClass( a, 'downloadAttach' );
						}
						imgBrdCl.addClass( a, 'downloadActive' );
					},
					activate: function(doc){
						clog("[downloader] activate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var thumbs = imgBrdCl.queryAll('counted');
						for( var i = 0, len = thumbs.length; i < len; ++i )
							this.activateImage( thumbs[i] );
						imgBrdDw.downloadOn();
						imgBrdSt.downloadMode = true;
					},
					deactivate: function(doc){
						clog("[downloader] deactivate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var activ = imgBrdCl.queryAll('downloadActive');
						clog("active.length: ", activ.length);
						for( var i = 0, len = activ.length; i < len; ++i )
							imgBrdCl.removeClass( activ[i], 'downloadActive' );
						imgBrdDw.downloadOff();
						imgBrdSt.downloadMode = false;
					},
					downloadAll: function(){
						imgBrdDw.downloadAll.click();// =)
					},
				},
				'userMenu': {
					init: function(doc, type){
						clog("userMenu init..");
						siteList.init(type);
						var siteObj = siteList.val(type);
						siteObj.createDiv( imgBrdId, doc);
						userMenu.init(imgBrdId, doc);
					},
				},
				'keyboard': {
					val: null,
					init: function(){
						if( !this.isActive )
							this.activate();
					},
					get isActive(){ return !!this.val;},
					activate: function(){
						activateKeyboard();
						this.val = true;
					},
					deactivate: function(){
						deactivateKeyboard();
						this.val = false;
					},
				},
				'viewer': {
					init: function(doc, type){
						clog("viewer init..");
						siteList.init(type);
						var siteObj = siteList.val(type);
						siteObj.createDiv( imgBrdId, doc);
						imgBrdVw.init(imgBrdId, doc, siteObj.viewDivInsertionPlace);
					},
					activateImage: function( thumb ){
						if( !thumb )
							return;
						var a = thumb.parentNode;
						if( !imgBrdCl.hasClass(thumb, 'ready' ) )
							return;
						else if( !imgBrdCl.hasClass( a, 'viewAttach' ) )
						{
							a.addEventListener('click', handleViewerEvent, false);
							imgBrdCl.addClass( a, 'viewAttach' );
						}
						imgBrdCl.addClass( a, 'viewActive' );
					},
					activate: function(doc){
						clog("viewer activate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var thumbs = imgBrdCl.queryAll('counted');
						for( var i = 0, len = thumbs.length; i < len; ++i )
							this.activateImage( thumbs[i] );
						imgBrdVw.viewerOn();
						imgBrdSt.viewMode = true;
					},
					deactivate: function(doc){
						clog("viewer deactivate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var activ = imgBrdCl.queryAll('viewActive');
						clog("active.length: ", activ.length);
						for( var i = 0, len = activ.length; i < len; ++i )
							imgBrdCl.removeClass( activ[i], 'viewActive' );
						imgBrdVw.viewerOff();
						imgBrdSt.viewMode = false;
					},
					isActive: function(){
						//return imgBrdVw.isActive();
						return imgBrdSt.viewMode;
					},
				},
			},
			init: function(doc){
				for( var key in this.data )
					this.data[key].init(doc);
			},
			fix: function(){
				this.data.images.fix();
			},
			initDiv: function(doc){
				doc = doc || document;
				var div = doc.querySelector('#' + imgBrdId),
					siteObj = siteList.val();
				if( !div )
					div = siteObj.createDiv(imgBrdId);
				if( !hasClass(div, 'image-board-div-activated') )
				{
					div.addEventListener('click', handleImageBoardEvent, false);
					addClass(div, 'image-board-div-activated');
				}
			},
		};
		retVal.init(d);
		setTimeout(function(){retVal.initDiv(d);}, 100);
		if( userOptions.val('autoRun') )
			retVal.fix();
		return retVal;
	}
	function handleImageBoardEvent(event)
	{
		var t = event.target,
			dId = 'image-board-download-switch-' + RANDOM,
			aId = 'image-board-download-all-' + RANDOM,
			vId = 'image-board-viewer-button-' + RANDOM,
			mId = 'image-board-user-menu-id-' + RANDOM;
		if( t.tagName === 'SPAN' )
			t = t.parentNode;
		if( t.tagName !== 'BUTTON' )
			return;
		else if( t.id === dId )
		{
			handleDownloadSwitchEvent();
		}
		else if( t.id === aId )
		{
			handleDownloadAllEvent();
		}
		else if( t.id === vId )
		{
			handleViewerSwitchEvent();
		}
		else if( t.id === mId )
		{
			handleUserMenuEvent();
		}else
			console.error("unknown element: ", t);
	}
	//------------------------------------ IMAGE BOARD -----------------------------------//
	//------------------------------------------------------------------------------------//
	//----------------------------------- XRH IMAGE DATA ---------------------------------//
	function xhrImageData(xhr)
	{
		var imgD = imageBoard.images.list[xhr.context.index];
		if( xhr.status !== 200 )
		{
			var context = xhr.context;
			console.error("xhr.status: ", xhr.status, xhr.statusText );
			console.error("index: ", context ? context.index : null);
			console.error("postUrl: ", context && context.url || null );
			if( imgD.state !== 'ready' )
				imgD.state = 'empty';
			removeClass( document.querySelectorAll('#progress-stripe-' + context.index), 'progress-animated' );
			return;
		}
		if( !imgD || imgD.state === 'ready' )
		{
			console.error("invalid context: ", imgD);
			return;
		}
		var siteObj = imageBoard.siteList.val(imgD.type);
		if( !siteObj )
		{
			console.error("invalid site type: ", imgD.type);
			return;
		}
		var doc = document.implementation.createHTMLDocument("");
		doc.documentElement.innerHTML = xhr.response;
		siteObj.setImageDataDoc(imgD, doc);
		clog("xhrImageData[" + imgD.index + "].state : " + imgD.state);
		if( imgD.state === 'ready' )
		{
			setReadyImage( imgD );
		}
	}
	function setReadyImage( imgD, imgBrdCl, imgBrdDt, imgBrdDw, imgBrdVw )
	{
		if( (!imgBrdCl || !imgBrdDt || !imgBrdDw || !imgBrdVw) && imageBoard )
		{
			imgBrdCl = imageBoard.imgBrdCl;
			imgBrdDt = imageBoard.imgBrdDt;
			imgBrdDw = imageBoard.imgBrdDw;
			imgBrdVw = imageBoard.imgBrdVw;
		}
		var thumb = imgBrdDt.query('index', imgD.index + ''),
			stripe = document.querySelectorAll('#progress-stripe-' + imgD.index);
		addClass(stripe, 'image-ready');
		removeClass(stripe, 'progress-animated');
		imgBrdCl.addClass( thumb, 'ready' );
		imgBrdDt.val( thumb, 'source', imgD.source );
		if( imgD.bytes ) imgBrdDt.val( thumb, 'bytes', imgD.bytes );
		imgBrdDw.total += 1;
		imgBrdVw.total += 1;
		clog("name: " + imgD.name);
		if( imageBoard )
		{
			if( imageBoard.downloader.isActive() )
				imageBoard.downloader.activateImage( thumb );
			if( imageBoard.viewer.isActive() )
				imageBoard.viewer.activateImage( thumb );
		}
	}
	//----------------------------------- XRH IMAGE DATA ---------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------- SITE LIST ------------------------------------//
	function initSiteList()
	{
		var retVal = {
			settings: {
				'gelbooru':   getGelbooruSettings,
				'rule34.xxx': getRule34Settings,
				'yande.re':   getYandereSettings,
				'donmai':     getDonmaiSettings,
				'sankaku':    getSankakuSettings,
				'behoimi':    getBehoimiSettings,
				'youhate':    getGelbooruSettings,
				'safebooru':  getSafebooruSettings,
				'uberbooru':  getUberbooruSettings,
				'bronibooru': getBronibooruSettings,
				'mspabooru':  getMspabooruSettings,
				'e926.net':   getE926netSettings,
				'e621.net':   getE621netSettings,
				'.booru.org': getBooruorgSettings,
				'atfbooru':   getAtfbooruSettings,
				'lolibooru':  getLolibooruSettings,
				'hypnohub':   getHypnohubSettings,
				'tbib':       getTbibSettings,
				'konachan':   getKonachanSettings,
				'paheal.net': getPahealSettings,
			},
			data: null,
			get: function( type, prop1, prop2 ){
				var obj;
				if( !type )
					obj = this.currentObj;
				else{
					this.data[type].init();
					obj = this.data[type];
				}
				return nodeWalk.call( obj, prop1, prop2 );
			},
			style: function(type){
				return this.get( type, 'style' );
			},
			val: function(type){
				return this.get( type, 'val' );
			},
			needXHR: function(type){
				return this.get( type, 'needXHR' );
			},
			init: function(type, prefix){
				if( !this.data )
				{
					this.data = {};
					for( var key in this.settings )
						this.data[key] = getSiteObject( key, this.settings[key], prefix );
				}
				if( !type )
					this.initCurrent();
				else if( this.data[type] )
					this.data[type].init();
			},
			getSiteType: function(url){
				url = url || window.location.href;
				for( var key in this.data )
				{
					if( this.data[key].regexp.test(url) )
						return key;
				}
				console.error("no site object found for this host");
				return null;
			},
			initCurrent: function(){
				if( !this.currentObj )
				{
					var type = this.getSiteType();
					if( !type )
						return;
					this.currentObj = this.data[type];
				}
				this.currentObj.init();
			},
		};
		retVal.init();
		clog("siteList.current: ", retVal.val());
		return retVal;
	}
	//------------------------------------- SITE LIST ------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------- SITE OBJECT ----------------------------------//
	function getSiteObject( siteName, getSiteSettings, prefix )
	{
		return {
			val: null,
			name: siteName,
			regexp: new RegExp( siteName ),
			get needXHR(){return this.val.needXHR;},
			get style(){return this.val.style;},
			get settings(){
				var s = ( typeof getSiteSettings === 'function' ? getSiteSettings(prefix) : null);
				Object.defineProperty( this, 'settings', {
					get: function(){return s;},
					enumerable: true,
					configurable: true,
				});
				return s;
			},
			init: function(){
				this.val = this.val || initSiteObject( this.settings );
			},
		};
	}
	function initSiteObject( siteSettings )
	{
		var retVal = {
			data: null,
			
			get name(){ return this.data.name; },
			get prefixedName(){
				var prefix = this.prefix,
					name = this.shortName;
				if( prefix )
					name = prefix + name;
				Object.defineProperty( this, 'prefixedName', {
					get: function(){return name;},
					enumerable: true,
					configurable: true,
				});
				return name;
			},
			get prefix(){return this.data.prefix; },
			get shortName(){
				var name = this.name.replace(/^\./, '');
				Object.defineProperty( this, 'shortName', {
					get: function(){return name;},
					enumerable: true,
					configurable: true,
				});
				return name;
			},
			get hostname(){return this.data.hostname; },
			get imageHostname(){return this.data.imageHostname;},
			get imageDir(){return this.data.imageDir; },
			get style(){return this.data.style;},
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get listDivInsertionPlace(){return this.data.listDivInsertionPlace;},
			get viewDivInsertionPlace(){return this.data.viewDivInsertionPlace;},
			get methodsMap(){return this.data.methodsMap;},
			get needXHR(){return (typeof this.data.needXHR === 'boolean' ? this.data.needXHR : true);},
			init: function( settings ){
				this.data = this.data || settings;
				if( !this.data )
				{
					console.error("[initSiteObject] can't init siteObject, invalid data: ", this.data);
					return;
				}
				for( var i = 0, len = methodsObject.list.length, name, type, map = this.methodsMap || {}; i < len; ++i )
				{
					name = methodsObject.list[i];
					type = map[name] || 'booru';
					if( typeof methodsObject.method(type, name) === 'function' )
						this[name] = methodsObject.method(type, name);
				}
			},
		};
		retVal.init( siteSettings );
		return retVal;
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- GELBOORU ------------------------------------//
	function getGelbooruSettings()
	{
		return {
			name: 'gelbooru',
			hostname: 'gelbooru.com',
			imageDir: '/images',
			imageHostname: 'simg3.gelbooru.com',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: '.contain-push',
			viewDivInsertionPlace: '.padding15',
			style: {
				color: '#fff',
				width: '180px',
				background: '#0773fb',
				backgroundHover: '#fbb307',
				colorHover: '#fff',
				backgroundView: '#fff',
			},
			methodsMap: {
				isPost: 'gelbooru',
				getPostId: 'gelbooru',
				getPostUrl: 'gelbooru',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//--------------------------------------- RULE34 -------------------------------------//
	function getRule34Settings()
	{
		return {
			name: 'rule34.xxx',
			hostname: 'rule34.xxx',
			imageDir: '/images',
			imageHostname: 'img.rule34.xxx',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list',
			style: {
				color: '#fff',
				width: '180px',
				background: '#84AE83',
				backgroundHover: '#A4CEA3',
				colorHover: '#fff',
			},
			methodsMap: {
				isPost: 'gelbooru',
				getPostId: 'gelbooru',
				getPostUrl: 'gelbooru',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//------------------------------------- YANDE.RE -------------------------------------//
	function getYandereSettings()
	{
		return {
			name: 'yande.re',
			hostname: 'yande.re',
			imageDir: 'image',
			imageHostname: 'files.yande.re',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list',
			style: {
				color: '#ee8887',
				width: '180px',
				background: '#222',
				backgroundHover: '#444',
				colorHover: '#ee8887',
			},
			methodsMap: null,
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- DONMAI --------------------------------------//
	function getDonmaiSettings( prefix )
	{
		var prefixList = ['safebooru.', 'danbooru.', 'sonohara.', 'hijiribe.'],
			hostnameSuffix = 'donmai.us';
		prefix = getHostnamePrefix( hostnameSuffix, prefixList, prefix );
		var hostname = prefix + hostnameSuffix;
		return {
			name: 'donmai',
			prefix: prefix,
			hostname: hostname,
			imageHostname: hostname,
			imageDir: 'data',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: '#posts',
			viewDivInsertionPlace: '#page', //'#c-posts',
			style: {
				color: '#0073ff',
				width: '180px',
				background: '#f5f5ff',
				backgroundHover: '#f5f5ff',
				colorHover: '#80b9ff',
			},
			methodsMap: {
				isPost: 'donmai',
				getPostId: 'donmai',
				getPostUrl: 'donmai',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- SANKAKU -------------------------------------//
	function getSankakuSettings(prefix)
	{
		var prefixList = ['chan.', 'idol.'],
			hostnameSuffix = 'sankakucomplex.com';
		prefix = getHostnamePrefix( hostnameSuffix, prefixList, prefix );
		var hostname = prefix + hostnameSuffix,
			imageHostnamePrefix = (prefix ? prefix[0] + 's.' : '');
		return {
			name: 'sankaku',
			prefix: prefix,
			hostname: hostname,
			imageHostname: imageHostnamePrefix + hostnameSuffix,
			imageDir: 'data',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: '#content',
			viewDivInsertionPlace: '#content',
			style: {
				color: '#ff761c',
				width: '180px',
				background: '',
				backgroundHover: '',
				colorHover: '#666',
			},
			methodsMap: null,
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- BEHOIMI -------------------------------------//
	function getBehoimiSettings()
	{
		return {
			name: 'behoimi',
			hostname: 'behoimi.org',
			imageHostname: 'behoimi.org',
			imageDir: 'data',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list',
			style: {
				color: '#43333f',
				width: '180px',
				background: '',
				backgroundHover: '',
				colorHover: '#354d99',
			},
			methodsMap: null,
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- SAFEBOORU -----------------------------------//
	function getSafebooruSettings()
	{
		return {
			name: 'safebooru',
			hostname: 'safebooru.org',
			imageHostname: 'safebooru.org',
			imageDir: '/images',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list',
			style: {
				color: '#fff',
				width: '180px',
				background: '#006ffa',
				backgroundHover: '#006ffa',
				colorHover: '#33cfff',
			},
			methodsMap: {
				isPost: 'gelbooru',
				getPostId: 'gelbooru',
				getPostUrl: 'gelbooru',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- UBERBOORU -----------------------------------//
	function getUberbooruSettings()
	{
		return {
			name: 'uberbooru',
			hostname: 'uberbooru.com',
			imageHostname: 'uberbooru.com',
			imageDir: 'data',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: '#posts',
			viewDivInsertionPlace: 'div#page', // 'div#c-posts',
			style: {
				color: '#000',
				width: '180px',
				background: '#e6e6e6',
				backgroundHover: '#e6e6e6',
				colorHover: '#008',
			},
			methodsMap: {
				isPost: 'donmai',
				getPostId: 'donmai',
				getPostUrl: 'donmai',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//------------------------------------- BRONIBOORU -----------------------------------//
	function getBronibooruSettings()
	{
		return {
			name: 'bronibooru',
			hostname: 'bronibooru.com',
			imageHostname: 's3.amazonaws.com',
			imageDir: 'bronibooru',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: '#posts',
			viewDivInsertionPlace: 'div#page', // 'div#c-posts',
			style: {
				color: '#0073ff',
				width: '180px',
				background: '#f7f7ff',
				backgroundHover: '#f7f7ff',
				colorHover: '#80b9ff',
			},
			methodsMap: {
				isPost: 'donmai',
				getPostId: 'donmai',
				getPostUrl: 'donmai',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- MSPABOORU -----------------------------------//
	function getMspabooruSettings()
	{
		return {
			name: 'mspabooru',
			hostname: 'mspabooru.com',
			imageHostname: 'mspabooru.com',
			imageDir: '/images',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list', // 'div#content',
			style: {
				color: '#fff',
				width: '180px',
				background: '#006ffa',
				backgroundHover: '#006ffa',
				colorHover: '#33cfff',
			},
			methodsMap: {
				isPost: 'gelbooru',
				getPostId: 'gelbooru',
				getPostUrl: 'gelbooru',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//--------------------------------------- E926NET ------------------------------------//
	function getE926netSettings()
	{
		return {
			name: 'e926.net',
			hostname: 'e926.net',
			imageHostname: 'static1.e926.net',
			imageDir: 'data',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content-post',
			viewDivInsertionPlace: 'div#content', // 'div#post-list',
			style: {
				color: '#fff',
				width: '180px',
				background: '#152f56',
				backgroundHover: '#152f56',
				colorHover: '#2e76b4',
			},
			methodsMap: null,
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//--------------------------------------- E621NET ------------------------------------//
	function getE621netSettings()
	{
		return {
			name: 'e621.net',
			hostname: 'e621.net',
			imageHostname: 'static1.e621.net',
			imageDir: 'data',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content-post',
			viewDivInsertionPlace: 'div#content', // 'div#post-list',
			style: {
				color: '#fff',
				width: '180px',
				background: '#152f56',
				backgroundHover: '#152f56',
				colorHover: '#2e76b4',
			},
			methodsMap: null,
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//--------------------------------------- *.BOORU ------------------------------------//
	function getBooruorgSettings(prefix)
	{
		var prefixList = [], hostnameSuffix = 'booru.org';
		prefix = getHostnamePrefix( hostnameSuffix, prefixList, prefix );
		var hostname = prefix + hostnameSuffix;
		return {
			name: '.booru.org',
			prefix: prefix,
			hostname: hostname,
			imageHostname: 'img.booru.org',
			imageDir: prefix + '//images',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#content', // 'div#post-list',
			style: {
				color: '#fff',
				width: '180px',
				background: '#0773fb',
				backgroundHover: '#fbb307',
				colorHover: '#fff',
			},
			methodsMap: {
				isPost: 'gelbooru',
				getPostId: 'gelbooru',
				getPostUrl: 'gelbooru',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//--------------------------------------- ATFBOORU -----------------------------------//
	function getAtfbooruSettings()
	{
		return {
			name: 'atfbooru',
			hostname: 'atfbooru.ninja',
			imageHostname: 'atfbooru.ninja',
			imageDir: 'data',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: '#posts',
			viewDivInsertionPlace: '#page', //'#c-posts',
			style: {
				color: '#0073ff',
				width: '180px',
				background: '#f5f5ff',
				backgroundHover: '#f5f5ff',
				colorHover: '#80b9ff',
			},
			methodsMap: {
				isPost: 'donmai',
				getPostId: 'donmai',
				getPostUrl: 'donmai',
			},
			needXHR: true,
		};// donmai like
	}
	//------------------------------------------------------------------------------------//
	//------------------------------------- LOLIBOORU ------------------------------------//
	function getLolibooruSettings()
	{
		return {
			name: 'lolibooru',
			hostname: 'lolibooru.moe',
			imageDir: 'image',
			imageHostname: 'lolibooru.moe',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list',
			style: {
				color: '#ee8887',
				width: '180px',
				background: '#222',
				backgroundHover: '#444',
				colorHover: '#ee8887',
			},
			methodsMap: null,
			needXHR: true,
		};// yande.re like
	}
	//------------------------------------------------------------------------------------//
	//------------------------------------- HYPNOHUB -------------------------------------//
	function getHypnohubSettings()
	{
		return {
			name: 'hypnohub',
			hostname: 'hypnohub.net',
			imageDir: '/data/image',
			imageHostname: 'hypnohub.net',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list',
			style: {
				color: '#ee8887',
				width: '180px',
				background: '#222',
				backgroundHover: '#444',
				colorHover: '#ee8887',
			},
			methodsMap: null,
			needXHR: true,
		};// yande.re like
	}
	//------------------------------------------------------------------------------------//
	//---------------------------------------- TBIB --------------------------------------//
	function getTbibSettings()
	{
		return {
			name: 'tbib',
			hostname: 'tbib.org',
			imageDir: '/images',
			imageHostname: 'tbib.org',
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list',
			style: {
				color: '#fff',
				width: '180px',
				background: '#0773fb',
				backgroundHover: '#fbb307',
				colorHover: '#fff',
			},
			methodsMap: {
				isPost: 'gelbooru',
				getPostId: 'gelbooru',
				getPostUrl: 'gelbooru',
			},
			needXHR: true,
		};// gelbooru like
	}
	//------------------------------------------------------------------------------------//
	//------------------------------------- KONACHAN -------------------------------------//
	function getKonachanSettings()
	{
		var hostname = window.location.hostname;
		return {
			name: 'konachan',
			hostname: hostname,
			imageDir: 'image',
			imageHostname: hostname,
			postDivInsertionPlace: '#image',
			listDivInsertionPlace: 'div.content',
			viewDivInsertionPlace: 'div#post-list',
			style: {
				color: '#ee8887',
				width: '180px',
				background: '#222',
				backgroundHover: '#444',
				colorHover: '#ee8887',
			},
			methodsMap: null,
			needXHR: true,
		};// yande.re like
	}
	//------------------------------------------------------------------------------------//
	//--------------------------------------- PAHEAL -------------------------------------//
	function getPahealSettings()
	{
		return {
			name: 'paheal.net',
			prefix: 'rule34.',
			hostname: 'rule34.paheal.net',
			imageDir: '_images',
			imageHostname: '.paheal.net',
			postDivInsertionPlace: '#main_image',
			listDivInsertionPlace: '#imagelist',
			viewDivInsertionPlace: '#imagelist',
			style: {
				color: '#fff',
				width: '180px',
				background: '#84AE83',
				backgroundHover: '#A4CEA3',
				colorHover: '#fff',
			},
			methodsMap: {
				isPost: 'paheal',
				getPostId: 'paheal',
				getPostUrl: 'paheal',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//------------------------------------- HOST PREFIX ----------------------------------//
	function getHostnamePrefix( hostnameSuffix, prefixList, prefix )
	{
		var hostname,
			errorMessage = "[getHostnamePrefix](hostnameSuffix='" + hostnameSuffix +
			"', prefixList=[" + prefixList.join(',') + "]" + (prefix ? ", prefix='" + prefix + "'" : "") + ") ",
			regExp;
		if( prefix )
		{
			if( prefixList.indexOf(prefix) == -1 )
			{
				console.error(errorMessage + "\nnot supported prefix");
				return '';
			}
		}else{
			hostname = window.location.hostname;
			if( hostname.indexOf(hostnameSuffix) == -1 )
			{
				console.error(errorMessage + "\ninvalid hostname: " + hostname );
				return '';
			}
			for( var i = 0, len = prefixList.length; i < len; ++i )
			{
				if( hostname.indexOf(prefixList[i]) == -1 )
					continue;
				prefix = prefixList[i];
				break;
			}
		}
		if( !prefix )
		{
			try{
				regExp = new RegExp('([^\\.]+\\.)(' + hostnameSuffix + ')' );
				prefix = hostname.match(regExp)[1];
			}catch(e){
				console.error(e);
				console.error(errorMessage + "\nno valid prefix for hostname: " + hostname );
			}
		}
		return prefix || '';
	}
	//------------------------------------------------------------------------------------//
	//----------------------------------- METHODS OBJECT ---------------------------------//
	function initMethodsObject()
	{
		var retVal = {
			get list(){return this.map.list;},
			map: {
				list: [
					'isPost',
					'getPostId',// get post id from href
					'getPostUrl',// get post url by postId
					// method of thumbnail data grabbing
					'getImageThumbs',
					'setImageDataThumb',
					// methods of image data getting from image post page
					'getPostImage',
					'setImageOriginalResolution',
					'setImageDataSize',
					'setImageDataSourceLowres',
					'setImageDataSourceHighres',
					'setImageDataTags',
					'setImageDataName',
					'setImageDataExtension',
					'setImageDataBytes',
					'setImageDataDoc',
					// create place for buttons insertion
					'getPostDivInsertionPlace',
					'getListDivInsertionPlace',
					'createDiv',
				],
			},
			data: {
				'booru': {
					val: null,
					init: function(){
						this.val = this.val || getBooruMethodsObject();
					},
				},
				'gelbooru': {
					val: null,
					init: function(){
						this.val = this.val || getGelbooruMethodsObject();
					},
				},
				'donmai': {
					val: null,
					init: function(){
						this.val = this.val || getDonmaiMethodsObject();
					},
				},
				'paheal': {
					val: null,
					init: function(){
						this.val = this.val || getPahealMethodsObject();
					},
				},
			},
			init: function(){
				for( var type in this.data )
					this.data[type].init();
			},
			method: function( type, name ){
				if( this.data[type] )
				{
					if( name )
						return this.data[type].val[name];
					return this.data[type].val;
				}
				return null;
			},
		};
		retVal.init();
		return retVal;
	}
	//----------------------------------- METHODS OBJECT ---------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------- BOORU METHODS OBJECT ------------------------------//
	function getBooruMethodsObject()
	{
		var retVal = {
			isPost: function(url){
				url = url || window.location.pathname || window.location.href;
				return /\/post\/show\/\d+/.test(url);
			},
			getPostId: function(url){
				url = url || window.location.href;
				if( this.isPost(url) )
					return getLocation(url, 'pathname').match(/\d+/)[0];
				return null;
			},
			getPostUrl: function(postId){
				return window.location.protocol + '//' + this.hostname + '/post/show/' + postId;
			},
			getPostDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector( this.postDivInsertionPlace );
				if( insertPlace )
				{
					var parent = insertPlace.parentNode;
					if( parent.tagName === 'A' )
						return parent.nextSibling || parent;
					return insertPlace.nextSibling || insertPlace;
				}
				return null;
			},
			getListDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector(this.listDivInsertionPlace);
				if( insertPlace )
					return insertPlace.firstChild || insertPlace;
				return null;
			},
			getPostImage: function(doc){
				doc = doc || document;
				return doc.querySelector('#image') || doc.querySelector('#main_image');//paheal
			},
			getImageThumbs: function( doc ){
				doc = doc || document;
				var thumbs = doc.querySelectorAll('img.preview');
				if( thumbs && thumbs.length === 0 )
					thumbs = doc.querySelectorAll('article > a > img');// donmai, uberbooru
				if( thumbs && thumbs.length === 0 )
					thumbs = doc.querySelectorAll('img[itemprop="thumbnailUrl"]');// donmai
				if( thumbs && thumbs.length === 0 )
					thumbs = doc.querySelectorAll('span.thumb > a > img');// *.booru.org
				if( thumbs && thumbs.length === 0 )
					thumbs = doc.querySelectorAll('a > img[id*="thumb_"]');// rule34.paheal.net
				return thumbs;
			},
			setImageDataThumb: function( imgD, thumb ){
				if( thumb && imgD )
				{
					if( thumb.dataset && thumb.dataset.original )
						imgD.thumbSource = thumb.dataset.original;
					else
						imgD.thumbSource = thumb.src;
					imgD.postUrl = thumb.parentNode.href;
					if( thumb.parentNode.id && /\d+/.test(thumb.parentNode.id) )
						imgD.postId = thumb.parentNode.id.match(/\d+/)[0];
					else
						imgD.postId = this.getPostId( imgD.postUrl );
					if( thumb.title )
						imgD.thumbTitle = thumb.title;
				}
			},
			setImageDataSourceLowres: function( imgD, doc ){
				var img = this.getPostImage(doc);
				if( img )
					imgD.lowresSource = img.src;
				else
					return 1;
				return 0;
			},
			setImageOriginalResolution: function( imgD, img ){
				if( !img )
					return false;
				var width, height;
				width = img.getAttribute('large_width');
				height = img.getAttribute('large_height');
				if( !width || !height )
				{
					width = img.getAttribute('data-original-width');
					height = img.getAttribute('data-original-height');
				}
				if( !width || !height )
				{
					// sankaku
					width = img.getAttribute('orig_width');
					height = img.getAttribute('orig_height');
				}
				if( !width || !height )
				{
					// e926.net, e621.net
					width = img.getAttribute('data-orig_width');
					height = img.getAttribute('data-orig_height');
				}
				if( (!width || !height) && this.name === 'paheal.net' )
				{
					// paheal.net
					width = img.getAttribute('data-width');
					height = img.getAttribute('data-height');
				}
				if( width && height )
				{
					imgD.width = width;
					imgD.height = height;
					return true;
				}
				return false;
			},
			setImageDataSize: function( imgD, doc ){
				doc = doc || document;
				var img = this.getPostImage(doc), res;
				if( this.setImageOriginalResolution )
					res = this.setImageOriginalResolution( imgD, img );
				if( res )
					return;
				var lis = doc.querySelectorAll('li'), i, li, len = lis.length;
				for( i = 0; i < len; ++i )
				{
					li = lis[i];
					if( li.innerHTML.indexOf('Size:') != -1 )
						break;
				}
				var match = li.innerHTML.match(/(\d+)x(\d+)/);
				if( i < len && match )
				{
					imgD.width = match[1];
					imgD.height = match[2];
				}else
					console.error("[setImageDataSize] can't find image size (width x height)");
			},
			setImageDataSourceHighres: function( imgD, doc ){
				doc = doc || document;
				var imgHost = this.imageHostname || this.hostname,
					i, l, href,
					link = doc.querySelectorAll('li > a[href*="' + imgHost + '/' + this.imageDir + '/"]');
				if( link.length === 0 )// same origin imageboards
					link = doc.querySelectorAll('li > a[href*="/' + this.imageDir + '/"]');
				if( link.length > 0 )
				{
					for( i = 0, href = null; i < link.length; ++i )
					{
						l = link[i];
						if( l.href.indexOf('sample') == -1 )
						{
							href = l.href;
							break;
						}
					}
					imgD.source = href ? href : last(link).href;
				}
				else if( imgD.lowresSource )
					imgD.source = imgD.lowresSource;
				else{
					console.error("[setImageDataSourceHighres] no image source found");
					return 1;
				}
				// jpeg image for yande.re like imageboards
				var jpeg = doc.querySelector('li > a[href*="' + imgHost + '/jpeg/"]');
				if( jpeg )
					imgD.jpegSource = jpeg.href;
				clog("imgD.source: " + imgD.source);
				this.setType = this.setType || function( _type, _source, _imgD )
				{
					_imgD[_type + '-source'] = _source;
					_imgD[_type + '-extension'] = getFileExt(_source);
				};
				this.setType( 'thumnail', imgD.thumbSource, imgD );
				if( /mp4|webm|ogv|ogg/.test(getFileExt(imgD.source)) )
				{
					this.setType( 'vid_file', imgD.source, imgD );
					imgD.viewType = 'vid_file';
				}else{
					this.setType( 'orig_img', imgD.source, imgD );
					imgD.viewType = 'orig_img';
					if( imgD.jpegSource )
					{
						this.setType( 'jpeg_img', imgD.jpegSource, imgD );
						imgD.viewType = 'jpeg_img';
					}
					if( !isSameLink(imgD.source, imgD.lowresSource) )
					{
						this.setType( 'samp_img', imgD.lowresSource, imgD );
						imgD.viewType = 'samp_img';
					}
				}
				return 0;
			},
			setImageDataTags: function( imgD, doc ){
				doc = doc || document;
				this.getTagName = this.getTagName || function( tagElm, fl)
				{
					if( tagElm.querySelectorAll('a').length === 0 )
						return '';
					if( fl )
						return tagElm.querySelectorAll('a')[0].innerText.trim().replace(/\s+/g, '_');// sankaku, safebooru.org
					return last(tagElm.querySelectorAll('a')).innerText.trim().replace(/\s+/g, '_');
				};
				this.tagsId = this.tagsId || {
					'general'  : '0',
					'artist'   : '1',
					'copyright': '3',
					'character': '4',
					'metadata' : '5',
					// 3dbooru tags
					'species'  : '-1',
					'model'    : '-1',
					'idol'     : '-1',
					'photo_set': '-1',
					'circle'   : '-1',
					'medium'   : '-1',
					'faults'   : '-1',
				};
				this.createTagObj = this.createTagObj || function( tagElm, tagsClass, fl )
				{
					try{
					var links = tagElm.querySelectorAll('a'),
						post_count = tagElm.querySelector('span.post-count') || tagElm.querySelector('span[style]'),
						searchLink = null,
						obj = {};
					if( tagsClass === 'tag-type' )
					{
						if( fl )
						{
							obj.href = links[0].href;
							obj.wiki = links[1].href;
							searchLink = links[0];
						}else{
							obj.href = last(links).href;
							obj.wiki = (links.length == 1 ? null : links[0].href);
						}
						obj.category = tagElm.className.match(/tag-type-([^\s]+)/)[1];
					}
					else if( tagsClass === 'category' )
					{
						obj.href = last(links).href;
						obj.wiki = (links.length == 1 ? null : links[0].href);
						obj.category = tagElm.className.match(/category-([^\s]+)/)[1];
					}
					else if( tagsClass === 'tag_name_cell' )
					{
						obj.href = links[1].href;
						obj.wiki = links[0].href;
						post_count = tagElm.querySelector('span.tag_count');
						obj.category = null;
					}else{
						obj.href = links[0].href;
						obj.wiki = null;
						post_count = links[0].nextSibling;
						obj.category = null;
					}
					if( obj.category )
						obj.class = tagsClass;
					searchLink = searchLink || last(links);
					obj.count = parseInt(post_count.textContent, 10);
					obj.name = searchLink.textContent;
					return obj;
					}catch(e){console.error(e);}
				};
				/*
				-------------->  n1 [ n2 ]
				n1 - number of links in tag element
				n2 - index 0..(n1-1) of search tag link (here, tag link)
				-------------->  2  1
				--> gelbooru:
				ul#tag-list
					div
						div[style] -- tag category name
						li.tag-type-
							a[href] -- wiki link
							a[href] -- tag link
							span[style]
				-------------->  1
				--> rule34.xxx:
				ul#tag-sidebar
					li -- tag category name
					li.tag-type- [class="tag"]
						a[href] -- tag
						span[style]
				-------------->  2  1
				--> yande.re,
				--> 3dbooru (behoimi):
				ul#tag-sidebar
					li.tag-type-
						a[href] -- wiki link
						a[href] -- tag link
						span.post-count
				-------------->  2  0
				--> sankaku:
				ul#tag-sidebar
					li.tag-type-
						a[href, title] -- tag link
						span.tag-extra-info
							a[href] -- wiki link
							span.post-count
				-------------->  1
				--> safebooru,
				--> mspabooru,
				--> tbib:
				ul#tag-sidebar
					li.tag-type-
						a[href] -- tag link
						span[style]
				-------------->  2  1
				--> e621.net,
				--> e926.net:
				ul#tag-sidebar
					li.category- -- tag category name
					li.tag-type-
						a[style, href] -- wiki
						a[href] -- tag
						span.post-count
				-------------->  1
				--> booru.org:
				div#tag_list
					ul
						li
							span[style]
								?
								a[href] -- tag
								number -- post count
				-------------->  2  1
				--> uberbooru,
				--> bronibooru:
				section#tag-list
					h2 -- tag category name
					ul
						li.category-
							a.wiki-link [href]
							a.search-tag [href]
							span.post-count
				-------------->  2  1
				--> donmai,
				--> atfbooru:
				section#tag-list
					h2.copyright-tag-list -- tag category name
					ul.copyright-tag-list
						li.category-
							a.wiki-link [href]
							a.search-tag [href]
							span.post-count
				-------------->  2  1
				--> lolibooru,
				--> hypnohub,
				--> konachan:
				ul#tag-sidebar
					li.tag-type- [class="tag-link", data-name="name of tag", data-type="tag category"]
						a[href] -- wiki
						a[href] -- tag
						span.post-count
				-------------->  2  1
				--> rule34.paheal.net:
				table.tag_list
					tbody tr
						td.tag_info_link_cell
							a.tag_info_link [href] -- wiki
						td.tag_name_cell
							a.tag_name [href] -- tag
						td.tag_count_cell
							span.tag_count
				*/
				var getTagName = this.getTagName,
					tagsId = this.tagsId,
					createTagObj = this.createTagObj,
					tagsClass = '';
				var nameList = ['sankaku'],
					tagsOrder = userOptions.val('tagsOrder'),
					iter = 0, _fl = null, i, k, tags, tagType;
				imgD.tags = imgD.tags || [];
				imgD.tags.length = 0;
				if( doc.querySelectorAll('li[class*="tag-type-"]').length > 0 )
					tagsClass = 'tag-type';
				else if( doc.querySelectorAll('li[class*="category-"]').length > 0 )
					tagsClass = 'category';
				for( i = 0, _fl = (nameList.indexOf(this.name) != -1); i < tagsOrder.length; ++i )
				{
					tagType = tagsOrder[i];
					if( tagsClass === 'tag-type' )
						tags = doc.querySelectorAll('li.tag-type-' + tagType);
					else if( tagsClass === 'category' )
						tags = doc.querySelectorAll('li.category-' + tagsId[tagType]);// donmai like
					for( k = 0; tags && k < tags.length; ++k, ++iter )
					{
						imgD.tags.push( getTagName(tags[k], _fl) );
					}
				}
				if( iter === 0 )
				{
					// not categorized tags
					tagsClass = '';
					tags = doc.querySelectorAll('div#tag_list li');// *.booru.org
					if( !tags || tags.length === 0 )
					{
						tags = doc.querySelectorAll('.tag_name_cell');// paheal.net
						tagsClass = 'tag_name_cell';
					}
					for( k = 0, _fl = (nameList.indexOf(this.name) != -1); tags && k < tags.length; ++k )
					{
						imgD.tags.push( getTagName(tags[k], _fl) );
					}
				}
				// tag object
				imgD.tagList = imgD.tagList || [];
				imgD.tagList.length = 0;
				if( tagsClass === 'tag-type' )
					tags = doc.querySelectorAll('li[class*="tag-type-"]');
				else if( tagsClass === 'category' )
					tags = doc.querySelectorAll('ul li[class*="category-"]');
				else if( tagsClass === 'tag_name_cell' )
					tags = doc.querySelectorAll('table.tag_list > tbody tr');// paheal.net
				else
					tags = doc.querySelectorAll('div#tag_list ul li span');// .booru.org
				for( i = 0; i < tags.length; ++i )
				{
					imgD.tagList.push( createTagObj(tags[i], tagsClass, _fl) );
				}
			},
			createDiv: function(id, doc){
				doc = doc || document;
				var div = doc.querySelector('#' + id);
				clog("[createDiv] div#" + id + ": ", div);
				if( div )
					return div;
				div = document.createElement('div');
				var insertPlace;
				if( this.isPost() )
					insertPlace = this.getPostDivInsertionPlace(doc);
				else
					insertPlace = this.getListDivInsertionPlace(doc);
				if( !insertPlace )
					return null;
				div.setAttribute('id', id);
				if( insertPlace.tagName !== 'IMG' )
					div = insertPlace.parentNode.insertBefore( div, insertPlace.nextSibling);// check_it_out
				else
					div = insertPlace.parentNode.appendChild(div);
				if( typeof this.keyboardDiv === 'function' )
					this.keyboardDiv( id, doc );
				return div;
			},
			setImageDataDoc: function( imgD, doc ){
				if( !imgD || imgD.state === 'ready' )
					return 1;
				doc = doc || document;
				// size
				this.setImageDataSize( imgD, doc );
				// lowres
				var errN = this.setImageDataSourceLowres( imgD, doc );
				// highres
				errN += this.setImageDataSourceHighres( imgD, doc );
				if( errN > 1 )
					return errN;
				if( !imgD.lowresSource )
					imgD.lowresSource = (imgD.jpegSource || imgD.source);
				// tags
				this.setImageDataTags( imgD, doc );
				// name
				this.setImageDataName( imgD );
				// extension
				this.setImageDataExtension( imgD );
				imgD.state = 'ready';
				return 0;
			},
			setImageDataName: function( imgD ){
				if( !imgD || !imgD.tags )
					return;
				var tagsLen = imgD.tags.length,
					uLen = userOptions.val('maxTagsInName'),
					tagsDelim = userOptions.val('tagsDelim'),
					imageId = imgD.postId,
					boardName = '', name = '',
					ignoredTags = userOptions.val('ignoredTags'),
					tagName;
				if( userOptions.val('addImgBrdName') )
				{
					boardName = (userOptions.val('prefixedName') ? this.prefixedName : this.shortName);
					imageId = boardName + tagsDelim + imgD.postId;
				}
				for( var i = 0; i < tagsLen && i < uLen; ++i )
				{
					tagName = imgD.tags[i];
					if( tagName.length > 0 && ignoredTags.indexOf(tagName) == -1 )
						name += tagName + tagsDelim;
				}
				if( userOptions.val('imgIdAtNameEnd') )
					imgD.name = name + imageId;
				else
					imgD.name = imageId + tagsDelim + name.slice(0, -tagsDelim.length);
			},
			setImageDataExtension: function( imgD ){
				imgD.extension = getFileExt( imgD.source );
				if( imgD.jpegSource )
					imgD.jpegExtension = getFileExt( imgD.jpegSource );
			},
		};
		return retVal;
	}
	//-------------------------------- BOORU METHODS OBJECT ------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------- GELBOORU METHODS OBJECT ----------------------------//
	function getGelbooruMethodsObject()
	{
		var retVal = {
			isPost: function( url ){
				url = url || window.location.href;
				if( this.getPostId(url) )
					return true;
				return false;
			},
			getPostId: function( postUrl ){
				postUrl = postUrl || window.location.href;
				var srch = getLocation( postUrl, 'search' ),
					keys = getSearchObject( srch );
				if( keys.s === 'view' && keys.page === 'post' )
					return keys.id;
				else
					return null;
			},
			getPostUrl: function( postId ){
				return window.location.protocol + this.hostname + '/index.php?page=post&s=view&id=' + postId;
			},
		};
		return retVal;
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------- DONMAI METHODS OBJECT -----------------------------//
	function getDonmaiMethodsObject()
	{
		var retVal = {
			isPost: function(url){
				url = url || window.location.href;
				return /\/posts\/\d+/.test(url);
			},
			getPostId: function(url){
				url = url || window.location.href;
				if( this.isPost(url) )
					return getLocation(url, 'pathname').match(/(\/posts\/)?(\d+)?/)[2];
				return null;
			},
			getPostUrl: function(postId){
				return window.location.protocol + '//' + this.hostname + '/posts/' + postId;
			},
		};
		return retVal;
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------- PAHEAL METHODS OBJECT -----------------------------//
	function getPahealMethodsObject()
	{
		var retVal = {
			isPost: function(url){
				url = url || window.location.href;
				return /\/post\/view\/\d+/.test(url);
			},
			getPostId: function(url){
				if( this.isPost(url) )
					return getLocation(url, 'pathname').match(/(\/post\/view\/)?(\d+)?/)[2];
				return null;
			},
			getPostUrl: function(postId){
				return window.location.protocol + '//' + this.hostname + '/post/view/' + postId;
			},
		};
		return retVal;
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- DATASET -------------------------------------//
	function initImageBoardDataset(d)
	{
		var retVal = {
			data: {
				source: 'data-image-board-source',
				index: 'data-image-board-index',
				extension: 'data-image-board-extension',
				bytes: 'data-image-board-bytes',
			},
			val: function(elm, propName, v){
				if( this.data[propName] && elm )
				{
					if( v !== undefined )
						elm.setAttribute(this.data[propName], v);
					else
						return elm.getAttribute(this.data[propName]);
				}
				return null;
			},
			init: function(doc){
				this.doc = doc || document;
			},
			getSelector: function(propName, v){
				var sel = this.data[propName];
				if( sel )
				{
					if( v !== undefined )
					{
						var pos = v.indexOf('=');// &=, *=, ^=
						if( pos > -1 && pos < 2 )
							sel += v;
						else
							sel += '="' + v + '"';
					}
					return '[' + sel + ']';
				}
				return null;
			},
			query: function(propName, v){
				var sel = this.getSelector(propName, v);
				if( sel )
					return this.doc.querySelector(sel);
				return null;
			},
			queryAll: function(propName, v){
				var sel = this.getSelector(propName, v);
				if( sel )
					return this.doc.querySelectorAll(sel);
				return null;
			},
		};
		retVal.init(d);
		return retVal;
	}
	//-------------------------------------- DATASET -------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- CLASSES -------------------------------------//
	function initImageBoardClasses(d)
	{
		var retVal = {
			get counted(){return this.data.counted;},
			get viewActive(){return this.data.viewActive;},
			get viewAttach(){return this.data.viewAttach;},
			get ready(){return this.data.ready;},
			get downloaded(){return this.data.downloaded;},
			get downloadAttach(){ return this.data.downloadAttach;},
			get downloadActive(){ return this.data.downloadActive;},
			data: {
				counted: 'image-board-counted',
				viewAttach: 'image-board-attach-view-event',
				viewActive: 'image-board-active-for-view',
				ready: 'image-board-has-original-source',
				downloaded: 'image-board-downloaded-original',
				downloadAttach: 'image-board-attach-download-event',
				downloadActive: 'image-board-active-for-download',
			},
			hasClass: function(elm, propName){
				if( this.data[propName] )
					return hasClass(elm, this.data[propName]);
				return false;
			},
			addClass: function(elm, propName){
				if( this.data[propName] )
					addClass(elm, this.data[propName]);
			},
			removeClass: function(elm, propName){
				if( this.data[propName] )
					removeClass(elm, this.data[propName]);
			},
			toggleClass: function(elm, newPropName, oldPropName){
				if( oldPropName && !this.data[oldPropName] )
					return;
				else if( !newPropName || !this.data[newPropName] )
					return;
				toggleClass( elm, this.data[newPropName], this.data[oldPropName] );
			},
			queryAll: function(propName){
				if( this.data[propName] )
					return this.doc.querySelectorAll('.' + this.data[propName]);
				return null;
			},
			init: function(doc){this.doc = doc || document;},
		};
		retVal.init(d);
		return retVal;
	}
	//-------------------------------------- CLASSES -------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------- DOWNLOADER -----------------------------------//
	function initImageBoardDownloader(d)
	{
		var iter = {
			total : 0,
			done: 0,
		};
		var retVal = {
			get total(){return iter.total;},
			set total(n){
				iter.total = n;
				this.downloadAllHtml(iter.total, iter.done);
			},
			get done(){return iter.done;},
			set done(n){
				iter.done = n;
				this.downloadAllHtml(iter.total, iter.done);
			},
			data: {
				downloaderId: 'image-board-downloader-' + RANDOM,
				downloadAllId: 'image-board-download-all-' + RANDOM,
				downloadSwitchId: 'image-board-download-switch-' + RANDOM,
				classBtn: 'image-board-downloader-button',
				classOn: 'image-board-downloader-on',
				classOff: 'image-board-downloader-off',
				classActive: 'image-board-downloader-active',
			},
			get downloaderId(){return this.data.downloaderId;},
			get downloadAllId(){return this.data.downloadAllId;},
			get downloadSwitchId(){return this.data.downloadSwitchId;},
			get classBtn(){return this.data.classBtn;},
			get classOn(){return this.data.classOn;},
			get classOff(){return this.data.classOff;},
			get classActive(){return this.data.classActive;},
			init: function(id, doc){
				doc = doc || document;
				clog("[initImageBoardDownloader] init, doc: ", doc);
				var div = doc.querySelector('div#' + id), html = '', btn;
				clog("div: ", div, id);
				if( !div )
				{
					console.error("[initImageBoardDownloader] can't find div#" + id);
					return;
				}
				var downloadSwitch = doc.querySelector('#' + this.downloadSwitchId);
				if( !downloadSwitch )
				{
					btn = document.createElement('button');
					btn.setAttribute('id', this.downloadSwitchId);
					btn.setAttribute('class', this.classOff + ' ' + this.classBtn );
					btn.setAttribute('title', 'Press \'Shift+D\' to switch download mode on/off');
					btn.appendChild(document.createTextNode('Donwload Mode'));
					downloadSwitch = div.appendChild( btn );
				}
				var downloadAll = doc.querySelector('#' + this.downloadAllId);
				if( !downloadAll )
				{
					btn = document.createElement('button');
					btn.setAttribute('id', this.downloadAllId );
					btn.setAttribute('class', this.classBtn );
					btn.appendChild(document.createTextNode('Donwload All (0)'));
					downloadAll = div.appendChild( btn );
				}
				return div;
			},
			downloadAllHtml: function( total, loaded, elm ){
				if( !elm ) elm = document.querySelector('#' + this.downloadAllId );
				elm.textContent = 'Download All (' + (loaded ? loaded + ' / ': '') + (total ? total : 0) + ')';
			},
			downloadOn: function(elm){
				if( !elm ) elm = document.querySelector('#' + this.downloadSwitchId);
				if( elm )
					toggleClass( elm, this.classOn, this.classOff );
				else
					console.error("[downloadOn] empty elm: ", elm );
			},
			downloadOff: function(elm){
				if( !elm ) elm = document.querySelector('#' + this.downloadSwitchId);
				if( elm )
					toggleClass( elm, this.classOff, this.classOn );
				else
					console.error("[downloadOff] empty elm: ", elm );
			},
			isActive: function(elm){
				if( !elm ) elm = document.querySelector('#' + this.downloadSwitchId);
				return hasClass(elm, this.classOn);
			},
		};
		return retVal;
	}
	//------------------------------------- DOWNLOADER -----------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ DOWNLOADER-2 ----------------------------------//
	function handleDownloadEvent(event)
	{
		if( !imageBoard.imgBrdCl.hasClass( this, 'downloadActive' ) )
			return;
		event.preventDefault();
		var thumb = event.target,
			index = imageBoard.imgBrdDt.val( thumb, 'index' ),
			imgD = imageBoard.images.list[index];
		downloadFile( imgD );
	}
	function downloadFile( imgD )
	{
		if( imgD.state !== 'ready' || imgD.downloadState === 'downloaded' || imgD.downloadState === 'inProgress' )
			return;
		imgD.downloadState = 'inProgress';
		var hostname = getLocation(imgD.source, 'hostname'), source, ext, stripe;
		if( userOptions.val('downloadJPEG') && imgD.jpegSource )
			source = imgD.jpegSource;
		else
			source = imgD.source;
		ext = getFileExt(source);
		stripe = document.querySelectorAll('#progress-stripe-' + imgD.index);
		addClass( stripe, 'download-in-progress' );
		if( userOptions.val('animateProgress') )
			addClass( stripe, 'progress-animated' );
		attr( stripe, 'style', 'width:0%;' );
		if( hostname === window.location.hostname )
		{
			imageBoardDownloader( imgD, source, ext );
			return;
		}
		GM.xmlHttpRequest({
			url: source,
			method: 'GET',
			context: {
				'index': imgD.index,
				'url': source,
				'ext': ext,
				'stripe': stripe,
			},
			responseType: 'blob',
			onload: blibBlobDownloader,
			onprogress: downloadProgress,
		});
	}
	function downloadProgress( xhr )
	{
		try{
		if( !xhr.lengthComputable )
			return;
		var stripe = xhr.context.stripe || document.querySelectorAll('#progress-stripe-' + xhr.context.index),
			width = Math.floor(xhr.loaded/xhr.total*100);
		attr(stripe, 'style', 'width:' + width + '%');
		}catch(e){console.error(e);}
	}
	function blibBlobDownloader( xhr )
	{
		var imgD = imageBoard.images.list[xhr.context.index];
		if( xhr.status !== 200 )
		{
			console.error("xhr.status: ", xhr.status, xhr.statusText);
			console.error("url: " + xhr.context.url);
			if( imgD && imgD.downloadState === 'inProgress' )
				imgD.downloadState = '';
			return;
		}
		var wndURL = window.webkitURL || window.URL,
			resource = wndURL.createObjectURL(xhr.response);
		imageBoardDownloader( imgD, resource, xhr.context.ext );
		wndURL.revokeObjectURL( resource );
	}
	function imageBoardDownloader( imgD, resource, extension )
	{
		var name = imgD.name + '.' + (extension || imgD.extension);
		fileDownloader( name, resource );
		var thumb = imageBoard.imgBrdDt.query('index', imgD.index + ''),
			stripe = document.querySelectorAll('#progress-stripe-' + imgD.index);
		imageBoard.imgBrdCl.addClass( thumb, 'downloaded' );
		if( imgD.downloadState !== 'downloaded' )
			imageBoard.imgBrdDw.done += 1;
		imgD.downloadState = 'downloaded';
		attr(stripe, 'style', 'width:100%');
		//setTimeout(function(){
		removeClass( stripe, 'download-in-progress' );
		removeClass( stripe, 'progress-animated' );
		addClass( stripe, 'progress-complete' );
		//}, 50 );
	}
	function fileDownloader( name, resource )
	{
		var a = document.createElement('a'),
			body = document.body || document.getElementsByTagName('body')[0];
		a.setAttribute('download', name);
		a.href = resource;
		body.appendChild(a);
		a.click();
		body.removeChild(a);
	}
	function handleDownloadAllEvent(event)
	{
		var list = imageBoard.images.list;
		for( var i = 0, len = list.length, imgD; i < len; ++i )
		{
			imgD = list[i];
			downloadFile( imgD );
		}
	}
	function handleDownloadSwitchEvent(event)
	{
		if( imageBoard.imgBrdDw.isActive() )
		{
			imageBoard.downloader.deactivate();
		}else{
			imageBoard.downloader.activate();
		}
	}
	//------------------------------------ DOWNLOADER-2 ----------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- KEYBOARD ------------------------------------//
	function activateKeyboard()
	{
		window.addEventListener('keydown', handleKeyboardEvent, false);
		clog("--------> keyboard activated");
	}
	function deactivateKeyboard()
	{
		window.removeEventListener('keydown', handleKeyboardEvent, false);
		clog("--------> keyboard deactivated");
	}
	function handleKeyboardEvent(event)
	{
		var charCode = event.keyCode || event.which,
			str = String.fromCharCode(charCode).toLowerCase();
		if( !event.shiftKey || event.ctrlKey || event.altKey )
			return;
		else if( str === 'a' )
		{
			handleDownloadAllEvent();
		}
		else if( str === 'd' )
		{
			handleDownloadSwitchEvent();
		}
		else if( str === 'i' )
		{
			if( imageBoard )
			{
				imageBoard.init();
				imageBoard.fix();
			}
		}
		else if( str === 'm' )
		{
			handleUserMenuEvent();
		}
		else if( str === 'v' )
		{
			handleViewerSwitchEvent();
		}
	}
	//-------------------------------------- KEYBOARD ------------------------------------//
	//------------------------------------------------------------------------------------//
	//--------------------------------------- VIEWER -------------------------------------//
	function initImageBoardViewer(d)
	{
		var iter = {
			curr: 0,
			total: 0,
		};
		var retVal = {
			get curr(){return iter.curr;},
			set curr(n){
				n = parseInt(n, 10);
				var elm = (this.doc || document).querySelector('#' + this.currentId);
				if( elm )
					elm.textContent = '' + (n + 1);
				iter.curr = n;
			},
			set total(n){iter.total = parseInt(n, 10);},
			get total(){return iter.total;},
			data: {
				buttonId: 'image-board-viewer-button-' + RANDOM,
				containerId: 'image-board-viewer-container-' + RANDOM,
				tagsId: 'image-board-viewer-tags-' + RANDOM,
				listId: 'image-board-viewer-list-' + RANDOM,
				bottomId: 'image-board-viewer-bottom-' + RANDOM,
				thumbsId: 'image-board-viewer-thumbs-' + RANDOM,
				// bottom div panel
				prevId: 'image-board-viewer-show-prev-' + RANDOM,
				nextId: 'image-board-viewer-show-next-' + RANDOM,
				downloadId: 'image-board-viewer-downlaod-' + RANDOM,
				sourceId: 'image-board-viewer-source-' + RANDOM,
				currentId: 'image-board-viewer-current-' + RANDOM,
				// classes
				classActive: 'image-board-viewer-active',
				classOn: 'image-board-viewer-on',
				classOff: 'image-board-viewer-off',
				classBtn: 'image-board-viewer-btn',
				classBottom: 'image-board-viewer-bottom-class',
			},
			get buttonId(){return this.data.buttonId;},
			get containerId(){return this.data.containerId;},
			get tagsId(){return this.data.tagsId;},
			get listId(){return this.data.listId;},
			get bottomId(){return this.data.bottomId;},
			get thumbsId(){return this.data.thumbsId;},
			
			get prevId(){return this.data.prevId;},
			get nextId(){return this.data.nextId;},
			get downloadId(){return this.data.downloadId;},
			get sourceId(){return this.data.sourceId;},
			get currentId(){return this.data.currentId;},
			
			get classActive(){return this.data.classActive;},
			get classOn(){return this.data.classOn;},
			get classOff(){return this.data.classOff;},
			get classBtn(){return this.data.classBtn;},
			get classBottom(){return this.data.classBottom;},
			init: function(id, doc, selector){
				if( !userOptions.val('createViewer') )
					return;
				doc = doc || document;
				this.doc = doc;
				var div = doc.querySelector('#' + id), viewDiv, html;
				if( !div )
				{
					console.error("[initImageBoardViewer] imageBoard div not found, id: " + id);
					return;
				}
				var btn = doc.querySelector('#' + this.buttonId);
				if( !btn )
				{
					btn = document.createElement('button');
					btn.setAttribute('id', this.buttonId);
					btn.setAttribute('class', this.classOff);
					btn.appendChild(document.createTextNode('Viewer'));
					btn = div.insertBefore( btn, div.firstChild );
				}
				var cont = doc.querySelector('#' + this.containerId);
				if( !cont )
				{
					cont = document.createElement('div');
					var obj = {
						'id': this.containerId,
						'class': this.classOff + ' image-board-viewer-container',
						'data-class-button': this.classBtn,
						'data-prev-id': this.prevId,
						'data-next-id': this.nextId,
						'data-download-id': this.downloadId,
						'data-current-id': this.currentId,
						'data-source-id': this.sourceId,
						'data-list-id': this.listId,
					};
					for( var key in obj )
						cont.setAttribute( key, obj[key] );
					html = '' +
					'<div id="' + this.tagsId + '" class="viewer-tag-list" tabindex="1000"></div>' +
					'<div id="' + this.thumbsId + '" class="viewer-thumb-list" tabindex="1001"></div>' +
					'<div id="' + this.listId + '" class="viewer-img-list" style="text-align:center;"></div>' +
					'<div id="' + this.bottomId + '" class="' + this.classBottom + ' viewer-bottom">' +
						'<button id="' + this.prevId + '" class="' + this.classBtn + ' viewer-navigation-bar">Prev</button>' +
						'<button id="' + this.sourceId + '" class="' + this.classBtn + ' viewer-navigation-bar">Source</button>' +
						'<button id="' + this.currentId + '" class="viewer-navigation-bar" style="width:40px;">' + '-' + '</button>' +
						'<button id="' + this.downloadId + '" class="' + this.classBtn + ' viewer-navigation-bar">Download</button>' +
						'<button id="' + this.nextId + '" class="' + this.classBtn + ' viewer-navigation-bar">Next</button>' +
					'</div>';
					cont.insertAdjacentHTML('beforeend', html);
					if( userOptions.val('fixedThumbs') )
						addClass( cont, 'viewer-thumb-list-fixed' );
					if( userOptions.val('fixedTags') )
						addClass( cont, 'viewer-tag-list-fixed' );
					doc.body.appendChild(cont);
				}
				if( !cont.classList.contains(this.classActive) )
				{
					cont.addEventListener('click', handleViewerNavigationEvent, false);
					cont.classList.add(this.classActive);
					activateViewerKeyboard();
					var activateSidebar = function(elm)
					{
						if( elm )
						{
							elm.addEventListener('mouseover', function(){this.focus();}, false);
							elm.addEventListener('mouseout', function(){this.blur();}, false);
						}
					};
					activateSidebar(doc.querySelector('#' + this.tagsId ));
					activateSidebar(doc.querySelector('#' + this.thumbsId ));
				}
			},
			showNext: function(){
				if( !this.isActive() )
					return;
				try{
					var idx = (this.curr + this.total + 1)%this.total;
					viewImage(idx);
				}catch(e){console.error(e);}
			},
			showPrev: function(){
				if( !this.isActive() )
					return;
				try{
					var idx = (this.curr + this.total - 1)%this.total;
					viewImage(idx);
				}catch(e){console.error(e);}
			},
			isActive: function(){
				if( !this.btn ) this.btn = document.querySelector('#' + this.buttonId);
				return hasClass( this.btn, this.classOn );
			},
			viewerOn: function(){
				if( !this.btn ) this.btn = document.querySelector('#' + this.buttonId);
				if( !this.cont ) this.cont = document.querySelector('#' + this.containerId);
				toggleClass(this.btn, this.classOn, this.classOff);
				toggleClass(this.cont, this.classOn, this.classOff);
				this.setOverflow = this.setOverflow || function(elm, val){if(elm) elm.style.overflow = val;};
				try{
					var html = this.cont.querySelector('#' + this.currentId).textContent;
					if( userOptions.val('viewFirst') && html === '-' )
						viewImage(0);
					else
						viewImage('-');
					this.setOverflow( document.body || document.getElementsByTagName('body')[0], 'hidden' );
					this.setOverflow( document.getElementsByTagName('html')[0], 'hidden' );
				}catch(e){console.error(e);}
				resumeVideo(this.curr);
			},
			viewerOff: function(){
				this.setOverflow = this.setOverflow || function(elm, val){if(elm) elm.style.overflow = val;};
				this.setOverflow( document.body || document.getElementsByTagName('body')[0], 'auto' );
				this.setOverflow( document.getElementsByTagName('html')[0], 'auto' );
				toggleClass(this.btn, this.classOff, this.classOn);
				toggleClass(this.cont, this.classOff, this.classOn);
				historyChange( null );
				pauseVideo(this.curr);
			},
		};
		return retVal;
	}
	//--------------------------------------- VIEWER -------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- VIEWER-2 ------------------------------------//
	function activateViewerKeyboard()
	{
		window.addEventListener('keydown', handleViewerKeyboardEvent, false);
	}
	function deactivateViewerKeyboard()
	{
		window.removeEventListener('keydown', handleViewerKeyboardEvent, false);
	}
	function handleViewerKeyboardEvent(event)
	{
		var charCode = event.keyCode || event.which,
			useCtrl = userOptions.val('holdCtrl') || window.location.hostname.indexOf('donmai.us') != -1,
			condition1 = event.shiftKey || !event.ctrlKey || event.altKey,
			condition2 = event.shiftKey ||  event.ctrlKey || event.altKey;
		if( (useCtrl && condition1) || (!useCtrl && condition2) )
			return;
		var viewer = imageBoard.imgBrdVw;
		if( charCode == 37 )
			viewer.showPrev();
		else if( charCode == 39 )
			viewer.showNext();
	}
	function handleViewerEvent(event)
	{
		if( !imageBoard.imgBrdCl.hasClass( this, 'viewActive' ) )
			return;
		event.preventDefault();
		var t = event.target;
		if( t.tagName !== 'IMG' )
			t = t.firstChild;
		if( t.tagName !== 'IMG' )
			return;
		var idx = imageBoard.imgBrdDt.val(t, 'index');
		clog("[handleViewerEvent] index: " + idx);
		if( idx !== null && idx !== undefined )
			viewImage( idx );
		else
			console.error("image index not found, img: ", t.src);
	}
	function viewImage( idx )
	{
		if( !imageBoard )
			return;
		var viewer = imageBoard.imgBrdVw,
			hostname = window.location.hostname,
			div, imgD, dwSource;
		if( !viewer || !viewer.isActive() )
			return;
		idx = parseInt(idx, 10);
		idx = (viewer.total + idx)%viewer.total;
		makeThumbListHTML();
		makeImageListHTML();
		imgD = imageBoard.images.list[idx];
		if( !imgD || imgD.state !== 'ready' )
			return;
		setImageList(true);// loop over all .viewer-img-list > div
		if( hostname.indexOf('sankakucomplex') != -1 )
			historyChange( imgD.postUrl );
		div = document.querySelector('div[data-image-index="' + imgD.index + '"]');
		loadImage(imgD.index, div);
		if( userOptions.val('downloadJPEG') && imgD.jpegSource )
			dwSource = imgD.jpegSource;
		else
			dwSource = imgD.source;
		removeClass( document.querySelectorAll('div.viewer-img-div'), 'img-show' );
		addClass( div, 'img-show' );
		makeTagListHTML(idx);
		setViewerBottom( viewer, dwSource, imgD.name );
		pauseVideo(viewer.curr);
		viewer.curr = idx;
	}
	//------------------------------------------------------------------------------------//
	//--------------------------------- VIEWER-THUMB-LIST --------------------------------//
	function makeThumbListHTML()
	{
		var thumbListDiv = document.querySelector('.viewer-thumb-list');
		if( !thumbListDiv )
		{
			console.error("[makeThumbListHTML] can't find div");
			return;
		}
		var imageList = imageBoard.images.list,
			thumbTitle = '', html = '',
			oldLen = thumbListDiv.getAttribute('data-viewer-thumb-length') || 0,
			animate = userOptions.val('animateProgress'),
			showProgress = userOptions.val('showProgress'),
			i, len, imgD, img;
		oldLen = parseInt(oldLen, 10);
		for( i = oldLen, len = imageList.length; i < len; ++i )
		{
			imgD = imageList[i];
			if( imgD.tags )
				thumbTitle = imgD.tags.join(' ');
			else if( imgD.thumbTitle )
				thumbTitle = imgD.thumbTitle;
			else
				thumbTitle = '';
			html += '<div class="viewer-thumb-div" data-viewer-thumb-div-index="' + imgD.index + '">';
			html += '<span class="viewer-thumb-span">';
			html += '<img data-viewer-thumb-index="' + imgD.index + '" ' +
			'class="viewer-thumb" src="' + (imgD.thumbSource || imgD.lowresSource) + '" ' +
			'title="' + thumbTitle + '"/>';
			html += '</span>';
			html += (showProgress ? makeProgressBarHTML( imgD, animate) : '');
			html += '</div>';
		}
		thumbListDiv.setAttribute('data-viewer-thumb-length', len);
		thumbListDiv.insertAdjacentHTML('beforeend', html);
		if( hasClass(thumbListDiv, 'viewer-thumb-list-activated') )
			return;
		thumbListDiv.addEventListener('click', handleViewerThumbEvent, false);
		addClass(thumbListDiv, 'viewer-thumb-list-activated');
	}
	function makeProgressBarHTML( imgD, animate )
	{
		return '<div class="progress-bar">' +
			'<div id="progress-stripe-' + imgD.index + '" class="progress-stripe progress-counted' +
			(imgD.state === 'ready' ? ' image-ready' : '') + '' +
			(imgD.state === 'busy' && !!animate ? ' animate-progess' : '') + '' +
			(imgD.downloadState === 'inProgress' ? ' download-in-progress' : '') + '' +
			(imgD.downloadState === 'downloaded' ? ' progress-complete': '') + '' +
			'"></div></div>';
	}
	function showViewerProgressBar( showProgress )
	{
		var animate = userOptions.val('animateProgress'),
			divList = document.querySelectorAll('.viewer-thumb-div'),
			imageList = imageBoard.images.list,
			animate = userOptions.val('animateProgress'),
			html, i, len, div, index, imgD, stripe;
		for( i = 0, len = divList.length; i < len; ++i )
		{
			div = divList[i];
			index = div.getAttribute('data-viewer-thumb-div-index');
			stripe = div.querySelector('#progress-stripe-' + index);
			if( !showProgress )
				hide(stripe);
			else if( !stripe )
			{
				imgD = imageList[index];
				html = makeProgressBarHTML(imgD, animate);
				div.insertAdjacentHTML('beforeend', html);
			}else
				show(stripe);
		}
	}
	function handleViewerThumbEvent(event)
	{
		var t = event.target;
		if( t.tagName !== 'IMG' )
			return;
		var idx = t.getAttribute('data-viewer-thumb-index');
		viewImage(idx);
	}
	//--------------------------------- VIEWER-THUMB-LIST --------------------------------//
	//------------------------------------------------------------------------------------//
	//--------------------------------- VIEWER-IMAGE-LIST --------------------------------//
	function makeImageListHTML()
	{
		var imgListDiv = document.querySelector('.viewer-img-list');
		if( !imgListDiv )
			return;
		var imageList = imageBoard.images.list,
			imgLen = imgListDiv.getAttribute('data-image-list-length') || 0,
			html = '', i, len;
		imgLen = parseInt(imgLen, 10);
		for( i = imgLen, len = imageList.length; i < len; ++i )
		{
			html += '<div data-image-index="' + i + '" class="viewer-img-div">';
			html += '<video class="vid_file" controls loop></video>';
			html += '<img class="orig_img"></img>';
			html += '<img class="jpeg_img"></img>';
			html += '<img class="samp_img"></img>';
			html += '</div>';
		}
		imgListDiv.setAttribute('data-image-list-length', len);
		imgListDiv.insertAdjacentHTML('beforeend', html);
	}
	function setImageList( reset )
	{
		var imgListDiv = document.querySelector('.viewer-img-list');
		if( !imgListDiv )
			return;
		var imageDivs = imgListDiv.querySelectorAll('.viewer-img-div'),
			imageList = imageBoard.images.list,
			imgLen = imgListDiv.getAttribute('data-image-list-length') || 0,
			viewOriginal = userOptions.val('viewOriginal'),
			viewJPEG = userOptions.val('viewJPEG'),
			i, imgD, imgDiv, viewType;
		imgLen = parseInt(imgLen, 10);
		for( i = 0; i < imgLen; ++i )
		{
			imgD = imageList[i];
			imgDiv = imageDivs[i];
			viewType = imgDiv.getAttribute('data-image-view-type');
			if( reset || !viewType || viewType === 'none_src' )
				imgDiv.setAttribute('data-image-view-type', getImageViewType(imgD, viewOriginal, viewJPEG) );
		}
	}
	function getImageViewType( imgD, viewOriginal, viewJPEG )
	{
		if( imgD['vid_file-source'] )
			imgD.viewType = 'vid_file';
		else if( imgD['jpeg_img-source'] && viewJPEG && viewOriginal )
			imgD.viewType = 'jpeg_img';
		else if( imgD['orig_img-source'] && (viewOriginal || !imgD['samp_img-source']) )
			imgD.viewType = 'orig_img';
		else if( imgD['samp_img-source'] )
			imgD.viewType = 'samp_img';
		else
			imgD.viewType = 'none_src';
		return imgD.viewType;
	}
	//--------------------------------- VIEWER-IMAGE-LIST --------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ LOAD-IMAGE ------------------------------------//
	function loadImage(idx, div)
	{
		idx = parseInt(idx, 10);
		div = div || document.querySelector('div[data-image-index="' + idx + '"]');
		var viewType = div.getAttribute('data-image-view-type');
		if( !viewType || viewType === 'none_src' )
			return;
		var img = div.querySelector('.' + viewType),
			//complete = (img.tagName === 'IMG' && img.complete),// || (img.tagName === 'VIDEO' && img.readyState > 2),
			imgD = imageBoard.images.list[idx],
			total = imageBoard.imgBrdVw.total,
			curr = imageBoard.imgBrdVw.curr,
			diff = (total + idx - curr)%total,
			next = (total + idx + diff)%total;
		if( !img.src )
			img.src = imgD[viewType + '-source'];
		if( (diff == 1 || diff == (total-1)) && total > 1 )
		{
			if( (img.tagName === 'IMG' && img.complete) || img.tagName === 'VIDEO' )
				loadImage( next, diff == 1 ? div.nextSibling : div.previousSibling );
			else{
				img.setAttribute('data-index-diff', (diff == 1 ? 1 : -1) );
				img.setAttribute('data-index-next', next );
				img.addEventListener('load', preloadImageEvent, false);
			}
		}
		if( img.tagName === 'VIDEO' && img.src )
			resumeVideo( idx, div, img );
	}
	function preloadImageEvent(event)
	{
		var t = this,
			p = t.parentNode,
			diff = t.getAttribute('data-index-diff'),
			next = t.getAttribute('data-index-next');
		if( diff == 1 )
			loadImage(next, p.nextSibling);
		else if( diff == -1 )
			loadImage(next, p.previousSibling);
		else
			loadImage(next);
		t.removeAttribute('data-index-diff');
		t.removeAttribute('data-index-next');
		setTimeout(function(){
			t.removeEventListener('load', preloadImageEvent, false);
		}, 100);
	}
	function getVideoElm( idx, div, img )
	{
		if( !img || img.tagName === 'VIDEO' )
		{
			div = div || document.querySelector('[data-image-index="' + idx + '"]');
			if( div.getAttribute('data-image-view-type') != 'vid_file' )
				return;
			img = div.querySelector('.vid_file');
		}
		return img;
	}
	function pauseVideo( idx, div, img )
	{
		img = getVideoElm( idx, div, img );
		if( !img )
			return;
		else if( img.paused )
			addClass( img, 'video-paused');
		else
			removeClass( img, 'video-paused');
		img.pause();
	}
	function resumeVideo( idx, div, img, forcePause )
	{
		img = getVideoElm( idx, div, img );
		if( !img )
			return;
		else if( forcePause || hasClass(img, 'video-paused') )
			img.pause();
		else
			img.play();
	}
	//------------------------------------------------------------------------------------//
	//---------------------------------- VIEWER-TAG-LIST ---------------------------------//
	function makeTagListHTML( idx )
	{
		var tagListDiv = document.querySelector('.viewer-tag-list');
		if( !tagListDiv )
		{
			console.error("[makeTagListHTML] can't find div");
			return;
		}
		var imgD = imageBoard.images.list[idx],
			tagList = imgD.tagList,
			tagClass = (tagList && tagList[0] || {'class': ''}).class,
			templ, html = '';
		if( tagClass === 'tag-type' )
			templ = '<li class="tag-type-';
		else if( tagClass === 'category' )
			templ = '<li class="category-';
		else
			templ = '<li class="empty-category';
		html = '<h4 style="color:#a0a0a0;">Tags</h4>';
		for( var i = 0, tagObj; i < tagList.length; ++i )
		{
			tagObj = tagList[i];
			html += templ + (tagObj.category !== null ? tagObj.category : '') + '">';
			html += makeTagHTML( tagObj );
			html += '</li>';
		}
		tagListDiv.innerHTML = html;
	}
	function makeTagHTML( tagObj )
	{
		var html = (tagObj.wiki ? '<a href="' + tagObj.wiki + '"> ? </a>' : '') + '' +
		'<a href="' + tagObj.href + '">' + tagObj.name + '</a>' +
		'<span class="post-count" style="color:#a0a0a0;"> ' + (!!tagObj.count ? tagObj.count : '') + ' </span>';
		return html;
	}
	//---------------------------------- VIEWER-TAG-LIST ---------------------------------//
	//------------------------------------------------------------------------------------//
	function historyChange( postURL )
	{
		this.href = this.href || window.location.href;
		if( postURL )
			window.history.replaceState(null, null, postURL);
		else
			window.history.replaceState(null, null, this.href);
	}
	function setViewerBottom( viewer, source, name )
	{
		var doc = document,
			prevElm = doc.querySelector('#' + viewer.prevId ),
			nextElm = doc.querySelector('#' + viewer.nextId ),
			sourceElm = doc.querySelector('#' + viewer.sourceId ),
			downloadElm = doc.querySelector('#' + viewer.downloadId ),
			useCtrl = userOptions.val('holdCtrl') || window.location.hostname.indexOf('donmai.us') != -1;
		prevElm.setAttribute('title', (useCtrl ? 'Ctrl+' : '') + 'Left');
		nextElm.setAttribute('title', (useCtrl ? 'Ctrl+' : '') + 'Right');
		sourceElm.setAttribute('title', source );
		downloadElm.setAttribute('title', name );
	}
	function handleViewerSwitchEvent(event)
	{
		if( imageBoard.viewer.isActive() )
		{
			imageBoard.viewer.deactivate();
		}else{
			imageBoard.viewer.activate();
		}
	}
	function handleViewerNavigationEvent(event)
	{
		var t = event.target,
			viewer, idx, total, imgD;
		if( !hasClass(t, this.getAttribute('data-class-button')) )
			return;
		viewer = imageBoard.imgBrdVw;
		if( !viewer )
			return;
		idx = viewer.curr;
		total = viewer.total;
		clog("[navigation] index: " + idx);
		clog("[navigation] total: " + total);
		idx = parseInt(idx, 10);
		total = parseInt(total, 10);
		if( t.id == this.getAttribute('data-prev-id') )
		{
			viewImage( (idx + total - 1)%total );
		}
		else if( t.id == this.getAttribute('data-next-id') )
		{
			viewImage( (idx + total + 1)%total );
		}
		else if( t.id == this.getAttribute('data-download-id') )
		{
			imgD = imageBoard.images.list[idx];
			downloadFile( imgD );
		}
		else if( t.id == this.getAttribute('data-source-id') )
		{
			imgD = imageBoard.images.list[idx];
			window.open( imgD.source );
		}
	}
	//-------------------------------------- VIEWER-2 ------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------- USER MENU ------------------------------------//
	function initUserMenu()
	{
		var retVal = {
			data: {
				'container-id': 'image-board-user-menu-container-' + RANDOM,
				'container-class': 'image-board-user-menu-container',
				'title-class': 'image-board-user-menu-title',
				'content-class': 'image-board-user-menu-content',
				'bottom-class': 'image-board-user-menu-bottom',
				'open-class': 'image-board-user-menu-open',
				'close-class': 'image-board-user-menu-close',
			},
			get containerId(){return this.data['container-id'];},
			get containerClass(){return this.data['container-class'];},
			get titleClass(){return this.data['title-class'];},
			get contentClass(){return this.data['content-class'];},
			get bottomClass(){return this.data['bottom-class'];},
			get openClass(){return this.data['open-class'];},
			get closeClass(){return this.data['close-class'];},
			init: function(id, doc){
				doc = doc || document;
				var div = doc.querySelector('div#' + id), btn;
				clog("div: ", div, id);
				if( !div )
				{
					console.error("[initUserMenu] can't find div#" + id);
					return;
				}
				var userMenuId = 'image-board-user-menu-id-' + RANDOM,
					userMenuActive = 'image-board-user-menu-button-active',
					userMenuBtn = div.querySelector('#' + userMenuId );
				if( !userMenuBtn )
				{
					btn = document.createElement('button');
					btn.setAttribute('id', userMenuId );
					for( var key in this.data )
						btn.setAttribute('data-' + key, this.data[key] );
					btn.setAttribute('title', 'Press \'Shift+M\' to open/close User Menu');
					btn.appendChild(document.createTextNode('User Menu'));
					userMenuBtn = div.insertBefore( btn, div.firstChild );
				}
				return div;
			},
		};
		return retVal;
	}
	function handleUserMenuEvent(event)
	{
		var dataSet = (this.dataset && this.dataset.containerId ? this.dataset : imageBoard.userMenu ),
			div = document.querySelector('#' + dataSet.containerId ),
			body = document.body, contentHtml, bottomHtml,
			html = '';
		if( !div )
		{
			contentHtml = makeUserMenuContentHtml();
			bottomHtml = makeUserMenuBottomHtml();
			div = document.createElement('div');
			div.setAttribute('id', dataSet.containerId);
			div.setAttribute('class', dataSet.containerClass);
			html = '' +
			'<div class="' + dataSet.titleClass + '">' +
				'<span>' + GM.info.script.name + ' v' + GM.info.script.version + '</span>' +
				'<span class="image-board-user-menu-x"></span>' +
			'</div>' +
			'<div class="' + dataSet.contentClass + '">' + contentHtml + '</div>' +
			'<div class="' + dataSet.bottomClass + '">' + bottomHtml + '</div>';
			div.insertAdjacentHTML('beforeend', html);
			div = body.appendChild(div);
			addClass( div, dataSet.closeClass );
			activateUserMenu();
		}
		if( hasClass(div, dataSet.openClass) )
		{
			closeUserMenu.call(this);
			imageBoard.imgBrdSt.userMenu = false;
		}
		else if( hasClass(div, dataSet.closeClass) )
		{
			openUserMenu.call(this);
			imageBoard.imgBrdSt.userMenu = true;
		}
	}
	function makeUserMenuContentHtml()
	{
		var typeList = ['checkbox', 'number', 'text'],
			longOptions = ['tagsOrder', 'ignoredTags'],
			html = '', key, dt, inp, labl, inputWidth, i, k, catName, keyList;
		html += '<div class="image-board-user-menu-tabs-navigation">';
		html += '<ul class="image-board-user-menu-tabs-list">';
		for( i = 0; i < userOptions.categoryList.length; ++i )
		{
			catName = userOptions.categoryList[i];
			html += '<li id="image-board-user-menu-tab-nav-' + catName.replace(/\s+/g, '-') + '" ' +
				'class="' + (catName === 'General' ? 'tab-nav-active' : '') + '"><span>' + catName + '</span></li>';
		}
		html += '</ul></div>';
		html += '<div class="image-board-user-menu-tabs-content">';
		for( i = 0; i < userOptions.categoryList.length; ++i )
		{
			catName = userOptions.categoryList[i];
			keyList = userOptions.category(catName);
			html += '<div id="image-board-user-menu-tab-' + catName.replace(/\s+/g, '-') + '" ' +
				'tab-selected="' + (catName === 'General' ? 'true' : 'false') + '">';
			for( k = 0; k < keyList.length; ++k )
			{
				key = keyList[k];
				dt = userOptions.data[key];
				if( typeList.indexOf(dt.type) == -1 )
					continue;
				inputWidth = (longOptions.indexOf(key) != -1 ? '200px' : '70px');
				inp = '<input id="image-board-user-menu-' + key + '-val" type="' + dt.type + '" ' +
					'style="' + (dt.type!=='checkbox' ? 'text-align: center; width: ' + inputWidth: '') + '"/>';
				labl = '<label id="image-board-user-menu-' + key + '-caption" ' + (key == 'holdCtrl' ? 'title="Hodor" ': '') + '' +
					'for="image-board-user-menu-' + key + '-val" style="cursor: pointer;">' + dt.getDesc() + '</label>';
				html += '<section class="image-board-user-menu-section">' +
					(dt.type === 'checkbox' ? inp + labl : labl + inp ) + '</section>';
			}
			html += '</div>';
		}
		html += '</div>';
		return html;
	}
	function getUserOptionsListOf( prop )
	{
		var propList = [], key, dt;
		for( key in userOptions.data )
		{
			dt = userOptions.data[key];
			if( propList.indexOf(dt[prop]) == -1 )
				propList.push(dt[prop]);
		}
		return propList;
	}
	function makeUserMenuBottomHtml()
	{
		this.btnList = this.btnList || {
			'reset': {
				html: 'Reset',
				title: 'Reset all options to default ones',
			},
			'remove': {
				html: 'Remove',
				title: 'Remove all saved options',
			},
			'save': {
				html: 'Save Settings',
				title: '',
			},
		};
		var key, val, html = '';
		for( key in this.btnList )
		{
			val = this.btnList[key];
			html += '<button id="image-board-user-menu-' + key + '-button" class="user-menu-button" ' +
			'title="' + val.title + '">' + val.html + '</button>';
		}
		return html;
	}
	function activateUserMenu()
	{
		var doc = document,
			active = 'image-board-user-menu-button-active',
			btn, key;
		var userMenuMethodsObj = {
			'save': saveUserMenu,
			'remove': removeUserMenu,
			'reset': resetUserMenu,
			'x': closeUserMenu,
			'tabs-navigation': tabsUserMenu,
		};
		for( key in userMenuMethodsObj )
		{
			btn = doc.querySelector('#image-board-user-menu-' + key + '-button');
			if( !btn )
				btn = doc.querySelector('.image-board-user-menu-' + key );
			if( btn && !btn.classList.contains(active) )
			{
				btn.addEventListener('click', userMenuMethodsObj[key], false );
				btn.classList.add(active);
			}
		}
	}
	function setUserMenu()
	{
		var doc = document,
			key, dt, valueElm, captionElm;
		for( key in userOptions.data )
		{
			dt = userOptions.data[key];
			valueElm = doc.querySelector('#image-board-user-menu-' + key + '-val');
			if( !valueElm )
				continue;
			else if( dt.type === 'checkbox' )
				valueElm.checked = dt.val;
			else if( dt.type === 'number' || dt.type === 'text' )
				valueElm.value = _toString_( dt.val, ', ' );
			captionElm = doc.querySelector('#image-board-user-menu-' + key + '-caption');
			if( captionElm )
				captionElm.textContent = dt.getDesc();
		}
	}
	function saveUserMenu()
	{
		var doc = document,
			key, dt, valueElm;
		for( key in userOptions.data )
		{
			dt = userOptions.data[key];
			valueElm = doc.querySelector('#image-board-user-menu-' + key + '-val');
			if( !valueElm )
				continue;
			else if( dt.type === 'checkbox' )
				userOptions.val(key, valueElm.checked );
			else if( dt.type === 'number' || dt.type === 'text' )
				userOptions.val( key, valueElm.value );
		}
		userOptions.saveData();
		closeUserMenu();
		renameImages();
		resetViewerSettings();
		showViewerProgressBar( userOptions.val('showProgress') );
	}
	function renameImages()
	{
		if( !imageBoard )
			return;
		try{
			var list = imageBoard.images.list,
				site = imageBoard.siteList.val();
			for( var i = 0, len = list.length; i < len; ++i )
				site.setImageDataName( list[i] );
		}catch(error){
			console.error(error);
		}
	}
	function resetViewerSettings()
	{
		if( !imageBoard )
			return;
		var viewer = imageBoard.imgBrdVw,
			container = document.querySelector('#' + viewer.containerId);
		if( userOptions.val('fixedThumbs') )
			addClass( container, 'viewer-thumb-list-fixed' );
		else
			removeClass( container, 'viewer-thumb-list-fixed' );
		if( userOptions.val('fixedTags') )
			addClass( container, 'viewer-tag-list-fixed' );
		else
			removeClass( container, 'viewer-tag-list-fixed' );
	}
	function removeUserMenu()
	{
		userOptions.removeData();
	}
	function resetUserMenu()
	{
		userOptions.setDefs();
		userOptions.saveData();
		setUserMenu();
		renameImages();
	}
	function closeUserMenu()
	{
		var dataSet = imageBoard.userMenu.data,
			userMenu = document.querySelector('#' + dataSet['container-id']);
		toggleClass( userMenu, dataSet['close-class'], dataSet['open-class'] );
		imageBoard.imgBrdSt.userMenu = false;
	}
	function openUserMenu()
	{
		var dataSet = imageBoard.userMenu.data,
			userMenu = document.querySelector('#' + dataSet['container-id']);
		toggleClass( userMenu, dataSet['open-class'], dataSet['close-class'] );
		setUserMenu();
		imageBoard.imgBrdSt.userMenu = true;
	}
	function tabsUserMenu(event)
	{
		var t = event.target, categoryName, tabsNav, tabs, activeTab, i;
		if( t.tagName === 'SPAN' )
			t = t.parentNode;
		if( t.tagName !== 'LI' )
			return;
		tabsNav = document.querySelectorAll('.image-board-user-menu-tabs-navigation li');
		tabs = document.querySelectorAll('div[tab-selected]');
		for( i = 0; i < tabs.length; ++i )
		{
			tabs[i].setAttribute('tab-selected', 'false');
			removeClass( tabsNav[i], 'tab-nav-active' );
		}
		categoryName = t.id.replace('image-board-user-menu-tab-nav-', '');
		activeTab = document.querySelector('#image-board-user-menu-tab-' + categoryName);
		if( !activeTab )
			return;
		activeTab.setAttribute('tab-selected', 'true');
		addClass(t, 'tab-nav-active');
	}
	//------------------------------------- USER MENU ------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ USER OPTIONS ----------------------------------//
	async function initOptions()
	{
		function _setDef(){this.val = this.def;}
		var tagsType = ['character', 'copyright', 'artist', 'species', 'model', 'idol', 'photo_set', 'circle', 'medium', 'metadata', 'general', 'faults'];
		var retVal = {
			data: {
				'autoRun': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'General',
					setDef: _setDef,
					getDesc: function(){return 'Initialize the Script on start';},
				},
				'createViewer': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'General',
					setDef: _setDef,
					getDesc: function(){return 'Add Image Viewer to ImageBoard';},
				},
				'downloadJPEG': {
					val: null,
					def: false,
					type: 'checkbox',
					category: 'General',
					setDef: _setDef,
					getDesc: function(){return 'Donwload jpeg instead of png (yande.re option)';},
				},
				'animateProgress': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'General',
					setDef: _setDef,
					getDesc: function(){return 'Animate initialization/downloading progress'; },
				},
				'maxTagsInName': {
					val: null,
					def: 10,
					type: 'number',
					category: 'Filename',
					setDef: _setDef,
					getDesc: function(){return 'Maximum tags in file name:';},
					validator: function( v ){
						return v > 3 && v < 100;
					},
				},
				'tagsOrder': {
					_val: null,
					set val(s){
						if( !this._val )
							this._val = [];
						if( typeof s === 'string' )
							s = s.split(/\s?\,\s?/i);
						copy( this._val, s );
					},
					get val(){return this._val;},
					def: tagsType.join(','),
					type: 'text',
					category: 'Filename',
					setDef: _setDef,
					getDesc: function(){return 'Tags order in file name:';},
					validator: function(s){
						if( typeof s === 'string' )
							s = s.trim().split(/\s?\,\s?/i);
						for( var i = 0, len = s.length; i < len; ++i )
						{
							if( tagsType.indexOf(s[i]) == -1 )
								return false;
						}
						return true;
					},
				},
				'ignoredTags': {
					_val: null,
					set val(s){
						if( !this._val )
							this._val = [];
						if( !s )
							s = [];
						else if( typeof s === 'string' )
							s = s.trim().split(',');
						s.forEach(function(item){item = item.trim();});
						this._val.length = 0;
						for( var i = 0; i < s.length; ++i )
							this._val[i] = s[i].trim();
					},
					get val(){return this._val;},
					def: '',
					type: 'text',
					category: 'Filename',
					setDef: _setDef,
					getDesc: function(){return 'Ignored tag names:';},
				},
				'tagsDelim': {
					val: null,
					def: '-',
					type: 'text',
					category: 'Filename',
					setDef: _setDef,
					getDesc: function(){return 'Tags delimeter:';},
					validator: function(v){
						v = v.toString();
						return v.length > 0 && v.length < 5;
					},
				},
				'addImgBrdName': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'Filename',
					setDef: _setDef,
					getDesc: function(){return 'Add ImageBoard name to file name';},
				},
				'prefixedName': {
					val: null,
					def: false,
					type: 'checkbox',
					category: 'Filename',
					setDef: _setDef,
					getDesc: function(){return 'Prefixed ImageBoard name';},
				},
				'imgIdAtNameEnd': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'Filename',
					setDef: _setDef,
					getDesc: function(){return 'Image ID, and ImageBoard name at file name end';},
				},
				'viewOriginal': {
					val: null,
					def: false,
					type: 'checkbox',
					category: 'Viewer',
					setDef: _setDef,
					getDesc: function(){return 'View original images';},
				},
				'viewJPEG': {
					val: null,
					def: false,
					type: 'checkbox',
					category: 'Viewer',
					setDef: _setDef,
					getDesc: function(){return 'View jpeg image (yande.re option)';},
				},
				'viewFirst': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'Viewer',
					setDef: _setDef,
					getDesc: function(){return 'Load 1st image on viewer activation';},
				},
				'holdCtrl': {
					val: null,
					def: false,
					type: 'checkbox',
					category: 'Viewer',
					setDef: _setDef,
					getDesc: function(){return 'Hold Ctrl key to left/right navigate when viewing';},
				},
				'fixedTags': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'Viewer',
					setDef: _setDef,
					getDesc: function(){return 'Fix tag list';},
				},
				'fixedThumbs': {
					val: null,
					def: false,
					type: 'checkbox',
					category: 'Viewer',
					setDef: _setDef,
					getDesc: function(){return 'Fix thumb list';},
				},
				'showProgress': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'Viewer',
					setDef: _setDef,
					getDesc: function(){return 'Show progress/status bar';},
				},
			},
			get storageKey(){ return 'user-options-storage-key';},
			get categoryList()
			{
				var catList = [], key, opt;
				for( key in this.data )
				{
					opt = this.data[key];
					if( catList.indexOf(opt.category) == -1 )
						catList.push(opt.category);
				}
				Object.defineProperty(this, 'categoryList', {
					get: function(){return catList;},
					enumerable: true,
					configurable: true,
				});
				return catList;
			},
			category: function( categoryName )
			{
				if( this.keyList === undefined )
				{
					this.keyList = {};
					var opt, key, list, i, catName;
					for( i = 0; i < this.categoryList.length; ++i )
					{
						catName = this.categoryList[i];
						list = this.keyList[catName] = [];
						for( key in this.data )
						{
							opt = this.data[key];
							if( opt.category === catName )
								list.push(key);
						}
					}
					Object.defineProperty(this, 'category', {
						value: function(name)
						{
							if( this.categoryList.indexOf(name) == -1 )
								return null;
							return this.keyList[name];
						},
						enumerable: true,
						configurable: true,
					});
				}
				return this.keyList[categoryName];
			},
			val: function( opt, v )
			{
				if( this.data[opt] )
				{
					if( v !== undefined )
					{
						if( typeof this.data[opt].validator !== 'function' && v !== null )
							this.data[opt].val = v;
						else if( this.data[opt].validator(v) )
							this.data[opt].val = v;
					}
					return this.data[opt].val;
				}else
					return null;
			},
			fixStorage: async function(){
				// backward compatibility with v0.2.0 and older
				var oldKey = 'user-options-storage-key-1681238',
					objStr = await GM.getValue( oldKey, '' );
				if( objStr )
				{
					GM.deleteValue(oldKey);
					GM.setValue( this.storageKey, objStr );
				}
				// backward compatibility with v0.7.0 and older
				var obj = null;
				objStr = await GM.getValue(this.storageKey, '');
				if( objStr )
					obj = JSON.parse(objStr);
				if( obj && obj.viewSample !== undefined )
				{
					obj.viewOriginal = !obj.viewSample;
					delete obj.viewSample;
					GM.deleteValue(this.storageKey);
					GM.setValue(this.storageKey, JSON.stringify(obj));
				}
			},
			saveData: function(){
				var storageObj = {};
				for( var key in this.data )
					storageObj[key] = this.data[key].val;
				GM.setValue( this.storageKey, JSON.stringify(storageObj) );
			},
			removeData: function(){
				GM.deleteValue(this.storageKey);
			},
			loadData: async function(){
				var storageObj = await GM.getValue(this.storageKey, null), v;
				if( storageObj )
					storageObj = JSON.parse(storageObj);
				else
					storageObj = {};
				for( var key in this.data )
				{
					v = storageObj[key];
					if( v !== undefined )
						this.val( key, v );
					else
						this.data[key].setDef();
				}
				this.saveData();
			},
			setDefs: function(){
				for( var key in this.data )
					this.data[key].setDef();
				this.saveData();
			},
			init: async function(){
				await this.fixStorage();
				await this.loadData();
			},
		};
		await retVal.init();
		return retVal;
	}
	//------------------------------------ USER OPTIONS ----------------------------------//
	//------------------------------------------------------------------------------------//
	function newCssClasses()
	{
		generalCssClass();
		userMenuCssClass();
		imageViewerCssClass();
		progressBarCssClass();
	}
	function generalCssClass()
	{
		var id = 'general-css-' + RANDOM;
		addCssClass(`
			#image-board-div-${RANDOM} {
				text-align: right;
				position: relative;
			}
			#image-board-user-menu-container-${RANDOM} button,
			#image-board-div-${RANDOM} button {
				margin: 3px 10px;
				color: ${imageBoard.siteList.style().color};
				font-weight: bold;
				width: 180px;
				border: 0px;
				padding: 5px;
				background: ${imageBoard.siteList.style().background};
				cursor: pointer;
			}
			.image-board-viewer-bottom-class button:hover ,
			#image-board-user-menu-container-${RANDOM} button:hover ,
			#image-board-div-${RANDOM} button:hover {
				background: ${imageBoard.siteList.style().backgroundHover};
				color: ${imageBoard.siteList.style().colorHover};
			}
			div.image-board-viewer-on ,
			.image-board-user-menu-open {
				display: initial;
			}
			div.image-board-viewer-off ,
			.image-board-user-menu-close {
				display: none;
			}
			.image-board-downloader-off::after {
				content: " [off]";
			}
			.image-board-downloader-on::after {
				content: " [on]";
			}
			button.image-board-viewer-off::after {
				content: " [+]";
			}
			button.image-board-viewer-on::after {
				content: " [\u2013]";
			}
			.image-board-active-for-view,
			.image-board-active-for-download {
				cursor: default;
			}
		`, id);
	}
	function userMenuCssClass()
	{
		var id = 'user-menu-css-' + RANDOM;
		addCssClass(`
			.image-board-user-menu-title,
			#image-board-user-menu-container-${RANDOM} {
				border-top-left-radius: 5px;
				border-top-right-radius: 5px;
			}
			.image-board-user-menu-bottom,
			#image-board-user-menu-container-${RANDOM} {
				border-bottom-left-radius: 5px;
				border-bottom-right-radius: 5px;
			}
			#image-board-user-menu-container-${RANDOM} {
				position: fixed;
				bottom: 10px;
				right: 10px;
				z-index: 100200;
				background-color: #e7e7e7;
				width: 40%;
				height: 40%;
			}
			div.image-board-user-menu-title {
				font-weight: bold;
				line-height: 30px;
				color: ${imageBoard.siteList.style().color};
				background-color: ${imageBoard.siteList.style().background};
				position: absolute;
				width: 100%;
				height: 30px;
			}
			div.image-board-user-menu-title > span {
				padding-left: 8px;
			}
			.image-board-user-menu-x::after,
			.image-board-user-menu-x::before {
				content: "";
				position: absolute;
				width: 2px;
				height: 1.5em;
				background: ${imageBoard.siteList.style().color} !important;
				display: block;
				transform: rotate(45deg);
				left: 50%;
				margin: -1px 0 0 -1px;
				top: 0;
			}
			.image-board-user-menu-x::before {
				transform: rotate(-45deg);
			}
			.image-board-user-menu-x:hover::after,
			.image-board-user-menu-x:hover::before {
				background: ${imageBoard.siteList.style().colorHover} !important;
			}
			.image-board-user-menu-x:hover {
				background: ${imageBoard.siteList.style().backgroundHover};
			}
			.image-board-user-menu-x {
				position: absolute;
				width: 1.3em;
				height: 1.3em;
				cursor: pointer;
				top: 8px;
				right: 1px;
			}
			div.image-board-user-menu-content {
				background-color: #eeeeee;
				overflow-x: auto;
				overflow-y: hidden;
				position: absolute;
				top: 30px;
				right: 0px;
				bottom: 30px;
				left: 0px;
			}
			div.image-board-user-menu-content label {
				font-family: verdana, sans-serif;
				font-weight: initial;
				font-size: 12px;
				color: #7d7d7d !important;
				line-height: 30px;
				display: initial !important;
				white-space: initial !important;
			}
			.image-board-user-menu-content label {
				padding: 0 3px;
			}
			.image-board-user-menu-tabs-navigation {
				position: absolute;
				left: 0;
				width: 100px;
				height: 100%;
				overflow-y: auto;
				background-color: #e0e0e0;
			}
			.image-board-user-menu-tabs-navigation ul {
				padding: 0;
			}
			.image-board-user-menu-tabs-navigation li {
				list-style-type: none;
				color: #7d7d7d !important;
				height: 30px;
				line-height: 30px;
				padding-left: 10px;
				margin: 0;
				cursor: pointer;
			}
			.image-board-user-menu-tabs-navigation li:hover ,
			.image-board-user-menu-tabs-navigation li.tab-nav-active {
				background-color: #d0d0d0;
			}
			.image-board-user-menu-tabs-navigation li.tab-nav-active {
				font-weight: bold;
			}
			div.image-board-user-menu-tabs-content {
				position: absolute;
				left: 100px;
				right: 0;
				padding-left: 10px;
				overflow-y: auto;
				min-width: 240px;
				height: 100%;
			}
			div.image-board-user-menu-tabs-content div {
				display: none;
			}
			div.image-board-user-menu-tabs-content div[tab-selected="true"] {
				display: initial;
			}
			.image-board-user-menu-bottom {
				/*text-align: right;*/
				background-color: ${imageBoard.siteList.style().background};
				position: absolute;
				bottom: 0px;
				width: 100%;
				height: 30px;
			}
			#image-board-user-menu-reset-button {
				left: 10px;
			}
			#image-board-user-menu-save-button {
				position: absolute;
				right: 10px;
			}
			#image-board-user-menu-container-${RANDOM} button {
				width: initial;
				margin: 1px 2px;
				padding: 4px 6px;
		`, id);
	}
	function imageViewerCssClass()
	{
		var id = 'image-viewer-css-' + RANDOM,
			col = '#000';
		addCssClass(`
			.image-board-viewer-container {
				position: fixed;
				top: 0;
				right: 0;
				bottom: 0;
				left: 0;
				z-index: 100100;
				background-color: ${col};
			}
			button.image-board-viewer-btn {
				cursor: pointer;
			}
			.viewer-tag-list li {
				list-style-type: none;
				line-height: 1.8em;
				display: block;
				padding-left: 4px;
			}
			.viewer-thumb-div,
			.viewer-tag-list * {
				background-color: #303030;
			}
			.viewer-tag-list li.category-0 a,
			.viewer-tag-list li.tag-type-general a,
			.viewer-tag-list li.empty-category a {
				color: #337ab7;
			}
			div.viewer-tag-list-fixed > div.viewer-tag-list {
				opacity: 1;
				left: 0;
			}
			div.viewer-tag-list-fixed > div.viewer-img-list {
				left: 200px;
			}
			.viewer-tag-list:hover {
				opacity: 1;
				left: 0;
			}
			.viewer-tag-list:hover + * + .viewer-img-list {
				left: 200px;
			}
			.viewer-tag-list {
				position: absolute;
				width: 200px;
				min-width: 50px;
				top: 0;
				left: -170px;
				overflow-y: auto;
				height: 100%;
				/*padding: 3px 10px;*/
				background-color: #303030;
				opacity: 0.2;
				transition: all 0.75s;
			}
			div.viewer-thumb-list-fixed > div.viewer-thumb-list {
				opacity: 1;
				right: 0;
			}
			div.viewer-thumb-list-fixed > div.viewer-img-list {
				right: 200px;
			}
			.viewer-thumb-list:hover {
				opacity: 1;
				right: 0;
			}
			.viewer-thumb-list {
				position: absolute;
				width: 200px;
				min-width: 50px;
				top: 0;
				right: -170px;
				opacity: 0.2;
				overflow-y: auto;
				height: 100%;
				background-color: #303030;
				text-align: right;
				transition: all 0.75s;
			}
			.viewer-thumb-div {
				max-width: 200px;
				padding: 2px 1px 2px 0;
			}
			.viewer-thumb {
				max-width: 180px;
				/*max-height: 240px;*/
			}
			.viewer-thumb-list:hover + .viewer-img-list {
				right: 200px;
			}
			.viewer-img-list {
				position: absolute;
				top: 0;
				left: 30px;
				right: 30px;
				bottom: 5px;
				transition: all 0.75s;
				background-color: ${col};
			}
			.viewer-img-list > .viewer-img-div:not(.img-show) ,
			div[data-image-view-type="none_src"],
			div[data-image-view-type="vid_file"] > *:not(.vid_file) ,
			div[data-image-view-type="orig_img"] > *:not(.orig_img) ,
			div[data-image-view-type="jpeg_img"] > *:not(.jpeg_img) ,
			div[data-image-view-type="samp_img"] > *:not(.samp_img) {
				display: none;
			}
			.viewer-img-div.img-show {
				width: 100%;
				height: 100%;
				text-align: center;
				background-color: ${col};
			}
			.viewer-img-div > * {
				max-width: 100%;
				max-height: 100%;
			}
			.viewer-img-div:before {
				content: "";
				display: inline-block;
				height: 100%;
				vertical-align: middle;
			}
			.viewer-bottom:hover {
				opacity: 1;
			}
			.viewer-bottom {
				transition: all 0.5s;
				opacity: 0.2;
				background-color: ${col};
			}
			.image-board-viewer-bottom-class {
				position: absolute;
				left: 200px;
				right: 200px;
				bottom: 0px;
				text-align: center;
			}
			.image-board-viewer-bottom-class button {
				color: ${imageBoard.siteList.style().color};
				background-color: #303030;/*${imageBoard.siteList.style().background};*/
				cursor: initial;
				margin: 1px 1px 3px 1px;
				padding: 1px 5px;
				border: 0;
				font-weight: bold;
			}
		`, id);
	}
	function progressBarCssClass()
	{
		var id = 'progress-bar-css-' + RANDOM;
		addCssClass(`
			@-webkit-keyframes progression{
				from{background-position:0px 0px;}
				to{background-position:50px 0px;}
			}
			@-o-keyframes progression{
				from{background-position:0px 0px;}
				to{background-position:50px 0px;}
			}
			@keyframes progression{
				from{background-position:0px 0px;}
				to{background-position:50px 0px;}
			}
			.progress-bar div.progress-counted {
				background-color: #da504e;
				width: 100%;
			}
			.progress-bar div.image-ready {
				background-color: #fda02e;
			}
			div.progress-bar > div.progress-complete {
				background-color: #5db75d;
			}
			div.progress-stripe {
				background-image:-webkit-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);
				background-image:-o-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);
				background-image:linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);
				background-size: 50px 50px;
				height: 12px;
			}
			div.progress-bar{
				margin: 2px 2px 0px 0px;
			}
			.progress-bar > div.download-in-progress {
				background-color: #0773fb;
			}
			div.progress-animated {
				animation: progression 2s linear infinite;
			}
		`, id);
	}
	function addCssClass(cssClass, id)
	{
		var style = document.createElement('style'),
			head = document.querySelector('head');
		style.type = 'text/css';
		if( id )
			style.setAttribute('id', id);
		if( style.styleSheets )
			style.styleSheets.cssText = cssClass;
		else
			style.appendChild(document.createTextNode(cssClass));
		return head.appendChild(style);
	}
	function resetCssClass(cssClass, id)
	{
		var style = document.getElementById(id);
		if( style && style.type === 'text/css' )
			remove( style );
		addCssClass(cssClass, id);
	}
	function attr( elm, name, val )
	{
		var cond = (val === null || val === undefined);
		if( !elm || !name )
			return;
		if( cond )
		{
			if( elm.length === undefined )
				return elm.getAttribute(name);
			return null;
		}
		else if( elm.length > 0 )
			[].forEach.call(elm, function(it){it.setAttribute(name, val);});
		else
			elm.setAttribute(name, val);
	}
	function addClass( elm, name )
	{
		if( elm && name )
		{
			if( elm.length > 0 )
				[].forEach.call(elm, function(it){it.classList.add(name);});
			else
				elm.classList.add(name);
		}
	}
	function removeClass( elm, name )
	{
		if( elm && name )
		{
			if( elm.length > 0 )
				[].forEach.call(elm, function(it){it.classList.remove(name);});
			else
				elm.classList.remove(name);
		}
	}
	function hasClass( elm, name )
	{
		if( elm && name )
			return elm.classList.contains(name);
		return false;
	}
	function toggleClass( elm, newClass, oldClass )
	{
		if( !elm || !newClass )
			return;
		if( oldClass )
		{
			elm.classList.remove(oldClass);
			elm.classList.add(newClass);
		}
		else if( elm.classList.contains(newClass) )
			elm.classList.remove(newClass);
		else
			elm.classList.add(newClass);
	}
	function getLocation( url, attr )
	{
		if( !url || !attr )
			return null;
		this.link = this.link || document.createElement('a');
		this.link.href = url;
		return this.link[attr];
	}
	function getFileExt( source )
	{
		var ext = source ? getLocation( source, 'pathname' ) : null;
		ext = ext ? ext.match(/\.([^\.]+)$/) : null;
		ext = ext ? ext[1] : null;
		return ext;
	}
	function getSearchObject( search )
	{
		var keys = {};
		if( search )
		{
			search = search.replace(/^\?/, '');
			search.split('&').forEach(function(item){
				item = item.split('=');
				keys[item[0]] = item[1];
			});
		}
		return keys;
	}
	function isSameLink( lhs, rhs )
	{
		lhs = getLocation(lhs, 'href');
		rhs = getLocation(rhs, 'href');
		return lhs === rhs;
	}
	function last( arr )
	{
		if( arr && arr.length > 0 )
			return arr[arr.length-1];
		return null;
	}
	function copy( arr, v )
	{
		arr = arr || [];
		if( v && v.length !== undefined )
		{
			arr.length = 0;
			for( var i = 0, len = v.length; i < len; ++i )
				arr[i] = v[i];
		}
		return arr;
	}
	function _toString_( obj, delim )
	{
		if( typeof obj === 'string' )
			return obj;
		else if( obj && obj.length !== undefined )
			try{
				return obj.join( delim || ', ' );
			}catch(e){
				return obj.toString();
			}
		else if( obj )
			return obj.toString();
		return '';
	}
	function nodeWalk()
	{
		var len = arguments.length, obj = this, i, arg;
		for( i = 0; i < len; ++i )
		{
			arg = arguments[i];
			if( arg === undefined )
				return obj;
			else if( obj[arg] === undefined )
				return null;
			obj = obj[arg];
		}
		return obj;
	}
	function hide( elm )
	{
		if( !elm )
			return;
		else if( elm.length === undefined )
			elm.style.display = 'none';
		else{
			try{
			for( var i = 0, len = elm.length; i < len; ++i )
				elm[i].style.display = 'none';
			}catch(e){console.error(e);}
		}
	}
	function show( elm )
	{
		if( elm )
			elm.style.display = '';
	}
	function remove( elm )
	{
		if( elm && elm.parentNode )
			return elm.parentNode.removeChild(elm);
		return null;
	}
	function parent( elm, n )
	{
		if( !elm || n === null || n === undefined )
			return elm;
		else if( /^\d+$/.test(n.toString()) )
		{
			n = parseInt(n, 10);
			for( var i = 0; i < n && elm; ++i )
				elm = elm.parentNode;
		}
		else if( typeof n === 'string' )
		{
			n = n.toUpperCase();
			while( elm && elm.tagName !== n )
				elm = elm.parentNode;
		}
		return elm;
	}
})();