ImageBoard Downloader

The original fullsize images downloader, and (optional) viewer for the various imageboards

As of 2017-11-29. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        ImageBoard Downloader
// @description The original fullsize images downloader, and (optional) viewer for the various imageboards
// @namespace   https://greasyfork.org/users/155308
// -------- 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*
// -------- EXCLUDE
// @exclude     *://simg3.gelbooru.com*//images/*
// @exclude     *://img.rule34.xxx*//images/*
// @exclude     *://files.yande.re*/images/*
// @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/*
// -------- 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
// -------- VERSION
// @version     0.3.0
// @grant       GM_xmlhttpRequest
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_listValues
// @grant       GM_deleteValue
// @grant       GM_info
// ==/UserScript==

/*
 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);
var IMAGEBOARDVersion = 'v' + GM_info.script.version;
console.log('start..');
(function(){
	function consoleLog(){window.console.log.apply(this, arguments);}
	function blank(){}
	var clog = consoleLog;
	clog = blank;
	var userOptions = initOptions();
	var methodsObject = initMethodsObject();
	var imageBoard = initImageBoard();
	newCssClasses();
	
	//------------------------------------------------------------------------------------//
	//------------------------------------ IMAGE BOARD -----------------------------------//
	function initImageBoard( d )
	{
		var imgBrdCl = initImageBoardClasses(d),
			imgBrdDt = initImageBoardDataset(d),
			siteList = initSiteList(),
			imgBrdDw = initImageBoardDownloader(d),
			userMenu = initUserMenu(),
			imgBrdVw = initImageBoardViewer(d),
			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 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;
						function addNewImage( list, img, isPost )
						{
							list.push({});
							var imgD = last(list);
							imgD.state = 'empty';
							imgD.index = list.length - 1;
							imgD.type = siteObj.name;
							if( isPost )
							{
								imgD.postId = siteObj.getPostId();
								imgD.postUrl = window.location.href;
								siteObj.setImageDataDoc(imgD);
							}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;
						}
						if( isPost )
						{
							var img = siteObj.getPostImage();
							if( img && !imgBrdCl.hasClass( img, 'counted') )
								addNewImage( this.list, img, true );
						}
						var thumbs = this.doc.querySelectorAll('img.preview');
						if( thumbs && thumbs.length === 0 )
							thumbs = this.doc.querySelectorAll('article > a > img');// donmai, uberbooru
						if( thumbs && thumbs.length === 0 )
							thumbs = this.doc.querySelectorAll('img[itemprop="thumbnailUrl"]');// donmai
						if( thumbs && thumbs.length === 0 )
							thumbs = this.doc.querySelectorAll('span.thumb > a > img');// *.booru.org
						clog("thumbs.length: ", thumbs.length);
						for( var i = 0, len = thumbs.length, thumb; i < len; ++i )
						{
							thumb = thumbs[i];
							if( imgBrdCl.hasClass( thumb, 'counted' ) )
								continue;
							addNewImage( this.list, thumb );
						}
					},
					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();
						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);
						}
					},
					getImageData: function(imgD)
					{
						if( siteList.needXHR(imgD.type) )
						{
							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;
					},
					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("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();
					},
					deactivate: function(doc){
						clog("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();
					},
					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();
					},
					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();
					},
					isActive: function(){
						return imgBrdVw.isActive();
					},
				},
			},
			init: function(doc){
				for( var key in this.data )
					this.data[key].init(doc);
			},
			fix: function(){
				this.data.images.fix();
			},
		};
		retVal.init(d);
		if( userOptions.val('autoRun') )
			retVal.fix();
		return retVal;
	}
	//------------------------------------ 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';
			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 + '');
		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':     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,
			},
			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.replace(/^\./,'');
				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',
			},
			methodsMap: {
				isPost: 'gelbooru',
				getPostId: 'gelbooru',
				getPostUrl: 'gelbooru',
			},
			needXHR: true,
		};
	}
	//------------------------------------------------------------------------------------//
	//--------------------------------------- RULE34 -------------------------------------//
	function getRule34Settings()
	{
		return {
			name: 'rule34',
			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,
		};
	}
	//------------------------------------------------------------------------------------//
	//------------------------------------- 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
					'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();
					},
				},
			},
			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){
				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){
				return (doc || document).querySelector('#image');
			},
			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 )
						imgD.postId = thumb.parentNode.id.slice(1);
					else
						imgD.postId = this.getPostId( imgD.postUrl );
				}
			},
			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 )
				{
					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 link = doc.querySelector('li > a[href*="' + (this.imageHostname || this.hostname) + '/' + 
					this.imageDir + '/"]');
				if( !link && this.name === 'donmai' )// same origin for donmai
					link = doc.querySelector('li > a[href*="/' + this.imageDir + '/"]');
				if( link )
					imgD.source = link.href;
				else if( imgD.lowresSource )
					imgD.source = imgD.lowresSource;
				else{
					console.error("[setImageDataSourceHighres] no image source found");
					return 1;
				}
				// + yande.re:
				var jpeg = doc.querySelector('li > a[href*="' + this.hostname + '/jpeg/"]');
				if( jpeg )
					imgD.jpegSource = jpeg.href;
				return 0;
			},
			setImageDataTags: function( imgD, doc ){
				doc = doc || document;
				var 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, '_');
				};
				var tagsType = ['character', 'copyright', 'artist', 'species', 'model', 'idol', 'photo_set', 'circle', 'medium', 'general', 'faults'],
					tagsMap = ['4', '3', '1', '-1', '-1', '-1', '-1', '-1', '-1', '0', '-1'];// donmai
				var nameList = ['sankaku', 'safebooru'],
					iter = 0, i, k, _fl, tags;
				imgD.tags = imgD.tags || [];
				imgD.tags.length = 0;
				for( i = 0, _fl = (nameList.indexOf(this.name) != -1); i < tagsType.length; ++i )
				{
					tags = doc.querySelectorAll('li.tag-type-' + tagsType[i] );
					if( tags.length === 0 )
						tags = doc.querySelectorAll('li.category-' + tagsMap[i]);// donmai
					for( k = 0; tags && k < tags.length; ++k, ++iter )
						imgD.tags.push( getTagName(tags[k], _fl) );
				}
				if( iter === 0 )
					tags = doc.querySelectorAll('div#tag_list li');// *.booru.org
				for( k = 0, _fl = (nameList.indexOf(this.name) != -1); tags && k < tags.length; ++k )
					imgD.tags.push( getTagName(tags[k], _fl) );
			},
			createDiv: function(id, doc){
				doc = doc || document;
				var div = doc.querySelector('#' + id);
				clog("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);
				div = insertPlace.parentNode.insertBefore( div, insertPlace);
				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 = '';
				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 )
					name += (imgD.tags[i].length > 0 ? imgD.tags[i] + tagsDelim  + '' : '');
				if( userOptions.val('imgIdAtNameEnd') )
					imgD.name = name + imageId;
				else
					imgD.name = imageId + tagsDelim + name.slice(0, -tagsDelim.length);
			},
			setImageDataExtension: function( imgD ){
				var pathname = getLocation( imgD.source, 'pathname' );
				imgD.extension = pathname.match(/\.([a-z\d]+)$/)[1];
				if( imgD.jpegSource )
				{
					pathname = getLocation( imgD.jpegSource, 'pathname' );
					imgD.jpegExtension = pathname.match(/\.([a-z\d]+)$/)[1];
				}
			},
		};
		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){
				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;
	}
	//------------------------------------------------------------------------------------//
	//-------------------------------------- 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] )
				{
					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("imgBrdDw.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;
				}
				this.downloadSwitch = div.querySelector('#' + this.downloadSwitchId);
				if( !this.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.innerHTML = 'Donwload Mode';
					this.downloadSwitch = div.appendChild( btn );
				}
				this.downloadAll = div.querySelector('#' + this.downloadAllId);
				if( !this.downloadAll )
				{
					btn = document.createElement('button');
					btn.setAttribute('id', this.downloadAllId );
					btn.setAttribute('class', this.classBtn );
					btn.innerHTML = 'Donwload All (0)';
					this.downloadAll = div.appendChild( btn );
				}
				if( !this.downloadAll.classList.contains(this.classActive) )
				{
					this.downloadAll.addEventListener('click', handleDownloadAllEvent, false);
					this.downloadAll.classList.add(this.classActive);
				}
				if( !this.downloadSwitch.classList.contains(this.classActive) )
				{
					//activateKeyboard();
					this.downloadSwitch.addEventListener('click', handleDownloadSwitchEvent, false);
					this.downloadSwitch.classList.add(this.classActive);
				}
				return div;
			},
			downloadAllHtml: function( total, loaded ){
				if( !this.downloadAll )
					return;
				this.downloadAll.innerHTML = '' +
				'Download All (' + (loaded ? loaded + ' / ': '') + (total ? total : 0) + ')';
			},
			downloadOn: function(){
				toggleClass( this.downloadSwitch, this.classOn, this.classOff );
			},
			downloadOff: function(){
				toggleClass( this.downloadSwitch, this.classOff, this.classOn );
			},
			isActive: function(){
				if( this.downloadSwitch )
					return this.downloadSwitch.classList.contains(this.classOn);
				return false;
			},
		};
		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;
		if( userOptions.val('downloadJPEG') && imgD.jpegSource )
			source = imgD.jpegSource;
		else
			source = imgD.source;
		if( hostname === window.location.hostname )
		{
			imageBoardDownloader( imgD, source );
			return;
		}
		GM_xmlhttpRequest({
			url: source,
			method: 'GET',
			context: {
				'index': imgD.index,
				'url': source,
			},
			responseType: 'blob',
			onload: blibBlobDownloader,
		});
	}
	function blibBlobDownloader( xhr )
	{
		if( xhr.status !== 200 )
		{
			console.error("xhr.status: ", xhr.status, xhr.statusText);
			console.error("url: " + xhr.context.url);
			return;
		}
		var wndURL = window.webkitURL || window.URL,
			resource = wndURL.createObjectURL(xhr.response),
			imgD = imageBoard.images.list[xhr.context.index],
			extension = getLocation(xhr.context.url, 'pathname').match(/\.([^\.]+)$/);
		extension = extension ? extension[1] : null;
		imageBoardDownloader( imgD, resource, extension );
		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 + '');
		imageBoard.imgBrdCl.addClass( thumb, 'downloaded' );
		if( imgD.downloadState !== 'downloaded' )
			imageBoard.imgBrdDw.done += 1;
		imgD.downloadState = 'downloaded';
	}
	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 === '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,
			//preload: 2,
		};
		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.innerHTML = '' + (n + 1);
				iter.curr = n;
			},
			set total(n){iter.total = parseInt(n, 10);},
			get total(){return iter.total;},
			/*
			set preload(n){iter.preload = n;},
			get preload(){return iter.preload;},
			*/
			data: {
				buttonId: 'image-board-viewer-button-' + RANDOM,
				containerId: 'image-board-viewer-container-' + RANDOM,
				listId: 'image-board-viewer-list-' + RANDOM,
				bottomId: 'image-board-viewer-bottom-' + 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 listId(){return this.data.listId;},
			get bottomId(){return this.data.bottomId;},
			
			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;
				}
				this.btn = doc.querySelector('#' + this.buttonId);
				if( !this.btn )
				{
					this.btn = document.createElement('button');
					this.btn.setAttribute('id', this.buttonId);
					this.btn.setAttribute('class', this.classOff);
					this.btn.innerHTML = 'Viewer';
					this.btn = div.insertBefore( this.btn, div.firstChild );
				}
				if( !this.btn.classList.contains(this.classActive) )
				{
					this.btn.addEventListener('click', handleViewerSwitchEvent, false);
					this.btn.classList.add(this.classActive);
				}
				this.cont = doc.querySelector('#' + this.containerId);
				if( !this.cont )
				{
					this.cont = document.createElement('div');
					this.cont.setAttribute('id', this.containerId);
					this.cont.setAttribute('class', this.classOff );
					this.cont.setAttribute('data-class-button', this.classBtn);
					this.cont.setAttribute('data-prev-id', this.prevId);
					this.cont.setAttribute('data-next-id', this.nextId);
					this.cont.setAttribute('data-download-id', this.downloadId);
					this.cont.setAttribute('data-current-id', this.currentId);
					this.cont.setAttribute('data-source-id', this.sourceId);
					this.cont.setAttribute('data-list-id', this.listId);
					html = '' +
					'<div id="' + this.listId + '" style="text-align:center;"></div>' +
					'<div id="' + this.bottomId + '" class="' + this.classBottom + '">' +
						'<button id="' + this.prevId + '" class="' + this.classBtn + '">Prev</button>' +
						'<button id="' + this.sourceId + '" class="' + this.classBtn + '">Source</button>' +
						'<button id="' + this.currentId + '" style="width:40px;">' + '-' + '</button>' +
						'<button id="' + this.downloadId + '" class="' + this.classBtn + '">Download</button>' +
						'<button id="' + this.nextId + '" class="' + this.classBtn + '">Next</button>' +
					'</div>';
					this.cont.innerHTML = html;
					if( selector )
					{
						viewDiv = doc.querySelector(selector);
						if( viewDiv.firstChild )
							this.cont = viewDiv.insertBefore( this.cont, viewDiv.firstChild );
						else
							this.cont = viewDiv.appendChild( this.cont );
					}else{
						viewDiv = div;
						if( viewDiv.nextSibling )
							this.cont = viewDiv.parentNode.insertBefore( this.cont, viewDiv.nextSibling );
						else
							this.cont = viewDiv.parentNode.appendChild( this.cont );
					}
				}
				if( !this.cont.classList.contains(this.classActive) )
				{
					this.cont.addEventListener('click', handleViewerNavigationEvent, false);
					this.cont.classList.add(this.classActive);
					activateViewerKeyboard();
				}
				//this.preload = userOptions.val('preloadView') || 2;
			},
			showNext: function(){
				if( !this.isActive() )
					return;
				try{
					this.curr = (this.curr + this.total + 1)%this.total;
					viewImage(this.curr);
				}catch(e){console.error(e);}
			},
			showPrev: function(){
				if( !this.isActive() )
					return;
				try{
					this.curr = (this.curr + this.total - 1)%this.total;
					viewImage(this.curr);
				}catch(e){console.error(e);}
			},
			isActive: function(){return hasClass( this.btn, this.classOn );},
			viewerOn: function(){
				toggleClass(this.btn, this.classOn, this.classOff);
				toggleClass(this.cont, this.classOn, this.classOff);
				try{
					var html = this.cont.querySelector('#' + this.currentId).innerHTML;
					if( userOptions.val('viewFirst') && html === '-' )
						viewImage(0);
				}catch(e){console.error(e);}
			},
			viewerOff: function(){
				toggleClass(this.btn, this.classOff, this.classOn);
				toggleClass(this.cont, this.classOff, this.classOn);
			},
		};
		return retVal;
	}
	//--------------------------------------- VIEWER -------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- VIEWER-2 ------------------------------------//
	function activateViewerKeyboard()
	{
		window.addEventListener('keydown', handleViewerKeyboardEvent, false);
	}
	function handleViewerKeyboardEvent(event)
	{
		var charCode = event.keyCode || event.which;
		if( event.shiftKey || !event.ctrlKey || event.altKey )
			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,
			elm, siteObj, imgD, source, ext, html = '';
		var hostname = window.location.hostname,
			pathname = window.location.pathname;
		if( !viewer )
			return;
		elm = document.querySelector('#' + viewer.listId);
		if( !elm )
		{
			console.error("[viewImage] viewer images container not found");
			return;
		}
		imgD = imageBoard.images.list[idx];
		if( imgD.state !== 'ready' )
			return;
		if( userOptions.val('viewSample') )
			source = imgD.lowresSource;
		else if( userOptions.val('viewJPEG') && imgD.jpegSource )
			source = imgD.jpegSource;
		else
			source = imgD.source;
		ext = getFileExt( getLocation(source, 'pathname') );
		clog("view image[" + idx + "]: " + source);
		if( /(mp4|webm|ogv|ogg)/.test(ext) )
		{
			html = '<video src="' + source + '" controls></video>';
		}
		else if( /(jpe?g|png|svg|gif)/.test(ext) )
		{
			html = '<img src="' + source + '"></img>';
		}else{
			console.error("[veiwImage] not matched file extension: " + ext);
			console.error("[viewImage] file src: " + source);
		}
		if( hostname.indexOf('sankakucomplex') != -1 )
		{
			siteObj = imageBoard.siteList.val();
			if( !siteObj.isPost(pathname) )
				window.history.pushState({source: source, name: imgD.name}, null, imgD.postUrl);
			else
				window.history.replaceState({source: source, name: imgD.name}, null, imgD.postUrl);
		}
		elm = document.querySelector('#' + viewer.listId);
		elm.innerHTML = html;
		viewer.curr = idx;
	}
	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.innerHTML = 'User Menu';
					userMenuBtn = div.insertBefore( btn, div.firstChild );
				}
				if( !hasClass( userMenuBtn, userMenuActive ) )
				{
					userMenuBtn.addEventListener('click', handleUserMenuEvent, false);
					addClass( userMenuBtn, userMenuActive );
				}
				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;
		if( !div )
		{
			contentHtml = makeUserMenuContentHtml();
			bottomHtml = makeUserMenuBottomHtml();
			div = document.createElement('div');
			div.setAttribute('id', dataSet.containerId);
			div.setAttribute('class', dataSet.containerClass);
			div.innerHTML = '' +
			'<div class="' + dataSet.titleClass + '">' +
				'<span>ImageBoard Downloader ' + IMAGEBOARDVersion + '</span>' +
				'<span class="image-board-user-menu-x"></span>' +
			'</div>' +
			'<div class="' + dataSet.contentClass + '">' + contentHtml + '</div>' +
			'<div class="' + dataSet.bottomClass + '">' + bottomHtml + '</div>';
			div = body.appendChild(div);
			addClass( div, dataSet.closeClass );
			activateUserMenu();
		}
		if( hasClass(div, dataSet.openClass) )
			closeUserMenu.call(this);
		else if( hasClass(div, dataSet.closeClass) )
			openUserMenu.call(this);
	}
	function makeUserMenuContentHtml()
	{
		var typeList = ['checkbox', 'number', 'text'],
			//categoryList = getUserOptionsListOf('category'),
			html = '', key, dt, inp, labl;
		//var _clog = console.log;
		//_clog("categoryList: " + categoryList.join(', '));
		for( key in userOptions.data )
		{
			dt = userOptions.data[key];
			if( typeList.indexOf(dt.type) != -1 )
			{
				inp = '<input id="image-board-user-menu-' + key + '-val" type="' + dt.type + '" ' +
				'style="' + (dt.type!=='checkbox' ? 'text-align: center; width: 50px;': '') + '"/>';
				labl = '<label id="image-board-user-menu-' + key + '-caption" ' +
				'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>';
			}
		}
		return '<div class="image-board-user-menu-tabs-content">' + html + '</div>';
	}
	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,
		};
		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 = dt.val;
			captionElm = doc.querySelector('#image-board-user-menu-' + key + '-caption');
			if( captionElm )
				captionElm.innerHTML = 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();
	}
	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 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'] );
	}
	function openUserMenu()
	{
		var dataSet = imageBoard.userMenu.data,
			userMenu = document.querySelector('#' + dataSet['container-id']);
		toggleClass( userMenu, dataSet['open-class'], dataSet['close-class'] );
		setUserMenu();
	}
	//------------------------------------- USER MENU ------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ USER OPTIONS ----------------------------------//
	function initOptions()
	{
		function _setDef(){this.val = this.def;}
		var retVal = {
			data: {
				'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;
					},
				},
				'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';},
				},
				'autoRun': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'general',
					setDef: _setDef,
					getDesc: function(){return 'Initialize the Script on start';},
				},
				'downloadJPEG': {
					val: null,
					def: false,
					type: 'checkbox',
					category: 'general',
					setDef: _setDef,
					getDesc: function(){return 'Donwload jpeg instead of png (yande.re option)';},
				},
				'createViewer': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'general',
					setDef: _setDef,
					getDesc: function(){return 'Add Image Viewer to ImageBoard';},
				},
				'viewSample': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'general',
					setDef: _setDef,
					getDesc: function(){return 'View image samples';},
				},
				'viewJPEG': {
					val: null,
					def: false,
					type: 'checkbox',
					category: 'general',
					setDef: _setDef,
					getDesc: function(){return 'View jpeg image (yande.re option)';},
				},
				'viewFirst': {
					val: null,
					def: true,
					type: 'checkbox',
					category: 'general',
					setDef: _setDef,
					getDesc: function(){return 'Load 1st image on viewer activation';},
				},
				/*
				'maxHeight': {
					val: null,
					def: 600,
					type: 'number',
					category: 'general',
					setDef: _setDef,
					getDesc: function(){return 'Maximum height of image';},
				},
				*/
			},
			get storageKey(){ return 'user-options-storage-key';},
			val: function( opt, v )
			{
				if( this.data[opt] )
				{
					if( v !== undefined )
					{
						if( typeof this.data[opt].validator !== 'function' )
							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: function(){
				// compatibility with v0.2.0 and older (till v0.1.0)
				var oldKey = 'user-options-storage-key-' + RANDOM,
					objStr = GM_getValue( oldKey, '' );
				if( objStr )
				{
					GM_deleteValue(oldKey);
					GM_setValue( this.storageKey, objStr );
				}
			},
			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: function(){
				var storageObj = JSON.parse(GM_getValue(this.storageKey, '{}')), v;
				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: function(){
				this.fixStorage();
				this.loadData();
			},
		};
		retVal.init();
		return retVal;
	}
	//------------------------------------ USER OPTIONS ----------------------------------//
	//------------------------------------------------------------------------------------//
	function newCssClasses()
	{
		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};
			}
			.image-board-user-menu-title,
			#image-board-user-menu-container-${RANDOM} {
				border-top-left-radius: 10px;
				border-top-right-radius: 10px;
			}
			.image-board-user-menu-bottom,
			#image-board-user-menu-container-${RANDOM} {
				border-bottom-left-radius: 10px;
				border-bottom-right-radius: 10px;
			}
			#image-board-user-menu-container-${RANDOM} {
				position: fixed;
				bottom: 10px;
				right: 10px;
				z-index: 999;
				background-color: #e7e7e7;
				width: 35%;
				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: 8px;
			}
			div.image-board-user-menu-content {
				/*padding: 5px 10px;*/
				background-color: #eeeeee;
				overflow-y: auto;
				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;
			}
			div.image-board-user-menu-tabs-content {
				padding: 5px 10px;
			}
			.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;
			}
			.image-board-viewer-bottom-class {
				text-align: center;
			}
			.image-board-viewer-bottom-class button {
				color: ${imageBoard.siteList.style().color};
				background-color: ${imageBoard.siteList.style().background};
				cursor: initial;
				margin: 1px 1px 3px 1px;
				padding: 1px 5px;
				border: 0;
			}
			/*
			button.image-board-viewer-btn::hover {
				color: ${imageBoard.siteList.style().colorHover};
				background-color: ${imageBoard.siteList.style().backgroundHover};
			}
			*/
			button.image-board-viewer-btn {
				cursor: pointer;
			}
			.image-board-user-menu-open {
				display: initial;
			}
			.image-board-user-menu-close {
				display: none;
			}
			.image-board-downloader-off::after {
				content: " [off]";
			}
			.image-board-downloader-on::after {
				content: " [on]";
			}
			div.image-board-viewer-off {
				display: none;
			}
			div.image-board-viewer-on {
				display: initial;
			}
			button.image-board-viewer-off::after {
				content: " [+]";
			}
			button.image-board-viewer-on::after {
				content: " [\u2013]";
			}
			img.image-board-has-original-source {
				border-bottom: 5px solid green !important;
			}
			.image-board-active-for-view,
			.image-board-active-for-download {
				cursor: default;
			}
		`);
	}
	function addCssClass(cssClass)
	{
		var style = document.createElement('style'),
			head = document.querySelector('head');
		style.type = 'text/css';
		if( style.styleSheets )
			style.styleSheets.cssText = cssClass;
		else
			style.appendChild(document.createTextNode(cssClass));
		return head.appendChild(style);
	}
	function addClass( elm, name )
	{
		if( elm && name )
			elm.classList.add(name);
	}
	function removeClass( elm, name )
	{
		if( elm && name )
			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( !attr )
			return null;
		url = url || window.location.href;
		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 last( arr )
	{
		if( arr && arr.length > 0 )
			return arr[arr.length-1];
		return null;
	}
	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;
	}
})();