ImageBoard Downloader

The original fullsize images downloader for gelbooru, and rule34

Version vom 17.10.2017. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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 for gelbooru, and rule34
// @namespace   https://greasyfork.org/users/155308
// @include     https://gelbooru.com*
// @include     https://rule34.xxx*
// @version     0.0.5
// @grant       GM_xmlhttpRequest
// ==/UserScript==

/*
 0.0.5
	+ rule34 downloader
	+ add the imageboard name to the image name [true]
 0.0.3
	+ gelbooru downloader
	+ maximum tags in the image name [10]
	+ tags delimeter in the image name ['-']
*/
var clog = function(){};//console.log;
var RANDOM = Math.floor(Math.random()*1e6 + 1e6);
console.log('start..');
(function(){
	var userOptions = initOptions();
	var methodsObject = initMethodsObject();
	userOptions.set({
		'maxTagsInName': 8,
		'tagsDelim': ' ',
		'addImgBrdName': false,// add the imageboard name to the image name
	});
	var imageBoard = initImageBoard();
	newCssClasses();
	
	//------------------------------------------------------------------------------------//
	//------------------------------------- SITE LIST ------------------------------------//
	function initSiteList()
	{
		var retVal = {
			get current(){return this.currentObj.val;},
			get gelbooru(){return this.data.gelbooru.val;},
			get rule34(){return this.data.rule34.val;},
			data: {
				'gelbooru': {
					val: null,
					regexp: /gelbooru/,
					style: {
						color: '#fff',
						width: '180px',
						background: '#0773fb',
						backgroundHover: '#fbb307',
						colorHover: '#fff',
					},
					init: function(){
						this.val = this.val || initGelbooruObject();
					},
					name: 'gelbooru',
				},
				'rule34': {
					val: null,
					regexp: /rule34/,
					style: {
						color: '#fff',
						width: '180px',
						background: '#84AE83',
						backgroundHover: '#fff',
						colorHover: '#84AE83',
					},
					init: function(){
						this.val = this.val || initRule34Object();
					},
					name: 'rule34',
				},
			},
			getData: function( type ){
				if( type === undefined || type === null )
					return this.currentObj;
				else if( this.data[type] )
					return this.data[type];
				return null;
			},
			getDataProp: function( type, prop ){
				var dt = this.getData(type);
				if( dt && dt[prop] )
					return dt[prop];
				return null;
			},
			style: function(type){
				return this.getDataProp( type, 'style' );
			},
			val: function(type){
				return this.getDataProp( type, 'val' );
			},
			init: function(type){
				if( type === undefined || type === null )
					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.current);
		return retVal;
	}
	//------------------------------------- SITE LIST ------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ IMAGE BOARD -----------------------------------//
	function initImageBoard( d )
	{
		var imgBrdCl = initImageBoardClasses(d),
			imgBrdDt = initImageBoardDataset(d),
			siteList = initSiteList(),
			imgBrdDw = initImageBoardDownloader(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 images(){return this.data.images;},
			get downloader(){return this.data.downloader;},
			data: {
				'images': {
					list: null,
					init: function( doc, type ){
						clog("init");
						siteList.init(type);
						imgBrdDt.init(doc);
						imgBrdCl.init(doc);
						this.list = this.list || [];
						this.doc = doc || document;
						var thumbs = this.doc.querySelectorAll('img.preview'),
							siteObj = siteList.val(type);
						clog("siteObj: ", siteObj);
						var 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 )
								siteObj.setImageDataDoc(imgD);
							else
								siteObj.setImageDataThumb( imgD, thumb );
							imgBrdDt.val( img, 'index', imgD.index);
							imgBrdCl.addClass( img, 'counted' );
							return imgD;
						}
						if( isPost )
						{
							var img = siteObj.getPostImage();
							if( img && !imgBrdCl.hasClass( img, 'counted') )
							{
								imgD = addNewImage( this.list, img, true );
								if( imgD.state === 'ready' )
								{
									siteObj.createDiv( imgBrdId, this.doc);
									imgBrdDw.init(imgBrdId, this.doc);
									setReadyImage( imgD, imgBrdCl, imgBrdDt, imgBrdDw );
								}
							}
						}
						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();
						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)
					{
						GM_xmlhttpRequest({
							url: imgD.postUrl,
							method: 'GET',
							context: {
								'index': imgD.index,
							},
							onload: xhrImageData,
						});
					},
				},
				'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;
					},
					activate: function(doc){
						clog("activate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var thumbs = imgBrdCl.queryAll('counted');
						for( var i = 0, len = thumbs.length, thumb, a; i < len; ++i )
						{
							thumb = thumbs[i];
							a = thumb.parentNode;
							if( !imgBrdCl.hasClass(thumb, 'ready' ) )
								continue;
							else if( !imgBrdCl.hasClass( a, 'downloadAttach' ) )
							{
								a.addEventListener('click', handleDownloadEvent, false);
								imgBrdCl.addClass( a, 'downloadAttach' );
							}
							imgBrdCl.addClass( a, 'downloadActive' );
						}
						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();// =)
					},
				},
			},
			init: function(doc){
				for( var key in this.data )
					this.data[key].init(doc);
			},
			fix: function(){
				this.data.images.fix();
			},
		};
		retVal.init(d);
		retVal.fix();
		return retVal;
	}
	//------------------------------------ IMAGE BOARD -----------------------------------//
	//------------------------------------------------------------------------------------//
	//----------------------------------- XRH IMAGE DATA ---------------------------------//
	function xhrImageData(xhr)
	{
		if( xhr.status !== 200 )
		{
			console.error("xhr.status: ", xhr.status, xhr.statusText );
			console.error("index: ", xhr.context ? xhr.context.index : null);
			console.error("postUrl: ", this.url );
			return;
		}
		var imgD = imageBoard.images.list[xhr.context.index];
		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 )
	{
		if( (!imgBrdCl || !imgBrdDt || !imgBrdDw) && imageBoard )
		{
			imgBrdCl = imageBoard.imgBrdCl;
			imgBrdDt = imageBoard.imgBrdDt;
			imgBrdDw = imageBoard.imgBrdDw;
		}
		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;
		if( imageBoard && imageBoard.downloader.isActive() )
		{
			imageBoard.downloader.activate();
		}
	}
	//----------------------------------- XRH IMAGE DATA ---------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- GELBOORU ------------------------------------//
	function initGelbooruObject()
	{
		var retVal = {
			data: {
				name: 'gelbooru',
				hostname: 'gelbooru.com',
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			methodsMap: {
				getPostImage: 'booru',
				isPost: 'booru',
				getPostId: 'booru',
				getPostUrl: 'booru',
				
				setImageDataThumb: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				createDiv: 'booru',
				
				setImageDataDoc: 'general',
			},
			init: function(){
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			}
		};
		retVal.init();
		return retVal;
	}
	//-------------------------------------- GELBOORU ------------------------------------//
	//------------------------------------------------------------------------------------//
	//--------------------------------------- RULE34 -------------------------------------//
	function initRule34Object()
	{
		var retVal = {
			data: {
				name: 'rule34',
				hostname: 'rule34.xxx',
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			methodsMap: {
				getPostImage: 'booru',
				isPost: 'booru',
				getPostId: 'booru',
				getPostUrl: 'booru',
				
				setImageDataThumb: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				createDiv: 'booru',
				
				setImageDataDoc: 'general',
			},
			init: function(){
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			}
		};
		retVal.init();
		return retVal;
	}
	//--------------------------------------- RULE34 -------------------------------------//
	//------------------------------------------------------------------------------------//
	//----------------------------------- METHODS OBJECT ---------------------------------//
	function initMethodsObject()
	{
		var retVal = {
			data: {
				'booru': {
					val: null,
					init: function(){
						this.val = this.val || getBooruMethodsObject();
					},
				},
				'general': {
					val: null,
					init: function(){
						this.val = this.val || getGeneralMethodsObject();
					},
				},
			},
			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;
	}
	function getBooruMethodsObject()
	{
		var retVal = {
			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;
					imgD.postId = thumb.parentNode.id.slice(1);
				}
			},
			setImageDataSourceLowres: function( imgD, doc ){
				var img = this.getPostImage(doc);
				if( img )
					imgD.lowresSource = img.src;
				else
					return 1;
				return 0;
			},
			setImageDataSize: function( imgD, doc ){
				doc = doc || document;
				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.hostname + '//images/"]');
				if( link )
					imgD.source = link.href;
				else if( imgD.lowresSource )
					imgD.source = imgD.lowresSource;
				else{
					console.error("[setImageDataSourceHighres] no image source found");
					return 1;
				}
			},
			setImageDataTags: function( imgD, doc ){
				doc = doc || document;
				var getTagName = function( tagElm )
				{
					return last(tagElm.querySelectorAll('a')).innerText.trim().replace(/\s+/g, '_');
				};
				var tagsType = ['character', 'copyright', 'artist', 'circle', 'general', 'faults'];
				imgD.tags = imgD.tags || [];
				imgD.tags.length = 0;
				for( var i = 0, tags; i < tagsType.length; ++i )
				{
					tags = doc.querySelectorAll('li.tag-type-' + tagsType[i] );
					for( var k = 0; tags && k < tags.length; ++k )
						imgD.tags.push( getTagName(tags[k]) );
				}
			},
			isPost: function( url ){
				if( this.getPostId(url) )
					return true;
				return false;
			},
			getPostId: function( postUrl ){
				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 'https://' + this.hostname + '/index.php?page=post&s=view&id=' + postId;
			},
			createDiv: function(id, doc){
				doc = doc || document;
				var div = doc.querySelector('#' + id);
				if( div )
					return div;
				div = document.createElement('div');
				var insertPlace;
				if( this.isPost() )
				{
					insertPlace = doc.querySelector('h4 > a#showCommentBox');// gelbooru
					if( !insertPlace )
						insertPlace = doc.querySelector('h4 > a');// rule34
					if( insertPlace )
						insertPlace = insertPlace.parentNode;
					else
						return null;
				}else{
					insertPlace = doc.querySelector('.contain-push > div');// gelbooru
					if( !insertPlace )
						insertPlace = doc.querySelector('div#top');// rule34
					if( insertPlace )
						insertPlace = insertPlace.nextSibling;
					else
						return null;
				}
				clog("insertPlace: ", insertPlace);
				div.setAttribute('id', id);
				return insertPlace.parentNode.insertBefore( div, insertPlace);
			},
		};
		return retVal;
	}
	function getGeneralMethodsObject()
	{
		var retVal = {
			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.source;
				// tags
				this.setImageDataTags( imgD, doc );
				// name
				this.setImageDataName( imgD );
				// extension
				this.setImageDataExtension( imgD );
				imgD.state = 'ready';
				//clog("imgD[" + imgD.index + "]: ", imgD.source, imgD.state);
				return 0;
			},
			setImageDataName: function( imgD ){
				var tagsLen = imgD.tags.length,
					uLen = userOptions.val('maxTagsInName'),
					tagsDelim = userOptions.val('tagsDelim');
				imgD.name = '';
				if( userOptions.val('addImgBrdName') )
					imgD.name += this.name + tagsDelim;
				for( var i = 0; i < tagsLen && i < uLen; ++i )
					imgD.name += imgD.tags[i] + tagsDelim;
				imgD.name += imgD.postId;
			},
			setImageDataExtension: function( imgD ){
				var pathname = getLocation( imgD.source, 'pathname' );
				imgD.extension = pathname.match(/\.([a-z\d]+)$/)[1];
			},
		};
		return retVal;
	}
	//----------------------------------- METHODS OBJECT ---------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- 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);
			},
			id: {
				downloader: 'image-board-downloader-' + RANDOM,
				downloadAll: 'image-board-download-all-' + RANDOM,
				downloadSwitch: 'image-board-download-switch-' + RANDOM,
			},
			'class': {
				on: 'image-board-downloader-on',
				off: 'image-board-downloader-off',
				active: 'image-board-downloader-active',
			},
			init: function(id, doc){
				doc = doc || document;
				var div = doc.querySelector('div#' + id);
				if( !div )
				{
					console.error("[initImageBoardDownloader] can't init div");
					return;
				}
				this.val = div.querySelector('#' + this.id.downloader );
				if( !this.val )
				{
					this.val = document.createElement('div');
					this.val.setAttribute('id', this.id.downloader);
					var html = '' +
					'<button id="' + this.id.downloadSwitch + '" class="' + this.class.off + '">Download Mode</button>' +
					'<button id="' + this.id.downloadAll + '" class="">Download All (0)</button>' +
					'';
					this.val.innerHTML = html;
					this.val = div.appendChild(this.val);
				}
				this.downloadAll = this.val.querySelector('#' + this.id.downloadAll);
				if( !this.downloadAll.classList.contains(this.class.active) )
				{
					this.downloadAll.addEventListener('click', handleDownloadAllEvent, false);
					this.downloadAll.classList.add(this.class.active);
				}
				this.downloadSwitch = this.val.querySelector('#' + this.id.downloadSwitch);
				if( !this.downloadSwitch.classList.contains(this.class.active) )
				{
					activateKeyboard();
					this.downloadSwitch.addEventListener('click', handleDownloadSwitchEvent, false);
					this.downloadSwitch.classList.add(this.class.active);
				}
				return this.val;
			},
			downloadAllHtml: function( total, loaded ){
				if( !this.val )
					return;
				this.downloadAll.innerHTML = '' +
				'Download All (' + (loaded ? loaded + ' / ': '') + (total ? total : 0) + ')';
			},
			downloadOn: function(){
				toggleClass( this.downloadSwitch, this.class.on, this.class.off );
			},
			downloadOff: function(){
				toggleClass( this.downloadSwitch, this.class.off, this.class.on );
			},
			isActive: function(){
				if( this.downloadSwitch )
					return this.downloadSwitch.classList.contains(this.class.on);
				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 handleDownloadAllEvent(event)
	{
		var list = imageBoard.images.list;
		for( var i = 0, len = list.length, imgD; i < len; ++i )
		{
			imgD = list[i];
			downloadFile( imgD );
		}
	}
	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 downloadFile( imgD )
	{
		if( imgD.state !== 'ready' || imgD.downloadState === 'downloaded' || imgD.downloadState === 'inProgress' )
			return;
		imgD.downloadState = 'inProgress';
		var hostname = getLocation(imgD.source, 'hostname');
		if( hostname === window.location.hostname )
		{
			imageBoardDownloader( imgD, imgD.source );
			return;
		}
		GM_xmlhttpRequest({
			url: imgD.source,
			method: 'GET',
			context: {
				'index': imgD.index,
			},
			responseType: 'blob',
			onload: blibBlobDownloader,
		});
	}
	function blibBlobDownloader( xhr )
	{
		if( xhr.status !== 200 )
		{
			console.error("xhr.status: ", xhr.status, xhr.statusText);
			console.erorr("url: " + this.url);
			return;
		}
		var wndURL = window.webkitURL || window.URL,
			resource = wndURL.createObjectURL(xhr.response),
			imgD = imageBoard.images.list[xhr.context.index];
		imageBoardDownloader( imgD, resource );
		wndURL.revokeObjectURL( resource );
	}
	function imageBoardDownloader( imgD, resource )
	{
		var name = imgD.name + '.' + imgD.extension;
		fileDownloader( name, resource );
		var thumb = imageBoard.imgBrdDt.query('index', imgD.index + '');
		imageBoard.imgBrdCl.addClass( thumb, 'downloaded' );
		imgD.downloadState = 'downloaded';
		imageBoard.imgBrdDw.done += 1;
	}
	function handleDownloadSwitchEvent(event)
	{
		if( imageBoard.imgBrdDw.isActive() )
			imageBoard.downloader.deactivate();
		else
			imageBoard.downloader.activate();
	}
	function activateKeyboard()
	{
		window.addEventListener('keydown', handleKeyboardEvent, false);
	}
	function deactivateKeyboard()
	{
		window.removeEventListener('keydown', handleKeyboardEvent, false);
	}
	function handleKeyboardEvent(event)
	{
		var charCode = event.keyCode || event.which,
			str = String.fromCharCode(charCode);
		if( str === 'd' || str === 'D' )
		{
			handleDownloadSwitchEvent();
		}
		/*
		else if( str === 'v' || str === 'V' )
		{
			handleViewerSwitchEvent();// dummy function
		}
		else if( str === 's' || str === 'S' )
		{
			if( imageBoard )
			{
				imageBoard.init();
				imageBoard.fix();
			}
		}
		*/
	}
	//------------------------------------ DOWNLOADER-2 ----------------------------------//
	//------------------------------------------------------------------------------------//
	function handleViewerSwitchEvent(event){}// TODO
	//------------------------------------------------------------------------------------//
	//------------------------------------ USER OPTIONS ----------------------------------//
	function initOptions()
	{
		function _setDef(){this.val = this.def;}
		var retVal = {
			data: {
				'maxTagsInName': {
					val: null,
					def: 10,
					setDef: _setDef,
				},
				'tagsDelim': {
					val: null,
					def: '-',
					setDef: _setDef,
				},
				'addImgBrdName': {
					val: null,
					def: true,
					setDef: _setDef,
				},
				/*
				'autoRunSource': {
					val: null,
					def: true,
					setDef: _setDef,
				},
				'autoRunDownloadOn': {
					val: null,
					def: false,
					setDef: _setDef,
				},
				*/
			},
			val: function( opt, v )
			{
				if( this.data[opt] )
				{
					if( v === undefined )
						return this.data[opt].val;
					else
						this.data[opt].val = v;
				}else
					return null;
			},
			setDefs: function(){
				for( var key in this.data )
					this.data[key].setDef();
			},
			set: function(obj){
				for( var key in obj )
					this.val(key, obj[key]);
			},
		};
		retVal.setDefs();
		return retVal;
	}
	//------------------------------------ USER OPTIONS ----------------------------------//
	//------------------------------------------------------------------------------------//
	function newCssClasses()
	{
		addCssClass(`
			#image-board-downloader-${RANDOM} {
				position: relative;
				text-align: right;
				top: 2px;
				right: 10px;
				bottom: 2px;
			}
			#image-board-downloader-${RANDOM} > button {
				margin: 3px 10px;
				//color: #fff;
				color: ${imageBoard.siteList.style().color};
				font-weight: bold;
				width: 180px;
				border: 0px;
				padding: 5px;
				//background: #0773fb;
				background: ${imageBoard.siteList.style().background};
			}
			#image-board-downloader-${RANDOM} > button:hover {
				//background: #fbb307;
				background: ${imageBoard.siteList.style().backgroundHover};
				color: ${imageBoard.siteList.style().colorHover};
			}
			.image-board-downloader-off::after {
				content: " [off]";
			}
			.image-board-downloader-on::after {
				content: " [on]";
			}
			img.image-board-has-original-source {
				border-bottom: 5px solid green !important;
			}
			.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 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;
	}
})();