ImageBoard Downloader

The original fullsize images downloader for gelbooru

As of 2017-10-15. 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 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 for gelbooru
// @namespace   https://greasyfork.org/users/155308
// @include     https://gelbooru.com*
// @version     0.0.3
// @grant       GM_xmlhttpRequest
// ==/UserScript==

/*
 0.0.3
	+ gelbooru downloader
*/
var clog = console.log;
var RANDOM = Math.floor(Math.random()*1e6 + 1e6);
(function(){
	var userOptions = initOptions();
	userOptions.set({
		'maxTagsInName': 8,
		'tagsDelim': ' ',
	});
	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/,
					init: function(){
						this.val = this.val || initGelbooruObject();
					},
					name: 'gelbooru',
				},
				'rule34': {
					val: null,
					regexp: /rule34/,
					init: function(){
						//this.val = this.val || initRule34Object();// TODO
					},
					name: 'rule34',
				},
			},
			val: function(type){
				if( type === undefined || type === null )
					return this.current;
				else if( this.data[type] )
					return this.data[type].val;
				else
					return null;
			},
			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();
		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),
							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 )
	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 = {
			name: 'gelbooru',
			setImageDataThumb: function( imgD, thumb ){
				if( thumb && imgD )
				{
					imgD.thumbSource = thumb.dataset.original;
					imgD.postUrl = thumb.parentNode.href;
					imgD.postId = thumb.parentNode.id.slice(1);
				}
			},
			getPostImage: function(doc){
				doc = doc || document;
				return doc.querySelector('#image');
			},
			setImageDataDoc: function( imgD, doc ){
				if( !imgD || imgD.state === 'ready' )
					return 1;
				doc = doc || document;
				var errN = 0,
					img = doc.querySelector('#image');
				if( img )
				{
					// lowresSource
					imgD.lowresSource = img.src;
					// size
					imgD.width = img.dataset.originalWidth;
					imgD.height = img.dataset.originalHeight;
				}else{
					console.error("[setImageDataDoc] no image found");
					++errN;
				}
				// highresSource
				var link = doc.querySelector('#tag-list a[href*="gelbooru.com//images/"]');
				if( link )
					imgD.source = link.href;
				else if( imgD.lowresSource )
					imgD.source = imgD.lowresSource;
				else{
					console.error("[setImageDataDoc] no image source found");
					return 2;
				}
				if( !imgD.lowresSource )
					imgD.lowresSource = imgD.source;
				// tags
				this.setImageDataTags( imgD, doc );
				// name
				var tagsLen = imgD.tags.length,
					uLen = userOptions.val('maxTagsInName'),
					tagsDelim = userOptions.val('tagsDelim');
				imgD.name = '';
				for( var i = 0; i < tagsLen && i < uLen; ++i )
					imgD.name += imgD.tags[i] + tagsDelim;
				imgD.name += imgD.postId;
				// extension
				var pathname = getLocation( imgD.source, 'pathname' );
				imgD.extension = pathname.match(/\.([a-z\d]+)$/)[1];
				imgD.state = 'ready';
				//clog("imgD[" + imgD.index + "]: ", imgD.source, imgD.state);
				return 0;
			},
			setImageDataTags: function( imgD, doc ){
				doc = doc || document;
				this.tagTypes = this.tagTypes || ['character', 'copyright', 'artist', 'general'];
				var obj = {};
				imgD.tags = imgD.tags || [];
				imgD.tags.length = 0;
				for( var i = 0, tagType, objTag; i < this.tagTypes.length; ++i )
				{
					tagType = this.tagTypes[i];
					obj[tagType] = this._getTags(doc, tagType);
					objTag = obj[tagType];
					if( objTag && objTag.length > 0 )
					{
						for( var k = 0; k < objTag.length; ++k )
							imgD.tags.push( this._getTagName(objTag[k]) );
					}
				}
			},
			isPost: function( url ){ return this._isPostView(url); },
			getPostId: function( postUrl ){ return this._getPostViewId(postUrl); },
			getPostUrl: function( postId ){ return 'https://gelbooru.com/index.php?page=post&s=view&id=' + postId;},
			
			_getTagName: function( tagElm ){
				return tagElm.querySelectorAll('a')[1].innerText.trim().replace(/\s+/g, '_');
			},
			_getTags: function( doc, type ){
				doc = doc || document;
				return doc.querySelectorAll('li.tag-type-' + type);
			},
			_isPostViewKeys: function( keys ){
				return keys['s'] === 'view' && keys['page'] === 'post';
			},
			_isPostListKeys: function( keys ){
				return keys['s'] === 'list' && keys['page'] === 'post';
			},
			_isPostView: function( url ){
				var srch = getLocation( url, 'search' ),
					keys = getSearchObject( srch );
				return this._isPostViewKeys( keys );
			},
			_getPostViewId: function( postUrl ){
				var srch = getLocation( postUrl, 'search' ),
					keys = getSearchObject( srch );
				if( this._isPostViewKeys( keys ) )
					return keys['id'];
				return null;
			},
			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');
					if( insertPlace )
						insertPlace = insertPlace.parentNode;
					else
						return null;
				}else{
					insertPlace = doc.querySelector('div.thumbnail-preview');
					if( insertPlace )
					{
						insertPlace = insertPlace.parentNode;
						insertPlace = insertPlace.children[1];
					}else
						return null;
				}
				div.setAttribute('id', id);
				return insertPlace.parentNode.insertBefore( div, insertPlace);
			},
		};
		return retVal;
	}
	//-------------------------------------- GELBOORU ------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- 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,
				},
				/*
				'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;
				font-weight: bold;
				width: 180px;
				border: 0px;
				padding: 5px;
				background: #0773fb;
			}
			#image-board-downloader-${RANDOM} > button:hover {
				background: #fbb307;
			}
			.image-board-downloader-off::after {
				content: " [off]";
			}
			.image-board-downloader-on::after {
				content: " [on]";
			}
			.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;
	}
})();